package jnpf.dmsService; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.audit.AuditConsts; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.model.AuditRelatedSignDTO; import jnpf.dmsIntegration.audit.DmsSystemAudit; import jnpf.base.UserInfo; import jnpf.dmsEntity.fenfa.DmsWenjianDayinJiluEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinRenwuEntity; import jnpf.dmsEntity.fenfa.DmsWenjianFenfaRenwuEntity; import jnpf.dmsEntity.fenfa.DmsWenjianBupinghengExecution; import jnpf.dmsEntity.fenfa.vo.DmsWenjianBupinghengResultVO; import jnpf.dmsMapper.DmsWenjianBupinghengMapper; import jnpf.dmsMapper.DmsWenjianFenfaMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.exception.DataException; import jnpf.util.RandomUtil; import jnpf.util.JsonUtil; 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 DmsWenjianBupinghengService { private final DmsCurrentIdentity identity; private final DmsWenjianChongdaGuard reprintGuard; private final DmsWenjianFenfaMapper fenfaMapper; private final DmsWenjianDayinHaomaService numbers; private final DmsWenjianBupinghengMapper mapper; private final DmsSystemAudit systemAudit; /** 只执行已由原生表单保存的措施;失败时原单据保留,业务副作用全部回滚。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "FILE_DISTRIBUTION_IMBALANCE", label = "执行文件不平衡措施", bizModule = "文件分发", bizType = "DMS_FILE_IMBALANCE", bizCodeExpr = "", bizCodeRequired = false, targetTable = "dms_wenjian_bupingheng_dengji", targetIdExpr = "#p0", extraExpr = "{'results': #result, 'after_status': 'yizuofei'}", diff = false, entityClass = DmsWenjianBupinghengExecution.class) public List execute(String documentId) throws DataException { if (blank(documentId)) throw new DataException("登记单ID不能为空"); UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); return executeStored(documentId, tenantId, user.getUserId(), null); } /** 仅供服务内定时补偿调用,不暴露为 HTTP 接口。租户和版本来自已保存登记单。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "FILE_DISTRIBUTION_IMBALANCE_RETRY", bizCodeExpr = "#p1", recordSuccess = false, targetTable = "dms_wenjian_bupingheng_dengji", targetIdExpr = "#p1") public List executeScheduled(String tenantId, String documentId, Long expectedVersion) throws DataException { if (blank(tenantId) || blank(documentId) || expectedVersion == null) throw new DataException("补偿执行缺少租户、登记单或版本"); // 显式系统身份;不改变交互式 execute 的 requireUser 契约。 return executeStored(documentId, tenantId, "0", expectedVersion); } private List executeStored(String documentId, String tenantId, String userId, Long expectedVersion) throws DataException { DmsWenjianBupinghengExecution document = mapper.lockDocument(tenantId, documentId); if (document == null) throw new DataException("请先通过原生表单保存不平衡登记单"); if (blank(document.getBizSign())) throw new DataException("登记单缺少签名,不能执行措施"); if (expectedVersion != null && document.getZhixingShijian() == null && !expectedVersion.equals(document.getVersion())) throw new DataException("登记单已修改,本轮补偿取消,等待重新复查"); List savedItems = mapper.lockItems(tenantId, documentId); if (savedItems == null || savedItems.isEmpty() || savedItems.size() > 200) throw new DataException("登记单须有1至200条明细"); Map items = new TreeMap<>(); for (DmsWenjianBupinghengExecution.Item item : savedItems) { if (blank(item.getId()) || blank(item.getDayinJiluId()) || items.put(item.getDayinJiluId().trim(), item) != null) throw new DataException("登记单存在空白或重复文件副本"); if (!Arrays.asList("cuoshi1", "cuoshi2").contains(item.getCuoshi()) || blank(item.getYuanyin())) throw new DataException("登记单的措施或原因不完整"); } // 主单行锁串行化同一单据的重复请求;结果在同一业务事务内写回。 if (document.getZhixingShijian() != null) { List completed = new ArrayList<>(); for (DmsWenjianBupinghengExecution.Item item : items.values()) completed.add(result(documentId, item)); return completed; } Map copies = new TreeMap<>(); Set taskIds = new TreeSet<>(); // 与交接一致,先锁副本,再锁分发任务、打印任务;批量按ID排序。 for (String id : items.keySet()) { DmsWenjianDayinJiluEntity copy = fenfaMapper.selectPrintForUpdate(tenantId, id, null); if (copy == null) throw new DataException("存在无效、已删除或跨租户的文件副本"); reprintGuard.requireUnlocked(tenantId, id); eligible(copy.getBizStatus()); copies.put(id, copy); taskIds.add(copy.getDayinRenwuId()); } Map tasks = new HashMap<>(); for (String taskId : taskIds) { DmsWenjianFenfaRenwuEntity distribution = fenfaMapper.selectDistributionTaskByPrintTaskForUpdate(tenantId, taskId); if (distribution == null) throw new DataException("分发任务不存在或已取消"); eligible(distribution.getBizStatus()); DmsWenjianDayinRenwuEntity task = fenfaMapper.selectPrintTaskForUpdate(tenantId, taskId); if (task == null) throw new DataException("原打印任务不存在或已删除"); tasks.put(taskId, task); } Date now = new Date(); List result = new ArrayList<>(); List numberedReplacements = new ArrayList<>(); for (Map.Entry entry : items.entrySet()) { String id = entry.getKey(); DmsWenjianBupinghengExecution.Item item = entry.getValue(); DmsWenjianBupinghengResultVO row = new DmsWenjianBupinghengResultVO(); row.setDengjiId(documentId); String detailId = item.getId(); row.setDayinJiluId(id); row.setCuoshi(item.getCuoshi()); changed(mapper.voidPrint(tenantId, id, userId, now)); if ("cuoshi1".equals(item.getCuoshi())) { DmsWenjianDayinRenwuEntity replacement = new DmsWenjianDayinRenwuEntity(); BeanUtils.copyProperties(tasks.get(copies.get(id).getDayinRenwuId()), replacement); replacement.setId(RandomUtil.uuId()); replacement.setLaiyuanLeixing("wenjian_bupingheng"); replacement.setLaiyuanDanjuId(row.getDengjiId()); replacement.setLaiyuanMingxiId(detailId); replacement.setDayinFenshu(1); replacement.setBizStatus("daidayin"); replacement.setCreatorUserId(userId); replacement.setCreatorTime(now); replacement.setLastModifyUserId(null); replacement.setLastModifyTime(null); replacement.setDeleteMark(null); changed(fenfaMapper.insertPrintTaskIfAbsent(replacement)); numberedReplacements.add(replacement); row.setXinDayinRenwuId(replacement.getId()); } if (row.getXinDayinRenwuId() != null) changed(mapper.linkReplacement(tenantId, detailId, documentId, row.getXinDayinRenwuId())); result.add(row); } numberedReplacements.sort(java.util.Comparator.comparing(DmsWenjianDayinRenwuEntity::getWenjianBianhao).thenComparing(DmsWenjianDayinRenwuEntity::getBanbenhao).thenComparing(DmsWenjianDayinRenwuEntity::getId)); for (DmsWenjianDayinRenwuEntity replacement : numberedReplacements) numbers.ensureTaskNumbers(tenantId, replacement, 0); for (String taskId : taskIds) mapper.finishDistribution(tenantId, taskId, userId, now); changed(mapper.completeDocument(tenantId, documentId, now)); if (expectedVersion != null) { Map extra = new LinkedHashMap<>(); extra.put("dengji_ren_id", document.getDengjiRenId()); extra.put("results", result); extra.put("trigger", "scheduled"); systemAudit.record(AuditEventDTO.builder().eventType(AuditConsts.TYPE_BIZ_ACTION) .actionCode("FILE_DISTRIBUTION_IMBALANCE_RETRY").actionLabel("定时补偿文件不平衡措施") .tenantId(tenantId).operatorId("0").operatorName("系统定时任务") .bizModule("文件分发").bizType("DMS_FILE_IMBALANCE").bizCode(documentId) .targetTable("dms_wenjian_bupingheng_dengji").targetId(documentId) .relatedSigns(Collections.singletonList(AuditRelatedSignDTO.builder() .signId(document.getBizSign()).role("PRIMARY").build())) .extra(JsonUtil.getObjectToString(extra)).build()); } return result; } private DmsWenjianBupinghengResultVO result(String documentId, DmsWenjianBupinghengExecution.Item item) { DmsWenjianBupinghengResultVO row = new DmsWenjianBupinghengResultVO(); row.setDengjiId(documentId); row.setDayinJiluId(item.getDayinJiluId()); row.setCuoshi(item.getCuoshi()); row.setXinDayinRenwuId(item.getXinDayinRenwuId()); return row; } private void eligible(String status) throws DataException { if (!"daifafang".equals(status) && !"daiqianshou".equals(status)) throw new DataException("仅待发放、待签收文件可执行此操作,请刷新列表"); } private void changed(int count) throws DataException { if (count != 1) throw new DataException("文件状态已变化,登记单已保留,本次措施未执行,请核实后重试"); } private boolean blank(String value) { return value == null || value.trim().isEmpty(); } }