package jnpf.dmsService.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import jnpf.audit.sdk.annotation.AuditLog; import org.springframework.transaction.annotation.Propagation; import jnpf.base.Pagination; import jnpf.base.UserInfo; import jnpf.dmsEntity.DmsWenjianBianhaoEntity; import jnpf.dmsEntity.DmsWenjianBianhaoGuizeEntity; import jnpf.dmsEntity.DmsWenjianBianhaoGuizeZixiangEntity; import jnpf.dmsEntity.DmsWenjianBianhaoShenqingEntity; import jnpf.dmsEntity.DmsWenjianMingchengBiangengShenqingEntity; import jnpf.dmsEntity.DmsWenjianZiyuanEntity; import jnpf.dmsEntity.wenjianbianhao.DmsFlowEventType; import jnpf.dmsEntity.wenjianbianhao.form.DmsWenjianBianhaoSubmitCheckForm; import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoOptionVO; import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoPreviewVO; import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoRuleVO; import jnpf.dmsMapper.DmsWenjianBianhaoGuizeMapper; import jnpf.dmsMapper.DmsWenjianBianhaoGuizeZixiangMapper; import jnpf.dmsMapper.DmsWenjianBianhaoLiushuiJishuMapper; import jnpf.dmsMapper.DmsWenjianBianhaoMapper; import jnpf.dmsMapper.DmsWenjianBianhaoShenqingMapper; import jnpf.dmsMapper.DmsWenjianMingchengBiangengShenqingMapper; import jnpf.dmsMapper.DmsWenjianZiyuanMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsService.DmsWenjianBianhaoApplicationService; import jnpf.dmsService.DmsWenjianBianhaoPermissionService; import jnpf.dmsService.support.DmsWenjianBianhaoFlowStateResolver; import jnpf.dmsService.support.DmsWenjianBianhaoFlowStateResolver.Action; import jnpf.exception.DataException; import jnpf.permission.UserApi; import jnpf.permission.entity.UserEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @Slf4j @Service @RequiredArgsConstructor public class DmsWenjianBianhaoApplicationServiceImpl implements DmsWenjianBianhaoApplicationService { private static final String ENABLED = "enabled"; private static final int APPROVED_FLOW_STATE = 2; private static final String DRAFT = "draft"; private static final String PENDING_REVIEW = "pending_review"; private static final String USING = "using"; private static final String REJECTED = "rejected"; private static final String CANCELLED = "cancelled"; private static final Pattern SERIAL_TOKEN = Pattern.compile("\\{SERIAL:(\\d+)\\}"); private static final ObjectMapper SNAPSHOT_MAPPER = new ObjectMapper() .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); private final DmsCurrentIdentity identity; private final DmsWenjianBianhaoPermissionService permissionService; private final DmsWenjianBianhaoGuizeMapper ruleMapper; private final DmsWenjianBianhaoGuizeZixiangMapper partMapper; private final DmsWenjianBianhaoShenqingMapper applicationMapper; private final DmsWenjianBianhaoMapper numberMapper; private final DmsWenjianBianhaoLiushuiJishuMapper counterMapper; private final DmsWenjianMingchengBiangengShenqingMapper nameChangeMapper; private final DmsWenjianZiyuanMapper resourceMapper; private final UserApi userApi; @Override public List enabledRules( Pagination pagination, String guizeId, String searchValue) throws DataException { UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); List result = new ArrayList<>(); List rules = ruleMapper.selectEnabled(tenantId); if (rules == null || rules.isEmpty()) { pagination.setTotal(0); return Collections.emptyList(); } String resolvedRuleId = trim(guizeId); String keyword = blank(searchValue) ? trim(pagination.getKeyword()) : trim(searchValue); for (DmsWenjianBianhaoGuizeEntity rule : rules) { if (!resolvedRuleId.isEmpty() && !resolvedRuleId.equals(rule.getId())) { continue; } if (!keyword.isEmpty() && !matchesRule(rule, keyword)) { continue; } if (!permissionService.canApplyRule(user.getUserId(), rule.getId())) { continue; } RuleSnapshot snapshot; try { snapshot = loadSnapshot(tenantId, rule.getId()); } catch (DataException exception) { // 候选列表跳过无效规则,实际申请仍由 loadSnapshot 严格校验。 log.warn("跳过不可申请的文件编号规则,tenantId={}, ruleId={}, reason={}", tenantId, rule.getId(), exception.getMessage()); continue; } DmsWenjianBianhaoRuleVO vo = new DmsWenjianBianhaoRuleVO(); vo.setId(rule.getId()); vo.setGuizeMingcheng(rule.getGuizeMingcheng()); vo.setGuizeLeixing(rule.getGuizeLeixing()); vo.setChangqu(rule.getChangqu()); vo.setZidingyiTishi(blank(snapshot.customHint) ? "/" : snapshot.customHint); vo.setWenjianMulu(rule.getWenjianMulu()); vo.setWenjianMuluId(rule.getWenjianMuluId()); vo.setYulanBianhao(snapshot.preview); vo.setGuizeXianshi(display(rule.getGuizeMingcheng(), snapshot.preview)); result.add(vo); } pagination.setTotal(result.size()); long pageSize = Math.max(pagination.getPageSize(), 1L); long currentPage = Math.max(pagination.getCurrentPage(), 1L); if (currentPage - 1 > Long.MAX_VALUE / pageSize) { return Collections.emptyList(); } long offset = (currentPage - 1) * pageSize; if (offset >= result.size()) { return Collections.emptyList(); } int fromIndex = (int) offset; long available = result.size() - offset; int toIndex = (int) (offset + Math.min(pageSize, available)); return new ArrayList<>(result.subList(fromIndex, toIndex)); } private boolean matchesRule(DmsWenjianBianhaoGuizeEntity rule, String keyword) { String normalized = keyword.toLowerCase(); return trim(rule.getGuizeMingcheng()).toLowerCase().contains(normalized) || trim(rule.getGuizeLeixing()).toLowerCase().contains(normalized) || trim(rule.getWenjianMulu()).toLowerCase().contains(normalized); } @Override public DmsWenjianBianhaoPreviewVO preview(String guizeId, String zidingyiNeirong) throws DataException { UserInfo user = identity.requireUser(); requirePermission(user.getUserId(), guizeId); RuleSnapshot snapshot = loadSnapshot(identity.tenantId(user), guizeId, zidingyiNeirong, false); return new DmsWenjianBianhaoPreviewVO(guizeId, snapshot.preview, snapshot.hasSerial, snapshot.hasCustom, snapshot.customHint); } @Override public List usingNumbers( Pagination pagination, String bianhaoId, String searchValue) throws DataException { UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); if (blank(pagination.getKeyword()) && !blank(searchValue)) { pagination.setKeyword(trim(searchValue)); } List numbers = numberMapper.selectUsing( tenantId, trim(bianhaoId), pagination); if (numbers == null || numbers.isEmpty()) { return Collections.emptyList(); } return toNumberOptions(user, numbers); } @Override public List availableNameChangeNumbers( Pagination pagination, String bianhaoId, String searchValue) throws DataException { UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); if (blank(pagination.getKeyword()) && !blank(searchValue)) { pagination.setKeyword(trim(searchValue)); } List numbers = numberMapper.selectNameChangeAvailable( tenantId, trim(bianhaoId), pagination); if (numbers == null || numbers.isEmpty()) { return Collections.emptyList(); } return toNumberOptions(user, numbers, true); } @Override public List availableNewNumbers( Pagination pagination, String bianhaoId, String searchValue) throws DataException { UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); if (blank(pagination.getKeyword()) && !blank(searchValue)) { pagination.setKeyword(trim(searchValue)); } List numbers = numberMapper.selectAvailableNew( tenantId, trim(bianhaoId), pagination); if (numbers == null || numbers.isEmpty()) { return Collections.emptyList(); } return toNumberOptions(user, numbers); } private List toNumberOptions( UserInfo user, List numbers) { return toNumberOptions(user, numbers, false); } private List toNumberOptions( UserInfo user, List numbers, boolean includeSite) { List allowedNumbers = new ArrayList<>(numbers.size()); for (DmsWenjianBianhaoEntity number : numbers) { if (permissionService.canViewNumber(user.getUserId(), number.getId())) { allowedNumbers.add(number); } } Map userNames = loadUserNames(allowedNumbers); List result = new ArrayList<>(allowedNumbers.size()); for (DmsWenjianBianhaoEntity number : allowedNumbers) { DmsWenjianBianhaoOptionVO vo = new DmsWenjianBianhaoOptionVO(); vo.setId(number.getId()); vo.setWenjianBianhao(number.getWenjianBianhao()); vo.setWenjianMingcheng(number.getDangqianWenjianMingcheng()); vo.setMingchengBanben(number.getMingchengBanben()); vo.setGuizeMingcheng(number.getGuizeMingchengKuaizhao()); if (includeSite) { vo.setChangqu(numberSnapshotSite(number.getGuizeKuaizhao())); } vo.setShiyongRenId(number.getShiyongRenId()); vo.setShiyongRen(userNames.get(number.getShiyongRenId())); vo.setBianhaoFafangShijian(number.getBianhaoFafangShijian()); vo.setBianhaoXianshi(display(number.getWenjianBianhao(), number.getDangqianWenjianMingcheng())); result.add(vo); } return result; } private String numberSnapshotSite(String source) { if (blank(source)) { return null; } try { JsonNode root = SNAPSHOT_MAPPER.readTree(source); if (root == null || !root.isObject()) { return null; } // 顶层字段存在时保留其空值语义,仅旧快照回退到 rule.changqu。 JsonNode site = root.has("changqu") ? root.path("changqu") : root.path("rule").path("changqu"); return site.isTextual() ? site.textValue() : null; } catch (JsonProcessingException ignored) { return null; } } private Map loadUserNames(List numbers) { List userIds = new ArrayList<>(); for (DmsWenjianBianhaoEntity number : numbers) { String userId = trim(number.getShiyongRenId()); if (!userId.isEmpty() && !userIds.contains(userId)) { userIds.add(userId); } } if (userIds.isEmpty()) { return Collections.emptyMap(); } List users = userApi.getUserName(userIds); if (users == null || users.isEmpty()) { return Collections.emptyMap(); } Map result = new HashMap<>(); for (UserEntity user : users) { if (user != null && !blank(user.getId())) { result.put(user.getId(), user.getRealName()); } } return result; } @Override @Transactional(readOnly = true) public void validateSubmission(DmsWenjianBianhaoSubmitCheckForm form) throws DataException { UserInfo user = identity.requireUser(); String tenantId = identity.tenantId(user); if (form == null || form.getItems() == null || form.getItems().isEmpty()) { throw new DataException("文件编号申请至少需要一条编号明细"); } String applicationId = trim(form.getApplicationId()); Map persisted = submissionRows(tenantId, user.getUserId(), form); DmsWenjianBianhaoShenqingEntity application = applicationId.isEmpty() ? null : applicationMapper.selectActiveById(tenantId, applicationId); boolean useFrozenSnapshot = application != null && !Set.of(0, 9).contains(application.getFlowState()) && !(Integer.valueOf(8).equals(application.getFlowState()) && Objects.equals(user.getUserId(), application.getShenqingRenId())); Map, RuleSnapshot> rules = new HashMap<>(); Map fixedNumbers = new LinkedHashMap<>(); Set rowIds = new HashSet<>(); for (int i = 0; i < form.getItems().size(); i++) { DmsWenjianBianhaoSubmitCheckForm.Item item = form.getItems().get(i); if (item == null) throw new DataException("编号申请明细不能为空"); String ruleId = requireText(item.getGuizeId(), "第" + (i + 1) + "行未选择编号规则"); String rowId = trim(item.getId()); if (!rowId.isEmpty() && !rowIds.add(rowId)) { throw new DataException("编号申请明细重复,请删除重复行后提交"); } DmsWenjianBianhaoEntity stored = persisted.get(rowId); if (stored != null && !blank(stored.getWenjianBianhao())) { throw new DataException("已发放的文件编号不能重新提交:" + stored.getWenjianBianhao()); } String template; boolean hasSerial; if (useFrozenSnapshot && stored != null && Objects.equals(ruleId, stored.getGuizeId()) && hasReusableSnapshot(stored) && Objects.equals(trim(stored.getZidingyiNeirong()), trim(item.getZidingyiNeirong()))) { // Approval also invokes beforeSubmit; use only the persisted snapshot, never client hidden fields. template = stored.getBianhaoMuban(); hasSerial = SERIAL_TOKEN.matcher(template).find(); } else { List key = List.of(ruleId, trim(item.getZidingyiNeirong())); RuleSnapshot snapshot = rules.get(key); if (snapshot == null) { requirePermission(user.getUserId(), ruleId); snapshot = loadSnapshot(tenantId, ruleId, item.getZidingyiNeirong(), true); rules.put(key, snapshot); } template = snapshot.template; hasSerial = snapshot.hasSerial; } if (!hasSerial) { Integer previous = fixedNumbers.putIfAbsent(template, i + 1); if (previous != null) { throw new DataException("第" + previous + "行与第" + (i + 1) + "行生成相同编号【" + template + "】,无流水号的编号只能申请一次,请删除重复明细"); } } } if (fixedNumbers.isEmpty()) return; List conflicts = numberMapper.selectFixedNumberConflicts( tenantId, new ArrayList<>(fixedNumbers.keySet()), applicationId.isEmpty() ? null : applicationId); if (!conflicts.isEmpty()) { DmsWenjianBianhaoEntity conflict = conflicts.get(0); boolean issued = !blank(conflict.getWenjianBianhao()); String number = issued ? conflict.getWenjianBianhao() : conflict.getBianhaoMuban(); throw new DataException("编号【" + number + "】" + (issued ? "已发放,不能重复申请" : "已被其他审批中的申请占用,不能重复申请")); } } private Map submissionRows( String tenantId, String userId, DmsWenjianBianhaoSubmitCheckForm form) throws DataException { Map rows = new HashMap<>(); String applicationId = trim(form.getApplicationId()); if (applicationId.isEmpty()) return rows; DmsWenjianBianhaoShenqingEntity application = applicationMapper.selectActiveById(tenantId, applicationId); if (application == null || application.getFlowState() == null || !Set.of(0, 1, 5, 6, 8, 9, 10).contains(application.getFlowState())) { throw new DataException("编号申请不存在或已结束,请刷新后重试"); } boolean applicant = Objects.equals(userId, application.getShenqingRenId()); boolean draft = Set.of(0, 9).contains(application.getFlowState()); // A returned task can be at an approval node rather than at its applicant. boolean editable = draft || (application.getFlowState() == 8 && applicant); if (draft && !applicant) { throw new DataException("只能提交本人未结束的编号申请"); } for (DmsWenjianBianhaoEntity row : numberMapper.selectByApplication(tenantId, applicationId)) { rows.put(row.getId(), row); } if (!editable) { Set selected = new HashSet<>(); for (DmsWenjianBianhaoSubmitCheckForm.Item item : form.getItems()) { DmsWenjianBianhaoEntity row = item == null ? null : rows.get(trim(item.getId())); if (row == null || !Objects.equals(row.getGuizeId(), trim(item.getGuizeId())) || !Objects.equals(trim(row.getZidingyiNeirong()), trim(item.getZidingyiNeirong())) || !selected.add(row.getId())) { throw new DataException("审批中的编号明细与原申请不一致,不能变更编号规则或自定义内容"); } } if (selected.size() != rows.size()) { throw new DataException("审批中的编号明细与原申请不一致,不能增删明细"); } } return rows; } private String display(String first, String second) { String left = trim(first); String right = trim(second); if (left.isEmpty()) { return right; } return right.isEmpty() ? left : left + " / " + right; } @Override @Transactional(rollbackFor = Exception.class) public void syncApplicationFlow(String flowId, String taskId, Integer flowState, DmsFlowEventType eventType) throws DataException { UserInfo currentUser = identity.requireUser(); String tenantId = identity.tenantId(currentUser); String resolvedFlowId = requireText(flowId, "流程ID不能为空"); String resolvedTaskId = requireText(taskId, "流程任务ID不能为空"); Action action = resolveAction(flowState); DmsWenjianBianhaoShenqingEntity application = findApplicationForUpdate( tenantId, resolvedFlowId, resolvedTaskId); verifyPersistedFlowState(application.getFlowState(), flowState, eventType); List numbers = numberMapper.selectByApplicationForUpdate( tenantId, application.getId()); if (numbers == null || numbers.isEmpty()) { throw new DataException("文件编号申请至少需要一条编号明细"); } switch (action) { case RESTORE_DRAFT: changeUnissuedStatus(numbers, DRAFT); break; case FREEZE_SNAPSHOT: freezeSnapshots(application, numbers); break; case ISSUE_NUMBER: issueNumbers(tenantId, numbers); break; case REJECT: changeUnissuedStatus(numbers, REJECTED); break; case CANCEL: changeUnissuedStatus(numbers, CANCELLED); break; case REVOKE: revokeUnissuedNumbers(numbers); break; case HOLD: break; case ERROR: throw new DataException("文件编号申请流程处于异常状态,请修复流程后重试"); default: throw new DataException("不支持的文件编号申请流程动作:" + action); } log.info("文件编号申请流程事件同步完成,flowId={}, taskId={}, event={}, flowState={}, action={}", resolvedFlowId, resolvedTaskId, eventType, flowState, action); } @Override @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @AuditLog(action = "DMS_NUMBER_ISSUE_FAILED", label = "回写发号失败", bizModule = "DMS", bizType = "DMS_NUMBER_APPLICATION", bizCodeExpr = "#p1", bizCodeRequired = false, targetTable = "dms_wenjian_bianhao_shenqing", targetIdExpr = "#p1") public void markIssuanceFailed(String flowId, String taskId) throws DataException { String tenantId = identity.tenantId(identity.requireUser()); DmsWenjianBianhaoShenqingEntity application = findApplicationForUpdate(tenantId, requireText(flowId, "流程ID不能为空"), requireText(taskId, "流程任务ID不能为空")); verifyPersistedFlowState(application.getFlowState(), APPROVED_FLOW_STATE, DmsFlowEventType.END); // Lock in the same order as issuance. Never overwrite a concurrently completed number. numberMapper.selectByApplicationForUpdate(tenantId, application.getId()); numberMapper.markUnissuedFailure(tenantId, application.getId()); } @Override @Transactional(rollbackFor = Exception.class) public void syncNameChangeFlow(String flowId, String taskId, Integer flowState, DmsFlowEventType eventType) throws DataException { UserInfo currentUser = identity.requireUser(); String tenantId = identity.tenantId(currentUser); String resolvedFlowId = requireText(flowId, "流程ID不能为空"); String resolvedTaskId = requireText(taskId, "流程任务ID不能为空"); Action action = resolveAction(flowState); DmsWenjianMingchengBiangengShenqingEntity change = findNameChangeForUpdate( tenantId, resolvedFlowId, resolvedTaskId); verifyPersistedFlowState(change.getFlowState(), flowState, eventType); if (action == Action.FREEZE_SNAPSHOT) { freezeNameChange(tenantId, change); } else if (action == Action.ISSUE_NUMBER) { completeNameChange(tenantId, change); } else if (action == Action.ERROR) { throw new DataException("文件名称变更流程处于异常状态,请修复流程后重试"); } log.info("文件名称变更流程事件同步完成,flowId={}, taskId={}, event={}, flowState={}, action={}", resolvedFlowId, resolvedTaskId, eventType, flowState, action); } private DmsWenjianBianhaoShenqingEntity findApplicationForUpdate( String tenantId, String flowId, String taskId) throws DataException { List applications = applicationMapper.selectByFlowForUpdate(tenantId, flowId, taskId); if (applications == null || applications.isEmpty()) { throw new DataException("未找到文件编号申请流程关联记录"); } if (applications.size() > 1) { throw new DataException("多个文件编号申请关联同一流程任务"); } return applications.get(0); } private DmsWenjianMingchengBiangengShenqingEntity findNameChangeForUpdate( String tenantId, String flowId, String taskId) throws DataException { List changes = nameChangeMapper.selectByFlowForUpdate(tenantId, flowId, taskId); if (changes == null || changes.isEmpty()) { throw new DataException("未找到文件名称变更流程关联记录"); } if (changes.size() > 1) { throw new DataException("多个文件名称变更申请关联同一流程任务"); } return changes.get(0); } private void verifyPersistedFlowState(Integer persistedState, Integer workflowState, DmsFlowEventType eventType) throws DataException { if (eventType == null) { throw new DataException("流程事件类型不能为空"); } if (!Objects.equals(persistedState, workflowState)) { throw new DataException("流程状态尚未同步完成:表单状态=" + persistedState + ",任务状态=" + workflowState + ",请稍后重试"); } } private Action resolveAction(Integer flowState) throws DataException { return DmsWenjianBianhaoFlowStateResolver.resolve(flowState) .orElseThrow(() -> new DataException("未知的工作流状态:" + flowState)); } private void freezeSnapshots(DmsWenjianBianhaoShenqingEntity application, List numbers) throws DataException { String applicantId = requireText(application.getShenqingRenId(), "编号申请人不能为空"); for (DmsWenjianBianhaoEntity number : numbers) { if (!blank(number.getWenjianBianhao())) { throw new DataException("已发放的文件编号不能重新提交:" + number.getWenjianBianhao()); } String originalName = requireText(number.getYuanWenjianMingcheng(), "文件名称不能为空"); requirePermission(applicantId, number.getGuizeId()); RuleSnapshot snapshot = loadSnapshot(application.getTenantId(), number.getGuizeId(), number.getZidingyiNeirong(), true); number.setGuizeKuaizhaoId(number.getGuizeId()); number.setGuizeMingchengKuaizhao(snapshot.ruleName); // 目录已在填写编号申请时保存,提交不能用规则的新目录覆盖。 String folderId = requireText(number.getWenjianMuluId(), "编号申请缺少目录,请重新选择目录"); ObjectNode frozen = snapshotFields(snapshot.json); frozen.put("wenjian_mulu_id", folderId); number.setGuizeKuaizhao(frozen.toString()); number.setBianhaoMuban(snapshot.template); number.setYulanBianhao(snapshot.preview); number.setShenqingBianhao(snapshot.preview); if (blank(number.getShiyongRenId())) { number.setShiyongRenId(applicantId); } number.setDangqianWenjianMingcheng(originalName); number.setMingchengBanben(1); number.setBizStatus(PENDING_REVIEW); requireSingleUpdate(numberMapper.updateById(number), "冻结文件编号规则快照失败"); } } private boolean hasReusableSnapshot(DmsWenjianBianhaoEntity number) throws DataException { return PENDING_REVIEW.equals(number.getBizStatus()) && !blank(number.getWenjianMuluId()) && number.getGuizeKuaizhao() != null && number.getGuizeKuaizhao().contains("\"wenjian_mulu_id\"") && !blank(number.getGuizeKuaizhao()) && !blank(number.getBianhaoMuban()) && Objects.equals(number.getGuizeKuaizhaoId(), number.getGuizeId()) && customSnapshotMatches(number); } private void issueNumbers(String tenantId, List numbers) throws DataException { // 一张申请可能使用多个流水组;统一获取顺序,避免并发申请反向持锁。 Map groups = new IdentityHashMap<>(); List unissued = new ArrayList<>(); for (DmsWenjianBianhaoEntity number : numbers) { if (!blank(number.getWenjianBianhao())) continue; if (!PENDING_REVIEW.equals(number.getBizStatus()) || blank(number.getGuizeKuaizhao()) || blank(number.getBianhaoMuban()) || blank(number.getGuizeKuaizhaoId())) { throw new DataException("文件编号缺少发起时冻结的规则快照,不能发号:" + number.getId()); } groups.put(number, SERIAL_TOKEN.matcher(number.getBianhaoMuban()).find() ? sequenceGroup(tenantId, number) : new SequenceGroup("", "")); unissued.add(number); } // List.sort 为稳定排序,同组仍按明细原顺序发号。 unissued.sort(Comparator.comparing((DmsWenjianBianhaoEntity n) -> groups.get(n).family) .thenComparing(n -> groups.get(n).key)); Map assignedNumbers = new IdentityHashMap<>(); for (DmsWenjianBianhaoEntity number : unissued) { assignedNumbers.put(number, renderFinalNumber(tenantId, number, groups.get(number))); } for (DmsWenjianBianhaoEntity number : numbers) { if (!blank(number.getWenjianBianhao())) { boolean changed = false; if (!Objects.equals(number.getShenqingBianhao(), number.getWenjianBianhao())) { number.setShenqingBianhao(number.getWenjianBianhao()); changed = true; } if (!USING.equals(number.getBizStatus())) { number.setBizStatus(USING); changed = true; } if (changed) { requireSingleUpdate(numberMapper.updateById(number), "恢复已发放文件编号状态失败"); } continue; } if (!PENDING_REVIEW.equals(number.getBizStatus()) || blank(number.getGuizeKuaizhao()) || blank(number.getBianhaoMuban()) || blank(number.getGuizeKuaizhaoId())) { throw new DataException("文件编号缺少发起时冻结的规则快照,不能发号:" + number.getId()); } AssignedNumber assigned = assignedNumbers.get(number); number.setWenjianBianhao(assigned.number); number.setShenqingBianhao(assigned.number); number.setLiushuiHao(assigned.serial); number.setBianhaoFafangShijian(new Date()); number.setBizStatus(USING); requireSingleUpdate(numberMapper.updateById(number), "写入正式文件编号失败"); } } private AssignedNumber renderFinalNumber(String tenantId, DmsWenjianBianhaoEntity number, SequenceGroup group) throws DataException { String template = number.getBianhaoMuban(); Matcher matcher = SERIAL_TOKEN.matcher(template); if (!matcher.find()) { return new AssignedNumber(template, null); } int start = matcher.start(); int end = matcher.end(); int width = parseSerialWidth(matcher.group(1)); if (matcher.find()) { throw new DataException("一个编号规则只能配置一个流水号组成项"); } Long value = counterMapper.next(tenantId, group.family, group.key, frozenSerialStart(number.getGuizeKuaizhao())); if (value == null) { throw new DataException("流水号分配失败"); } String serial = leftPad(value, width); return new AssignedNumber(template.substring(0, start) + serial + template.substring(end), serial); } private SequenceGroup sequenceGroup(String tenantId, DmsWenjianBianhaoEntity number) throws DataException { ObjectNode snapshot = snapshotFields(number.getGuizeKuaizhao()); String family = trim(snapshot.path("guize_liushuihao").asText("")); String nested = trim(snapshot.path("rule").path("guize_liushuihao").asText("")); if (!family.isEmpty() && !nested.isEmpty() && !family.equals(nested)) { throw new DataException("冻结规则流水号不一致,不能发号:" + number.getId()); } if (family.isEmpty()) family = nested; if (family.isEmpty()) { // 仅兼容缺失标识的旧快照,包含停用/软删除版本,绝不查询最新版本。 DmsWenjianBianhaoGuizeEntity historical = ruleMapper.selectHistoricalById( tenantId, number.getGuizeKuaizhaoId()); family = historical == null ? "" : trim(historical.getGuizeLiushuihao()); } if (family.isEmpty()) { throw new DataException("冻结规则缺少规则流水号,需核实历史归属后发号:" + number.getId()); } // 自定义内容已在发起时代入模板;组成方式和版本不改变流水归属。 return new SequenceGroup(family, "template:" + number.getBianhaoMuban()); } private static final class SequenceGroup { private final String family; private final String key; private SequenceGroup(String family, String key) { this.family = family; this.key = key; } } private void changeUnissuedStatus(List numbers, String targetStatus) throws DataException { for (DmsWenjianBianhaoEntity number : numbers) { if (!blank(number.getWenjianBianhao()) || USING.equals(number.getBizStatus())) { continue; } if (targetStatus.equals(number.getBizStatus())) { continue; } number.setBizStatus(targetStatus); requireSingleUpdate(numberMapper.updateById(number), "更新文件编号状态失败"); } } private void revokeUnissuedNumbers(List numbers) throws DataException { for (DmsWenjianBianhaoEntity number : numbers) { if (!blank(number.getWenjianBianhao()) || USING.equals(number.getBizStatus())) { log.warn("已发放编号关联流程被撤销,保留编号并等待人工处理,numberId={}, number={}", number.getId(), number.getWenjianBianhao()); continue; } if (!CANCELLED.equals(number.getBizStatus())) { number.setBizStatus(CANCELLED); requireSingleUpdate(numberMapper.updateById(number), "撤销文件编号失败"); } } } private void freezeNameChange(String tenantId, DmsWenjianMingchengBiangengShenqingEntity change) throws DataException { String numberId = requireText(change.getBianhaoId(), "文件编号ID不能为空"); requireText(change.getBiangengHouWenjianMingcheng(), "变更后文件名称不能为空"); DmsWenjianBianhaoEntity number = numberMapper.selectActiveByIdForUpdate(tenantId, numberId); assertNameChangeAllowed(tenantId, numberId); if (number == null || !USING.equals(number.getBizStatus()) || blank(number.getWenjianBianhao())) { throw new DataException("文件编号不存在或尚未发放"); } if (nameChangeMapper.countActiveByNumber(tenantId, numberId, change.getId()) > 0) { throw new DataException("该文件编号已有进行中的名称变更申请,请勿重复提交"); } change.setYuanWenjianMingcheng(number.getDangqianWenjianMingcheng()); change.setYuanMingchengBanben(currentNameVersion(number)); requireSingleUpdate(nameChangeMapper.updateById(change), "冻结文件名称版本失败"); } private void completeNameChange(String tenantId, DmsWenjianMingchengBiangengShenqingEntity change) throws DataException { String numberId = requireText(change.getBianhaoId(), "文件编号ID不能为空"); String newName = requireText(change.getBiangengHouWenjianMingcheng(), "变更后文件名称不能为空"); if (change.getYuanMingchengBanben() == null) { throw new DataException("名称变更申请缺少发起时冻结的名称版本"); } DmsWenjianBianhaoEntity number = numberMapper.selectActiveByIdForUpdate(tenantId, numberId); assertNameChangeAllowed(tenantId, numberId); if (number == null || !USING.equals(number.getBizStatus())) { throw new DataException("文件编号不存在或尚未发放"); } int currentVersion = currentNameVersion(number); int expectedVersion = change.getYuanMingchengBanben(); if (currentVersion == expectedVersion + 1 && Objects.equals(number.getDangqianWenjianMingcheng(), newName)) { return; } if (currentVersion != expectedVersion) { throw new DataException("文件名称已被其他变更流程更新,请重新发起"); } number.setDangqianWenjianMingcheng(newName); number.setMingchengBanben(currentVersion + 1); requireSingleUpdate(numberMapper.updateById(number), "更新文件名称失败"); } private void assertNameChangeAllowed(String tenantId, String numberId) throws DataException { if (numberMapper.numberAlreadyUsed(tenantId, numberId)) { throw new DataException("文件编号已被使用(包括审核草稿),不能变更文件名称"); } } private int currentNameVersion(DmsWenjianBianhaoEntity number) { return number.getMingchengBanben() == null ? 1 : number.getMingchengBanben(); } private boolean customSnapshotMatches(DmsWenjianBianhaoEntity number) throws DataException { if (blank(number.getGuizeKuaizhao())) return true; ObjectNode snapshot = snapshotFields(number.getGuizeKuaizhao()); return !snapshot.path("hasCustom").asBoolean(false) || Objects.equals(snapshot.path("customValue").asText(""), trim(number.getZidingyiNeirong())); } private RuleSnapshot loadSnapshot(String tenantId, String ruleId) throws DataException { return loadSnapshot(tenantId, ruleId, null, false); } private RuleSnapshot loadSnapshot(String tenantId, String ruleId, String customValue, boolean requireCustom) throws DataException { String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空"); String resolvedTenantId = requireText(tenantId, "租户ID不能为空"); DmsWenjianBianhaoGuizeEntity rule = ruleMapper.selectActiveById( resolvedTenantId, resolvedRuleId); if (rule == null || !ENABLED.equals(rule.getBizEnabled()) || !Objects.equals(APPROVED_FLOW_STATE, rule.getFlowState())) { throw new DataException("文件编号规则不存在或不可用,请重新选择已启用且审批通过的规则"); } List parts = partMapper.selectActiveByRule( resolvedTenantId, resolvedRuleId); if (parts == null || parts.isEmpty()) { throw new DataException("文件编号规则未配置明细"); } StringBuilder template = new StringBuilder(); ObjectNode rawRule = snapshotFields(rule.getSnapshotData()); ObjectNode json = SNAPSHOT_MAPPER.createObjectNode(); // 原始行完整保留;兼容键继续供已有申请/审核逻辑读取。 json.set("rule", rawRule); json.put("ruleId", resolvedRuleId); json.put("ruleName", trim(rule.getGuizeMingcheng())); json.put("ruleType", trim(rule.getGuizeLeixing())); json.put("banbenhao", rule.getBanbenhao()); json.set("guize_liushuihao", rawRule.get("guize_liushuihao")); json.put("changqu", rule.getChangqu()); json.put("qianpiye_id", rule.getQianpiyeId()); json.put("qianpiye_files", rule.getQianpiyeFiles()); ArrayNode snapshotParts = json.putArray("parts"); boolean hasSerial = false; boolean hasCustom = false; String customHint = ""; String serialPreview = ""; for (int i = 0; i < parts.size(); i++) { DmsWenjianBianhaoGuizeZixiangEntity part = parts.get(i); String type = trim(part.getBianhaoZuheLeixing()); String value = trim(part.getZhi()); String separator = trim(part.getFengefu()); if (isSerialType(type)) { if (hasSerial) { throw new DataException("一个编号规则只能配置一个流水号组成项"); } long serialStart = parseSerialStart(value); serialPreview = value; int width = value.length(); json.put("serialStart", serialStart); template.append("{SERIAL:").append(width).append('}'); hasSerial = true; } else if ("zidingyi".equalsIgnoreCase(type)) { if (hasCustom) throw new DataException("一个编号规则只能配置一个自定义组成项"); hasCustom = true; customHint = value; value = trim(customValue); if (requireCustom && value.isEmpty()) throw new DataException("请填写自定义编号内容"); if (value.contains("{SERIAL:")) throw new DataException("自定义编号内容不能包含流水号模板标记"); if (value.length() > 255) throw new DataException("自定义编号内容不能超过255个字符"); json.put("customValue", value); template.append(value.isEmpty() ? "待填写" : value); } else { template.append(value); } template.append(separator); ObjectNode snapshotPart = snapshotParts.addObject(); snapshotPart.put("order", i + 1); snapshotPart.put("id", trim(part.getId())); snapshotPart.put("type", type); snapshotPart.put("value", value); snapshotPart.put("separator", separator); snapshotPart.set("fields", snapshotFields(part.getSnapshotData())); } json.put("hasCustom", hasCustom); String preview = SERIAL_TOKEN.matcher(template).replaceAll(serialPreview); if (hasCustom && template.length() > 255) throw new DataException("完整编号模板不能超过255个字符"); return new RuleSnapshot(rule.getGuizeMingcheng(), template.toString(), preview, hasSerial, hasCustom, customHint, json.toString()); } private ObjectNode snapshotFields(String source) throws DataException { if (!blank(source)) { try { JsonNode fields = SNAPSHOT_MAPPER.readTree(source); if (fields instanceof ObjectNode) { return (ObjectNode) fields; } } catch (JsonProcessingException ignored) { // 不输出原始字段,避免把附件或签名内容写入异常日志。 } } throw new DataException("文件编号规则完整快照读取失败,请重新选择规则后提交"); } private void requirePermission(String userId, String ruleId) throws DataException { String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空"); if (!permissionService.canApplyRule(userId, resolvedRuleId)) { throw new DataException("无权申请该文件编号规则"); } } private long parseSerialStart(String value) throws DataException { if (blank(value) || !value.matches("[0-9]+")) { throw new DataException("流水号模板必须由数字组成,位数决定补零长度,非零值为起始号"); } try { return Math.max(1L, Long.parseLong(value)); } catch (NumberFormatException exception) { throw new DataException("流水号起始值超出支持范围"); } } private long frozenSerialStart(String snapshot) throws DataException { try { JsonNode root = SNAPSHOT_MAPPER.readTree(snapshot); if (root != null && !root.has("serialStart")) { return 1L; // 历史快照沿用原来从1开始的发号规则。 } JsonNode value = root == null ? null : root.get("serialStart"); if (value != null && value.isIntegralNumber() && value.canConvertToLong() && value.longValue() > 0) { return value.longValue(); } } catch (JsonProcessingException exception) { throw new DataException("流水号起始值快照无效"); } throw new DataException("流水号起始值快照无效"); } private boolean isSerialType(String type) { return "liushuihao".equalsIgnoreCase(type) || "流水号".equals(type); } private int parseSerialWidth(String width) throws DataException { try { int value = Integer.parseInt(width); if (value < 1) { throw new NumberFormatException(); } return value; } catch (NumberFormatException e) { throw new DataException("流水号模板长度无效:" + width); } } private String leftPad(Long value, int width) throws DataException { String text = String.valueOf(value); if (text.length() > width) { throw new DataException("流水号超过规则配置长度"); } return String.format("%0" + width + "d", value); } private void requireSingleUpdate(int affectedRows, String message) throws DataException { if (affectedRows != 1) { throw new DataException(message + ",请刷新后重试"); } } private String requireText(String value, String message) throws DataException { String resolved = trim(value); if (resolved.isEmpty()) { throw new DataException(message); } return resolved; } private boolean blank(String value) { return trim(value).isEmpty(); } private String trim(String value) { return value == null ? "" : value.trim(); } private static final class RuleSnapshot { private final String ruleName; private final String template; private final String preview; private final boolean hasSerial; private final boolean hasCustom; private final String customHint; private final String json; private RuleSnapshot(String ruleName, String template, String preview, boolean hasSerial, boolean hasCustom, String customHint, String json) { this.ruleName = ruleName; this.template = template; this.preview = preview; this.hasSerial = hasSerial; this.hasCustom = hasCustom; this.customHint = customHint; this.json = json; } } private static final class AssignedNumber { private final String number; private final String serial; private AssignedNumber(String number, String serial) { this.number = number; this.serial = serial; } } }