From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/impl/DmsWenjianBianhaoApplicationServiceImpl.java |  650 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 650 insertions(+), 0 deletions(-)

diff --git a/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/impl/DmsWenjianBianhaoApplicationServiceImpl.java b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/impl/DmsWenjianBianhaoApplicationServiceImpl.java
new file mode 100644
index 0000000..9e9fa25
--- /dev/null
+++ b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsService/impl/DmsWenjianBianhaoApplicationServiceImpl.java
@@ -0,0 +1,650 @@
+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<DmsWenjianBianhaoRuleVO> enabledRules(
+            Pagination pagination, String guizeId, String searchValue) throws DataException {
+        UserInfo user = identity.requireUser();
+        String tenantId = identity.tenantId(user);
+        List<DmsWenjianBianhaoRuleVO> result = new ArrayList<>();
+        List<DmsWenjianBianhaoGuizeEntity> 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<DmsWenjianBianhaoOptionVO> 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<DmsWenjianBianhaoEntity> numbers = numberMapper.selectUsing(
+                tenantId, trim(bianhaoId), pagination);
+        if (numbers == null || numbers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<DmsWenjianBianhaoEntity> allowedNumbers = new ArrayList<>(numbers.size());
+        for (DmsWenjianBianhaoEntity number : numbers) {
+            if (permissionService.canViewNumber(user.getUserId(), number.getId())) {
+                allowedNumbers.add(number);
+            }
+        }
+        Map<String, String> userNames = loadUserNames(allowedNumbers);
+        List<DmsWenjianBianhaoOptionVO> 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<String, String> loadUserNames(List<DmsWenjianBianhaoEntity> numbers) {
+        List<String> 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<UserEntity> users = userApi.getUserName(userIds);
+        if (users == null || users.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> 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<DmsWenjianBianhaoEntity> 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("鏂囦欢缂栧彿鐢宠娴佺▼浜嬩欢鍚屾瀹屾垚锛宖lowId={}, 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("鏂囦欢鍚嶇О鍙樻洿娴佺▼浜嬩欢鍚屾瀹屾垚锛宖lowId={}, taskId={}, event={}, flowState={}, action={}",
+                resolvedFlowId, resolvedTaskId, eventType, flowState, action);
+    }
+
+    private DmsWenjianBianhaoShenqingEntity findApplicationForUpdate(
+            String tenantId, String flowId, String taskId) throws DataException {
+        List<DmsWenjianBianhaoShenqingEntity> 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<DmsWenjianMingchengBiangengShenqingEntity> 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<DmsWenjianBianhaoEntity> 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<DmsWenjianBianhaoEntity> 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), "鍐欏叆姝e紡鏂囦欢缂栧彿澶辫触");
+        }
+    }
+
+    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<DmsWenjianBianhaoEntity> 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<DmsWenjianBianhaoEntity> numbers) throws DataException {
+        for (DmsWenjianBianhaoEntity number : numbers) {
+            if (!blank(number.getWenjianBianhao()) || USING.equals(number.getBizStatus())) {
+                log.warn("宸插彂鏀剧紪鍙峰叧鑱旀祦绋嬭鎾ら攢锛屼繚鐣欑紪鍙峰苟绛夊緟浜哄伐澶勭悊锛宯umberId={}, 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<DmsWenjianBianhaoGuizeZixiangEntity> 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;
+        }
+    }
+}

--
Gitblit v1.8.0