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 |  591 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 573 insertions(+), 18 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 60b78c1..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
@@ -1,66 +1,616 @@
 package cn.iocoder.yudao.module.mes.service.qc.seedquality;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
 import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.*;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
+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.wm.batch.MesWmBatchService;
-import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
+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;
+import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
+import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
+import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.jdbc.BadSqlGrammarException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED;
 
 @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_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;
+    @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper;
+    @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper batchMapper;
     @Resource private MesProBatchService proBatchService;
-    @Resource private MesWmBatchService mesWmBatchService;
-    @Resource private MesWmMaterialStockService materialStockService;
+    @Resource private MesWmBatchMapper mesWmBatchMapper;
+    @Resource private MesWmMaterialStockMapper materialStockMapper;
+    @Resource private MesWmMaterialStockBagMapper materialStockBagMapper;
+    @Resource private MesWmMaterialStockBagService materialStockBagService;
+    @Resource private MesWmTransactionService wmTransactionService;
+    @Resource private MesWmWarehouseService warehouseService;
+    @Resource private MesWmWarehouseLocationService warehouseLocationService;
+    @Resource private MesWmWarehouseAreaService warehouseAreaService;
+
+    @Override
+    public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) {
+        if (StrUtil.isNotBlank(reqVO.getCode())) {
+            MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(reqVO.getCode());
+            if (bag != null) reqVO.setCode(bag.getCode());
+        }
+        return qualityMapper.selectPage(reqVO);
+    }
+
+    @Override
+    public Map<String, Object> resolveScan(String code) {
+        MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
+        if (bag == null) throw new IllegalArgumentException("鏈В鏋愬埌鏈夋晥琚嬬爜锛岃鎵弿琚嬬爜");
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", bag.getCode());
+        result.put("businessCode", bag.getBusinessCode());
+        result.put("batchCode", bag.getBatchCode());
+        result.put("inbound", bag.getInboundEventId() != null);
+        result.put("canceled", Integer.valueOf(30).equals(bag.getStatus()));
+        MesQcSeedQualityDO quality = qualityMapper.selectByCode(bag.getCode());
+        result.put("qualityId", quality == null ? null : quality.getId());
+        result.put("result", quality == null ? null : quality.getResult());
+        return result;
+    }
+
+    private record Scope(String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { }
+
+    private Scope scope(String code, String type, boolean lock) {
+        if ("BAG".equals(type)) {
+            MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
+            if (bag == null) throw new IllegalArgumentException("鏈壘鍒拌鐮侊紝璇锋牳瀵规壂鐮佺被鍨�");
+            return new Scope(bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(), Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
+        }
+        if ("PALLET".equals(type)) {
+            var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
+            if (pallet == null) throw new IllegalArgumentException("鏈壘鍒版墭鐩樼爜锛岃鏍稿鎵爜绫诲瀷");
+            if (pallet.getBatchId() == null) throw new IllegalArgumentException("鎵樼洏灏氭湭缁戝畾鐢熶骇鎵规锛屾棤娉曡川妫�");
+            return new Scope(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
+                    Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
+        }
+        if ("BATCH".equals(type)) {
+            var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
+            if (batch == null) throw new IllegalArgumentException("鏈壘鍒扮敓浜ф壒娆$爜锛岃鏍稿鎵爜绫诲瀷");
+            return new Scope(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
+                    Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
+        }
+        throw new IllegalArgumentException("涓嶆敮鎸佺殑璐ㄦ鎵爜绫诲瀷");
+    }
+
+    @Override
+    public Map<String, Object> resolveScan(String code, String scanType) {
+        Scope scope = scope(code, scanType, false);
+        List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
+        Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream()
+                .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
+        Map<String, Object> result = new HashMap<>();
+        result.put("scanType", scanType); result.put("code", scope.code()); result.put("businessCode", scope.businessCode());
+        result.put("batchCode", scope.batchCode()); result.put("canceled", scope.canceled());
+        result.put("totalCount", effective.size());
+        result.put("inbound", !effective.isEmpty() && effective.stream().allMatch(b -> b.getInboundEventId() != null));
+        result.put("pendingBagIds", effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
+        result.put("completedBagIds", effective.stream().filter(b -> inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
+        if ("BAG".equals(scanType)) {
+            MesQcSeedQualityDO quality = qualityMapper.selectByCode(scope.code());
+            result.put("qualityId", quality == null ? null : quality.getId()); result.put("result", quality == null ? null : quality.getResult());
+        }
+        return result;
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
+        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())).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())).map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
+        if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
+            throw new IllegalArgumentException("琚嬬爜褰掑睘宸插彉鍖栵紝璇烽噸鏂版壂鎻忔牳瀵�");
+        Set<String> inspected = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream()
+                .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
+        List<MesProBagCodeDO> pending = locked.stream().filter(b -> !inspected.contains(b.getCode())).sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
+        List<Long> confirmed = req.getBagIds().stream().sorted().toList();
+        if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesProBagCodeDO::getId).toList()))
+            throw new IllegalArgumentException("寰呰川妫�娓呭崟宸插彉鍖栨垨宸插叏閮ㄨ川妫�锛岃閲嶆柊鎵弿锛岀姝㈤噸澶嶅鐞�");
+        for (MesProBagCodeDO bag : pending) {
+            MesQcSeedQualitySubmitReqVO single = new MesQcSeedQualitySubmitReqVO();
+            single.setCode(bag.getCode());single.setIndicators(req.getIndicators());single.setReason(req.getReason());
+            single.setQualifiedWarehouseId(req.getQualifiedWarehouseId());
+            // 澶栧眰浜嬪姟瑕嗙洊鍏ㄩ儴琚嬬爜锛涗换浣曚竴琚嬪簱瀛樻祦杞け璐ラ兘浼氬洖婊氭暣娆℃搷浣溿��
+            submit(single);
+        }
+        return Map.of("processedCount", pending.size());
+    }
 
     @Override @Transactional(rollbackFor = Exception.class)
     public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
         if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("蹇呴』鎻愪氦鍥涢」鎸囨爣");
-        if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("璇ヨ鐮佸凡璐ㄦ锛岀姝㈤噸澶嶆彁浜�");
+        // 绉嶅瓙璐ㄦ蹇呴』浠ヨ鐮侊紙鍗曡锛夋彁浜わ紝鐗╂枡淇℃伅鐢辫鐮佽嚜鍔ㄨВ鏋愶紝鍓嶇涓嶅啀鍗曠嫭閫夋嫨鐗╂枡
+        MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(reqVO.getCode());
+        if (bag == null) {
+            throw new IllegalArgumentException("鏈В鏋愬埌鏈夋晥琚嬬爜锛岀瀛愯川妫�蹇呴』浠ヨ鐮佹彁浜�");
+        }
+        if (Integer.valueOf(30).equals(bag.getStatus()) || bag.getInboundEventId() == null)
+            throw new IllegalArgumentException("琚嬬爜宸蹭綔搴熸垨灏氭湭鍏ュ簱锛屾棤娉曡川妫�");
+        // 鍙岀爜閿佸畾鍚屼竴琚嬶紝骞朵互杩芥函鐮佽褰曠粨鏋滐紝闃叉鎹㈢爜鎴栧苟鍙戦噸澶嶆墸鍑忓簱瀛樸��
+        if (qualityMapper.selectByCodeForUpdate(bag.getCode()) != null) throw new IllegalArgumentException("璇ヨ鐮佸凡璐ㄦ锛岀姝㈤噸澶嶆彁浜�");
+        MesProBatchDO batch = bag.getBatchId() == null ? null : proBatchService.getBatch(bag.getBatchId());
+        if (batch == null || batch.getItemId() == null) {
+            throw new IllegalArgumentException("鏈牴鎹鐮佽В鏋愬埌鐗╂枡淇℃伅锛岃纭鎵爜鍐呭鎴栨壒娆″彿姝g‘");
+        }
+        if (Integer.valueOf(40).equals(batch.getStatus())) throw new IllegalArgumentException("鐢熶骇鎵规宸蹭綔搴燂紝鏃犳硶璐ㄦ");
+        // 琚嬬爜瀵瑰簲鎵规鐨勫簱瀛樺繀椤诲浜庢殏瀛樺簱锛堝緟璐ㄦ锛変腑锛屾墠鑳借繘琛岀瀛愯川妫�
+        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
+        if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0) {
+            throw new IllegalArgumentException("璇ヨ鐮�/鎵规瀵瑰簲鐨勫簱瀛樹笉鍦ㄦ殏瀛樺簱涓紝鏃犳硶杩涜璐ㄦ");
+        }
+        // 鏍¢獙鐩爣鍚堟牸搴擄細蹇呴』瀛樺湪涓斾粨搴撶被鍨嬩负鍚堟牸搴�(20)
+        if (warehouseService.getFirstQualifiedWarehouse() == null) {
+            throw exception(WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED);
+        }
+        MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(reqVO.getQualifiedWarehouseId());
+        if (qualifiedWarehouse == null) {
+            throw exception(WM_WAREHOUSE_NOT_EXISTS);
+        }
+        if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) {
+            throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
+        }
         Set<String> codes = new HashSet<>();
         boolean passed = true;
-        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(reqVO.getItemId())
-                .batchCode(reqVO.getBatchCode()).reason(reqVO.getReason()).result(0)
+        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId())
+                .batchCode(batch.getCode()).batchId(batch.getId()).reason(reqVO.getReason()).result(0)
+                .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(getLoginUserId())
                 .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 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
+            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());
         }
         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_INITIAL_FAIL_REVIEW);
         qualityMapper.updateById(quality);
-        // 璐ㄦ鍚堟牸 鈫� 搴撳瓨鏁存壒鐢辨殏瀛樺簱锛堝緟璐ㄦ锛夎浆涓哄悎鏍煎簱锛堟寮忓簱瀛橈級
+        recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators());
+        // 鍒濇浠呰褰曠粨璁猴紱琚嬩粛鐣欏湪鏆傚瓨搴擄紝寰呭垵妫�瀹℃牳銆佸妫�鍙婃渶缁堝鏍稿悗鍐嶆斁琛屾垨鎶ュ簾銆�
+        return quality.getId();
+    }
+
+    private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) {
+        MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(quality.getCode());
+        if (bag == null || bag.getInboundEventId() == null || !Objects.equals(bag.getQcStatus(), MesProBagCodeQcStatusEnum.UNCHECKED.getStatus()))
+            throw new IllegalArgumentException("琚嬬爜鐘舵�佸凡鍙樺寲锛岀姝㈤噸澶嶅簱瀛樻祦杞�");
+        MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
+        if (batch == null) throw new IllegalArgumentException("琚嬬爜鎵�灞炵敓浜ф壒娆′笉瀛樺湪");
+        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
+        if (wmBatch == null) throw new IllegalArgumentException("鏆傚瓨搴撳瓨鎵�灞炰粨搴撴壒娆′笉瀛樺湪");
+        MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(quality.getQualifiedWarehouseId());
+        if (passed && (qualifiedWarehouse == null || Boolean.TRUE.equals(qualifiedWarehouse.getFrozen())
+                || !MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())))
+            throw new IllegalArgumentException("鐩爣鍚堟牸搴撲笉瀛樺湪鎴栫被鍨嬪凡鍙樺寲");
+        Long qualifiedLocationId = null;
+        Long qualifiedAreaId = null;
         if (passed) {
-            MesProBatchDO proBatch = proBatchService.getBatchByCode(reqVO.getBatchCode());
-            if (proBatch != null) {
-                MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(proBatch);
-                if (wmBatch != null) {
-                    int updated = materialStockService.updateStockStateByBatchId(wmBatch.getId(),
-                            MesWmStockStateEnum.QUALIFIED.getState(), MesWmStockStateEnum.TEMP.getState());
-                    log.info("[submit][璐ㄦ鍚堟牸锛屾壒娆� {} 搴撳瓨鐢辨殏瀛樺簱杞悎鏍煎簱锛屽奖鍝� {} 鏉", reqVO.getBatchCode(), updated);
+            List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
+            if (CollUtil.isNotEmpty(locations)) {
+                qualifiedLocationId = locations.get(0).getId();
+                List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
+                if (CollUtil.isNotEmpty(areas)) qualifiedAreaId = areas.get(0).getId();
+            }
+        }
+        // 鎸夎鐮佺矑搴︽祦杞簱瀛橈細閿佸畾璇ヨ鍦ㄦ殏瀛樺簱涓殑娲诲姩鏄庣粏锛岀姝㈡寜鎵规浠绘剰鎵e噺
+        MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode());
+        MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag);
+        // A. 鏆傚瓨鍑哄簱锛�-1锛夛細璇ヨ绂诲紑鏆傚瓨搴擄紝鍐欏叆璐ㄦ鏆傚瓨鍑哄簱浜嬪姟锛堣烦杩囪鐮佽嚜鍔ㄧ淮鎶わ紝鐢辫川妫�缁熶竴鎺у埗琚嬬爜鏈�缁堣惤鐐癸級
+        MesWmTransactionSaveReqDTO tempOutReq = new MesWmTransactionSaveReqDTO()
+                .setType(MesWmTransactionTypeEnum.OUT.getType())
+                .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
+                .setBizId(quality.getId())
+                .setBizCode(quality.getCode())
+                .setBizLineId(quality.getId())
+                .setItemId(batch.getItemId())
+                .setQuantity(BigDecimal.ONE.negate())
+                .setBatchId(tempStock.getBatchId())
+                .setBatchCode(tempStock.getBatchCode())
+                .setBagCodeId(bag.getId())
+                .setBagCode(bag.getCode())
+                .setWarehouseId(tempStock.getWarehouseId())
+                .setLocationId(tempStock.getLocationId())
+                .setAreaId(tempStock.getAreaId())
+                .setVendorId(tempStock.getVendorId())
+                .setMaterialStockId(tempStock.getId())
+                .setCheckFlag(true)
+                .setSkipBagManage(true);
+        Long tempOutTxId = wmTransactionService.createTransaction(tempOutReq);
+        if (passed) {
+            // B1. 璐ㄦ鍚堟牸 鈫� 璇ヨ杞叆鎿嶄綔鍛橀�夋嫨鐨勫悎鏍煎簱锛屽啓鍏ュ悎鏍煎叆搴撲簨鍔★紙璺宠繃琚嬬爜鑷姩缁存姢锛�
+            MesWmMaterialStockDO qualifiedStock = getOrCreateQualifiedStock(tempStock, qualifiedWarehouse.getId(), qualifiedLocationId, qualifiedAreaId);
+            MesWmTransactionSaveReqDTO qualInReq = new MesWmTransactionSaveReqDTO()
+                    .setType(MesWmTransactionTypeEnum.IN.getType())
+                    .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
+                    .setBizId(quality.getId())
+                    .setBizCode(quality.getCode())
+                    .setBizLineId(quality.getId())
+                    .setItemId(batch.getItemId())
+                    .setQuantity(BigDecimal.ONE)
+                    .setBatchId(qualifiedStock.getBatchId())
+                    .setBatchCode(qualifiedStock.getBatchCode())
+                    .setBagCodeId(bag.getId())
+                    .setBagCode(bag.getCode())
+                    .setWarehouseId(qualifiedStock.getWarehouseId())
+                    .setLocationId(qualifiedStock.getLocationId())
+                    .setAreaId(qualifiedStock.getAreaId())
+                    .setVendorId(qualifiedStock.getVendorId())
+                    .setMaterialStockId(qualifiedStock.getId())
+                    .setCheckFlag(false)
+                    .setStockState(MesWmStockStateEnum.QUALIFIED.getState())
+                    .setSkipBagManage(true);
+            Long qualInTxId = wmTransactionService.createTransaction(qualInReq);
+            // B2. 鍚屾杩佺Щ琚嬬爜鏄庣粏鍒板悎鏍煎簱瀛樿惤鐐�
+            materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId);
+            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus());
+            log.info("[finalizeStock][瀹℃壒鏀捐锛岃鐮� {} 鐢辨殏瀛樺簱杞悎鏍煎簱 {}]", bag.getCode(), qualifiedWarehouse.getName());
+        } else {
+            // C. 璐ㄦ涓嶅悎鏍� 鈫� 璇ヨ鎶ュ簾锛涙殏瀛樺嚭搴撲簨鍔″凡鎵e噺鏆傚瓨搴撳瓨锛屾爣璁拌鐮佹姤搴�
+            materialStockBagService.scrap(tempOutReq, tempOutTxId);
+            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus());
+            log.info("[finalizeStock][瀹℃壒鎶ュ簾锛岃鐮� {}]", bag.getCode());
+        }
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
+        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, 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(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))
+            existing.put(indicator.getIndicatorCode(), indicator);
+        for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
+            MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
+            if (indicator == null) throw new IllegalArgumentException("璐ㄦ鎸囨爣璁板綍缂哄け");
+            indicatorMapper.updateById(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
+                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
+        }
+    }
+
+    @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_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);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    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("褰撳墠鐘舵�佷笉鑳借繘琛屽妫�瀹℃牳");
+        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)
+                .companyApprovedAt(LocalDateTime.now()).build());
+        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void processApproveBatch(List<Long> ids, boolean approved, String opinion) {
+        for (Long id : approvalIds(ids)) processApprove(id, approved, opinion);
+    }
+
+    @Override @Transactional(rollbackFor = Exception.class)
+    public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) {
+        for (Long id : approvalIds(ids)) companyApprove(id, approved, opinion);
+    }
+
+    private List<Long> approvalIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty() || ids.size() > 500 || ids.stream().anyMatch(Objects::isNull))
+            throw new IllegalArgumentException("璇烽�夋嫨 1 鑷� 500 鏉¤川妫�璁板綍");
+        List<Long> sorted = ids.stream().distinct().sorted().toList();
+        if (sorted.size() != ids.size()) throw new IllegalArgumentException("璐ㄦ璁板綍涓嶈兘閲嶅閫夋嫨");
+        return sorted;
+    }
+
+    private MesQcSeedQualityDO requireStatus(Long id, int expected) {
+        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
+        if (quality == null || !Objects.equals(quality.getWorkflowStatus(), expected))
+            throw new IllegalArgumentException("璐ㄦ璁板綍鐘舵�佸凡鍙樺寲锛岃鍒锋柊鍚庨噸璇�");
+        return quality;
+    }
+
+    private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
+        if (indicators == null || indicators.size() != REQUIRED.size()) throw new IllegalArgumentException("蹇呴』鎻愪氦鍥涢」鎸囨爣");
+        Set<String> codes = new HashSet<>();
+        boolean passed = true;
+        for (MesQcSeedQualitySubmitReqVO.Indicator item : indicators) {
+            if (item == null || !REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())
+                    || 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;
+    }
+
+    private void recordAction(Long id, String action, Integer result, String opinion,
+                              List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
+        MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO();
+        event.setQualityId(id);
+        event.setAction(action);
+        event.setResult(result);
+        event.setOpinion(opinion);
+        event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators));
+        event.setOperatorId(getLoginUserId());
+        event.setCreateTime(LocalDateTime.now());
+        actionMapper.insert(event);
+    }
+
+    /**
+     * 瀹氫綅璇ヨ鐨勬殏瀛樺簱瀛樻眹鎬昏褰曪紙鍙畾浣嶏紝涓嶆墸鍑忥紱鎵e噺鐢辫川妫�鏆傚瓨鍑哄簱浜嬪姟瀹屾垚锛夈��
+     * <p>
+     * 浼樺厛涓ユ牸鎸夎鐮侀攣瀹氬叾娲诲姩鏄庣粏瀵瑰簲鐨勬殏瀛樿褰曪紱鍘嗗彶鏃犳槑缁嗘椂鍥為��鎸夋壒娆″畾浣嶅苟琛ュ缓鏄庣粏銆�
+     */
+    private MesWmMaterialStockDO resolveTempStock(MesProBagCodeDO bag, MesWmBatchDO wmBatch, MesWmMaterialStockBagDO stockBag) {
+        if (stockBag != null && stockBag.getMaterialStockId() != null) {
+            MesWmMaterialStockDO stock = materialStockMapper.selectById(stockBag.getMaterialStockId());
+            if (stock != null && MesWmStockStateEnum.TEMP.getState().equals(stock.getStockState())) {
+                return stock;
+            }
+        }
+        MesWmMaterialStockDO tempStock = materialStockMapper.selectStockListByBatchId(wmBatch.getId()).stream()
+                .filter(s -> MesWmStockStateEnum.TEMP.getState().equals(s.getStockState()))
+                .max(Comparator.comparing(MesWmMaterialStockDO::getQuantity))
+                .orElse(null);
+        if (tempStock == null) {
+            throw new IllegalArgumentException("璇ユ壒娆℃殏瀛樺簱瀛樹笉瓒筹紝鏃犳硶璐ㄦ");
+        }
+        getOrCreateStockBag(bag, tempStock);
+        return tempStock;
+    }
+
+    /**
+     * 璐ㄦ鍚堟牸锛氬皢璐ㄦ閫氳繃鐨勯偅琚嬭浆鍏ユ搷浣滃憳閫夋嫨鐨勫悎鏍煎簱锛堝悓缁勫悎閿悎鏍煎簱璁板綍 +1锛屼笉瀛樺湪鍒欐柊寤猴級銆�
+     *
+     * @param tempStock            浠庢殏瀛樺簱鎵e噺鍚庤繑鍥炵殑鏆傚瓨璁板綍锛堟惡甯� item/batch/vendor 缁村害锛�
+     * @param qualifiedWarehouseId 鐩爣鍚堟牸搴撲粨搴撶紪鍙�
+     * @param qualifiedLocationId  鐩爣鍚堟牸搴撳簱鍖虹紪鍙凤紙鏈厤缃垯涓� null锛�
+     * @param qualifiedAreaId      鐩爣鍚堟牸搴撳簱浣嶇紪鍙凤紙鏈厤缃垯涓� null锛�
+     */
+    private MesWmMaterialStockBagDO getOrCreateStockBag(MesProBagCodeDO bag, MesWmMaterialStockDO tempStock) {
+        MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectByBagCode(bag.getCode());
+        if (stockBag != null) {
+            return stockBag;
+        }
+        stockBag = MesWmMaterialStockBagDO.builder().materialStockId(tempStock.getId())
+                .bagCodeId(bag.getId()).bagCode(bag.getCode()).itemId(tempStock.getItemId())
+                .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId())
+                .warehouseId(tempStock.getWarehouseId()).locationId(tempStock.getLocationId()).areaId(tempStock.getAreaId())
+                .stockState(MesWmStockStateEnum.TEMP.getState()).status(1).quantity(BigDecimal.ONE)
+                .frozen(false).inboundTime(tempStock.getReceiptTime()).build();
+        materialStockBagMapper.insert(stockBag);
+        return stockBag;
+    }
+
+    private MesWmMaterialStockDO getOrCreateQualifiedStock(MesWmMaterialStockDO tempStock, Long qualifiedWarehouseId,
+                                         Long qualifiedLocationId, Long qualifiedAreaId) {
+        MesWmMaterialStockDO qualified = materialStockMapper.selectQualifiedByCompositeKey(
+                tempStock.getItemId(), qualifiedWarehouseId, qualifiedLocationId,
+                qualifiedAreaId, tempStock.getBatchId(), tempStock.getVendorId());
+        if (qualified != null) {
+            return qualified;
+        }
+        // 涓嶅瓨鍦ㄥ垯鏂板缓鍚堟牸搴撹褰曪紙浠撳簱/搴撳尯/搴撲綅閲囩敤鎵�閫夊悎鏍煎簱鐨勭淮搴︼紝鏁伴噺鐢辫川妫�鍏ュ簱浜嬪姟 +1锛�
+        MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder()
+                .itemId(tempStock.getItemId()).itemTypeId(tempStock.getItemTypeId())
+                .warehouseId(qualifiedWarehouseId).locationId(qualifiedLocationId).areaId(qualifiedAreaId)
+                .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId())
+                .quantity(BigDecimal.ZERO).receiptTime(LocalDateTime.now())
+                .stockState(MesWmStockStateEnum.QUALIFIED.getState())
+                .frozen(false)
+                .build();
+        materialStockMapper.insert(newStock);
+        return newStock;
+    }
+
+    /**
+     * 鐢辫鐮佹垨鐢熶骇鎵规瑙f瀽鎵�灞炵敓浜ф壒娆★紙琚嬬爜浼樺厛锛屾壒娆$爜鍏煎涓存椂鐮侊級
+     */
+    private MesProBatchDO resolveBatch(String code, String batchCode) {
+        if (StrUtil.isNotBlank(code)) {
+            MesProBagCodeDO bag = bagCodeMapper.selectByCode(code);
+            if (bag != null && bag.getBatchId() != null) {
+                MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
+                if (batch != null) {
+                    return batch;
                 }
             }
         }
-        // 搴撲綅鐢卞悗鍙伴厤缃湇鍔℃帴绠★紝褰撳墠鐗堟湰淇濈暀瀹¤闂幆锛岀姝㈠墠绔紶鍏ュ簱浣嶃��
-        return quality.getId();
+        if (StrUtil.isNotBlank(batchCode)) {
+            return proBatchService.getBatchByCode(batchCode);
+        }
+        return null;
     }
 
     @Override
@@ -84,4 +634,9 @@
             return List.of();
         }
     }
+
+    @Override
+    public List<MesQcSeedQualityActionDO> getActions(Long qualityId) {
+        return actionMapper.selectByQualityId(qualityId);
+    }
 }

--
Gitblit v1.9.3