package jnpf.dmsService.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.base.BillRuleApi; import jnpf.base.UserInfo; import jnpf.dms.DmsResourceApi; import jnpf.dmsEntity.DmsWenjianBanbenEntity; import jnpf.dmsEntity.permission.form.DmsPersonalFolderFileForm; import jnpf.dmsEntity.print.form.DmsWorkflowPrintFilesGrantForm; import jnpf.dmsEntity.print.vo.DmsWorkflowPrintFilesGrantResultVO; import jnpf.dmsEntity.recordprint.DmsJiluDayinLiushuiEntity; import jnpf.dmsEntity.recordprint.DmsJiluDayinShenqingEntity; import jnpf.dmsEntity.recordprint.DmsJiluDayinShenqingZixiangEntity; import jnpf.dmsEntity.recordprint.form.DmsRecordPrintHuishouForm; import jnpf.dmsEntity.recordprint.form.DmsRecordPrintHuishouItemForm; import jnpf.dmsEntity.recordprint.form.DmsRecordPrintIssueForm; import jnpf.dmsEntity.recordprint.form.DmsRecordPrintIssueItemForm; import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintCopyVO; import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintConfirmVO; import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintPrepareVO; import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintSerialVO; import jnpf.dmsEntity.preview.vo.DmsFilePageCountVO; import jnpf.dmsMapper.DmsJiluDayinLiushuiMapper; import jnpf.dmsMapper.DmsRecordPrintItemMapper; import jnpf.dmsMapper.DmsRecordPrintRequestMapper; import jnpf.dmsMapper.DmsWenjianBanbenMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsService.DmsFilePageCountService; import jnpf.dmsService.DmsRecordPrintService; import jnpf.dmsService.recordprint.DmsRecordPrintPageParser; import jnpf.dmsService.recordprint.DmsRecordPrintProperties; import jnpf.dmsService.recordprint.DmsRecordPrintReservation; import jnpf.dmsService.recordprint.DmsRecordPrintReservationStore; import jnpf.exception.DataException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * 记录文件打印准备业务实现。 */ @Service @RequiredArgsConstructor @Slf4j public class DmsRecordPrintServiceImpl implements DmsRecordPrintService { private static final String PRINT_TYPE_BOUND = "chengce"; private static final String PRINT_TYPE_LOOSE = "fensan"; private static final String PRINT_STATUS_PROCESSING = "dayinzhong"; private static final String PRINT_STATUS_COMPLETE = "dayin_wancheng"; private static final String CHAZHAO_LEIXING_HUISHOU = "huishou"; private static final String FLOWABLE_SERVICE_ID = "jnpf-flowable"; private final DmsJiluDayinLiushuiMapper mapper; private final DmsRecordPrintItemMapper itemMapper; private final DmsRecordPrintRequestMapper requestMapper; private final DmsWenjianBanbenMapper banbenMapper; private final DmsResourceApi dmsResourceApi; private final DmsCurrentIdentity identity; private final BillRuleApi billRuleApi; private final DmsFilePageCountService pageCountService; private final DmsRecordPrintPageParser pageParser; private final DmsRecordPrintProperties properties; private final DmsRecordPrintReservationStore reservationStore; @Override public void complete(String printItemId) throws DataException { String id = requireValue(printItemId, "打印申请子项ID"); DmsJiluDayinShenqingZixiangEntity item = new DmsJiluDayinShenqingZixiangEntity(); item.setDayinZhuangtai(PRINT_STATUS_COMPLETE); if (itemMapper.update(item, Wrappers.lambdaUpdate() .eq(DmsJiluDayinShenqingZixiangEntity::getId, id) .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) { throw new DataException("未找到有效的记录打印申请子项:" + id); } } /** * 查询流程对应的记录打印申请和全部子项,并为申请人授予永久打印权限。 * * @param flowId 流程实例 ID * @param flowTaskId 流程任务 ID * @return 文件打印授权结果 * @throws DataException 申请、子项或业务版本无效,或者授权接口调用失败 */ @Override public DmsWorkflowPrintFilesGrantResultVO authorize( String flowId, String flowTaskId) throws DataException { String normalizedFlowId = requireValue(flowId, "流程ID(flow_id)"); String normalizedFlowTaskId = requireValue(flowTaskId, "流程任务ID(flow_task_id)"); DmsJiluDayinShenqingEntity request = findPrintRequest( normalizedFlowId, normalizedFlowTaskId); List items = findPrintItems(request.getId()); Set versionIds = extractVersionIds(items); List versions = findBusinessVersions(versionIds); DmsWorkflowPrintFilesGrantForm form = buildPrintGrantForm(request, versions); ActionResult response; try { response = dmsResourceApi.grantWorkflowPrintFiles(FLOWABLE_SERVICE_ID, form); } catch (RuntimeException exception) { throw new DataException("调用DMS文件打印授权接口失败"); } if (response == null || !ActionResultCode.SUCCESS.getCode().equals(response.getCode()) || response.getData() == null) { String message = response == null ? null : response.getMsg(); throw new DataException(isBlank(message) ? "记录文件打印授权失败或响应数据不完整" : message); } return response.getData(); } /** 查询流程唯一对应的未删除记录打印申请。 */ private DmsJiluDayinShenqingEntity findPrintRequest(String flowId, String flowTaskId) { List rows = requestMapper.selectList( Wrappers.lambdaQuery() .eq(DmsJiluDayinShenqingEntity::getFlowId, flowId) .eq(DmsJiluDayinShenqingEntity::getFlowTaskId, flowTaskId) .isNull(DmsJiluDayinShenqingEntity::getDeleteMark)); if (rows.isEmpty()) { throw new DataException("未找到记录打印申请:flow_id=" + flowId + ", flow_task_id=" + flowTaskId); } if (rows.size() > 1) { throw new DataException("flow_id与flow_task_id关联了多条记录打印申请,无法授权"); } return rows.get(0); } /** 根据申请主键查询全部未删除的记录打印子项。 */ private List findPrintItems(String requestId) { List rows = itemMapper.selectList( Wrappers.lambdaQuery() .eq(DmsJiluDayinShenqingZixiangEntity::getForeignId, requestId) .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)); if (rows.isEmpty()) { throw new DataException("记录打印申请未关联打印子项,无法授权"); } return rows; } /** 提取子项业务版本 ID,并按原顺序去重。 */ private Set extractVersionIds(List items) { Set ids = new LinkedHashSet<>(); for (DmsJiluDayinShenqingZixiangEntity item : items) { ids.add(requireValue(item.getBanbenId(), "记录打印子项版本ID(banben_id)")); } return ids; } /** 批量查询未删除的业务文件版本。 */ private List findBusinessVersions(Set versionIds) { List rows = banbenMapper.selectList( Wrappers.lambdaQuery() .in(DmsWenjianBanbenEntity::getId, versionIds) .isNull(DmsWenjianBanbenEntity::getDeleteMark)); if (rows.size() != versionIds.size()) { throw new DataException("部分记录打印子项未找到对应的有效文件版本"); } return rows; } /** 组装申请人的永久文件打印授权请求。 */ private DmsWorkflowPrintFilesGrantForm buildPrintGrantForm( DmsJiluDayinShenqingEntity request, List versions) { List files = new ArrayList<>(versions.size()); for (DmsWenjianBanbenEntity version : versions) { DmsPersonalFolderFileForm.FileRelation relation = new DmsPersonalFolderFileForm.FileRelation(); try { relation.setDocumentId(UUID.fromString(requireValue( version.getWenjianZhongxinWenjianId(), "文件中心文件ID"))); relation.setFileVersionId(UUID.fromString(requireValue( version.getWenjianZhongxinBanbenId(), "文件中心版本ID"))); } catch (IllegalArgumentException exception) { throw new DataException("文件中心文件ID或版本ID格式不正确:banben_id=" + version.getId()); } relation.setValidTo(null); files.add(relation); } DmsWorkflowPrintFilesGrantForm form = new DmsWorkflowPrintFilesGrantForm(); form.setUserId(requireValue(request.getShenqingren(), "记录打印申请人(shenqingren)")); form.setFiles(files); return form; } /** 校验必填文本并返回去除首尾空白后的值。 */ private String requireValue(String value, String fieldName) { if (isBlank(value)) { throw new DataException(fieldName + "不能为空"); } return value.trim(); } /** 判断文本是否为空。 */ private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } /** * 校验整批流水后,使用同一时间完成发放和接收信息更新。 * * @param form 电子签名及发放明细 * @throws DataException 参数无效、流水不存在或条码已经发放 */ @Override @Transactional(rollbackFor = Exception.class) @AuditLog( action = "FAFANG", label = "记录打印发放", failureAction = "FAFANG_FAIL", failureLabel = "记录打印发放失败", bizModule = "记录打印发放", bizType = "DMS_RECORD_PRINT_FAFANG", bizCodeExpr = "#p0.data[0].fId", bizCodeRequired = false, targetTable = "dms_jilu_dayin_liushui", targetIdExpr = "#p0.data[0].fId", targetIdsExpr = "#p0.data.![fId]", diff = true, entityClass = DmsJiluDayinLiushuiEntity.class ) public void issue(DmsRecordPrintIssueForm form) throws DataException { Map items = resolveIssueItems(form); List records = mapper.selectList( Wrappers.lambdaQuery() .select(DmsJiluDayinLiushuiEntity::getId, DmsJiluDayinLiushuiEntity::getLiushuihao, DmsJiluDayinLiushuiEntity::getFafangren, DmsJiluDayinLiushuiEntity::getDayinZixiangId) .in(DmsJiluDayinLiushuiEntity::getLiushuihao, items.keySet()) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)); validateIssueRecords(items.keySet(), records); Date now = new Date(); Map issuedCounts = new LinkedHashMap<>(); for (DmsJiluDayinLiushuiEntity record : records) { String printItemId = requireValue(record.getDayinZixiangId(), "打印申请子项ID"); DmsRecordPrintIssueItemForm item = items.get(record.getLiushuihao()); LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .eq(DmsJiluDayinLiushuiEntity::getId, record.getId()) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark) .and(wrapper -> wrapper .isNull(DmsJiluDayinLiushuiEntity::getFafangren) .or() .eq(DmsJiluDayinLiushuiEntity::getFafangren, "")) .set(DmsJiluDayinLiushuiEntity::getFafangren, item.getFafangren().trim()) .set(DmsJiluDayinLiushuiEntity::getFafangBumen, item.getFafangBumen().trim()) .set(DmsJiluDayinLiushuiEntity::getJieshouren, item.getJieshouren().trim()) .set(DmsJiluDayinLiushuiEntity::getJieshouBumen, item.getJieshouBumen().trim()) .set(DmsJiluDayinLiushuiEntity::getFafangShijian, now) .set(DmsJiluDayinLiushuiEntity::getJieshouShijian, now); if (mapper.update(null, update) != 1) { throw new DataException("该条码已签收或发放请求重复操作"); } issuedCounts.merge(printItemId, 1, Integer::sum); } for (String printItemId : new TreeSet<>(issuedCounts.keySet())) { DmsJiluDayinShenqingZixiangEntity printItem = mapper.selectPrintItemForUpdate(printItemId); if (printItem == null) { throw new DataException("未找到有效的记录打印申请子项:" + printItemId); } int total = parsePositiveInt(printItem.getDayinFenshu(), "打印份数"); int issued = parseNonNegativeInt(printItem.getYiqianfaShuliang(), "已签发数量"); try { issued = Math.addExact(issued, issuedCounts.get(printItemId)); } catch (ArithmeticException exception) { throw new DataException("已签发数量超出允许范围:" + printItemId); } DmsJiluDayinShenqingZixiangEntity updated = new DmsJiluDayinShenqingZixiangEntity(); updated.setYiqianfaShuliang(String.valueOf(issued)); updated.setWeiqianfaShuliang(String.valueOf(total - issued)); if (itemMapper.update(updated, Wrappers.lambdaUpdate() .eq(DmsJiluDayinShenqingZixiangEntity::getId, printItemId) .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) { throw new DataException("更新记录打印申请子项发放数量失败:" + printItemId); } } } /** * 校验整批流水后,使用同一时间完成归还和回收信息更新。 * * @param form 电子签名及回收明细 * @throws DataException 参数无效、流水不存在或条码已经回收 */ @Override @Transactional(rollbackFor = Exception.class) @AuditLog( action = "HUISHOU", label = "记录打印回收", failureAction = "HUIUSHOU_FAIL", failureLabel = "记录打印回收失败", bizModule = "记录打印回收", bizType = "DMS_RECORD_PRINT_HUISHOU", bizCodeExpr = "#p0.data[0].fId", bizCodeRequired = false, targetTable = "dms_jilu_dayin_liushui", targetIdExpr = "#p0.data[0].fId", targetIdsExpr = "#p0.data.![fId]", diff = true, entityClass = DmsJiluDayinLiushuiEntity.class ) public void huishou(DmsRecordPrintHuishouForm form) throws DataException { Map items = resolveHuishouItems(form); List records = mapper.selectList( Wrappers.lambdaQuery() .select(DmsJiluDayinLiushuiEntity::getId, DmsJiluDayinLiushuiEntity::getLiushuihao, DmsJiluDayinLiushuiEntity::getGuihuanren, DmsJiluDayinLiushuiEntity::getDayinZixiangId) .in(DmsJiluDayinLiushuiEntity::getLiushuihao, items.keySet()) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)); validateIssueRecords(items.keySet(), records); Date now = new Date(); Map recoveredCounts = new LinkedHashMap<>(); for (DmsJiluDayinLiushuiEntity record : records) { String printItemId = requireValue(record.getDayinZixiangId(), "打印申请子项ID"); DmsRecordPrintHuishouItemForm item = items.get(record.getLiushuihao()); LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .eq(DmsJiluDayinLiushuiEntity::getId, record.getId()) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark) .and(wrapper -> wrapper .isNull(DmsJiluDayinLiushuiEntity::getGuihuanren) .or() .eq(DmsJiluDayinLiushuiEntity::getGuihuanren, "")) .set(DmsJiluDayinLiushuiEntity::getGuihuanren, item.getGuihuanren()) .set(DmsJiluDayinLiushuiEntity::getGuihuanBumen, item.getGuihuanBumen()) .set(DmsJiluDayinLiushuiEntity::getHuishouren, item.getHuishouren()) .set(DmsJiluDayinLiushuiEntity:: getHuishouBumen, item.getHuishouBumen()) .set(DmsJiluDayinLiushuiEntity:: getHuishouYuanyin, item.getHuishouYuanyin()) .set(DmsJiluDayinLiushuiEntity:: getHuishouLeixing, item.getHuishouLeixing()) .set(DmsJiluDayinLiushuiEntity::getGuihuanShijian, now) .set(DmsJiluDayinLiushuiEntity::getHuishouShijian, now); if (mapper.update(null, update) != 1) { throw new DataException("该条码已回收请求重复操作"); } recoveredCounts.merge(printItemId, 1, Integer::sum); } for (String printItemId : new TreeSet<>(recoveredCounts.keySet())) { DmsJiluDayinShenqingZixiangEntity printItem = mapper.selectPrintItemForUpdate(printItemId); if (printItem == null) { throw new DataException("未找到有效的记录打印申请子项:" + printItemId); } int recovered = parseNonNegativeInt(printItem.getYihuishouShuliang(), "已回收数量"); try { recovered = Math.addExact(recovered, recoveredCounts.get(printItemId)); } catch (ArithmeticException exception) { throw new DataException("已回收数量超出允许范围:" + printItemId); } DmsJiluDayinShenqingZixiangEntity updated = new DmsJiluDayinShenqingZixiangEntity(); updated.setYihuishouShuliang(String.valueOf(recovered)); if (itemMapper.update(updated, Wrappers.lambdaUpdate() .eq(DmsJiluDayinShenqingZixiangEntity::getId, printItemId) .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) { throw new DataException("更新记录打印申请子项回收数量失败:" + printItemId); } } } /** * 校验请求及签名,并按流水号整理发放明细。 * * @param form 发放请求 * @return 流水号到发放明细的有序映射 * @throws DataException 必填字段为空或流水号重复 */ private Map resolveIssueItems( DmsRecordPrintIssueForm form) throws DataException { if (form == null) { throw new DataException("发放参数不能为空"); } if (form.getData() == null || form.getData().isEmpty()) { throw new DataException("发放数据不能为空"); } Map items = new LinkedHashMap<>(); for (DmsRecordPrintIssueItemForm item : form.getData()) { if (item == null) { throw new DataException("发放数据中不能包含空记录"); } String serialNumber = requireText(item.getLiushuihao(), "流水号不能为空"); requireText(item.getFafangren(), "发放人不能为空"); requireText(item.getFafangBumen(), "发放部门不能为空"); requireText(item.getJieshouren(), "接收人不能为空"); requireText(item.getJieshouBumen(), "接收部门不能为空"); item.setLiushuihao(serialNumber); if (items.put(serialNumber, item) != null) { throw new DataException("发放数据中不能包含重复流水号"); } } return items; } /** * 校验请求及签名,并按流水号整理回收明细。 * * @param form 回收请求 * @return 流水号到回收明细的有序映射 * @throws DataException 必填字段为空或流水号重复 */ private Map resolveHuishouItems( DmsRecordPrintHuishouForm form) throws DataException { if (form == null) { throw new DataException("发放参数不能为空"); } if (form.getData() == null || form.getData().isEmpty()) { throw new DataException("发放数据不能为空"); } Map items = new LinkedHashMap<>(); for (DmsRecordPrintHuishouItemForm item : form.getData()) { if (item == null) { throw new DataException("发放数据中不能包含空记录"); } String serialNumber = requireText(item.getLiushuihao(), "流水号不能为空"); requireText(item.getGuihuanren(), "归还人人不能为空"); requireText(item.getGuihuanBumen(), "归还部门不能为空"); requireText(item.getHuishouren(), "回收人不能为空"); requireText(item.getHuishouBumen(), "回收部门不能为空"); item.setLiushuihao(serialNumber); if (items.put(serialNumber, item) != null) { throw new DataException("回收数据中不能包含重复流水号"); } } return items; } /** * 校验所有请求流水均存在且尚未发放。 * * @param serialNumbers 请求流水号 * @param records 查询出的 A 表实体 * @throws DataException 存在无效流水或已经发放的条码 */ private void validateIssueRecords(Set serialNumbers, List records) throws DataException { Set foundSerialNumbers = new LinkedHashSet<>(); for (DmsJiluDayinLiushuiEntity record : records) { foundSerialNumbers.add(record.getLiushuihao()); if (!trim(record.getFafangren()).isEmpty()) { throw new DataException("该条码已签收或发放请勿重复操作"); } } if (records.size() != serialNumbers.size() || !foundSerialNumbers.equals(serialNumbers)) { throw new DataException("存在未找到或已删除的打印流水号,无法发放"); } } /** * 根据流水号读取 A 表实体,校验发放状态后读取对应 B 表实体。 * * @param serialNumber 打印流水号 * @return 流水记录及文件信息 * @throws DataException 流水号为空、记录不存在或已经签收/发放 */ @Override public DmsRecordPrintSerialVO findBySerialNumber(String serialNumber, String chazhaoLeixing) throws DataException { String resolvedSerialNumber = requireText(serialNumber, "流水号不能为空"); DmsJiluDayinLiushuiEntity record = mapper.selectOne( Wrappers.lambdaQuery() .select(DmsJiluDayinLiushuiEntity::getId, DmsJiluDayinLiushuiEntity::getDayinZixiangId, DmsJiluDayinLiushuiEntity::getLiushuihao, DmsJiluDayinLiushuiEntity::getFafangren, DmsJiluDayinLiushuiEntity::getHuishouren, DmsJiluDayinLiushuiEntity::getJieshouren, DmsJiluDayinLiushuiEntity::getJieshouBumen) .eq(DmsJiluDayinLiushuiEntity::getLiushuihao, resolvedSerialNumber) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)); if (record == null) { throw new DataException("未找到该流水号对应的条码信息"); } if (!trim(record.getFafangren()).isEmpty() && !CHAZHAO_LEIXING_HUISHOU.equals(chazhaoLeixing)) { throw new DataException("该条码已签收或发放请勿重复操作"); } if (!trim(record.getHuishouren()).isEmpty() && CHAZHAO_LEIXING_HUISHOU.equals(chazhaoLeixing)) { throw new DataException("该条码已回签收请勿重复操作"); } DmsJiluDayinShenqingZixiangEntity item = itemMapper.selectOne( Wrappers.lambdaQuery() .select(DmsJiluDayinShenqingZixiangEntity::getId, DmsJiluDayinShenqingZixiangEntity::getWenjianMingcheng, DmsJiluDayinShenqingZixiangEntity::getWenjianBianhao, DmsJiluDayinShenqingZixiangEntity::getBanbenhao) .eq(DmsJiluDayinShenqingZixiangEntity::getId, record.getDayinZixiangId()) .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)); if (item == null) { throw new DataException("未找到该流水号对应的打印申请子项"); } DmsRecordPrintSerialVO result = new DmsRecordPrintSerialVO(); result.setLiushuihao(record.getLiushuihao()); result.setId(record.getId()); result.setWenjianMingcheng(item.getWenjianMingcheng()); result.setWenjianBianhao(item.getWenjianBianhao()); result.setBanbenhao(item.getBanbenhao()); result.setJieshouren(record.getJieshouren()); result.setJieshouBumen(record.getJieshouBumen()); return result; } /** * 锁定打印申请、计算打印单元并预占流水号,不写入打印业务表。 * * @param printItemId B 表打印申请子项主键 * @return 本次唯一打印内容所需的流水号映射 * @throws DataException 任一步业务校验或持久化失败 */ @Override @Transactional(rollbackFor = Exception.class) public DmsRecordPrintPrepareVO prepare(String printItemId) throws DataException { String resolvedId = requireText(printItemId, "打印申请子项ID不能为空"); DmsJiluDayinShenqingZixiangEntity item = mapper.selectPrintItemForUpdate(resolvedId); validatePrintable(item, resolvedId); UserInfo user = identity.requireUser(); String userId = requireText(user.getUserId(), "当前登录用户ID不能为空"); String tenantId = identity.tenantId(user); DmsRecordPrintReservation existing = reservationStore.findByPrintItem( tenantId, resolvedId); if (existing != null) { validateReservationIdentity(existing, tenantId, userId); validateReservationVersion(existing, item); if (!DmsRecordPrintReservation.STATUS_PENDING.equals(existing.getStatus()) || existing.getExpiresAt() <= System.currentTimeMillis()) { throw new DataException("打印预占状态无效或已过期,请重新预览"); } return buildPrepareResult(existing); } String printType = trim(item.getDayinLeixing()); int printCount = parsePositiveInt(item.getDayinFenshu(), "打印份数"); List selectedPages = resolveSelectedPages(item, tenantId); int serialCount = calculateSerialCount(printType, printCount, selectedPages); List serialNumbers = allocateSerialNumbers(serialCount); long ttlMillis = reservationTtlMillis(); long now = System.currentTimeMillis(); DmsRecordPrintReservation reservation = new DmsRecordPrintReservation(); reservation.setReservationId(UUID.randomUUID().toString()); reservation.setTenantId(tenantId); reservation.setUserId(userId); reservation.setPrintItemId(item.getId()); reservation.setItemVersion(item.getVersion()); reservation.setPrintType(printType); reservation.setPrintCount(printCount); reservation.setSelectedPages(new ArrayList<>(selectedPages)); reservation.setSerialNumbers(new ArrayList<>(serialNumbers)); reservation.setStatus(DmsRecordPrintReservation.STATUS_PENDING); reservation.setCreatedAt(now); reservation.setExpiresAt(now + ttlMillis); reservationStore.create(reservation, ttlMillis); return buildPrepareResult(reservation); } /** * 将预览阶段预占的流水号写入 A 表,并更新 B 表打印状态。 */ @Override @Transactional(rollbackFor = Exception.class) public DmsRecordPrintConfirmVO confirm(String reservationId) throws DataException { String resolvedReservationId = requireText( reservationId, "打印预占标识不能为空"); UserInfo user = identity.requireUser(); String userId = requireText(user.getUserId(), "当前登录用户ID不能为空"); String tenantId = identity.tenantId(user); DmsRecordPrintReservation reservation = reservationStore.findById( tenantId, resolvedReservationId); if (reservation == null || reservation.getExpiresAt() <= System.currentTimeMillis()) { throw new DataException("打印预占不存在或已过期,请重新预览"); } validateReservationIdentity(reservation, tenantId, userId); if (DmsRecordPrintReservation.STATUS_CONFIRMED.equals(reservation.getStatus())) { return buildConfirmResult(reservation); } if (!DmsRecordPrintReservation.STATUS_PENDING.equals(reservation.getStatus())) { throw new DataException("打印预占状态无效,请重新预览"); } DmsJiluDayinShenqingZixiangEntity item = mapper.selectPrintItemForUpdate( reservation.getPrintItemId()); if (item == null) { throw new DataException("打印申请子项不存在"); } List existingRecords = findActivePrintRecords(item.getId()); if (!existingRecords.isEmpty()) { DmsRecordPrintConfirmVO confirmed = resolveConfirmedRetry( reservation, existingRecords, userId); markConfirmedAfterCommit(reservation); return confirmed; } validatePrintable(item, item.getId()); validateReservationVersion(reservation, item); validateReservationLayout(reservation, item, tenantId); Date printTime = new Date(); List records = buildRecords( item, reservation.getSerialNumbers(), userId, printTime); int inserted = mapper.insertPrintRecords(records); if (inserted != records.size()) { throw new DataException("生成打印流水记录失败"); } markPrintItemProcessing(item, userId, printTime); reservation.setRecordIds(recordIds(records)); reservation.setStatus(DmsRecordPrintReservation.STATUS_CONFIRMED); markConfirmedAfterCommit(reservation); return buildConfirmResult(reservation); } /** * 校验打印申请存在、未打印且数据状态一致。 * * @param item B 表实体 * @param printItemId 打印申请子项主键 * @throws DataException 申请不存在、类型无效或已经打印 */ private void validatePrintable(DmsJiluDayinShenqingZixiangEntity item, String printItemId) throws DataException { if (item == null) { throw new DataException("打印申请子项不存在"); } String printType = trim(item.getDayinLeixing()); if (!PRINT_TYPE_BOUND.equals(printType) && !PRINT_TYPE_LOOSE.equals(printType)) { throw new DataException("打印类型无效:" + printType); } int activeRecords = mapper.countActivePrintRecords(printItemId); if (activeRecords > 0) { throw new DataException("该记录已经打印,不允许重复打印"); } int printedCount = parseNonNegativeInt(item.getYidayinFenshu(), "已打印份数"); if (printedCount > 0 || PRINT_STATUS_PROCESSING.equals(trim(item.getDayinZhuangtai()))) { throw new DataException("该记录已经打印,不允许重复打印"); } } /** * 按打印类型解析分散打印页码,成册打印返回空集合。 * * @param item B 表实体 * @return 分散打印有序页码集合 * @throws DataException 文件页数或页码集合无效 */ private List resolveSelectedPages(DmsJiluDayinShenqingZixiangEntity item, String tenantId) throws DataException { if (PRINT_TYPE_BOUND.equals(trim(item.getDayinLeixing()))) { return Collections.emptyList(); } String banbenId = requireText(item.getBanbenId(), "文件版本ID不能为空"); DmsWenjianBanbenEntity banben = banbenMapper.selectById(tenantId, banbenId); if (banben == null) { throw new DataException("未找到文件版本:" + banbenId); } String fileCenterVersionId = requireText( banben.getWenjianZhongxinBanbenId(), "文件版本缺少文件中心版本ID"); try { DmsFilePageCountVO pageCount = pageCountService.getPageCount( UUID.fromString(fileCenterVersionId)); return pageParser.parse(item.getYema(), pageCount.getPageCount()); } catch (IllegalArgumentException exception) { throw new DataException("文件中心版本ID格式不正确:" + fileCenterVersionId); } catch (IOException exception) { throw new DataException("获取文件总页数失败"); } } /** * 根据打印类型计算需要生成的流水号总数并校验上限。 * * @param printType 打印类型 * @param printCount 打印份数 * @param selectedPages 分散打印有序页码集合 * @return 流水号总数 * @throws DataException 数量溢出或超过配置上限 */ private int calculateSerialCount(String printType, int printCount, List selectedPages) throws DataException { final int serialCount; try { serialCount = PRINT_TYPE_BOUND.equals(printType) ? printCount : Math.multiplyExact(printCount, selectedPages.size()); } catch (ArithmeticException exception) { throw new DataException("打印流水号数量超出允许范围"); } int maxSerialCount = properties.getMaxSerialCount(); if (maxSerialCount <= 0) { throw new DataException("打印流水号数量上限配置无效"); } if (serialCount <= 0 || serialCount > maxSerialCount) { throw new DataException("打印流水号数量必须在1至" + maxSerialCount + "之间"); } return serialCount; } /** * 在 DMS 事务锁保护下调用单据规则接口生成本批全部流水号。 * * @param count 流水号数量 * @return 按生成顺序排列的流水号 * @throws DataException 锁获取异常、规则不可用或返回重复号码 */ private List allocateSerialNumbers(int count) throws DataException { Integer locked = mapper.lockSerialAllocation(); if (locked == null || locked != 1) { throw new DataException("获取记录打印流水号批量取号锁失败"); } String ruleCode = requireText(properties.getBillRuleCode(), "记录打印流水号规则编码不能为空"); List numbers = new ArrayList<>(count); Set uniqueNumbers = new HashSet<>(count); for (int index = 0; index < count; index++) { ActionResult result = billRuleApi.getBillNumber(ruleCode); String number = result == null ? "" : trim(result.getData()); if (number.isEmpty()) { throw new DataException("打印流水号取号失败:单据规则[" + ruleCode + "]不可用"); } if (!uniqueNumbers.add(number)) { throw new DataException("打印流水号规则返回重复号码:" + number); } numbers.add(number); } return numbers; } /** * 使用实体构造本次全部 A 表打印流水记录。 * * @param item B 表实体 * @param serialNumbers 流水号集合 * @param printUserId 打印人 * @param printTime 本批统一打印时间 * @return A 表实体集合 * @throws DataException 打印申请主表主键为空 */ private List buildRecords( DmsJiluDayinShenqingZixiangEntity item, List serialNumbers, String printUserId, Date printTime) throws DataException { String printId = requireText(item.getForeignId(), "打印申请主表ID不能为空"); List records = new ArrayList<>(serialNumbers.size()); for (String serialNumber : serialNumbers) { DmsJiluDayinLiushuiEntity record = new DmsJiluDayinLiushuiEntity(); record.setId(IdUtil.getSnowflakeNextIdStr()); record.setDayinId(printId); record.setDayinZixiangId(item.getId()); record.setLiushuihao(serialNumber); record.setDayinren(printUserId); record.setDayinShijian(printTime); record.setDeleteMark(null); records.add(record); } return records; } /** * 使用 B 表实体统一回写打印人、打印时间和打印中状态。 * * @param item B 表实体 * @param printUserId 打印人 * @param printTime 与 A 表相同的打印时间 * @throws DataException B 表状态未按预期更新 */ private void markPrintItemProcessing(DmsJiluDayinShenqingZixiangEntity item, String printUserId, Date printTime) throws DataException { item.setDayinren(printUserId); item.setDayinShijian(printTime); item.setDayinZhuangtai(PRINT_STATUS_PROCESSING); item.setYidayinFenshu(item.getDayinFenshu()); if (mapper.markPrintItemProcessing(item) != 1) { throw new DataException("更新打印申请子项状态失败"); } } /** * 按稳定顺序构造不含数据库记录主键的打印预览结果。 */ private DmsRecordPrintPrepareVO buildPrepareResult( DmsRecordPrintReservation reservation) { DmsRecordPrintPrepareVO result = new DmsRecordPrintPrepareVO(); result.setReservationId(reservation.getReservationId()); result.setExpiresAt(new Date(reservation.getExpiresAt())); result.setPrintItemId(reservation.getPrintItemId()); result.setPrintType(reservation.getPrintType()); result.setPrintCount(reservation.getPrintCount()); result.setSelectedPages(new ArrayList<>(reservation.getSelectedPages())); result.setSerialCount(reservation.getSerialNumbers().size()); result.setCopies(buildCopies(reservation, false)); return result; } /** * 构造已经写入 A 表的打印确认结果。 */ private DmsRecordPrintConfirmVO buildConfirmResult( DmsRecordPrintReservation reservation) { DmsRecordPrintConfirmVO result = new DmsRecordPrintConfirmVO(); result.setReservationId(reservation.getReservationId()); result.setPrintItemId(reservation.getPrintItemId()); result.setConfirmed(Boolean.TRUE); result.setCopies(buildCopies(reservation, true)); return result; } /** * 根据预占中的稳定顺序构造打印单元。 */ private List buildCopies( DmsRecordPrintReservation reservation, boolean includeRecordIds) { List serialNumbers = reservation.getSerialNumbers(); List recordIds = reservation.getRecordIds(); if (includeRecordIds && (recordIds == null || recordIds.size() != serialNumbers.size())) { throw new DataException("打印确认结果缺少数据库记录主键"); } List copies = new ArrayList<>(serialNumbers.size()); int recordIndex = 0; for (int copyIndex = 1; copyIndex <= reservation.getPrintCount(); copyIndex++) { if (PRINT_TYPE_BOUND.equals(reservation.getPrintType())) { copies.add(toCopy(includeRecordIds ? recordIds.get(recordIndex) : null, serialNumbers.get(recordIndex++), copyIndex, null)); continue; } for (Integer pageNo : reservation.getSelectedPages()) { copies.add(toCopy(includeRecordIds ? recordIds.get(recordIndex) : null, serialNumbers.get(recordIndex++), copyIndex, pageNo)); } } if (recordIndex != serialNumbers.size()) { throw new DataException("打印预占的流水号数量与打印单元数量不一致"); } return copies; } /** * 构造单个打印单元响应。 */ private DmsRecordPrintCopyVO toCopy(String recordId, String serialNumber, int copyIndex, Integer pageNo) { DmsRecordPrintCopyVO copy = new DmsRecordPrintCopyVO(); copy.setRecordId(recordId); copy.setCopyIndex(copyIndex); copy.setPageNo(pageNo); copy.setSerialNo(serialNumber); return copy; } /** * 校验预占只能由创建它的租户和用户确认。 */ private void validateReservationIdentity(DmsRecordPrintReservation reservation, String tenantId, String userId) throws DataException { if (!Objects.equals(tenantId, reservation.getTenantId())) { throw new DataException("打印预占不属于当前租户"); } if (!Objects.equals(userId, reservation.getUserId())) { throw new DataException("该打印申请正在被其他用户预览"); } } /** * 校验预览后打印申请没有被修改。 */ private void validateReservationVersion(DmsRecordPrintReservation reservation, DmsJiluDayinShenqingZixiangEntity item) throws DataException { if (!Objects.equals(reservation.getItemVersion(), item.getVersion())) { throw new DataException("打印申请在预览后已经变更,请重新预览"); } } /** * 重新计算打印布局,防止预览后参数发生变化。 */ private void validateReservationLayout(DmsRecordPrintReservation reservation, DmsJiluDayinShenqingZixiangEntity item, String tenantId) throws DataException { int printCount = parsePositiveInt(item.getDayinFenshu(), "打印份数"); List selectedPages = resolveSelectedPages(item, tenantId); String printType = trim(item.getDayinLeixing()); int serialCount = calculateSerialCount(printType, printCount, selectedPages); if (!Objects.equals(reservation.getPrintType(), printType) || !Objects.equals(reservation.getPrintCount(), printCount) || !Objects.equals(reservation.getSelectedPages(), selectedPages) || reservation.getSerialNumbers() == null || reservation.getSerialNumbers().size() != serialCount || new HashSet<>(reservation.getSerialNumbers()).size() != serialCount) { throw new DataException("打印参数在预览后已经变更,请重新预览"); } for (String serialNumber : reservation.getSerialNumbers()) { requireText(serialNumber, "打印预占中存在空流水号"); } } /** * 查询申请已经写入的有效打印流水,用于确认请求幂等恢复。 */ private List findActivePrintRecords(String printItemId) { List records = mapper.selectList( Wrappers.lambdaQuery() .select(DmsJiluDayinLiushuiEntity::getId, DmsJiluDayinLiushuiEntity::getLiushuihao, DmsJiluDayinLiushuiEntity::getDayinren) .eq(DmsJiluDayinLiushuiEntity::getDayinZixiangId, printItemId) .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)); return records == null ? Collections.emptyList() : records; } /** * 已存在完全相同的一批流水时,将网络重试恢复为成功响应。 */ private DmsRecordPrintConfirmVO resolveConfirmedRetry( DmsRecordPrintReservation reservation, List existingRecords, String userId) throws DataException { Map bySerial = new LinkedHashMap<>(); for (DmsJiluDayinLiushuiEntity record : existingRecords) { if (!Objects.equals(userId, record.getDayinren()) || bySerial.put(record.getLiushuihao(), record) != null) { throw new DataException("该记录已经打印,不允许重复打印"); } } List recordIds = new ArrayList<>(); for (String serialNumber : reservation.getSerialNumbers()) { DmsJiluDayinLiushuiEntity record = bySerial.remove(serialNumber); if (record == null) { throw new DataException("该记录已经使用其他流水号打印,不允许重复打印"); } recordIds.add(record.getId()); } if (!bySerial.isEmpty()) { throw new DataException("该记录已经使用其他流水号打印,不允许重复打印"); } reservation.setRecordIds(recordIds); reservation.setStatus(DmsRecordPrintReservation.STATUS_CONFIRMED); return buildConfirmResult(reservation); } private List recordIds(List records) { List ids = new ArrayList<>(records.size()); for (DmsJiluDayinLiushuiEntity record : records) { ids.add(record.getId()); } return ids; } /** * 数据库提交后保留一段时间的确认结果,为网络重试提供幂等响应。 */ private void markConfirmedAfterCommit(DmsRecordPrintReservation reservation) { Runnable completion = () -> { try { long ttlMillis = confirmedResultTtlMillis(); reservation.setExpiresAt(System.currentTimeMillis() + ttlMillis); reservationStore.markConfirmed(reservation, ttlMillis); } catch (RuntimeException exception) { log.error("更新打印预占确认状态失败,reservationId={}", reservation.getReservationId(), exception); } }; if (!TransactionSynchronizationManager.isSynchronizationActive()) { completion.run(); return; } TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { completion.run(); } }); } private long reservationTtlMillis() throws DataException { int minutes = properties.getReservationTtlMinutes(); if (minutes <= 0) { throw new DataException("打印预占有效期配置无效"); } return TimeUnit.MINUTES.toMillis(minutes); } private long confirmedResultTtlMillis() throws DataException { int hours = properties.getConfirmedResultTtlHours(); if (hours <= 0) { throw new DataException("打印确认结果有效期配置无效"); } return TimeUnit.HOURS.toMillis(hours); } /** * 解析必须为正整数的字符串字段。 * * @param value 原始字段值 * @param fieldName 字段显示名 * @return 正整数 * @throws DataException 为空、非整数或小于 1 */ private int parsePositiveInt(String value, String fieldName) throws DataException { int number = parseInteger(value, fieldName, false); if (number <= 0) { throw new DataException(fieldName + "必须大于0"); } return number; } /** * 解析允许为空或零的非负整数字段。 * * @param value 原始字段值 * @param fieldName 字段显示名 * @return 非负整数,空值按 0 处理 * @throws DataException 非整数或小于 0 */ private int parseNonNegativeInt(String value, String fieldName) throws DataException { String resolved = trim(value); if (resolved.isEmpty()) { return 0; } int number = parseInteger(resolved, fieldName, true); if (number < 0) { throw new DataException(fieldName + "不能小于0"); } return number; } /** * 解析整数字符串。 * * @param value 原始字段值 * @param fieldName 字段显示名 * @param allowEmpty 是否允许空值;空值由调用方提前处理 * @return 整数 * @throws DataException 字段为空或不是整数 */ private int parseInteger(String value, String fieldName, boolean allowEmpty) throws DataException { String resolved = trim(value); if (resolved.isEmpty() && !allowEmpty) { throw new DataException(fieldName + "不能为空"); } try { return Integer.parseInt(resolved); } catch (NumberFormatException exception) { throw new DataException(fieldName + "必须为整数"); } } /** * 校验并返回非空文本。 * * @param value 原始值 * @param message 为空时的错误消息 * @return 去除首尾空白后的值 * @throws DataException 值为空 */ private String requireText(String value, String message) throws DataException { String resolved = trim(value); if (resolved.isEmpty()) { throw new DataException(message); } return resolved; } /** * 将可空文本转换为去除首尾空白后的值。 * * @param value 原始值 * @return 非 null 文本 */ private String trim(String value) { return value == null ? "" : value.trim(); } }