From 8c65f898ed6e5b0c7a94c792c4562f6ec831dccd Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期六, 07 十一月 2026 08:50:53 +0800
Subject: [PATCH] feat: 完善质检、生产、销售、财务与人事模块并接入审批流程

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java |  158 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 150 insertions(+), 8 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 56ed686..1b9f291 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
@@ -1,11 +1,16 @@
 package cn.iocoder.yudao.module.mes.service.qc.ncr;
 
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.qc.ncr.MesQcNcrMapper;
+import cn.iocoder.yudao.module.mes.enums.qc.MesNcrDispositionEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesNcrStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
 import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
@@ -23,6 +28,7 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@@ -38,11 +44,31 @@
 @Validated
 public class MesQcNcrServiceImpl implements MesQcNcrService {
 
+    /**
+     * NCR 鍗忓悓瀹℃壒娴佺▼鍒嗙被/娴佺▼瀹氫箟 key
+     */
+    private static final String NCR_APPROVAL_PROCESS_KEY = "mes_ncr_approve";
+
+    /**
+     * 鏈�缁堝缃厑璁稿�硷細璁╂鏀捐銆佽繑淇�佽繑宸ャ�佹姤搴�
+     * <p>
+     * 閫�璐�(1)銆侀檷绾т娇鐢�(5) 浠嶄繚鐣欏湪 {@link MesNcrDispositionEnum} 涓庡瓧鍏镐腑锛岀敤浜庡巻鍙叉暟鎹樉绀猴紝
+     * 浣嗕笉鍐嶄綔涓鸿川閲忛儴鏈�缁堝缃殑鍙�夐」銆�
+     */
+    private static final Set<Integer> DISPOSITION_ALLOWED_TYPES = Set.of(
+            MesNcrDispositionEnum.SCRAP.getType(),
+            MesNcrDispositionEnum.REWORK.getType(),
+            MesNcrDispositionEnum.CONCESSION.getType(),
+            MesNcrDispositionEnum.REPAIR.getType());
+
     @Resource
     private MesQcNcrMapper ncrMapper;
 
     @Resource
     private MdmItemApi mdmItemApi;
+
+    @Resource
+    private BpmProcessInstanceApi processInstanceApi;
 
     @Resource
     @Lazy
@@ -105,6 +131,7 @@
                            Integer defectLevel, String reviewOpinion) {
         // 1. 鏍¢獙瀛樺湪 + 寰呰瘎瀹$姸鎬�
         validateNcrStatusPendingReview(id);
+        validateDisposition(disposition);
 
         // 2. 鏇存柊璇勫淇℃伅
         MesQcNcrDO updateObj = new MesQcNcrDO()
@@ -123,17 +150,20 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void disposeNcr(Long id, Long handlerUserId, String handleResult) {
-        // 1. 鏍¢獙瀛樺湪 + 宸茶瘎瀹$姸鎬�
+    public void disposeNcr(Long id, Integer disposition, Long handlerUserId, String handleResult) {
+        // 1. 鏍¢獙瀛樺湪 + 宸茶瘎瀹★紙瀹℃壒閫氳繃锛夌姸鎬�
         MesQcNcrDO ncr = validateNcrExists(id);
         if (!MesNcrStatusEnum.REVIEWED.getStatus().equals(ncr.getStatus())) {
-            throw exception(QC_NCR_NOT_REVIEWED);
+            throw exception(QC_NCR_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
+        // 2. 澶勭疆鏂瑰紡蹇呭~锛屼笖蹇呴』鏄悎娉曟灇涓惧��
+        validateDisposition(disposition);
 
-        // 2. 鏇存柊澶勭疆淇℃伅
+        // 3. 鏇存柊澶勭疆淇℃伅锛堝缃柟寮忎互鏈澶勭疆閫夋嫨涓哄噯骞惰惤搴擄級
         // TODO: WM 妯″潡闆嗘垚 鈥� 澶勭疆涓洪��璐ф椂瑙﹀彂渚涘簲鍟嗛��璐э紝澶勭疆涓烘姤搴熸椂瑙﹀彂鏉傞」鍑哄簱
         ncrMapper.updateById(new MesQcNcrDO()
                 .setId(id)
+                .setDisposition(disposition)
                 .setHandlerUserId(handlerUserId)
                 .setHandleDate(LocalDateTime.now())
                 .setHandleResult(handleResult)
@@ -142,11 +172,99 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    public void submitNcrApproval(Long id) {
+        // 璇存槑锛欱PM 鐨� createProcessInstance 鍐呴儴閫氳繃 FlowableUtils.executeAuthenticatedUserId 鎵ц锛�
+        // 璇ユ柟娉曚細鎶婃墍鏈夊紓甯哥粺涓�鍖呮垚 RuntimeException锛屽洜姝よ繖閲屼笉鑳界洿鎺� catch ServiceException锛�
+        // 蹇呴』娌� cause 閾惧垽鏂槸鍚︿负銆屾祦绋嬪畾涔変笉瀛樺湪銆嶃��
+        // 1. 鏍¢獙瀛樺湪 + 寰呰瘎瀹★紙鑽夌/瀹℃壒椹冲洖鍚庯級鐘舵��
+        MesQcNcrDO ncr = validateNcrExists(id);
+        if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
+            throw exception(QC_NCR_SUBMIT_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
+        }
+
+        // 2. 鍏ㄥ眬鍏嶅鎵癸細涓氬姟鏍¢獙閫氳繃鍚庣洿鎺ヨ涓哄鎵归�氳繃锛堝凡璇勫/寰呭缃級锛屾棤闇�娴佺▼妯″瀷
+        if (processInstanceApi.isGlobalAutoApprove()) {
+            ncrMapper.updateById(new MesQcNcrDO()
+                    .setId(id)
+                    .setStatus(MesNcrStatusEnum.REVIEWED.getStatus())
+                    .setProcessInstanceId(""));
+            return;
+        }
+
+        // 3. 鍒涘缓 BPM 娴佺▼瀹炰緥锛涘彇涓嶅埌娴佺▼瀹氫箟鏃舵寜绾﹀畾璧拌嚜鍔ㄩ�氳繃鍏滃簳
+        String processInstanceId;
+        try {
+            processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),
+                    new BpmProcessInstanceCreateReqDTO()
+                            .setProcessDefinitionKey(NCR_APPROVAL_PROCESS_KEY)
+                            .setBusinessKey(String.valueOf(id)));
+        } catch (RuntimeException ex) {
+            if (isProcessDefinitionNotExists(ex)) {
+                ncrMapper.updateById(new MesQcNcrDO()
+                        .setId(id)
+                        .setStatus(MesNcrStatusEnum.REVIEWED.getStatus())
+                        .setProcessInstanceId(""));
+                return;
+            }
+            throw ex;
+        }
+
+        // 4. 鏇存柊 NCR 鐘舵�佷负瀹℃壒涓紝骞跺洖鍐欐祦绋嬪疄渚嬬紪鍙�
+        ncrMapper.updateById(new MesQcNcrDO()
+                .setId(id)
+                .setStatus(MesNcrStatusEnum.APPROVING.getStatus())
+                .setProcessInstanceId(processInstanceId));
+    }
+
+    /**
+     * 鍒ゆ柇寮傚父閾句腑鏄惁鍖呭惈銆屾祦绋嬪畾涔変笉瀛樺湪銆嶏紙BPM 瀵瑰浼氬寘涓�灞� RuntimeException锛�
+     */
+    private static boolean isProcessDefinitionNotExists(Throwable ex) {
+        Throwable current = ex;
+        while (current != null) {
+            if (current instanceof ServiceException serviceException
+                    && cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS
+                            .getCode().equals(serviceException.getCode())) {
+                return true;
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateNcrAuditStatus(Long id, Integer bpmResult) {
+        // 1. 鏍¢獙瀛樺湪 + 瀹℃壒涓紙閬垮厤骞跺彂鎴栭噸澶嶅洖璋冭鐩栧凡澶勭疆/宸插叧闂姸鎬侊級
+        MesQcNcrDO ncr = validateNcrExists(id);
+        if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())) {
+            return;
+        }
+
+        // 2. 瀹℃壒閫氳繃 -> 宸茶瘎瀹★紙寰呭缃級锛涢┏鍥�/鍙栨秷 -> 寰呰瘎瀹★紙鍙噸鏂颁慨鏀瑰苟鎻愪氦锛�
+        if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
+            ncrMapper.updateById(new MesQcNcrDO()
+                    .setId(id)
+                    .setStatus(MesNcrStatusEnum.REVIEWED.getStatus()));
+            return;
+        }
+        if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult)
+                || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
+            // 淇濈暀 processInstanceId锛氶┏鍥�/鍙栨秷鍚庝粛鍙湪銆屾煡鐪嬪鎵广�嶄腑鏌ョ湅瀹℃壒鎰忚锛�
+            // 閲嶆柊鎻愪氦鏃朵細鐢ㄦ柊娴佺▼瀹炰緥缂栧彿瑕嗙洊銆�
+            ncrMapper.updateById(new MesQcNcrDO()
+                    .setId(id)
+                    .setStatus(MesNcrStatusEnum.PENDING_REVIEW.getStatus()));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
     public void closeNcr(Long id, String closeOpinion) {
         // 1. 鏍¢獙瀛樺湪 + 宸插缃姸鎬�
         MesQcNcrDO ncr = validateNcrExists(id);
         if (!MesNcrStatusEnum.DISPOSED.getStatus().equals(ncr.getStatus())) {
-            throw exception(QC_NCR_NOT_DISPOSED);
+            throw exception(QC_NCR_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
 
         // 2. 鏇存柊鍏抽棴淇℃伅
@@ -164,7 +282,7 @@
     public MesQcNcrDO validateNcrExists(Long id) {
         MesQcNcrDO ncr = ncrMapper.selectById(id);
         if (ncr == null) {
-            throw exception(QC_NCR_NOT_EXISTS);
+            throw exception(QC_NCR_NOT_EXISTS, id);
         }
         return ncr;
     }
@@ -175,9 +293,33 @@
     private MesQcNcrDO validateNcrStatusPendingReview(Long id) {
         MesQcNcrDO ncr = validateNcrExists(id);
         if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
-            throw exception(QC_NCR_NOT_PENDING_REVIEW);
+            throw exception(QC_NCR_STATUS_NOT_PENDING, ncr.getCode(), resolveStatusName(ncr.getStatus()));
         }
         return ncr;
+    }
+
+    /**
+     * 鏍¢獙澶勭疆鏂瑰紡锛氬繀濉� + 蹇呴』鏄悎娉曟灇涓惧��
+     */
+    private void validateDisposition(Integer disposition) {
+        if (disposition == null) {
+            throw exception(QC_NCR_DISPOSITION_REQUIRED);
+        }
+        if (!DISPOSITION_ALLOWED_TYPES.contains(disposition)) {
+            throw exception(QC_NCR_DISPOSITION_INVALID, disposition);
+        }
+    }
+
+    /**
+     * 鐘舵�佸�艰浆鍚嶇О锛岃В鏋愬け璐ュ洖閫�涓� ID 瀛楃涓�
+     */
+    private String resolveStatusName(Integer status) {
+        for (MesNcrStatusEnum value : MesNcrStatusEnum.values()) {
+            if (value.getStatus().equals(status)) {
+                return value.getName();
+            }
+        }
+        return "ID=" + status;
     }
 
     /**
@@ -186,7 +328,7 @@
     private void validateNcrCodeUnique(Long id, String code) {
         MesQcNcrDO existing = ncrMapper.selectByCode(code);
         if (existing != null && !existing.getId().equals(id)) {
-            throw exception(QC_NCR_CODE_DUPLICATE);
+            throw exception(QC_NCR_CODE_DUPLICATE, code);
         }
     }
 

--
Gitblit v1.9.3