package jnpf.dmsService; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.base.Pagination; import jnpf.base.UserInfo; import jnpf.dmsEntity.fenfa.DmsWenjianDayinQuerenEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinRenwuEntity; import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinQuerenForm; import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinYulanForm; import jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma; import jnpf.dmsEntity.fenfa.vo.DmsWenjianDayinJiluVO; import jnpf.dmsMapper.DmsWenjianDayinQuerenMapper; import jnpf.dmsMapper.DmsWenjianFenfaMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity.Batch; import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinFasongForm; @Service @RequiredArgsConstructor public class DmsWenjianDayinQuerenService { private final DmsCurrentIdentity identity; private final DmsWenjianFenfaMapper taskMapper; private final DmsWenjianDayinQuerenMapper mapper; private final DmsWenjianDayinTaizhangService ledger; private final DmsWenjianFenfaService distribution; private final DmsWenjianDayinHaomaService numbers; /** 只接收发送成功回传;事务同时提交正式记录、任务状态、分发和幂等凭据。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_CONFIRM", recordSuccess = false, bizCodeExpr = "#p0.requestId", targetTable = "dms_wenjian_dayin_queren", targetIdExpr = "#p0.requestId") public List confirm(DmsWenjianDayinQuerenForm form) throws DataException { if (form == null || !"SUBMITTED".equals(form.getClientResult()) || form.getCopies() == null || form.getCopies() < 1 || form.getCopies() > 999) { throw new DataException("必须收到打印客户端提交成功回传及有效份数"); } try { if (!UUID.fromString(form.getRequestId()).toString().equals(form.getRequestId())) { throw new IllegalArgumentException(); } } catch (RuntimeException error) { throw new DataException("打印确认请求ID无效"); } UserInfo actor = identity.requireUser(); String tenantId = identity.tenantId(actor); // 同任务确认统一先锁任务,再读取幂等记录,兼容响应丢失和并发重复回调。 DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenantId, form.getTaskId()); if (task == null) throw new DataException("打印任务不存在或已删除"); DmsWenjianDayinQuerenEntity saved = mapper.selectConfirmation(tenantId, form.getRequestId()); if (saved != null) { if (!Objects.equals(saved.getTaskId(), form.getTaskId()) || !Objects.equals(saved.getUserId(), actor.getUserId()) || !Objects.equals(saved.getFileVersionId(), form.getFileVersionId()) || !Objects.equals(saved.getCopies(), form.getCopies())) { throw new DataException("相同打印确认请求ID不能用于不同打印任务或参数"); } return JsonUtil.getJsonToList(saved.getResultJson(), DmsWenjianDayinJiluVO.class); } DmsWenjianDayinFasongEntity attempt = mapper.selectAttempt(tenantId, form.getRequestId()); if (attempt == null) throw new DataException("请先建立技术发送任务并等待全部批次成功回传"); requireOwner(attempt, actor, form.getTaskId(), form.getFileVersionId()); if (!Objects.equals(attempt.getCopies(), form.getCopies()) || !"OPEN".equals(attempt.getState()) || batches(attempt).isEmpty() || batches(attempt).stream().anyMatch(b -> !"SUBMITTED".equals(b.getState()))) { throw new DataException("打印批次尚未全部成功回传,不能创建正式记录"); } Pagination pagination = new Pagination(); pagination.setCurrentPage(1L); pagination.setPageSize(1L); if (ledger.list(pagination, null, "f_id", form.getTaskId()).isEmpty()) { throw new DataException("无当前打印任务的操作权限或任务已完成"); } DmsWenjianDayinQuerenEntity version = mapper.selectTaskVersion(tenantId, form.getTaskId()); if (version == null || !Objects.equals(version.getFileVersionId(), form.getFileVersionId()) || task.getDayinFenshu() == null || form.getCopies() > task.getDayinFenshu() - taskMapper.countConsumedPrints(tenantId, form.getTaskId())) { throw new DataException("打印任务版本或份数已变化,请核对已发送结果,勿重复打印"); } List result = distribution.printReserved(form.getTaskId(), form.getCopies(), JsonUtil.getJsonToList(attempt.getNumbersJson(), jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma.class)); DmsWenjianDayinQuerenEntity receipt = new DmsWenjianDayinQuerenEntity(); receipt.setId(form.getRequestId()); receipt.setTenantId(tenantId); receipt.setTaskId(form.getTaskId()); receipt.setFileVersionId(form.getFileVersionId()); receipt.setUserId(actor.getUserId()); receipt.setCopies(form.getCopies()); receipt.setResultJson(JsonUtil.getObjectToString(result)); if (mapper.insertConfirmation(receipt) != 1) throw new DataException("保存打印确认凭据失败"); attempt.setState("CONFIRMED"); saveAttempt(attempt); return result; } /** 复用固定份号;历史任务在任务锁内补齐,预览不创建发送或正式打印记录。 */ @Transactional(rollbackFor = Exception.class) public DmsWenjianDayinHaoma previewParameters(DmsWenjianDayinYulanForm form) throws DataException { if (form == null || form.getTaskId() == null || form.getTaskId().isBlank() || form.getFileVersionId() == null || form.getFileVersionId().isBlank()) { throw new DataException("打印预览参数无效"); } UserInfo actor = identity.requireUser(); String tenant = identity.tenantId(actor); DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId()); if (task == null) throw new DataException("打印任务不存在或已删除"); DmsWenjianDayinFasongEntity active = mapper.selectActiveAttempt(tenant, form.getTaskId()); List frozen; if (active != null) { requireOwner(active, actor, form.getTaskId(), form.getFileVersionId()); // 与prepare恢复契约一致;CONFIRMED仍取原发送号,不跳到下一份。 frozen = JsonUtil.getJsonToList(active.getNumbersJson(), DmsWenjianDayinHaoma.class); } else { requirePrintable(tenant, form.getTaskId(), form.getFileVersionId(), 1, task); int consumed = taskMapper.maxPrintSequence(tenant, task.getId()); frozen = numbers.ensureTaskNumbers(tenant, task, consumed).stream() .filter(n -> n.getTaskSequence() > consumed).limit(1).toList(); } if (frozen == null || frozen.isEmpty() || frozen.get(0) == null) { throw new DataException("任务固定份号不足,不能重新取号"); } DmsWenjianDayinHaoma number = frozen.get(0); if (number.getSerialNumber() == null || number.getSerialNumber().isBlank() || number.getCountSerialNumber() == null || !number.getCountSerialNumber().matches("[0-9]{8}")) { throw new DataException("任务固定流水号缺失,无法显示打印预览"); } return number; } /** 同任务排他:刷新和其他窗口必须恢复原发送,不创建第二个发送任务。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_PREPARE", recordSuccess = false, bizCodeExpr = "#p0.taskId") public DmsWenjianDayinFasongEntity prepare(DmsWenjianDayinFasongForm form) throws DataException { validateRequest(form); UserInfo actor = identity.requireUser(); String tenant = identity.tenantId(actor); DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId()); if (task == null) throw new DataException("打印任务不存在或已删除"); DmsWenjianDayinFasongEntity active = mapper.selectActiveAttempt(tenant, form.getTaskId()); if (active != null) { requireOwner(active, actor, form.getTaskId(), form.getFileVersionId()); return active; // 恢复原份数,绝不以新输入覆盖已开始发送的任务。 } if (mapper.selectAttempt(tenant, form.getRequestId()) != null) throw new DataException("发送请求已结束,请刷新台账"); requirePrintable(tenant, form.getTaskId(), form.getFileVersionId(), form.getCopies(), task); DmsWenjianDayinFasongEntity attempt = new DmsWenjianDayinFasongEntity(); attempt.setId(form.getRequestId()); attempt.setTenantId(tenant); attempt.setTaskId(form.getTaskId()); attempt.setFileVersionId(form.getFileVersionId()); attempt.setUserId(actor.getUserId()); attempt.setCopies(form.getCopies()); attempt.setState("OPEN"); attempt.setBatchesJson("[]"); int consumed = taskMapper.maxPrintSequence(tenant, task.getId()); List frozen = numbers.ensureTaskNumbers(tenant, task, consumed).stream() .filter(n -> n.getTaskSequence() > consumed).limit(form.getCopies()).toList(); if (frozen.size() != form.getCopies()) throw new DataException("任务固定份号不足,不能重新取号"); attempt.setNumbersJson(JsonUtil.getObjectToString(frozen)); if (mapper.insertAttempt(attempt) != 1) throw new DataException("建立打印发送任务失败"); return attempt; } /** 计划持久化后才能发;已有计划只能恢复,不能被另一窗口替换。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_PLAN", recordSuccess = false, bizCodeExpr = "#p0.requestId") public DmsWenjianDayinFasongEntity plan(DmsWenjianDayinFasongForm form) throws DataException { DmsWenjianDayinFasongEntity attempt = lockAttempt(form); if (!batches(attempt).isEmpty() || !"OPEN".equals(attempt.getState())) return attempt; validatePlan(form.getBatches(), attempt); attempt.setBatchesJson(JsonUtil.getObjectToString(form.getBatches())); saveAttempt(attempt); return attempt; } @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_BATCH_SPLIT", recordSuccess = false, bizCodeExpr = "#p0.requestId") public DmsWenjianDayinFasongEntity split(DmsWenjianDayinFasongForm form) throws DataException { DmsWenjianDayinFasongEntity attempt = lockAttempt(form); List list = batches(attempt); Batch batch = findBatch(list, form.getBatchId()); if (!"OPEN".equals(attempt.getState()) || !"READY".equals(batch.getState())) throw new DataException("已发送的批次不能拆分"); List parts = form.getBatches(); if (parts == null || parts.size() != 2) throw new DataException("拆分计划无效"); validateBatch(parts.get(0)); validateBatch(parts.get(1)); List pages = new ArrayList<>(parts.get(0).getPages()); pages.addAll(parts.get(1).getPages()); if (!parts.get(0).getItems().equals(batch.getItems()) || !parts.get(1).getItems().equals(batch.getItems()) || !pages.equals(batch.getPages())) throw new DataException("拆分页段与原批次不一致"); int index = list.indexOf(batch); list.remove(index); list.addAll(index, parts); if (list.stream().map(Batch::getId).distinct().count() != list.size()) throw new DataException("批次ID重复"); attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt); return attempt; } /** 非幂等派发许可:许可响应丢失也不能再次取得许可,必须保留结果未知。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_BATCH_BEGIN", recordSuccess = false, bizCodeExpr = "#p0.requestId") public DmsWenjianDayinFasongEntity begin(DmsWenjianDayinFasongForm form) throws DataException { DmsWenjianDayinFasongEntity attempt = lockAttempt(form); List list = batches(attempt); Batch batch = findBatch(list, form.getBatchId()); if (!"OPEN".equals(attempt.getState()) || !"READY".equals(batch.getState())) { throw new DataException("该批次已发送或发送结果未知,请恢复原任务,禁止重复发送"); } for (Batch previous : list) { if (previous == batch) break; if (!"SUBMITTED".equals(previous.getState())) throw new DataException("前一批次尚未确认,不能发送后续批次"); } requirePrintable(attempt.getTenantId(), attempt.getTaskId(), attempt.getFileVersionId(), attempt.getCopies(), taskMapper.selectPrintTaskForUpdate(attempt.getTenantId(), attempt.getTaskId())); requireUuid(form.getToken()); batch.setToken(form.getToken()); batch.setState("SENDING"); attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt); return attempt; } /** 包括迟到回传;保存失败可用同一令牌重试,绝不重新派发。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_BATCH_RECEIPT", recordSuccess = false, bizCodeExpr = "#p0.requestId") public DmsWenjianDayinFasongEntity submitted(DmsWenjianDayinFasongForm form) throws DataException { DmsWenjianDayinFasongEntity attempt = lockAttempt(form); List list = batches(attempt); Batch batch = findBatch(list, form.getBatchId()); requireUuid(form.getToken()); if (!Objects.equals(form.getToken(), batch.getToken()) || !("SENDING".equals(batch.getState()) || "SUBMITTED".equals(batch.getState()))) { throw new DataException("打印批次成功回传与发送任务不匹配"); } if ("SUBMITTED".equals(batch.getState())) return attempt; batch.setState("SUBMITTED"); attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt); return attempt; } /** 前端已收到正式确认才结束技术任务;确认响应丢失时仍能恢复。 */ @Transactional(rollbackFor = Exception.class) @AuditLog(action = "DMS_PRINT_FINISH", recordSuccess = false, bizCodeExpr = "#p0.requestId") public void finish(DmsWenjianDayinFasongForm form) throws DataException { DmsWenjianDayinFasongEntity attempt = lockAttempt(form); if ("CLOSED".equals(attempt.getState())) return; if (!"CONFIRMED".equals(attempt.getState())) throw new DataException("正式打印记录尚未确认,不能结束发送任务"); attempt.setState("CLOSED"); saveAttempt(attempt); } private void validateRequest(DmsWenjianDayinFasongForm form) throws DataException { if (form == null || form.getTaskId() == null || form.getTaskId().trim().isEmpty() || form.getFileVersionId() == null || form.getFileVersionId().trim().isEmpty() || form.getCopies() == null || form.getCopies() < 1 || form.getCopies() > 999) throw new DataException("打印发送参数无效"); requireUuid(form.getRequestId()); } private void requireUuid(String value) throws DataException { try { if (!UUID.fromString(value).toString().equals(value)) throw new IllegalArgumentException(); } catch (RuntimeException ex) { throw new DataException("打印发送关联ID无效"); } } private DmsWenjianDayinFasongEntity lockAttempt(DmsWenjianDayinFasongForm form) throws DataException { validateRequest(form); UserInfo actor = identity.requireUser(); String tenant = identity.tenantId(actor); if (taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId()) == null) throw new DataException("打印任务不存在或已删除"); DmsWenjianDayinFasongEntity attempt = mapper.selectAttempt(tenant, form.getRequestId()); if (attempt == null) throw new DataException("打印发送任务不存在"); requireOwner(attempt, actor, form.getTaskId(), form.getFileVersionId()); if (!Objects.equals(attempt.getCopies(), form.getCopies())) throw new DataException("打印发送份数不能改变"); return attempt; } private void requireOwner(DmsWenjianDayinFasongEntity attempt, UserInfo actor, String taskId, String versionId) throws DataException { if (!Objects.equals(attempt.getUserId(), actor.getUserId())) throw new DataException("该打印任务有其他操作人的未结束发送,请先核对原任务"); if (!Objects.equals(attempt.getTaskId(), taskId) || !Objects.equals(attempt.getFileVersionId(), versionId)) throw new DataException("打印发送任务版本不匹配"); } private void requirePrintable(String tenant, String taskId, String versionId, int copies, DmsWenjianDayinRenwuEntity task) throws DataException { Pagination pagination = new Pagination(); pagination.setCurrentPage(1L); pagination.setPageSize(1L); if (ledger.list(pagination, null, "f_id", taskId).isEmpty()) throw new DataException("无当前打印任务的操作权限或任务已完成"); DmsWenjianDayinQuerenEntity version = mapper.selectTaskVersion(tenant, taskId); if (version == null || !Objects.equals(version.getFileVersionId(), versionId) || task.getDayinFenshu() == null || copies > task.getDayinFenshu() - taskMapper.countConsumedPrints(tenant, taskId)) throw new DataException("打印任务版本或剩余份数已变化,请刷新台账"); } private List batches(DmsWenjianDayinFasongEntity attempt) { return JsonUtil.getJsonToList(attempt.getBatchesJson(), Batch.class); } private Batch findBatch(List list, String id) throws DataException { for (Batch batch : list) if (Objects.equals(batch.getId(), id)) return batch; throw new DataException("打印批次不存在,请恢复原任务"); } private void validateBatch(Batch batch) throws DataException { if (batch == null) throw new DataException("打印批次无效"); requireUuid(batch.getId()); if (batch.getPages() == null || batch.getPages().isEmpty() || batch.getPages().size() > 100 || batch.getItems() == null || batch.getItems().size() != 1 || batch.getItems().get(0) == null || batch.getItems().get(0) < 0) throw new DataException("打印页段无效"); batch.setState("READY"); batch.setToken(null); } private void validatePlan(List list, DmsWenjianDayinFasongEntity attempt) throws DataException { if (list == null || list.isEmpty() || list.size() > 100000) throw new DataException("打印计划无效"); Set ids = new HashSet<>(); int cursor = 0, expectedPages = 0; for (int copy = 0; copy < attempt.getCopies(); copy++) { int page = 1; while (cursor < list.size() && Collections.singletonList(copy).equals(list.get(cursor).getItems())) { Batch batch = list.get(cursor++); validateBatch(batch); if (!ids.add(batch.getId())) throw new DataException("批次ID重复"); for (Integer number : batch.getPages()) { if (!Objects.equals(number, page++) || page > 10001) throw new DataException("打印计划必须连续覆盖完整文档,不能重复或跳页"); } } if (page == 1 || (copy > 0 && page - 1 != expectedPages)) throw new DataException("每份必须包含相同完整页段"); expectedPages = page - 1; } if (cursor != list.size()) throw new DataException("打印计划份数无效"); } private void saveAttempt(DmsWenjianDayinFasongEntity attempt) throws DataException { if (mapper.updateAttempt(attempt) != 1) throw new DataException("保存打印发送进度失败"); } }