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.DmsFileRevisionRegisterForm; 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 DmsFileRevisionService { private final DmsVersionPropertiesService versionProperties; private static final String SOURCE_PREFIX = "revision:"; 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(DmsFileRevisionRegisterForm form, String serviceId) { return register(form, serviceId, false, null, null); } @Transactional public DmsFileUploadVO registerWorkflowPending(DmsFileRevisionRegisterForm form, String serviceId, String flowId, String taskId) { return register(form, serviceId, true, flowId, taskId); } /** Java-only entry: caller has locked and verified the complete approved batch. */ @Transactional public DmsFileUploadVO registerApproved(DmsFileRevisionRegisterForm 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(DmsFileRevisionRegisterForm form, String serviceId, boolean workflowPending, String flowId, String taskId) { return register(form, serviceId, workflowPending, flowId, taskId, null); } private DmsFileUploadVO register(DmsFileRevisionRegisterForm 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(); if (approvedActor == null) decisionService.requireAllowed(DmsResourceType.DOCUMENT, form.getDocumentId(), DmsPermissionAction.UPDATE); if (approvedActor == null) businessOccupancy.guardStandaloneActivation(tenantId,form.getBaseFileVersionId()); fileObjectMapper.lockSource(tenantId, serviceId, sourceId); DmsFileUploadVO existing = fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); if (existing != null) { versionProperties.verifyRetry(tenantId, existing.getFileVersionId(), form.getExtendedProperties()); validateRetry(existing, form, retryControlFlag(tenantId, form, approvedActor != null)); pdfGenerationTaskService.enqueueEligible(tenantId, form.getFileVersionId(), form.getFileName(), form.getChecksumSha256()); if (!workflowPending) { searchIndexJobService.enqueueContentUpsert(tenantId, existing.getDocumentId(), existing.getFileVersionId(), existing.getChecksumSha256()); } operationLogService.recordRevision(user, tenantId, existing.getDocumentId(), existing.getFileVersionId(), existing.getFileName(), serviceId, sourceId); return existing; } DmsDocumentEntity document = documentMapper.selectByTenantAndIdForUpdate(tenantId, form.getDocumentId()); if (document == null) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "待修订文件不存在"); } DmsFileVersionEntity current = fileVersionMapper.selectCurrentEffective(tenantId, form.getDocumentId()); if (current == null || !Objects.equals(current.getId(), form.getBaseFileVersionId())) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "待修订版本已不是现行有效版本,请刷新后重试"); } DmsVersionStateAuditRow currentState = versionStateMapper.selectAuditState( tenantId, current.getId()); String folderPath = folderMapper.selectPath(tenantId, document.getFolderId()); DmsFileAuditSnapshot before = snapshot(document, current, currentState, folderPath); long nextVersionNo = fileVersionMapper.selectNextVersionNo(tenantId, form.getDocumentId()); try { DmsFileVersionEntity newVersion = insertFileVersion(form, tenantId, user.getUserId(), nextVersionNo, approvedActor == null ? inheritedControlFlag(current.getShifouShoukong()) : requireApprovedControlFlag(form.getShifouShoukong())); insertRegistry(form, tenantId, serviceId, sourceId); insertFileObject(form, tenantId, user.getUserId(), serviceId, sourceId); pdfGenerationTaskService.enqueueEligible(tenantId, form.getFileVersionId(), form.getFileName(), form.getChecksumSha256()); if (workflowPending) { versionStateMapper.insertWorkflowPending(tenantId, form.getDocumentId(), form.getFileVersionId(), SOURCE_PREFIX + form.getBaseFileVersionId() + ":" + sourceId, flowId, taskId, "REVISE", user.getUserId()); } else { int superseded = versionStateMapper.supersedeCurrent(tenantId, form.getDocumentId(), form.getBaseFileVersionId(), form.getVersionLabel().trim(), user.getUserId()); if (superseded != 1) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "现行有效版本已发生变化,请刷新后重试"); } versionStateMapper.insertRevisionEffective(tenantId, form.getDocumentId(), form.getBaseFileVersionId(), form.getFileVersionId(), sourceId, form.getRevisionReason().trim(), user.getUserId()); searchIndexJobService.enqueueContentUpsert(tenantId, form.getDocumentId(), form.getFileVersionId(), form.getChecksumSha256()); } DmsVersionStateAuditRow previousAfter = versionStateMapper.selectAuditState( tenantId, current.getId()); DmsVersionStateAuditRow newState = versionStateMapper.selectAuditState( tenantId, newVersion.getId()); auditAdapter.fileChange(DmsFileAuditEvent.builder() .documentId(form.getDocumentId()) .actionCode("UPDATE") .actionLabel(workflowPending ? "暂存待审批 DMS 修订" : "修订 DMS 文件") .before(before) .after(snapshot(document, newVersion, newState, folderPath)) .previousVersionAfter(versionSnapshot(current, previousAfter, workflowPending ? "EFFECTIVE" : "SUPERSEDED")) .reason(form.getRevisionReason().trim()) .sourceServiceId(serviceId) .sourceId(sourceId) .relatedSigns(DmsAuditRequestSignResolver.resolve(form.getAuditSignParameters())) .build()); } catch (DuplicateKeyException exception) { throw conflict("修订登记的资源 ID、source_id、版本号、版本标签或对象键已被占用", exception); } operationLogService.recordRevision(user, tenantId, form.getDocumentId(), form.getFileVersionId(), form.getFileName(), serviceId, sourceId); return fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); } private DmsFileVersionEntity insertFileVersion(DmsFileRevisionRegisterForm form, String tenantId, String userId, long versionNo, String shifouShoukong) { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(form.getFileVersionId()); version.setTenantId(tenantId); version.setDocumentId(form.getDocumentId()); version.setVersionNo(versionNo); 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()); return version; } private void insertRegistry(DmsFileRevisionRegisterForm form, String tenantId, String serviceId, String sourceId) { DmsPermissionResourceEntity registry = new DmsPermissionResourceEntity(); registry.setId(form.getFileVersionId()); registry.setTenantId(tenantId); registry.setResourceType(DmsResourceType.FILE_VERSION.name()); registry.setSourceType("SYSTEM"); registry.setSourceId(SOURCE_PREFIX + sourceId + ":version"); registry.setRegisteredBy(serviceId); resourceMapper.insert(registry); } private void insertFileObject(DmsFileRevisionRegisterForm 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 validateRetry(DmsFileUploadVO existing, DmsFileRevisionRegisterForm form, String shifouShoukong) { if (!Objects.equals(existing.getDocumentId(), form.getDocumentId()) || !Objects.equals(existing.getBaseFileVersionId(), form.getBaseFileVersionId()) || !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) || !Objects.equals(existing.getRevisionReason(), form.getRevisionReason().trim())) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "source_id 对应的修订内容与本次请求不一致"); } } 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 retryControlFlag(String tenantId, DmsFileRevisionRegisterForm form, boolean approved) { if (approved) return requireApprovedControlFlag(form.getShifouShoukong()); DmsFileVersionEntity base = fileVersionMapper.selectByTenantAndId(tenantId, form.getBaseFileVersionId()); if (base == null || !Objects.equals(base.getDocumentId(), form.getDocumentId())) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "修订基准版本不存在或不属于目标文件"); } return inheritedControlFlag(base.getShifouShoukong()); } private String inheritedControlFlag(String inherited) { return "no".equals(inherited) ? "no" : "yes"; } 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 DmsFileAuditSnapshot snapshot(DmsDocumentEntity document, DmsFileVersionEntity version, DmsVersionStateAuditRow state, String folderPath) { return DmsFileAuditSnapshot.builder() .documentId(document.getId()) .documentNo(document.getDocumentNo()) .documentName(document.getName()) .documentType(document.getDocumentType()) .folderId(document.getFolderId()) .folderPath(folderPath) .responsibleDeptId(document.getResponsibleDeptId()) .currentVersion(versionSnapshot(version, state, "EFFECTIVE")) .build(); } private DmsFileVersionAuditSnapshot versionSnapshot(DmsFileVersionEntity version, DmsVersionStateAuditRow state, String fallbackStatus) { return 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 ? null : state.getStatusReason()) .build(); } private DmsPermissionException conflict(String message, Throwable cause) { DmsPermissionException exception = DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, message); exception.initCause(cause); return exception; } }