package jnpf.fileStorage.dms; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.config.ConfigValueUtil; import jnpf.constant.MsgCode; import jnpf.dms.DmsResourceApi; import jnpf.dmsEntity.storage.form.DmsFileRevisionRegisterForm; import jnpf.dmsEntity.storage.form.DmsFileUploadRegisterForm; import jnpf.dmsEntity.storage.vo.DmsFileUploadVO; import jnpf.exception.DataException; import jnpf.util.OptimizeUtil; import jnpf.util.StringUtil; import jnpf.util.UpUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileStorageService; import org.dromara.x.file.storage.core.hash.HashInfo; import org.dromara.x.file.storage.core.platform.MinioFileStorage; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.Locale; import java.util.Map; import java.util.UUID; @Slf4j @Service @RequiredArgsConstructor public class DmsFileStorageService { static final String STORAGE_PLATFORM = "seaweedfs-1"; static final String SERVICE_ID = "jnpf-file"; private final FileStorageService fileStorageService; private final DmsResourceApi dmsResourceApi; private final ConfigValueUtil configValueUtil; @jnpf.audit.sdk.annotation.AuditLog(action = "DMS_WORKFLOW_CONTENT_STORE", bizCodeExpr = "''") public DmsFileUploadRegisterForm storeWorkflowContent(UUID documentId, UUID fileVersionId, MultipartFile file) { UUID fileObjectId = UuidV7Generator.generate(); String originalFilename = safeFilename(file.getOriginalFilename()); String extension = UpUtil.getFileType(file); if (file.isEmpty() || !OptimizeUtil.fileType(configValueUtil.getAllowUploadFileType(), extension)) { throw new DataException("批准文件为空或类型不允许"); } FileInfo stored = fileStorageService.of(file).setPlatform(STORAGE_PLATFORM) .setPath(objectPath(documentId)) // Repeated delivery must never overwrite bytes already registered for this version. .setSaveFilename(fileObjectId + (StringUtil.isEmpty(extension) ? "" : "." + extension)) .setOriginalFilename(originalFilename).setHashCalculatorSha256().upload(); if (stored == null || stored.getHashInfo() == null || StringUtil.isEmpty(stored.getHashInfo().getSha256())) { deleteQuietly(stored); throw new DataException("SeaweedFS 上传失败或未生成 SHA-256"); } MinioFileStorage storage = fileStorageService.getFileStorage(STORAGE_PLATFORM); DmsFileUploadRegisterForm result = new DmsFileUploadRegisterForm(); result.setDocumentId(documentId); result.setFileVersionId(fileVersionId); result.setFileObjectId(fileObjectId); result.setFileName(originalFilename); result.setDocumentType(StringUtil.isEmpty(extension) ? "FILE" : extension.toUpperCase(Locale.ROOT)); result.setMimeType(trimToNull(stored.getContentType())); result.setSizeBytes(stored.getSize()); result.setChecksumSha256(stored.getHashInfo().getSha256()); result.setStoragePlatform(stored.getPlatform()); result.setBucketName(storage.getBucketName()); result.setBasePath(trimToEmpty(stored.getBasePath())); result.setObjectPath(stored.getPath()); result.setObjectName(stored.getFilename()); return result; } public DmsFileUploadVO upload(UUID folderId, String sourceId, String documentNo, String documentName, String documentType, String responsibleDeptId, String versionLabel, String preparedBy, LocalDate preparedOn, Map auditSignParameters, String auditBizSign, MultipartFile file) { validate(sourceId, documentNo, documentName, documentType, responsibleDeptId, versionLabel, preparedBy, preparedOn, file); String originalFilename = safeFilename(file.getOriginalFilename()); String extension = UpUtil.getFileType(file); if (!OptimizeUtil.fileType(configValueUtil.getAllowUploadFileType(), extension)) { throw new DataException(MsgCode.FA017.get()); } UUID documentId = UuidV7Generator.generate(); UUID fileVersionId = UuidV7Generator.generate(); UUID fileObjectId = UuidV7Generator.generate(); String objectPath = objectPath(documentId); String objectName = fileVersionId + (StringUtil.isEmpty(extension) ? "" : "." + extension); FileInfo stored = fileStorageService.of(file) .setPlatform(STORAGE_PLATFORM) .setPath(objectPath) .setSaveFilename(objectName) .setOriginalFilename(originalFilename) .setHashCalculatorSha256() .upload(); if (stored == null || stored.getHashInfo() == null || StringUtil.isEmpty(stored.getHashInfo().getSha256())) { deleteQuietly(stored); throw new DataException("SeaweedFS 上传失败或未生成 SHA-256"); } DmsFileUploadRegisterForm form = registration(folderId, sourceId, documentNo, documentName, documentType, responsibleDeptId, versionLabel, preparedBy, preparedOn, originalFilename, documentId, fileVersionId, fileObjectId, auditSignParameters, stored); try { ActionResult result = dmsResourceApi.registerUpload(SERVICE_ID, auditBizSign, form); requireRegistrationResult(result, stored); if (!sameStoredObject(result.getData(), stored)) { deleteQuietly(stored); } return result.getData(); } catch (RuntimeException exception) { logRegistrationFailure(stored, sourceId, exception); throw exception; } } public DmsFileUploadVO revise(UUID documentId, UUID baseFileVersionId, String sourceId, String versionLabel, String preparedBy, LocalDate preparedOn, String revisionReason, Map auditSignParameters, String auditBizSign, MultipartFile file) { validateRevision(documentId, baseFileVersionId, sourceId, versionLabel, preparedBy, preparedOn, revisionReason, file); String originalFilename = safeFilename(file.getOriginalFilename()); String extension = UpUtil.getFileType(file); if (!OptimizeUtil.fileType(configValueUtil.getAllowUploadFileType(), extension)) { throw new DataException(MsgCode.FA017.get()); } UUID fileVersionId = UuidV7Generator.generate(); UUID fileObjectId = UuidV7Generator.generate(); String objectPath = objectPath(documentId); String objectName = fileVersionId + (StringUtil.isEmpty(extension) ? "" : "." + extension); FileInfo stored = fileStorageService.of(file) .setPlatform(STORAGE_PLATFORM) .setPath(objectPath) .setSaveFilename(objectName) .setOriginalFilename(originalFilename) .setHashCalculatorSha256() .upload(); if (stored == null || stored.getHashInfo() == null || StringUtil.isEmpty(stored.getHashInfo().getSha256())) { deleteQuietly(stored); throw new DataException("SeaweedFS 上传失败或未生成 SHA-256"); } DmsFileRevisionRegisterForm form = revisionRegistration(documentId, baseFileVersionId, fileVersionId, fileObjectId, sourceId, versionLabel, preparedBy, preparedOn, revisionReason, originalFilename, auditSignParameters, stored); try { ActionResult result = dmsResourceApi.registerRevision(SERVICE_ID, auditBizSign, form); requireRegistrationResult(result, stored); if (!sameStoredObject(result.getData(), stored)) { deleteQuietly(stored); } return result.getData(); } catch (RuntimeException exception) { logRegistrationFailure(stored, sourceId, exception); throw exception; } } private void requireRegistrationResult(ActionResult result, FileInfo stored) { if (result == null || result.getCode() == null) { throw new DataException("DMS登记结果未知,保留上传对象,请使用原请求重试"); } if (!ActionResultCode.SUCCESS.getCode().equals(result.getCode())) { // 只对明确的请求拒绝补偿;服务端错误、超时和缺失数据不能证明事务未提交。 if (result.getCode() >= 400 && result.getCode() < 500) { deleteQuietly(stored); } throw new DataException(result.getMsg()); } if (result.getData() == null) { throw new DataException("DMS登记响应缺少数据,保留上传对象,请使用原请求重试"); } } private void logRegistrationFailure(FileInfo stored, String sourceId, RuntimeException exception) { log.error("DMS登记未正常返回,需要核对登记与对象状态: sourceId={}, platform={}, path={}, filename={}", sourceId, stored.getPlatform(), stored.getPath(), stored.getFilename(), exception); } private DmsFileUploadRegisterForm registration(UUID folderId, String sourceId, String documentNo, String documentName, String documentType, String responsibleDeptId, String versionLabel, String preparedBy, LocalDate preparedOn, String originalFilename, UUID documentId, UUID fileVersionId, UUID fileObjectId, Map auditSignParameters, FileInfo stored) { MinioFileStorage storage = fileStorageService.getFileStorage(STORAGE_PLATFORM); HashInfo hashInfo = stored.getHashInfo(); DmsFileUploadRegisterForm form = new DmsFileUploadRegisterForm(); form.setDocumentId(documentId); form.setFileVersionId(fileVersionId); form.setFileObjectId(fileObjectId); form.setFolderId(folderId); form.setDocumentNo(documentNo.trim()); form.setDocumentName(documentName.trim()); form.setDocumentType(documentType.trim()); form.setResponsibleDeptId(responsibleDeptId.trim()); form.setVersionLabel(versionLabel.trim()); form.setFileName(originalFilename); form.setMimeType(trimToNull(stored.getContentType())); form.setSizeBytes(stored.getSize()); form.setChecksumSha256(hashInfo.getSha256()); form.setPreparedBy(preparedBy.trim()); form.setPreparedOn(preparedOn); form.setStoragePlatform(stored.getPlatform()); form.setBucketName(storage.getBucketName()); form.setBasePath(trimToEmpty(stored.getBasePath())); form.setObjectPath(stored.getPath()); form.setObjectName(stored.getFilename()); form.setSourceId(sourceId.trim()); form.setAuditSignParameters(auditSignParameters); return form; } private DmsFileRevisionRegisterForm revisionRegistration(UUID documentId, UUID baseFileVersionId, UUID fileVersionId, UUID fileObjectId, String sourceId, String versionLabel, String preparedBy, LocalDate preparedOn, String revisionReason, String originalFilename, Map auditSignParameters, FileInfo stored) { MinioFileStorage storage = fileStorageService.getFileStorage(STORAGE_PLATFORM); HashInfo hashInfo = stored.getHashInfo(); DmsFileRevisionRegisterForm form = new DmsFileRevisionRegisterForm(); form.setDocumentId(documentId); form.setBaseFileVersionId(baseFileVersionId); form.setFileVersionId(fileVersionId); form.setFileObjectId(fileObjectId); form.setVersionLabel(versionLabel.trim()); form.setFileName(originalFilename); form.setMimeType(trimToNull(stored.getContentType())); form.setSizeBytes(stored.getSize()); form.setChecksumSha256(hashInfo.getSha256()); form.setPreparedBy(preparedBy.trim()); form.setPreparedOn(preparedOn); form.setRevisionReason(revisionReason.trim()); form.setStoragePlatform(stored.getPlatform()); form.setBucketName(storage.getBucketName()); form.setBasePath(trimToEmpty(stored.getBasePath())); form.setObjectPath(stored.getPath()); form.setObjectName(stored.getFilename()); form.setSourceId(sourceId.trim()); form.setAuditSignParameters(auditSignParameters); return form; } private boolean sameStoredObject(DmsFileUploadVO registered, FileInfo stored) { return Objects.equals(registered.getStoragePlatform(), stored.getPlatform()) && Objects.equals(registered.getBasePath(), trimToEmpty(stored.getBasePath())) && Objects.equals(registered.getObjectPath(), stored.getPath()) && Objects.equals(registered.getObjectName(), stored.getFilename()); } private String objectPath(UUID documentId) { return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/")) + documentId + "/"; } private String safeFilename(String value) { if (value == null || value.trim().isEmpty()) { throw new DataException("文件名不能为空"); } String normalized = value.replace('\\', '/'); String filename = Paths.get(normalized).getFileName().toString().trim(); if (filename.isEmpty() || filename.length() > 300) { throw new DataException("文件名长度必须在 1 到 300 之间"); } return filename; } private void validate(String sourceId, String documentNo, String documentName, String documentType, String responsibleDeptId, String versionLabel, String preparedBy, LocalDate preparedOn, MultipartFile file) { if (file == null || file.isEmpty()) { throw new DataException("上传文件不能为空"); } requireText(sourceId, "sourceId", 160); requireText(documentNo, "文件编号", 200); requireText(documentName, "文件名称", 300); requireText(documentType, "文件类型", 100); requireText(responsibleDeptId, "归属部门", 100); requireText(versionLabel, "版本号", 50); requireText(preparedBy, "制定人", 100); if (preparedOn == null) { throw new DataException("制定日期不能为空"); } fileStorageService.getFileStorageVerify(STORAGE_PLATFORM); } private void validateRevision(UUID documentId, UUID baseFileVersionId, String sourceId, String versionLabel, String preparedBy, LocalDate preparedOn, String revisionReason, MultipartFile file) { if (documentId == null || baseFileVersionId == null) { throw new DataException("文件及当前版本不能为空"); } if (file == null || file.isEmpty()) { throw new DataException("修订文件不能为空"); } requireText(sourceId, "sourceId", 120); requireText(versionLabel, "新版本号", 50); requireText(preparedBy, "制定人", 100); requireText(revisionReason, "修订原因", 1000); if (preparedOn == null) { throw new DataException("制定日期不能为空"); } fileStorageService.getFileStorageVerify(STORAGE_PLATFORM); } private void requireText(String value, String label, int maxLength) { if (value == null || value.trim().isEmpty() || value.trim().length() > maxLength) { throw new DataException(label + "长度必须在 1 到 " + maxLength + " 之间"); } } private String trimToEmpty(String value) { return value == null ? "" : value.trim(); } private String trimToNull(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private void deleteQuietly(FileInfo stored) { if (stored == null) { return; } try { if (!fileStorageService.delete(stored)) { log.error("DMS 上传补偿删除失败: platform={}, path={}, filename={}", stored.getPlatform(), stored.getPath(), stored.getFilename()); } } catch (Exception exception) { log.error("DMS 上传补偿删除异常: platform={}, path={}, filename={}", stored.getPlatform(), stored.getPath(), stored.getFilename(), exception); } } }