package jnpf.dmsService; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.dmsEntity.storage.approval.DmsDisableRequest; import jnpf.dmsEntity.storage.approval.DmsApprovedBatch; import jnpf.dmsMapper.DmsApprovedBatchMapper; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import jnpf.dmsMapper.DmsApprovedDisableMapper; import jnpf.dmsMapper.DmsWenjianZiyuanMapper; import jnpf.exception.DataException; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.TaskEntity; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; /** Short local transactions only. Native forms are read, never created or replaced here. */ @Service @RequiredArgsConstructor public class DmsApprovedDisableService { private final DmsApprovedDisableMapper mapper; private final DmsWenjianZiyuanMapper resources; private final WorkFlowApi workflow; private final DmsApprovedBatchMapper batches; @Value("${dms.approved-disable.enabled:false}") private boolean enabled; @Transactional(rollbackFor = Exception.class) @AuditLog(action="DMS_QUEUE_APPROVED_DISABLE", bizCodeExpr="#taskId") public DmsApprovedBatch enqueue(String tenantId, String flowId, String taskId, String operation) { requireEnabled(); requireOperation(operation); TaskEntity task = approved(flowId, taskId); List rows = new ArrayList<>(mapper.nativeRows(tenantId, flowId, taskId, operation)); if (rows.isEmpty()) throw new DataException("未找到已批准的原生停用申请明细"); Set files = new HashSet<>(); Set applications = new HashSet<>(); rows.sort(Comparator.comparing(DmsDisableRequest::getSourceRowId)); for (DmsDisableRequest row : rows) { applications.add(row.getApplicationId()); if (!files.add(row.getFileId()) || row.getFileId()==null || row.getBusinessVersionId()==null || row.getDocumentId()==null || row.getFileVersionId()==null || row.getFolderId()==null) throw new DataException("停用明细重复或缺少真实文件/版本关联,请先发布并补全表单"); row.setId(UUID.randomUUID()); row.setApprovedAt(task.getEndTime()); // getInfoSubmit only returns workflow identity/status; nativeRows resolves its persisted actor. if (row.getActorId()==null || row.getActorId().isBlank()) throw new DataException("批准任务缺少操作人"); row.setScheduledAt(DmsApprovedFileDates.disableScheduled(row.getRequestedAt(), task.getEndTime())); } if (applications.size()!=1) throw new DataException("流程任务关联了多个停用申请"); String auditType = auditType(operation); String digest = digest(rows); DmsApprovedBatch existing = batches.find(tenantId, taskId, auditType); if (existing != null) { if (!Objects.equals(existing.getSourceDigest(), digest) || !Objects.equals(existing.getApprovedAt(), task.getEndTime()) || !Objects.equals(existing.getFlowId(), flowId)) throw new DataException("批准后的停用来源已改变,停止自动处理"); return existing; } DmsApprovedBatch batch = new DmsApprovedBatch(); batch.setId(UUID.randomUUID()); batch.setTenantId(tenantId); batch.setFlowId(flowId); batch.setTaskId(taskId); batch.setAuditType(auditType); batch.setAuditId(rows.get(0).getApplicationId()); batch.setSourceDigest(digest); batch.setActorId(rows.get(0).getActorId()); batch.setApprovedAt(task.getEndTime()); batch.setStatus("WAITING"); batch.setScheduledAt(rows.stream().map(DmsDisableRequest::getScheduledAt).min(Date::compareTo).orElseThrow()); batches.insertBatch(batch); for (DmsDisableRequest row : rows) { row.setBatchId(batch.getId()); resources.selectByIdForUpdate(tenantId, row.getFileId()); validateTarget(row); mapper.enqueue(row); } return batch; } // Length prefixes preserve nulls and separators in native IDs; row order is canonical. private static String digest(List rows) { StringBuilder source = new StringBuilder(); for (DmsDisableRequest row : rows) { for (Object value : Arrays.asList(row.getSourceRowId(), row.getApplicationId(), row.getFileId(), row.getBusinessVersionId(), row.getDocumentId(), row.getFileVersionId(), row.getFolderId(), row.getSourceFileId(), row.getSourceVersionId(), row.getRequestedAt() == null ? null : row.getRequestedAt().getTime())) { String text = value == null ? null : value.toString(); source.append(text == null ? -1 : text.length()).append(':'); if (text != null) source.append(text); } } try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(source.toString().getBytes(StandardCharsets.UTF_8))); } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); } } static String auditType(String operation) { requireOperation(operation); return "yichexiao".equals(operation) ? "chexiao_shenqing" : "shixiao_shenqing"; } @Transactional(rollbackFor = Exception.class) public void refreshProgress(DmsApprovedBatch batch) { requireEnabled(); if (mapper.refreshProgress(batch) != 1) throw new DataException("统一任务进度更新失败或租约已失效"); } private void validateTarget(DmsDisableRequest row) { if (mapper.validateTarget(row)!=1) throw new DataException("业务文件版本或有效状态已改变,不能登记原停用申请"); if ("yishixiao".equals(row.getOperation()) && mapper.candidateExists(row)<1) throw new DataException("该文件不是已生效新增文件检出的同名旧文件"); } private TaskEntity approved(String flowId,String taskId) { TaskEntity task=workflow.getInfoSubmit(taskId); if (task==null || !taskId.equals(task.getId()) || !Integer.valueOf(2).equals(task.getStatus()) || task.getEndTime()==null || (!flowId.equals(task.getFlowId()) && !flowId.equals(task.getTemplateId()))) throw new DataException("文件停用必须先通过真实工作流审批"); return task; } static void requireOperation(String operation) { if (!"yichexiao".equals(operation) && !"yishixiao".equals(operation)) throw new DataException("文件停用类型无效"); } private void requireEnabled() { if (!enabled) throw new DataException("停用推送尚未启用:需先完成技术迁移和原生流程接入"); } }