package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.storage.entity.DmsFileObjectEntity; import jnpf.dmsEntity.storage.form.DmsFileUploadRegisterForm; import jnpf.dmsEntity.storage.vo.DmsFileUploadVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsIntegration.audit.DmsAuditRequestSignResolver; import jnpf.dmsIntegration.audit.DmsFileAuditEvent; import jnpf.dmsIntegration.audit.DmsFileAuditSnapshot; import jnpf.dmsIntegration.audit.DmsFileVersionAuditSnapshot; import jnpf.dmsIntegration.audit.DmsVersionStateAuditRow; import jnpf.dmsMapper.DmsDocumentMapper; import jnpf.dmsMapper.DmsFolderMapper; import jnpf.dmsMapper.DmsFileObjectMapper; import jnpf.dmsMapper.DmsFileVersionMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsMapper.DmsVersionStateMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsService.search.DmsSearchIndexJobService; import jnpf.dmsService.pdf.DmsPdfGenerationTaskService; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Locale; import java.util.Objects; @Service @RequiredArgsConstructor public class DmsFileUploadService { private final DmsVersionPropertiesService versionProperties; private static final String SOURCE_PREFIX = "upload:"; private final DmsCurrentIdentity currentIdentity; private final DmsPermissionDecisionService decisionService; private final DmsDocumentMapper documentMapper; private final DmsFolderMapper folderMapper; private final DmsFileVersionMapper fileVersionMapper; private final DmsPermissionResourceMapper resourceMapper; private final DmsFileObjectMapper fileObjectMapper; private final DmsVersionStateMapper versionStateMapper; private final DmsAuditAdapter auditAdapter; private final DmsFileOperationLogService operationLogService; private final DmsSearchIndexJobService searchIndexJobService; private final DmsPdfGenerationTaskService pdfGenerationTaskService; private final DmsBusinessLifecycleGuard businessOccupancy; @Transactional public DmsFileUploadVO register(DmsFileUploadRegisterForm form, String serviceId) { return register(form, serviceId, false); } @Transactional public DmsFileUploadVO registerWorkflowPending(DmsFileUploadRegisterForm form, String serviceId, String flowId, String taskId) { return register(form, serviceId, true, flowId, taskId); } private DmsFileUploadVO register(DmsFileUploadRegisterForm form, String serviceId, boolean workflowPending) { return register(form, serviceId, workflowPending, null, null); } /** Java-only entry: caller has locked and verified the complete approved batch. */ @Transactional public DmsFileUploadVO registerApproved(DmsFileUploadRegisterForm form, String serviceId, String flowId, String taskId, UserInfo actor) { if (!"jnpf-file".equals(serviceId) || actor == null || actor.getUserId() == null || actor.getTenantId() == null || flowId == null || taskId == null) throw new jnpf.exception.DataException("批准登记缺少可信来源"); return register(form, serviceId, true, flowId, taskId, actor); } private DmsFileUploadVO register(DmsFileUploadRegisterForm form, String serviceId, boolean workflowPending, String flowId, String taskId) { return register(form, serviceId, workflowPending, flowId, taskId, null); } private DmsFileUploadVO register(DmsFileUploadRegisterForm form, String serviceId, boolean workflowPending, String flowId, String taskId, UserInfo approvedActor) { UserInfo user = approvedActor == null ? currentIdentity.requireUser() : approvedActor; String tenantId = currentIdentity.tenantId(user); String sourceId = form.getSourceId().trim(); String shifouShoukong = approvedActor == null ? "yes" : requireApprovedControlFlag(form.getShifouShoukong()); if (approvedActor == null) decisionService.requireAllowed(DmsResourceType.FOLDER, form.getFolderId(), DmsPermissionAction.CREATE); if (approvedActor == null) businessOccupancy.guardStandaloneRegistration(tenantId, form.getDocumentNo()==null ? null : form.getDocumentNo().trim()); DmsFileUploadVO existing = fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); if (existing != null) { versionProperties.verifyRetry(tenantId, existing.getFileVersionId(), form.getExtendedProperties()); validateRetry(existing, form, shifouShoukong); pdfGenerationTaskService.enqueueEligible(tenantId, form.getFileVersionId(), form.getFileName(), form.getChecksumSha256()); if (!workflowPending) { searchIndexJobService.enqueueContentUpsert(tenantId, existing.getDocumentId(), existing.getFileVersionId(), existing.getChecksumSha256()); } operationLogService.recordUpload(user, tenantId, existing.getDocumentId(), existing.getFileVersionId(), existing.getFileName(), serviceId, sourceId); return existing; } try { DmsDocumentEntity document = insertDocument(form, tenantId, user.getUserId(), serviceId, sourceId); DmsFileVersionEntity version = insertFileVersion(form, tenantId, user.getUserId(), serviceId, sourceId, shifouShoukong); if (workflowPending) { versionStateMapper.insertWorkflowPending(tenantId, form.getDocumentId(), form.getFileVersionId(), sourceId, flowId, taskId, "CREATE", user.getUserId()); } else { insertDirectEffectiveState(form, tenantId, user.getUserId()); } insertFileObject(form, tenantId, user.getUserId(), serviceId, sourceId); pdfGenerationTaskService.enqueueEligible(tenantId, form.getFileVersionId(), form.getFileName(), form.getChecksumSha256()); if (!workflowPending) { searchIndexJobService.enqueueContentUpsert(tenantId, form.getDocumentId(), form.getFileVersionId(), form.getChecksumSha256()); } DmsVersionStateAuditRow state = versionStateMapper.selectAuditState( tenantId, form.getFileVersionId()); auditAdapter.fileChange(DmsFileAuditEvent.builder() .documentId(form.getDocumentId()) .actionCode("CREATE") .actionLabel(workflowPending ? "暂存待审批 DMS 文件" : "上传 DMS 文件") .after(snapshot(document, version, state, folderMapper.selectPath(tenantId, form.getFolderId()), workflowPending ? "PENDING_APPROVAL" : "EFFECTIVE")) .sourceServiceId(serviceId) .sourceId(sourceId) .relatedSigns(DmsAuditRequestSignResolver.resolve(form.getAuditSignParameters())) .build()); } catch (DuplicateKeyException exception) { throw conflict("上传登记的资源 ID、source_id、版本号或对象键已被占用", exception); } operationLogService.recordUpload(user, tenantId, form.getDocumentId(), form.getFileVersionId(), form.getFileName(), serviceId, sourceId); return fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); } private DmsDocumentEntity insertDocument(DmsFileUploadRegisterForm form, String tenantId, String userId, String serviceId, String sourceId) { DmsDocumentEntity document = new DmsDocumentEntity(); document.setId(form.getDocumentId()); document.setTenantId(tenantId); document.setFolderId(form.getFolderId()); document.setDocumentNo(form.getDocumentNo().trim()); document.setName(form.getDocumentName().trim()); document.setDocumentType(form.getDocumentType().trim()); document.setResponsibleDeptId(form.getResponsibleDeptId().trim()); document.setCreatedBy(userId); documentMapper.insert(document); insertRegistry(tenantId, form.getDocumentId(), DmsResourceType.DOCUMENT, registrySource(sourceId, "document"), serviceId); return document; } private DmsFileVersionEntity insertFileVersion(DmsFileUploadRegisterForm form, String tenantId, String userId, String serviceId, String sourceId, String shifouShoukong) { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(form.getFileVersionId()); version.setTenantId(tenantId); version.setDocumentId(form.getDocumentId()); version.setVersionNo(1L); version.setVersionLabel(form.getVersionLabel().trim()); version.setFileName(form.getFileName().trim()); version.setMimeType(trimToNull(form.getMimeType())); version.setSizeBytes(form.getSizeBytes()); version.setChecksumSha256(form.getChecksumSha256().toLowerCase(Locale.ROOT)); version.setPreparedBy(form.getPreparedBy().trim()); version.setPreparedOn(form.getPreparedOn()); version.setShifouShoukong(shifouShoukong); version.setCreatedBy(userId); versionProperties.insertVersion(version, form.getExtendedProperties()); insertRegistry(tenantId, form.getFileVersionId(), DmsResourceType.FILE_VERSION, registrySource(sourceId, "version:1"), serviceId); return version; } private void insertFileObject(DmsFileUploadRegisterForm form, String tenantId, String userId, String serviceId, String sourceId) { DmsFileObjectEntity object = new DmsFileObjectEntity(); object.setId(form.getFileObjectId()); object.setTenantId(tenantId); object.setFileVersionId(form.getFileVersionId()); object.setStoragePlatform(form.getStoragePlatform().trim()); object.setBucketName(form.getBucketName().trim()); object.setBasePath(trimToEmpty(form.getBasePath())); object.setObjectPath(form.getObjectPath().trim()); object.setObjectName(form.getObjectName().trim()); object.setSourceServiceId(serviceId); object.setSourceId(sourceId); object.setCreatedBy(userId); fileObjectMapper.insert(object); } private void insertDirectEffectiveState(DmsFileUploadRegisterForm form, String tenantId, String userId) { versionStateMapper.insertDirectEffective(tenantId, form.getDocumentId(), form.getFileVersionId(), userId); } private DmsFileAuditSnapshot snapshot(DmsDocumentEntity document, DmsFileVersionEntity version, DmsVersionStateAuditRow state, String folderPath, String fallbackStatus) { return DmsFileAuditSnapshot.builder() .documentId(document.getId()) .documentNo(document.getDocumentNo()) .documentName(document.getName()) .documentType(document.getDocumentType()) .folderId(document.getFolderId()) .folderPath(folderPath) .responsibleDeptId(document.getResponsibleDeptId()) .currentVersion(DmsFileVersionAuditSnapshot.builder() .fileVersionId(version.getId()) .versionNo(version.getVersionNo()) .versionLabel(version.getVersionLabel()) .fileName(version.getFileName()) .mimeType(version.getMimeType()) .sizeBytes(version.getSizeBytes()) .checksumSha256(version.getChecksumSha256()) .preparedBy(version.getPreparedBy()) .preparedOn(version.getPreparedOn()) .lifecycleStatus(state == null ? fallbackStatus : state.getLifecycleStatus()) .effectiveFrom(state == null ? null : state.getEffectiveFrom()) .effectiveTo(state == null ? null : state.getEffectiveTo()) .statusReason(state == null ? ("PENDING_APPROVAL".equals(fallbackStatus) ? "工作流文件已暂存,等待审批" : "DMS 临时上传直接生效") : state.getStatusReason()) .build()) .build(); } private void insertRegistry(String tenantId, java.util.UUID id, DmsResourceType type, String sourceId, String serviceId) { DmsPermissionResourceEntity registry = new DmsPermissionResourceEntity(); registry.setId(id); registry.setTenantId(tenantId); registry.setResourceType(type.name()); registry.setSourceType("SYSTEM"); registry.setSourceId(sourceId); registry.setRegisteredBy(serviceId); resourceMapper.insert(registry); } private void validateRetry(DmsFileUploadVO existing, DmsFileUploadRegisterForm form, String shifouShoukong) { if (!Objects.equals(existing.getFolderId(), form.getFolderId()) || !Objects.equals(existing.getDocumentNo(), form.getDocumentNo().trim()) || !Objects.equals(existing.getDocumentName(), form.getDocumentName().trim()) || !Objects.equals(existing.getDocumentType(), form.getDocumentType().trim()) || !Objects.equals(existing.getResponsibleDeptId(), form.getResponsibleDeptId().trim()) || !Objects.equals(existing.getVersionLabel(), form.getVersionLabel().trim()) || !Objects.equals(existing.getFileName(), form.getFileName().trim()) || !Objects.equals(existing.getMimeType(), trimToNull(form.getMimeType())) || !Objects.equals(existing.getSizeBytes(), form.getSizeBytes()) || !equalsIgnoreCase(existing.getChecksumSha256(), form.getChecksumSha256()) || !Objects.equals(existing.getPreparedBy(), form.getPreparedBy().trim()) || !Objects.equals(existing.getPreparedOn(), form.getPreparedOn()) || !Objects.equals(existing.getShifouShoukong(), shifouShoukong)) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "source_id 对应的上传内容与本次请求不一致"); } } private String registrySource(String sourceId, String suffix) { return SOURCE_PREFIX + sourceId + ":" + suffix; } private String trimToNull(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private String trimToEmpty(String value) { return value == null ? "" : value.trim(); } private String requireApprovedControlFlag(String value) { if ("yes".equals(value) || "no".equals(value)) return value; throw new jnpf.exception.DataException("批准文件受控属性必须为 yes 或 no"); } private boolean equalsIgnoreCase(String left, String right) { return left == null ? right == null : right != null && left.equalsIgnoreCase(right); } private DmsPermissionException conflict(String message, Throwable cause) { DmsPermissionException exception = DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, message); exception.initCause(cause); return exception; } }