From f02d0df2f1dfc7a8b775cf50d45375e57628a5ef Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 10 十一月 2026 11:26:22 +0800
Subject: [PATCH] feat(ncr): 完善NCR处置联动逻辑与关闭校验

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java |  168 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 155 insertions(+), 13 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
index 3f3bb69..e6b69ea 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
@@ -85,12 +85,29 @@
      */
     private static final Set<Integer> DISPOSITION_ALLOWED_TYPES = Set.of(
             MesNcrDispositionEnum.SCRAP.getType(),
+            MesNcrDispositionEnum.RETURN.getType(),
+            MesNcrDispositionEnum.REPRODUCTION.getType(),
             MesNcrDispositionEnum.REWORK.getType(),
             MesNcrDispositionEnum.CONCESSION.getType(),
             MesNcrDispositionEnum.REPAIR.getType());
 
     @Resource
     private MesQcNcrMapper ncrMapper;
+
+    @Resource
+    private MesQcNcrReinspectionValidator reinspectionValidator;
+
+    @Resource
+    private MesQcNcrStockClosureGuard stockClosureGuard;
+
+    @Resource private MesQcNcrScrapService scrapService;
+    @Resource private MesQcNcrReturnSalesLink returnSalesLink;
+    @Resource private MesQcPurchaseReturnService purchaseReturnService;
+    @Resource private MesQcPurchaseConcessionService purchaseConcessionService;
+    @Resource private MesQcNcrOqcSourceClaim oqcSourceClaim;
+    @Resource private MesQcNcrOqcConcessionService oqcConcessionService;
+    @Resource private MesQcNcrOqcReproductionService oqcReproductionService;
+    @Resource private MesQcNcrOqcReworkService oqcReworkService;
 
     @Resource
     private MdmItemApi mdmItemApi;
@@ -170,6 +187,11 @@
     public void updateNcr(MesQcNcrSaveReqVO updateReqVO) {
         // 1. 鏍¢獙瀛樺湪 + 鍙厑璁稿緟璇勫鐘舵�佺紪杈�
         MesQcNcrDO ncr = validateNcrStatusPendingReview(updateReqVO.getId());
+        if (ncr.getQcId() != null && (!Objects.equals(ncr.getItemId(), updateReqVO.getItemId())
+                || ncr.getUnqualifiedQuantity() == null || updateReqVO.getUnqualifiedQuantity() == null
+                || ncr.getUnqualifiedQuantity().compareTo(updateReqVO.getUnqualifiedQuantity()) != 0)) {
+            throw new ServiceException(400, "鏉ユ簮璐ㄦ鍗曠敓鎴愮殑涓嶅悎鏍肩墿鏂欏拰鏁伴噺涓嶈兘淇敼");
+        }
         validateNcrCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
         validateItemExists(updateReqVO.getItemId());
 
@@ -185,7 +207,10 @@
     @Transactional(rollbackFor = Exception.class)
     public void deleteNcr(Long id) {
         // 鏍¢獙瀛樺湪 + 鍙厑璁稿垹闄ゅ緟璇勫鐘舵��
-        validateNcrStatusPendingReview(id);
+        MesQcNcrDO ncr = validateNcrStatusPendingReview(id);
+        if (ncr.getQcId() != null) {
+            throw new ServiceException(400, "鏉ユ簮璐ㄦ鍗曠敓鎴愮殑涓嶅悎鏍煎鐞嗗崟涓嶈兘鍒犻櫎");
+        }
         ncrMapper.deleteById(id);
     }
 
@@ -194,8 +219,9 @@
     public void reviewNcr(Long id, Integer disposition, Long responsibleDeptId,
                            Integer defectLevel, String reviewOpinion) {
         // 1. 鏍¢獙瀛樺湪 + 寰呰瘎瀹$姸鎬�
-        validateNcrStatusPendingReview(id);
+        MesQcNcrDO ncr = validateNcrStatusPendingReview(id);
         validateDisposition(disposition);
+        validateDispositionForSource(ncr, disposition);
 
         // 2. 鏇存柊璇勫淇℃伅
         MesQcNcrDO updateObj = new MesQcNcrDO()
@@ -216,12 +242,13 @@
     @Transactional(rollbackFor = Exception.class)
     public MesQcNcrDisposeRespVO disposeNcr(MesQcNcrDisposeReqVO disposeReqVO) {
         // 1. 鏍¢獙瀛樺湪 + 宸茶瘎瀹★紙瀹℃壒閫氳繃锛夌姸鎬�
-        MesQcNcrDO ncr = validateNcrExists(disposeReqVO.getId());
+        MesQcNcrDO ncr = validateNcrExistsForUpdate(disposeReqVO.getId());
         if (!MesNcrStatusEnum.REVIEWED.getStatus().equals(ncr.getStatus())) {
             throw exception(QC_NCR_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
         // 2. 澶勭疆鏂瑰紡蹇呭~锛屼笖蹇呴』鏄悎娉曟灇涓惧��
         validateDisposition(disposeReqVO.getDisposition());
+        validateDispositionForSource(ncr, disposeReqVO.getDisposition());
         // 3. 涓嶅悎鏍兼暟閲忓繀椤诲ぇ浜� 0锛屽惁鍒欐棤娉曠敓鎴愬叆搴撳崟 / 鐢熶骇璁㈠崟
         if (ncr.getUnqualifiedQuantity() == null
                 || ncr.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -230,10 +257,71 @@
 
         // 4. 鎸夊缃柟寮忚仈鍔ㄧ敓鎴愪笟鍔″崟鎹�
         MesQcNcrDisposeRespVO respVO;
-        if (Objects.equals(MesNcrDispositionEnum.CONCESSION.getType(), disposeReqVO.getDisposition())
-                || Objects.equals(MesNcrDispositionEnum.SCRAP.getType(), disposeReqVO.getDisposition())) {
-            // 璁╂鏀捐 / 鎶ュ簾锛氱敓鎴愭潅椤瑰叆搴撳崟骞惰嚜鍔ㄥ畬鎴愬叆搴�
+        MesQcNcrScrapService.IpqcSource processSource = null;
+        MesQcNcrOqcSourceClaim.OqcClaimedSource oqcSource = null;
+        if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())) {
+            oqcSource = oqcSourceClaim.claim(ncr);
+        }
+        if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())) {
+            processSource = scrapService.validateIpqcSource(ncr);
+            scrapService.claimIpqcSource(ncr, processSource);
+            if (StrUtil.isNotBlank(processSource.batchCode()) && StrUtil.isNotBlank(disposeReqVO.getBatchCode())
+                    && !Objects.equals(processSource.batchCode(), StrUtil.trim(disposeReqVO.getBatchCode()))) {
+                throw new ServiceException(400, "澶勭疆鎵规蹇呴』涓庤繃绋嬫楠屽師鎵规涓�鑷�");
+            }
+            if (StrUtil.isNotBlank(processSource.batchCode())) disposeReqVO.setBatchCode(processSource.batchCode());
+        }
+        if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType())) {
+            String originalBatch = returnSalesLink.claim(ncr);
+            if (StrUtil.isNotBlank(originalBatch) && StrUtil.isNotBlank(disposeReqVO.getBatchCode())
+                    && !Objects.equals(originalBatch, StrUtil.trim(disposeReqVO.getBatchCode()))) {
+                throw new ServiceException(400, "澶勭疆鎵规蹇呴』涓庡鎴烽��璐у師鎵规涓�鑷�");
+            }
+            if (StrUtil.isNotBlank(originalBatch)) disposeReqVO.setBatchCode(originalBatch);
+        }
+        if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.RETURN.getType())) {
+            var doc = purchaseReturnService.createForNcr(ncr);
+            respVO = new MesQcNcrDisposeRespVO();
+            respVO.setDisposition(disposeReqVO.getDisposition());
+            respVO.setDocType("PURCHASE_RETURN");
+            respVO.setDocId(doc.getId());
+            respVO.setDocCode(doc.getCode());
+            respVO.setDocName("閲囪喘閫�璐у崟");
+            respVO.setMessage("閲囪喘閫�璐у崟宸茬敓鎴愶紝璇风敱浠撳簱纭閫�璐�");
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REPRODUCTION.getType())) {
+            respVO = processSource != null
+                    ? oqcReproductionService.reproduceIpqc(ncr, disposeReqVO.getDisposition(), processSource)
+                    : oqcReproductionService.reproduce(ncr, disposeReqVO.getDisposition());
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())
+                && Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) {
+            respVO = purchaseConcessionService.dispose(ncr, disposeReqVO);
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())
+                && Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())) {
+            oqcConcessionService.release(ncr, disposeReqVO);
+            respVO = new MesQcNcrDisposeRespVO();
+            respVO.setDisposition(disposeReqVO.getDisposition());
+            respVO.setDocType("PRODUCT_SALES");
+            MesQcOqcDO sourceOqc = oqcService.getOqc(ncr.getQcId());
+            respVO.setDocId(sourceOqc.getSourceDocId());
+            respVO.setDocCode(sourceOqc.getSourceDocCode());
+            respVO.setDocName("閿�鍞嚭搴撳崟");
+            respVO.setMessage("鍑鸿揣璁╂宸插畬鎴愬師閿�鍞嚭搴撳崟鎷h揣鍙婂嚭搴�");
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.SCRAP.getType())
+                && (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())
+                || Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType()))) {
+            respVO = scrapService.receive(ncr, disposeReqVO);
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())
+                && (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())
+                || Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType()))) {
             respVO = createMiscReceiptForDispose(ncr, disposeReqVO);
+        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())
+                || Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.SCRAP.getType())) {
+            stockClosureGuard.validateBeforeDispose(disposeReqVO.getDisposition());
+            throw new ServiceException(400, "璇ユ潵婧愭殏涓嶆敮鎸佹澶勭疆鏂瑰紡");
+        } else if (oqcSource != null && (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REWORK.getType())
+                || Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REPAIR.getType()))) {
+            // 鍑鸿揣杩斿伐/杩斾慨锛氭墸鍑忓師鎴愬搧搴撳瓨 鈫� 鍙栨秷鍘熼攢鍞嚭搴撳崟 鈫� 鍏嬮殕杩斿伐宸ュ崟
+            respVO = oqcReworkService.dispose(ncr, oqcSource, disposeReqVO);
         } else {
             // 杩斿伐 / 杩斾慨锛氱敓鎴愯嚜琛岀敓浜х被鍨嬬殑鐢熶骇璁㈠崟
             respVO = createReworkWorkOrderForDispose(ncr, disposeReqVO);
@@ -462,7 +550,7 @@
         // 璇ユ柟娉曚細鎶婃墍鏈夊紓甯哥粺涓�鍖呮垚 RuntimeException锛屽洜姝よ繖閲屼笉鑳界洿鎺� catch ServiceException锛�
         // 蹇呴』娌� cause 閾惧垽鏂槸鍚︿负銆屾祦绋嬪畾涔変笉瀛樺湪銆嶃��
         // 1. 鏍¢獙瀛樺湪 + 寰呰瘎瀹★紙鑽夌/瀹℃壒椹冲洖鍚庯級鐘舵��
-        MesQcNcrDO ncr = validateNcrExists(id);
+        MesQcNcrDO ncr = validateNcrExistsForUpdate(id);
         if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
             throw exception(QC_NCR_SUBMIT_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
@@ -519,10 +607,12 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void updateNcrAuditStatus(Long id, Integer bpmResult) {
+    public void updateNcrAuditStatus(Long id, String processInstanceId, Integer bpmResult) {
         // 1. 鏍¢獙瀛樺湪 + 瀹℃壒涓紙閬垮厤骞跺彂鎴栭噸澶嶅洖璋冭鐩栧凡澶勭疆/宸插叧闂姸鎬侊級
-        MesQcNcrDO ncr = validateNcrExists(id);
-        if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())) {
+        MesQcNcrDO ncr = validateNcrExistsForUpdate(id);
+        if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())
+                || StrUtil.isBlank(processInstanceId)
+                || !Objects.equals(processInstanceId, ncr.getProcessInstanceId())) {
             return;
         }
 
@@ -545,11 +635,38 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void closeNcr(Long id, String closeOpinion) {
+    public void closeNcr(Long id, String closeOpinion, Long reinspectionIpqcId) {
         // 1. 鏍¢獙瀛樺湪 + 宸插缃姸鎬�
-        MesQcNcrDO ncr = validateNcrExists(id);
+        MesQcNcrDO ncr = validateNcrExistsForUpdate(id);
         if (!MesNcrStatusEnum.DISPOSED.getStatus().equals(ncr.getStatus())) {
             throw exception(QC_NCR_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
+        }
+
+        if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.RETURN.getType())
+                && Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) {
+            purchaseReturnService.validateConfirmed(ncr.getId());
+        } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())
+                && Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())) {
+            oqcConcessionService.validateBeforeClose(ncr);
+        } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())
+                && Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())) {
+            // 閲囪喘璁╂鐨勫叆搴撳拰 ERP 鍥炲啓涓庡缃湪鍚屼竴浜嬪姟瀹屾垚銆�
+        } else if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPRODUCTION.getType())) {
+            throw new ServiceException(400, "閲嶆柊鐢熶骇鍚庨渶瀹屾垚澶嶆骞跺鐞嗗師鍑哄簱鍗曪紝鏆備笉鑳藉叧闂�");
+        } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())
+                && (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REWORK.getType())
+                || Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPAIR.getType()))) {
+            oqcReworkService.validateBeforeClose(ncr);
+        } else if (!Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())
+                && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType())) {
+            stockClosureGuard.validateBeforeClose(ncr);
+        }
+
+        if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REWORK.getType())
+                || Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPAIR.getType())) {
+            reinspectionValidator.validate(ncr, reinspectionIpqcId);
+        } else {
+            reinspectionIpqcId = null;
         }
 
         // 2. 鏇存柊鍏抽棴淇℃伅
@@ -558,6 +675,7 @@
                 .setCloseUserId(getLoginUserId())
                 .setCloseDate(LocalDateTime.now())
                 .setCloseOpinion(closeOpinion)
+                .setReinspectionIpqcId(reinspectionIpqcId)
                 .setStatus(MesNcrStatusEnum.CLOSED.getStatus()));
     }
 
@@ -572,11 +690,19 @@
         return ncr;
     }
 
+    private MesQcNcrDO validateNcrExistsForUpdate(Long id) {
+        MesQcNcrDO ncr = ncrMapper.selectByIdForUpdate(id);
+        if (ncr == null) {
+            throw exception(QC_NCR_NOT_EXISTS, id);
+        }
+        return ncr;
+    }
+
     /**
      * 鏍¢獙 NCR 瀛樺湪涓斾负寰呰瘎瀹$姸鎬�
      */
     private MesQcNcrDO validateNcrStatusPendingReview(Long id) {
-        MesQcNcrDO ncr = validateNcrExists(id);
+        MesQcNcrDO ncr = validateNcrExistsForUpdate(id);
         if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
             throw exception(QC_NCR_STATUS_NOT_PENDING, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
@@ -595,6 +721,22 @@
         }
     }
 
+    private void validateDispositionForSource(MesQcNcrDO ncr, Integer disposition) {
+        if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) {
+            if (!Objects.equals(disposition, MesNcrDispositionEnum.RETURN.getType())
+                    && !Objects.equals(disposition, MesNcrDispositionEnum.CONCESSION.getType())) {
+                throw new ServiceException(400, "閲囪喘妫�楠屼笉鍚堟牸浠呮敮鎸侀��璐ф垨璁╂鏀捐");
+            }
+        } else if (Objects.equals(disposition, MesNcrDispositionEnum.RETURN.getType())) {
+            throw new ServiceException(400, "鍙湁閲囪喘妫�楠屼笉鍚堟牸鏀寔閲囪喘閫�璐�");
+        }
+        if (Objects.equals(disposition, MesNcrDispositionEnum.REPRODUCTION.getType())
+                && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())
+                && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())) {
+            throw new ServiceException(400, "閲嶆柊鐢熶骇浠呮敮鎸佽繃绋嬫垨鍑鸿揣妫�楠屼笉鍚堟牸");
+        }
+    }
+
     /**
      * 鐘舵�佸�艰浆鍚嶇О锛岃В鏋愬け璐ュ洖閫�涓� ID 瀛楃涓�
      */

--
Gitblit v1.9.3