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<String,DmsApprovedItem> 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<DmsApprovedItem> expected = receipts.items(tenantId, batch.getId());
|
Map<String, DmsFileUploadRegisterForm> 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<String, DmsFileUploadRegisterForm> validateFiles(DmsApprovedRegistrationCallback form,
|
DmsApprovedAuditSource.Source nativeSource, List<DmsApprovedItem> expected) {
|
Map<String, DmsApprovedItem> 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<String, StoredFile> 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<String, DmsFileUploadRegisterForm> result = new HashMap<>();
|
Set<UUID> 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("批准文件集合不能为空");
|
}
|
|
}
|