package jnpf.dmsService; import jakarta.validation.Validator; import jnpf.base.UserInfo; import jnpf.dmsEntity.storage.approval.*; import jnpf.dmsEntity.storage.form.*; import jnpf.dmsEntity.storage.form.DmsApprovedRegistrationCallback.StoredFile; import jnpf.dmsEntity.storage.vo.DmsFileUploadVO; import jnpf.dmsMapper.DmsApprovedBatchMapper; import jnpf.exception.DataException; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.TaskEntity; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; @Service @RequiredArgsConstructor public class DmsApprovedFileAcceptanceService { private static final String FILE_SERVICE = "jnpf-file"; private final DmsApprovedAuditSource source; private final jnpf.dmsService.center.DmsCenterDirectoryService directories; private final jnpf.dmsService.center.DmsCenterRegistrationService center; private final jnpf.dmsService.center.DmsCenterQueryService centerQueries; private final DmsApprovedBatchMapper receipts; private final WorkFlowApi workflow; private final Validator validator; private final DmsQianpiyeRenderSnapshotService qianpiyeSnapshots; private final DmsApprovedVersionPropertiesService approvedProperties; private final jnpf.dmsMapper.DmsVersionPropertiesMapper versionProperties; private final DmsBusinessLifecycleGuard businessOccupancy; /** Persist approval even when preparing the native source subsequently fails. */ @Transactional(rollbackFor = Exception.class) public DmsApprovedBatch enqueue(String tenantId, String flowId, String taskId, String type, String actorId, Date approvedAt) { if (!"xinzeng_shenhe".equals(type) && !"xiuding_shenhe".equals(type) && !"wailai_wenjian_xinzeng_shenhe".equals(type)) throw new DataException("审核类型无效"); requireApprovedTask(flowId, taskId, approvedAt); DmsApprovedBatch batch = new DmsApprovedBatch(); batch.setId(UUID.randomUUID()); batch.setTenantId(tenantId); batch.setFlowId(flowId); batch.setTaskId(taskId); batch.setAuditType(type); batch.setActorId(actorId); batch.setApprovedAt(approvedAt); receipts.enqueue(batch); DmsApprovedBatch saved = receipts.find(tenantId, taskId, type); if (saved == null || !flowId.equals(saved.getFlowId())) throw new DataException("批准登记批次归属不一致"); return saved; } /** Reconstruct the full DMS push under a short transaction, preserving reserved IDs. */ @Transactional(rollbackFor = Exception.class) public DmsApprovedBatch stage(String tenantId, String flowId, String taskId, String type, String actorId, Date approvedAt) { if (!"xinzeng_shenhe".equals(type) && !"xiuding_shenhe".equals(type) && !"wailai_wenjian_xinzeng_shenhe".equals(type)) throw new DataException("审核类型无效"); requireApprovedTask(flowId, taskId, approvedAt); DmsApprovedBatch old = receipts.find(tenantId, taskId, type); if (old != null) old = receipts.lock(tenantId, old.getId()); if (old != null && !flowId.equals(old.getFlowId())) throw new DataException("登记批次流程归属不匹配"); if (old != null && !"PREPARING".equals(old.getStatus()) && !"REGISTERING".equals(old.getStatus())) { old.setItems(receipts.items(tenantId, old.getId())); return old; } DmsApprovedAuditSource.Source nativeSource = source.load(tenantId, flowId, taskId, type, actorId, approvedAt); requireUnsettled(nativeSource); DmsApprovedBatch batch = nativeSource.batch(); businessOccupancy.requireAuditSource(batch); qianpiyeSnapshots.attach(batch, old == null ? null : old.getApprovalRenderSnapshot(), nativeSource.preparedBy(), nativeSource.preparedOn()); // Source locks also serialize callers using the previous direct stage contract. if (old == null) old = receipts.find(tenantId, taskId, type); if (old != null && "REGISTERING".equals(old.getStatus())) { if (!Objects.equals(old.getSourceDigest(), batch.getSourceDigest())) throw new DataException("批准来源已改变,停止自动推送"); Map saved = new HashMap<>(); for (DmsApprovedItem item : receipts.items(tenantId, old.getId())) saved.put(item.getSourceKey(),item); if (saved.size() != batch.getItems().size()) throw new DataException("登记明细不完整"); for (DmsApprovedItem item : batch.getItems()) { DmsApprovedItem existing = saved.get(item.getSourceKey()); if (existing == null || !Objects.equals(existing.getChangeType(), item.getChangeType()) || !Objects.equals(existing.getBaseFileVersionId(),item.getBaseFileVersionId())) throw new DataException("登记明细与批准来源不一致"); item.setBatchId(old.getId()); item.setDocumentId(existing.getDocumentId()); item.setFileVersionId(existing.getFileVersionId()); item.setChecksumSha256(existing.getChecksumSha256()); item.setStorageReceipt(existing.getStorageReceipt()); if (item.requiresContentRegistration()) { requireControlSnapshot(existing, item); item.setShifouShoukong(existing.getShifouShoukong()); } // Repair legacy v4 IDs or document/version collisions caused by cached UUID allocation. // Repair only an unregistered reservation; never change a registered document/version. if (item.requiresContentRegistration() && ((item.getFileVersionId().version() != 7 || item.getFileVersionId().equals(item.getDocumentId())) || ("CREATE".equals(item.getChangeType()) && item.getDocumentId().version() != 7))) { centerQueries.requireUnusedReservation(tenantId, existing.getDocumentId(), existing.getFileVersionId(), "CREATE".equals(item.getChangeType())); if ("CREATE".equals(item.getChangeType()) && item.getDocumentId().version() != 7) item.setDocumentId(UUID.fromString(receipts.nextUuidV7())); if (item.getFileVersionId().version() != 7 || item.getFileVersionId().equals(item.getDocumentId())) item.setFileVersionId(UUID.fromString(receipts.nextUuidV7())); if (receipts.repairReservation(item, existing.getDocumentId(), existing.getFileVersionId()) != 1) throw new DataException("旧登记ID已被使用,不能自动替换预留ID"); } } if (!Objects.equals(old.getScheduledAt(), batch.getScheduledAt()) || !Objects.equals(old.getEffectiveTo(), batch.getEffectiveTo())) { old.setScheduledAt(batch.getScheduledAt()); old.setEffectiveTo(batch.getEffectiveTo()); if (receipts.refreshSchedule(old) != 1) throw new DataException("更新正式生效日期失败"); } old.setItems(batch.getItems()); return old; } batch.setId(old == null ? UUID.randomUUID() : old.getId()); batch.setStatus("REGISTERING"); if (old != null) batch.setLeaseToken(old.getLeaseToken()); for (DmsApprovedItem item : batch.getItems()) { item.setBatchId(batch.getId()); if (item.requiresContentRegistration()) { if (item.getDocumentId() == null) item.setDocumentId(UUID.fromString(receipts.nextUuidV7())); item.setFileVersionId(UUID.fromString(receipts.nextUuidV7())); } } if (old == null) receipts.insertBatch(batch); else if (receipts.prepared(batch) != 1) throw new DataException("更新登记准备进度失败"); for (DmsApprovedItem item : batch.getItems()) receipts.insertItem(item); return batch; } /** Existing file registration callback: register + approve atomically, then advance progress. */ @Transactional(rollbackFor = Exception.class) public DmsApprovedBatch accept(DmsApprovedRegistrationCallback form, String serviceId) { if (!FILE_SERVICE.equals(serviceId)) throw new DataException("仅允许文件服务返回登记结果"); return acceptVerifiedContent(form); } /** In-module orchestration has already obtained and persisted the physical API receipt. */ @Transactional(rollbackFor = Exception.class) public DmsApprovedBatch acceptStoredContent(DmsApprovedRegistrationCallback form) { return acceptVerifiedContent(form); } private DmsApprovedBatch acceptVerifiedContent(DmsApprovedRegistrationCallback form) { if (form == null || form.getBatchId() == null || form.getSourceDigest() == null || form.getFiles() == null) throw new DataException("文件登记回调缺少批次、摘要或存储集合"); String tenantId = form.getTenantId(); if (tenantId == null || tenantId.isBlank() || form.getAttemptToken() == null) throw new DataException("文件登记回调缺少租户或执行令牌"); DmsApprovedBatch batch = receipts.lock(tenantId, form.getBatchId()); if (batch == null || !Objects.equals(batch.getSourceDigest(), form.getSourceDigest())) throw new DataException("未找到匹配的DMS登记进度,不能接收文件结果"); if (!Objects.equals(batch.getLeaseToken(), form.getAttemptToken())) throw new DataException("文件登记执行令牌已失效,等待当前执行者恢复"); requireApprovedTask(batch.getFlowId(), batch.getTaskId(), batch.getApprovedAt()); if (!"REGISTERING".equals(batch.getStatus())) { batch.setItems(receipts.items(tenantId, batch.getId())); return batch; } DmsApprovedAuditSource.Source nativeSource = source.load(tenantId, batch.getFlowId(), batch.getTaskId(), batch.getAuditType(), batch.getActorId(), batch.getApprovedAt()); qianpiyeSnapshots.attach(nativeSource.batch(), batch.getApprovalRenderSnapshot(), nativeSource.preparedBy(), nativeSource.preparedOn()); if (!Objects.equals(batch.getSourceDigest(), nativeSource.batch().getSourceDigest())) throw new DataException("文件推送期间批准来源发生变化,不能登记确认"); businessOccupancy.requireAuditSource(nativeSource.batch()); List expected = receipts.items(tenantId, batch.getId()); Map storedFiles = validateFiles(form, nativeSource, expected); com.fasterxml.jackson.databind.JsonNode properties = null; for (DmsApprovedItem item : expected) { if (!item.requiresContentRegistration()) continue; DmsFileUploadRegisterForm stored = storedFiles.get(item.getSourceKey()); // Only the trusted registration adapter resolves persisted form personnel. Never trust // personnel values supplied by a physical-storage callback or rewrite an old version. var existing = versionProperties.find(tenantId, stored.getFileVersionId()); if (existing != null) { stored.setExtendedProperties(existing.getExtendedProperties()); } else { if (properties == null) properties = approvedProperties.create(nativeSource.preparedBy(), nativeSource.reviewerIds()); stored.setExtendedProperties(properties.deepCopy()); } DmsFileUploadVO result = center.register(stored, item.getBaseFileVersionId(), "文件修订审核通过:" + batch.getAuditId(), batch.getFlowId(), batch.getTaskId(), approvedActor(batch), batch.getApprovedAt(), batch.getScheduledAt(), batch.getEffectiveTo()); if (receipts.storeChecksum(tenantId, batch.getId(), item.getSourceKey(), result.getChecksumSha256()) != 1) throw new DataException("更新文件登记明细失败"); item.setChecksumSha256(result.getChecksumSha256()); } DmsApprovedItem main = expected.stream().filter(i -> i.getSourceKey().endsWith(":zhuwenjian") || i.getSourceKey().endsWith(":wailaiWenjian")) .findFirst().orElseThrow(() -> new DataException("批准来源缺少主文件")); directories.bind(tenantId, main.getDocumentId(), expected.stream() .filter(i -> i != main).map(DmsApprovedItem::getDocumentId).toList()); if (receipts.registered(tenantId, batch.getId()) != 1) throw new DataException("更新文件登记进度失败"); batch.setStatus("WAITING"); batch.setItems(expected); return batch; } private Map validateFiles(DmsApprovedRegistrationCallback form, DmsApprovedAuditSource.Source nativeSource, List expected) { Map targets = new HashMap<>(); if (expected != null) for (DmsApprovedItem item : expected) targets.put(item.getSourceKey(), item); if (targets.size() != nativeSource.batch().getItems().size()) throw new DataException("DMS登记明细不完整"); Map supplied = new HashMap<>(); for (StoredFile file : form.getFiles()) { if (file == null || file.getSourceKey() == null || file.getContent() == null || supplied.put(file.getSourceKey(), file) != null) throw new DataException("文件登记回调包含空项或重复来源"); } Map result = new HashMap<>(); Set documentIds = new HashSet<>(), versionIds = new HashSet<>(), objectIds = new HashSet<>(); for (DmsApprovedItem item : nativeSource.batch().getItems()) { DmsApprovedItem target = targets.get(item.getSourceKey()); if (target == null || !Objects.equals(target.getChangeType(), item.getChangeType()) || !Objects.equals(target.getBaseFileVersionId(), item.getBaseFileVersionId())) throw new DataException("DMS登记明细与批准来源不一致"); if (!item.requiresContentRegistration()) continue; StoredFile file = supplied.remove(item.getSourceKey()); if (file == null || !Objects.equals(item.getFileJson(), file.getFileJson())) throw new DataException("登记文件集合不完整或与批准文件引用不一致"); DmsFileUploadRegisterForm stored = file.getContent(); if (!Objects.equals(target.getDocumentId(), stored.getDocumentId()) || !Objects.equals(target.getFileVersionId(), stored.getFileVersionId())) throw new DataException("文件存储回执与DMS登记明细ID不一致"); if (!documentIds.add(stored.getDocumentId()) || !versionIds.add(stored.getFileVersionId()) || !objectIds.add(stored.getFileObjectId()) || Objects.equals(item.getBaseFileVersionId(), stored.getFileVersionId())) throw new DataException("存储回执包含重复文档、版本或对象ID"); requireControlSnapshot(target, item); // These fields always come from the freshly locked native source, never the callback. stored.setSourceId(item.getSourceKey()); stored.setFolderId(UUID.fromString(item.getFolderId())); stored.setDocumentNo(item.getDocumentNo()); stored.setDocumentName(item.getDocumentName()); stored.setResponsibleDeptId(item.getDepartmentId()); stored.setVersionLabel(item.getVersionLabel()); stored.setPreparedBy(nativeSource.preparedBy()); stored.setPreparedOn(nativeSource.preparedOn().toInstant().atZone(DmsApprovedFileDates.ZONE).toLocalDate()); stored.setShifouShoukong(target.getShifouShoukong()); Object registration = stored; if ("REVISE".equals(item.getChangeType())) { DmsFileRevisionRegisterForm revision = new DmsFileRevisionRegisterForm(); BeanUtils.copyProperties(stored, revision); revision.setBaseFileVersionId(item.getBaseFileVersionId()); revision.setRevisionReason("文件修订审核通过:" + nativeSource.batch().getAuditId()); registration = revision; } if (stored.getSizeBytes() == null || stored.getSizeBytes() <= 0 || !validator.validate(registration).isEmpty()) throw new DataException("文件存储回执或批准属性不完整"); result.put(item.getSourceKey(), stored); } if (!supplied.isEmpty()) throw new DataException("登记集合包含批准范围以外的文件"); return result; } private void requireControlSnapshot(DmsApprovedItem saved, DmsApprovedItem sourceItem) { String value = saved.getShifouShoukong(); if (!"yes".equals(value) && !"no".equals(value)) throw new DataException("推送任务缺少是否受控快照,请先同步历史任务"); if (!Objects.equals(value, sourceItem.getShifouShoukong())) throw new DataException("批准来源是否受控与推送任务快照不一致"); } private void requireApprovedTask(String flowId, String taskId, Date approvedAt) { TaskEntity task = workflow.getInfoSubmit(taskId); if (task == null || !taskId.equals(task.getId()) || (!flowId.equals(task.getFlowId()) && !flowId.equals(task.getTemplateId())) || !Integer.valueOf(2).equals(task.getStatus()) || approvedAt == null || !approvedAt.equals(task.getEndTime())) throw new DataException("流程任务尚未真实审核通过或批准时间不一致"); } private UserInfo approvedActor(DmsApprovedBatch batch) { UserInfo user = new UserInfo(); user.setUserId(batch.getActorId()); user.setTenantId(batch.getTenantId()); return user; } private void requireUnsettled(DmsApprovedAuditSource.Source nativeSource) { if (nativeSource.settled()) throw new DataException("历史业务单据已结算但缺少接收回执,需要单独迁移,不能自动补登记"); if (nativeSource.batch().getItems() == null || nativeSource.batch().getItems().isEmpty()) throw new DataException("批准文件集合不能为空"); } }