package jnpf.dmsService.impl; 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.wenjianbianhao.DmsFlowEventType; 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.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.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @Slf4j @Service @RequiredArgsConstructor public class DmsWenjianBianhaoApplicationServiceImpl implements DmsWenjianBianhaoApplicationService { private static final String DISABLED = "disabled"; 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 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 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; } DmsWenjianBianhaoRuleVO vo = new DmsWenjianBianhaoRuleVO(); vo.setId(rule.getId()); vo.setGuizeMingcheng(rule.getGuizeMingcheng()); vo.setGuizeLeixing(rule.getGuizeLeixing()); vo.setWenjianMulu(rule.getWenjianMulu()); RuleSnapshot snapshot = loadSnapshot(tenantId, rule.getId()); 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) throws DataException { UserInfo user = identity.requireUser(); requirePermission(user.getUserId(), guizeId); RuleSnapshot snapshot = loadSnapshot(identity.tenantId(user), guizeId); return new DmsWenjianBianhaoPreviewVO(guizeId, snapshot.preview, snapshot.hasSerial); } @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(); } 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()); 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 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; } 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(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(), "文件名称不能为空"); if (hasReusableSnapshot(number)) { refreshUnissuedBasicData(number, applicantId, originalName); continue; } requirePermission(applicantId, number.getGuizeId()); RuleSnapshot snapshot = loadSnapshot(application.getTenantId(), number.getGuizeId()); number.setGuizeKuaizhaoId(number.getGuizeId()); number.setGuizeMingchengKuaizhao(snapshot.ruleName); number.setGuizeKuaizhao(snapshot.json); 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) { return PENDING_REVIEW.equals(number.getBizStatus()) && !blank(number.getGuizeKuaizhao()) && !blank(number.getBianhaoMuban()) && Objects.equals(number.getGuizeKuaizhaoId(), number.getGuizeId()); } private void refreshUnissuedBasicData(DmsWenjianBianhaoEntity number, String applicantId, String originalName) throws DataException { boolean changed = false; if (blank(number.getShiyongRenId())) { number.setShiyongRenId(applicantId); changed = true; } if (!Objects.equals(number.getDangqianWenjianMingcheng(), originalName) || !Integer.valueOf(1).equals(number.getMingchengBanben())) { number.setDangqianWenjianMingcheng(originalName); number.setMingchengBanben(1); changed = true; } if (!Objects.equals(number.getShenqingBianhao(), number.getYulanBianhao())) { number.setShenqingBianhao(number.getYulanBianhao()); changed = true; } if (changed) { requireSingleUpdate(numberMapper.updateById(number), "刷新文件编号申请基础数据失败"); } } private void issueNumbers(String tenantId, List numbers) throws DataException { 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 = renderFinalNumber(tenantId, 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) 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, number.getGuizeKuaizhaoId(), template); if (value == null) { throw new DataException("流水号分配失败"); } String serial = leftPad(value, width); return new AssignedNumber(template.substring(0, start) + serial + template.substring(end), serial); } 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); 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); 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 int currentNameVersion(DmsWenjianBianhaoEntity number) { return number.getMingchengBanben() == null ? 1 : number.getMingchengBanben(); } private RuleSnapshot loadSnapshot(String tenantId, String ruleId) throws DataException { String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空"); String resolvedTenantId = requireText(tenantId, "租户ID不能为空"); DmsWenjianBianhaoGuizeEntity rule = ruleMapper.selectActiveById( resolvedTenantId, resolvedRuleId); if (rule == null || DISABLED.equals(rule.getBizEnabled())) { throw new DataException("文件编号规则不存在或已停用"); } List parts = partMapper.selectActiveByRule( resolvedTenantId, resolvedRuleId); if (parts == null || parts.isEmpty()) { throw new DataException("文件编号规则未配置明细"); } StringBuilder template = new StringBuilder(); StringBuilder json = new StringBuilder(); json.append("{\"ruleId\":\"").append(escape(resolvedRuleId)) .append("\",\"ruleName\":\"").append(escape(rule.getGuizeMingcheng())) .append("\",\"ruleType\":\"").append(escape(rule.getGuizeLeixing())) .append("\",\"parts\":["); boolean hasSerial = false; 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("一个编号规则只能配置一个流水号组成项"); } int width = serialTemplateWidth(value); template.append("{SERIAL:").append(width).append('}'); hasSerial = true; } else { template.append(value); } template.append(separator); if (i > 0) { json.append(','); } json.append("{\"order\":").append(i + 1) .append(",\"id\":\"").append(escape(part.getId())) .append("\",\"type\":\"").append(escape(type)) .append("\",\"value\":\"").append(escape(value)) .append("\",\"separator\":\"").append(escape(separator)).append("\"}"); } json.append("]}"); String preview = SERIAL_TOKEN.matcher(template).replaceAll("待审"); return new RuleSnapshot(rule.getGuizeMingcheng(), template.toString(), preview, hasSerial, json.toString()); } private void requirePermission(String userId, String ruleId) throws DataException { String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空"); if (!permissionService.canApplyRule(userId, resolvedRuleId)) { throw new DataException("无权申请该文件编号规则"); } } private int serialTemplateWidth(String value) throws DataException { if (blank(value) || !value.matches("0+")) { throw new DataException("流水号模板必须由一个或多个0组成"); } return value.length(); } 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 String escape(String value) { return trim(value).replace("\\", "\\\\").replace("\"", "\\\""); } private static final class RuleSnapshot { private final String ruleName; private final String template; private final String preview; private final boolean hasSerial; private final String json; private RuleSnapshot(String ruleName, String template, String preview, boolean hasSerial, String json) { this.ruleName = ruleName; this.template = template; this.preview = preview; this.hasSerial = hasSerial; 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; } } }