From 5072520172e8f45e3ccb8afc26cf0c8186ba4cdd Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 16:31:42 +0800
Subject: [PATCH] refactor(mes): 重构质量管理和批次绑定功能

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java |  153 +++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 136 insertions(+), 17 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
index 2a3ba96..386ab71 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -22,11 +22,14 @@
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcApproveNodeEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcApprovalTypeEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
 import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
+import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService;
 import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
 import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
 import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -52,9 +55,10 @@
 @Service @Validated @Slf4j
 public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
     private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
-    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_COMPANY = 30,
-            RELEASED = 40, WAIT_PROCESS_FAIL = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
+    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_INITIAL_FAIL_REVIEW = 25, WAIT_COMPANY = 30,
+            RELEASED = 40, WAIT_INITIAL_RECHECK = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
     @Resource private MesQcSeedQualityMapper qualityMapper;
+    @Resource private MesQcApproverService approverService;
     @Resource private MesQcSeedQualityActionMapper actionMapper;
     @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
     @Resource private MesProBagCodeMapper bagCodeMapper;
@@ -210,8 +214,13 @@
                 .inspectedAt(LocalDateTime.now()).build();
         qualityMapper.insert(quality);
         for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
+            if (item == null || item.getCode() == null || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
+                throw new IllegalArgumentException("鍥涢」鎸囨爣鐨勫疄娴嬪�煎拰鏈鍚堟牸鑼冨洿蹇呴』瀹屾暣");
             if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("鎸囨爣蹇呴』涓虹函搴︺�佸噣搴︺�佸彂鑺界巼銆佹按鍒嗕笖涓嶅彲閲嶅");
             if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺");
+            if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0
+                    || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0)
+                throw new IllegalArgumentException("鐧惧垎姣旀寚鏍囧繀椤诲浜� 0鈥�100");
             if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
             indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
                     .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
@@ -219,10 +228,10 @@
         if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("鍥涢」鎸囨爣涓嶅畬鏁�");
         if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("璐ㄦ涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
         quality.setResult(passed ? 1 : 2);
-        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST);
+        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW);
         qualityMapper.updateById(quality);
         recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators());
-        // 鍒濇浠呰褰曠粨璁猴紱琚嬩粛鐣欏湪鏆傚瓨搴擄紝寰呭妫�鍙婁袱绾у鎵瑰悗鍐嶆斁琛屾垨鎶ュ簾銆�
+        // 鍒濇浠呰褰曠粨璁猴紱琚嬩粛鐣欏湪鏆傚瓨搴擄紝寰呭垵妫�瀹℃牳銆佸妫�鍙婃渶缁堝鏍稿悗鍐嶆斁琛屾垨鎶ュ簾銆�
         return quality.getId();
     }
 
@@ -310,15 +319,24 @@
 
     @Override @Transactional(rollbackFor = Exception.class)
     public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
-        MesQcSeedQualityDO quality = requireStatus(id, WAIT_RETEST);
-        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("澶嶆琚嬬爜涓庡師璐ㄦ璁板綍涓嶄竴鑷�");
+        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+        if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳芥彁浜ゅ妫�锛岃鍒锋柊鍚庨噸璇�");
+        boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK);
+        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("妫�楠岃鐮佷笌鍘熻川妫�璁板綍涓嶄竴鑷�");
         boolean passed = validateIndicators(req.getIndicators());
-        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("澶嶆涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
-        recordAction(id, "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
-        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
-                .reason(passed ? "" : req.getReason().trim()).retestResult(passed ? 1 : 2).retestedAt(LocalDateTime.now())
+        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("妫�楠屼笉鍚堟牸蹇呴』濉啓鍘熷洜");
+        recordAction(id, initialRecheck ? "INITIAL_RECHECK" : "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
+        MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
+                .reason(passed ? "" : req.getReason().trim())
                 .inspectorId(getLoginUserId()).inspectedAt(LocalDateTime.now())
-                .workflowStatus(passed ? WAIT_PROCESS : WAIT_PROCESS_FAIL).build());
+                .workflowStatus(initialRecheck ? (passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW)
+                        : (passed ? WAIT_COMPANY : WAIT_COMPANY_FAIL)).build();
+        if (!initialRecheck) {
+            update.setRetestResult(passed ? 1 : 2);
+            update.setRetestedAt(LocalDateTime.now());
+        }
+        qualityMapper.updateById(update);
         // 椤甸潰銆佸叕寮�杩芥函璇诲彇涓绘寚鏍囪〃锛涘悓姝ヤ负鏈�鏂颁竴娆℃祴鍊硷紝鍘嗘娴嬪�煎湪鎿嶄綔鐣欑棔琛ㄤ繚鐣欍��
         Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>();
         for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id))
@@ -332,12 +350,108 @@
     }
 
     @Override @Transactional(rollbackFor = Exception.class)
+    public void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO req) {
+        List<Long> ids = approvalIds(req.getIds());
+        String batchCode = null;
+        Integer workflowStatus = null;
+        boolean first = true;
+        List<MesQcSeedQualityDO> qualities = new ArrayList<>();
+        for (Long id : ids) {
+            MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+            if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
+                throw new IllegalArgumentException("鎵�閫夎褰曞寘鍚笉鍙楠岀殑鐘舵�侊紝璇峰埛鏂板悗閲嶈瘯");
+            if (StrUtil.isBlank(quality.getBatchCode()))
+                throw new IllegalArgumentException("璁板綍缂哄皯鐢熶骇鎵规锛屼笉鑳芥壒閲忚川妫�");
+            if (first) {
+                batchCode = quality.getBatchCode();
+                workflowStatus = quality.getWorkflowStatus();
+                first = false;
+            } else if (!Objects.equals(batchCode, quality.getBatchCode()) || !Objects.equals(workflowStatus, quality.getWorkflowStatus())) {
+                throw new IllegalArgumentException("鎵归噺璐ㄦ鍙厑璁稿悓鐢熶骇鎵规銆佸悓娴佺▼鐘舵�佺殑璁板綍");
+            }
+            qualities.add(quality);
+        }
+        for (MesQcSeedQualityDO quality : qualities) {
+            cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO single =
+                    new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
+            single.setCode(quality.getCode());
+            single.setIndicators(req.getIndicators());
+            single.setReason(req.getReason());
+            retest(quality.getId(), single);
+        }
+    }
+
+    private void checkRetestScanType(String scanType) {
+        if (!Set.of("PALLET", "BATCH").contains(scanType))
+            throw new IllegalArgumentException("鎵归噺璐ㄦ鍙敮鎸佹墭鐩樼爜鎴栫敓浜ф壒娆$爜");
+    }
+
+    @Override
+    public Map<String, Object> resolveRetestScope(String code, String scanType) {
+        checkRetestScanType(scanType);
+        Scope scope = scope(code, scanType, false);
+        if (scope.canceled()) throw new IllegalArgumentException("璐ㄦ瀵硅薄宸蹭綔搴�");
+        List<String> codes = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
+                .map(MesProBagCodeDO::getCode).toList();
+        List<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes);
+        List<Long> retestIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_RETEST))
+                .map(MesQcSeedQualityDO::getId).sorted().toList();
+        List<Long> initialRecheckIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_RECHECK))
+                .map(MesQcSeedQualityDO::getId).sorted().toList();
+        long pendingProcessCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS)
+                || Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW)).count();
+        long pendingCompanyCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY)
+                || Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL)).count();
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", scope.code()); result.put("scanType", scanType); result.put("batchCode", scope.batchCode());
+        result.put("totalCount", codes.size()); result.put("retestIds", retestIds);
+        result.put("initialRecheckIds", initialRecheckIds);
+        result.put("pendingProcessCount", pendingProcessCount);
+        result.put("pendingCompanyCount", pendingCompanyCount);
+        return result;
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO req) {
+        checkRetestScanType(req.getScanType());
+        if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus()))
+            throw new IllegalArgumentException("鎵归噺璐ㄦ鐘舵�佹棤鏁�");
+        List<Long> confirmed = approvalIds(req.getQualityIds());
+        Scope scope = scope(req.getCode(), req.getScanType(), true);
+        if (scope.canceled()) throw new IllegalArgumentException("璐ㄦ瀵硅薄宸蹭綔搴�");
+        List<MesProBagCodeDO> effective = scope.bags().stream()
+                .filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null).toList();
+        if (effective.isEmpty()) throw new IllegalArgumentException("褰撳墠鑼冨洿娌℃湁宸插叆搴撶殑鏈夋晥琚嬬爜");
+        List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList());
+        if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
+            throw new IllegalArgumentException("琚嬬爜鐘舵�佸凡鍙樺寲锛岃閲嶆柊鎵弿");
+        Scope current = scope(req.getCode(), req.getScanType(), false);
+        Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
+                .map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
+        if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
+            throw new IllegalArgumentException("琚嬬爜褰掑睘宸插彉鍖栵紝璇烽噸鏂版壂鎻�");
+        List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList());
+        List<MesQcSeedQualityDO> pending = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus()))
+                .sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList();
+        if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesQcSeedQualityDO::getId).toList()))
+            throw new IllegalArgumentException("寰呰川妫�娓呭崟宸插彉鍖栵紝璇烽噸鏂版壂鎻忔牳瀵�");
+        for (MesQcSeedQualityDO quality : pending) {
+            var single = new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
+            single.setCode(quality.getCode()); single.setIndicators(req.getIndicators()); single.setReason(req.getReason());
+            retest(quality.getId(), single);
+        }
+        return Map.of("processedCount", pending.size());
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
     public void processApprove(Long id, boolean approved, String opinion) {
         MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
-        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_PROCESS_FAIL).contains(quality.getWorkflowStatus()))
-            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽姞宸ラ儴瀹℃壒");
-        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("璇峰~鍐欏鎵规剰瑙�");
-        int next = approved ? (quality.getWorkflowStatus() == WAIT_PROCESS ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
+        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽垵妫�瀹℃牳");
+        approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
+                MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
+        if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+        int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK;
         qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
                 .processApprovedAt(LocalDateTime.now()).build());
         recordAction(id, approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null);
@@ -347,8 +461,10 @@
     public void companyApprove(Long id, boolean approved, String opinion) {
         MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
         if (quality == null || !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
-            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽叕鍙歌川妫�瀹℃壒");
-        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("璇峰~鍐欏鎵规剰瑙�");
+            throw new IllegalArgumentException("褰撳墠鐘舵�佷笉鑳借繘琛屽妫�瀹℃牳");
+        approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
+                MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId());
+        if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
         if (approved) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY);
         int next = approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST;
         qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
@@ -390,6 +506,9 @@
                     || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
                 throw new IllegalArgumentException("鎸囨爣蹇呴』涓虹函搴︺�佸噣搴︺�佸彂鑺界巼銆佹按鍒嗕笖涓嶅彲閲嶅");
             if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺");
+            if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0
+                    || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0)
+                throw new IllegalArgumentException("鐧惧垎姣旀寚鏍囧繀椤诲浜� 0鈥�100");
             if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
         }
         return passed;

--
Gitblit v1.9.3