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/pro/feedback/MesProFeedbackServiceImpl.java |  411 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 355 insertions(+), 56 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
index 371a979..ded280e 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -1,16 +1,20 @@
 package cn.iocoder.yudao.module.mes.service.pro.feedback;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
+import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
 import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum;
 import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackAuxDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
@@ -21,10 +25,17 @@
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackAuxMapper;
 import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProProcessBillingTypeEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
+import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
+import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcWageFieldService;
 import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
 import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
 import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
@@ -32,6 +43,11 @@
 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService;
+import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackQualityResult;
+import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageBreakdown;
+import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageCalculator;
+import cn.iocoder.yudao.module.mes.service.pro.wage.MesProProcessWageRuleService;
+import cn.iocoder.yudao.module.mes.service.pro.worktime.MesProWorktimeService;
 import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
 import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
 import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
@@ -50,12 +66,17 @@
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -75,6 +96,22 @@
 
     @Resource
     private MesProFeedbackMapper feedbackMapper;
+    @Resource
+    private MesProFeedbackAuxMapper feedbackAuxMapper;
+    @Resource
+    private MesProWorktimeService worktimeService;
+    @Resource
+    private MesProFeedbackWageCalculator wageCalculator;
+    @Resource
+    private MesProProcessWageRuleService wageRuleService;
+    @Resource
+    private MesQcIpqcMapper ipqcMapper;
+    @Resource
+    @Lazy // IPQC 瀹屾垚鍥炲啓鎶ュ伐锛岄伩鍏嶅惊鐜緷璧�
+    private MesQcIpqcService ipqcService;
+    @Resource
+    @Lazy // 杩囩▼妫�楠屽崟宸ヨ祫瀛楁鍊硷紙璐ㄦ濉啓锛夛紝閬垮厤涓� IPQC 涓绘湇鍔″惊鐜緷璧�
+    private MesQcIpqcWageFieldService ipqcWageFieldService;
 
     @Resource
     private MesProWorkOrderService workOrderService;
@@ -122,7 +159,12 @@
                 .setId(null)
                 .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                 .setItemId(task.getItemId());
+        // 2.1 璁$畻鏍囧噯宸ユ椂蹇収涓庤緟鍔╁伐鏃舵槑缁嗭紙鍙敤浜庤�冩牳瀵规瘮锛屽弬鏁扮己澶变笉闃绘柇鎶ュ伐锛�
+        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, createReqVO.getAuxList());
+        // 2.2 鎸夊伐搴忚璐圭被鍨嬨�佸崟浠疯鍒欍�佽緟鍔╄钖笌宸ヨ祫瀛楁蹇収宸ヨ祫锛岃皝鎶ョ殑宸ュ氨璁板湪璋佸悕涓�
+        applyProcessBilling(feedback, processService.getProcess(task.getProcessId()), auxList);
         feedbackMapper.insert(feedback);
+        saveFeedbackAux(feedback.getId(), auxList);
         // 3. 淇濆瓨鍙傛暟鍊硷紱蹇呭~鍙傛暟鏍¢獙澶辫触鏃舵暣浣撳洖婊氾紝閬垮厤娈嬬暀鑽夌
         saveParamValues(task.getId(), createReqVO.getParamValues());
         return feedback.getId();
@@ -140,10 +182,26 @@
         // 2. 鏇存柊锛堣嚜鍔ㄥ~鍏� itemId锛�
         MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                 .setItemId(task.getItemId());
+        // 2.1 閲嶆柊璁$畻鏍囧噯宸ユ椂蹇収涓庤緟鍔╁伐鏃舵槑缁�
+        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(updateObj, updateReqVO.getAuxList());
+        // 2.2 鎸夊伐搴忓綋鍓嶇殑璁¤垂绫诲瀷/鍗曚环瑙勫垯/杈呭姪璁¤柂/宸ヨ祫瀛楁閲嶇畻宸ヨ祫锛堣崏绋块樁娈靛厑璁告敼宸ユ椂/鏁伴噺锛�
+        applyProcessBilling(updateObj, processService.getProcess(task.getProcessId()), auxList);
         feedbackMapper.updateById(updateObj);
+        feedbackAuxMapper.deleteByFeedbackId(updateReqVO.getId());
+        saveFeedbackAux(updateReqVO.getId(), auxList);
         equipmentService.resetSnapshot(updateReqVO.getId());
         // 3. 淇濆瓨鍙傛暟鍊硷紱蹇呭~鍙傛暟鏍¢獙澶辫触鏃舵暣浣撳洖婊�
         saveParamValues(task.getId(), updateReqVO.getParamValues());
+    }
+
+    private void saveFeedbackAux(Long feedbackId, List<MesProFeedbackAuxDO> auxList) {
+        if (CollUtil.isEmpty(auxList)) {
+            return;
+        }
+        auxList.forEach(aux -> {
+            aux.setId(null).setFeedbackId(feedbackId);
+            feedbackAuxMapper.insert(aux);
+        });
     }
 
     private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) {
@@ -164,8 +222,9 @@
         // 1. 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
         validateFeedbackStatusPrepare(id);
 
-        // 2. 鍒犻櫎
+        // 2. 鍒犻櫎锛堣緟鍔╁伐鏃舵槑缁嗕竴骞跺垹闄わ級
         feedbackMapper.deleteById(id);
+        feedbackAuxMapper.deleteByFeedbackId(id);
     }
 
     @Override
@@ -209,6 +268,7 @@
 
         MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
 
+        // 1.2 鎶ュ伐涓庤川妫�鍒嗙锛氭彁浜ゆ姤宸ヤ笉鍐嶈嚜鍔ㄧ敓鎴愯繃绋嬫楠屽崟锛岃川妫�鐢辫川妫�閮ㄩ棬鎵嬪伐寤哄崟
         if (!processInstanceApi.isGlobalAutoApprove() && workOrder != null
                 && Boolean.TRUE.equals(workOrder.getFeedbackApprove())
                 && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
@@ -235,7 +295,8 @@
             feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                     .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
             MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class);
-            approveFeedback(approveReqVO);
+            // 鑷姩瀹℃壒锛氳繃绋嬫楠屽崟鍒氱敓鎴愬皻鏈~鍐欙紝涓嶅仛璐ㄦ宸ヨ祫鍓嶇疆鏍¢獙锛堣川閲忚皟鏁寸暀绌猴級
+            approveFeedback(approveReqVO, false);
         }
     }
 
@@ -290,16 +351,21 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
+        return approveFeedback(reqVO, true);
+    }
+
+    private boolean approveFeedback(MesProFeedbackApproveReqVO reqVO, boolean enforceQualityGate) {
         equipmentService.lock();
         // 1.1 鏍¢獙瀛樺湪 + 瀹℃壒涓姸鎬�
         MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
         equipmentService.validateApproval(feedback);
         try (var equipmentScope = equipmentService.beginApproval(feedback)) {
-            return approveValidatedFeedback(reqVO, feedback);
+            return approveValidatedFeedback(reqVO, feedback, enforceQualityGate);
         }
     }
 
-    private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback) {
+    private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback,
+                                             boolean enforceQualityGate) {
         // 1.2 鏍¢獙鎶ュ伐鏁伴噺 > 0
         if (feedback.getFeedbackQuantity() == null
                 || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -308,22 +374,16 @@
         // 1.3 鏍¢獙浠诲姟鏈畬鎴�
         taskService.validateTaskNotFinished(feedback.getTaskId());
 
-        // 宸ュ簭妫�楠屾爣璇嗕笉鍐嶆帶鍒朵骇鍑哄叆搴擄細鎵�鏈夋柊鎶ュ伐浜у嚭鍧囬渶浜哄伐璐ㄦ銆�
-        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
-                feedback.getRouteId(), feedback.getProcessId());
-        boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag());
+        // 鎶ュ伐涓庤川妫�鍒嗙锛氭姤宸ュ鎵逛笉鍐嶄緷璧栥�屾槸鍚﹁川妫�銆嶅紑鍏筹紝璐ㄩ噺鏁版嵁鐢辫川妫�鍗曞洖濉��
+        // 1.4 宸ヨ祫鎸夋姤宸ユ椂鐐圭殑蹇収閲嶇畻锛岄伩鍏嶅伐搴忓畾棰濅簨鍚庤皟鏁村奖鍝嶅凡鎻愪氦鐨勬姤宸�
+        recalculateWageFromSnapshot(feedback, null);
 
-        // 3. 鐗╂枡娑堣�楋細鍊掑啿宸ュ簭鎸夋湰宸ュ簭鎶曟枡 BOM 鑷姩鎵f枡锛涢潪鍊掑啿宸ュ簭璺宠繃锛堢墿鏂欒蛋鎵嬪伐棰嗘枡/鎶曟枡锛�
-        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
-        if (backflushFlag) {
-            // 3.1 鏍¢獙鍊掑啿宸ュ簭蹇呴』閰嶇疆鏈伐搴忕殑鎶曟枡 BOM锛岄伩鍏嶉潤榛樹笉鎵f枡
-            //     锛堝彧鏍¢獙褰撳墠鎶ュ伐宸ュ簭鐨勬姇鏂欙紝鍏朵粬宸ュ簭鎶曟枡鐢卞悇鑷伐搴忔姤宸ユ椂鎵i櫎锛�
-            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
-                    feedback.getWorkOrderId(), feedback.getProcessId());
-            if (CollUtil.isEmpty(boms)) {
-                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
-            }
-            // 3.2 鐢熸垚娑堣�楄褰曞苟鎵ц鎵e噺锛堟湰宸ュ簭鎶曟枡鐢ㄩ噺姣斾緥 脳 鎶ュ伐鏁伴噺锛孎IFO 鎵g嚎杈瑰簱锛�
+        // 3. 鐗╂枡娑堣�楋細鍙鏈伐搴忛厤缃簡鐢熶骇 BOM 灏辫嚜鍔ㄦ墸鏂欙紝涓嶅啀渚濊禆鍊掑啿寮�鍏筹紱
+        //    鏈厤缃� BOM 鐨勫伐搴忚烦杩囷紙鐗╂枡璧版墜宸ラ鏂�/鎶曟枡锛夛紝涓嶉樆鏂姤宸ュ鎵�
+        List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
+                feedback.getWorkOrderId(), feedback.getProcessId());
+        if (CollUtil.isNotEmpty(boms)) {
+            // 3.1 鐢熸垚娑堣�楄褰曞苟鎵ц鎵e噺锛堟湰宸ュ簭鎶曟枡鐢ㄩ噺姣斾緥 脳 鎶ュ伐鏁伴噺锛孎IFO 鎵g嚎杈瑰簱锛�
             MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
             if (itemConsume != null) {
                 itemConsumeService.finishItemConsume(itemConsume.getId());
@@ -335,23 +395,23 @@
                 .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())
                 .setQualifiedQuantity(BigDecimal.ZERO)
                 .setUnqualifiedQuantity(BigDecimal.ZERO)
-                .setUncheckQuantity(feedback.getFeedbackQuantity()));
+                .setUncheckQuantity(feedback.getFeedbackQuantity())
+                .setBillingType(feedback.getBillingType())
+                .setWageRate(feedback.getWageRate())
+                .setWageRuleId(feedback.getWageRuleId())
+                .setWorkHours(feedback.getWorkHours())
+                .setWageAmount(feedback.getWageAmount())
+                .setAuxPaidHours(feedback.getAuxPaidHours())
+                .setAuxWageAmount(feedback.getAuxWageAmount())
+                .setQualityPassRate(feedback.getQualityPassRate())
+                .setQualityCoefficient(feedback.getQualityCoefficient())
+                .setQualityDeduction(feedback.getQualityDeduction())
+                .setWageBreakdown(feedback.getWageBreakdown()));
         feedback.setQualifiedQuantity(BigDecimal.ZERO).setUnqualifiedQuantity(BigDecimal.ZERO);
         productProduceService.generateProductProduce(feedback, true, reqVO);
 
-        // 6. 鏍规嵁鏄惁闇�瑕佹楠岋紝鍐冲畾浜у嚭鍜屽叆搴撴柟寮�
-        // 6.2 璋冩暣 鍙瀹℃壒鎴愬姛鐩存帴鏇存柊
-        // 鍏抽敭宸ュ簭锛氭洿鏂颁换鍔� + 宸ュ崟鐨勫凡鐢熶骇鏁伴噺锛涢潪鍏抽敭宸ュ簭锛氬彧鏇存柊浠诲姟鐨勫凡鐢熶骇鏁伴噺锛堝伐鍗曞彧鐪嬪叧閿伐搴忎骇鍑猴級
-        if (keyFlag) {
-            updateTaskAndWorkOrderByFeedback(feedback);
-        } else {
-            taskService.updateProducedQuantity(feedback.getTaskId(),
-                    feedback.getFeedbackQuantity(),
-                    ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
-                    ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
-                checkAndPublishProductionFinished(feedback.getWorkOrderId());
-
-        }
+        // 姣忛亾宸ュ簭鍙疮璁℃湰浠诲姟锛涘伐鍗曚笌 MPS 鐢辨渶鍚庝竴閬撳伐搴忓洖鍐欙紝涓庡叧閿伐搴忔爣璁版棤鍏炽��
+        updateTaskAndWorkOrderByFeedback(feedback);
         equipmentService.afterApproved(feedback);
         return false; // 浜у嚭绛夊緟璐ㄦ锛屾姤宸ヤ骇閲忓凡绱
     }
@@ -368,21 +428,17 @@
      *
      * @param feedback 鎶ュ伐鍗�
      */
-    private void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
+    void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
         // 鎶ュ伐瀹℃壒鍙疮璁′骇閲忥紱鍚堟牸涓庝笉鍚堟牸鏁伴噺鍦ㄨ川妫�瀹屾垚鏃剁疮璁°��
         taskService.updateProducedQuantity(feedback.getTaskId(),
                 feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO);
-        // 3. 鏇存柊璁㈠崟鐨勫凡鐢熶骇鏁伴噺
-        // 鏂板閫昏緫锛氬彧鏈夊湪璇ョ瑪璁㈠崟鏈�鍚庝竴涓伐搴忔墠鏇存柊鐢熶骇璁㈠崟鐨勫凡鐢熶骇鏁伴噺
+        // 鍙湁鏈�鍚庝竴閬撳伐搴忕殑鎶ュ伐璁″叆宸ュ崟涓� MPS锛岄伩鍏嶄腑闂村伐搴忛噸澶嶇疮璁°��
         MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
         if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
             workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                     feedback.getFeedbackQuantity());
-            // 4. 鏇存柊 MPS 鐨勫凡鐢熶骇鏁伴噺锛堝鏋滄槸閿�鍞鍗曟潵婧愶級
-            MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId());
-            if (mps != null) {
-                mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity());
-            }
+            // MPS 鍙褰曢攢鍞鍗曢渶姹傞噺锛涘疄闄呰秴浜т粛鐢卞伐鍗曞拰浜у嚭鍏ュ簱璁板綍瀹屾暣淇濈暀銆�
+            allocateMpsProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity());
         }
 
 
@@ -393,23 +449,45 @@
     /**
      * 妫�鏌ュ苟鍙戝竷鐢熶骇瀹屾垚浜嬩欢
      */
-    private void checkAndPublishProductionFinished(Long workOrderId) {
-        // 1. 鑾峰彇宸ュ崟鍏宠仈鐨� MPS
-        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId);
-        if (mps == null || mps.getSaleOrderId() == null
-                || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus())
-                || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
-            return; // 闈為攢鍞鍗曟潵婧愮殑宸ュ崟锛岃烦杩�
+    void checkAndPublishProductionFinished(Long workOrderId) {
+        Set<Long> newlyCompletedSaleOrderIds = new LinkedHashSet<>();
+        for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) {
+            if (mps.getSaleOrderId() == null
+                    || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus())
+                    || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
+                continue;
+            }
+            if (Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) {
+                newlyCompletedSaleOrderIds.add(mps.getSaleOrderId());
+            }
         }
-
-        // 2. 妫�鏌ュ苟鑷姩瀹屾垚 MPS锛堝鏋滄弧瓒虫潯浠讹級
-        if (!Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) {
-            return;
+        for (Long saleOrderId : newlyCompletedSaleOrderIds) {
+            if (mpsApi.checkSaleOrderProductionFinished(saleOrderId)) {
+                eventPublisher.publishEvent(new ProductionFinishedEvent(saleOrderId, java.time.LocalDateTime.now()));
+            }
         }
+    }
 
-        // 3. 妫�鏌ラ攢鍞鍗曟墍鏈夌敓浜ф槸鍚﹀畬鎴�
-        if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) {
-            eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now()));
+    void allocateMpsProducedQuantity(Long workOrderId, BigDecimal actualQuantity) {
+        BigDecimal remaining = actualQuantity;
+        for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) {
+            if (remaining.signum() <= 0) {
+                break;
+            }
+            if (ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
+                continue;
+            }
+            BigDecimal planned = mps.getQuantity() == null ? BigDecimal.ZERO : mps.getQuantity();
+            BigDecimal produced = mps.getQuantityProduced() == null ? BigDecimal.ZERO : mps.getQuantityProduced();
+            BigDecimal need = planned.subtract(produced).max(BigDecimal.ZERO);
+            BigDecimal assigned = remaining.min(need);
+            if (assigned.signum() > 0) {
+                mpsApi.updateMpsProducedQuantity(mps.getId(), assigned);
+                remaining = remaining.subtract(assigned);
+            }
+        }
+        if (remaining.signum() > 0) {
+            log.info("[allocateMpsProducedQuantity] 宸ュ崟({}) 瓒呬骇鏁伴噺({})淇濈暀鍦ㄤ骇鍑哄簱瀛橈紝涓嶅垎閰嶅埌閿�鍞鍗�", workOrderId, remaining);
         }
     }
 
@@ -479,6 +557,164 @@
         }
 
         return task;
+    }
+
+    /**
+     * 鎸夊伐搴忛厤缃妸璁¤垂绫诲瀷銆佸崟浠疯鍒欍�佽緟鍔╄钖笌宸ヨ祫瀛楁蹇収鍒版姤宸ュ崟锛屽苟璁$畻鏈鎶ュ伐宸ヨ祫銆�
+     *
+     * <p>鍙e緞锛�
+     * <ul>
+     *   <li>璁℃椂锛氫富宸ヨ祫 = 鏈鎶ュ伐宸ユ椂锛堝皬鏃讹級脳 鍗曚环锛堝厓/灏忔椂锛夛紱鍗曚环澶т簬 0 鏃跺繀椤诲~鍐欏伐鏃�</li>
+     *   <li>璁′欢锛氫富宸ヨ祫 = 鍚堟牸鏁伴噺锛堟湭濉椂鍙栨姤宸ユ暟閲忥級脳 鍗曚环锛堝厓/浠讹級</li>
+     *   <li>杈呭姪锛氳緟鍔╂槑缁嗗垎閽熸暟 梅 60 脳 杈呭姪灏忔椂鍗曚环锛涘伐搴忚緟鍔╁皬鏃跺伐璧勫厹搴�</li>
+     *   <li>瀛楁锛氱郴鏁拌繛涔樹綔鐢ㄤ簬锛堜富宸ヨ祫 + 杈呭姪宸ヨ祫锛夛紝鍐嶅彔鍔犲姞鍑忛噾棰濅笌璁′环鏁伴噺</li>
+     * </ul>
+     * 鍓嶇浼犲叆鐨� billingType/wageRate/wageAmount 涓�寰嬩互鍚庣璁$畻瑕嗙洊锛岄槻姝㈣绡℃敼銆�
+     * 鍗曚环涓� 0 琛ㄧず璇ュ伐搴忓皻鏈畾浠凤紝鎸� 0 鍏冭钖紝涓嶉樆鏂姤宸ワ紙鏍哥畻鏃跺彲鎸変汉璋冩暣锛夈��
+     */
+    private void applyProcessBilling(MesProFeedbackDO feedback, MesProProcessDO process,
+                                     List<MesProFeedbackAuxDO> auxList) {
+        if (process == null) {
+            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, feedback.getProcessId());
+        }
+        MesProFeedbackWageBreakdown breakdown = wageCalculator.calculate(process,
+                feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(),
+                feedback.getFeedbackQuantity(), feedback.getQualifiedQuantity(), auxList,
+                feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null,
+                getWageFieldValues(feedback.getId()));
+        applyWageBreakdown(feedback, breakdown);
+    }
+
+    /**
+     * 鎶婂伐璧勮绠楃粨鏋滃啓鍥炴姤宸ュ崟
+     */
+    private void applyWageBreakdown(MesProFeedbackDO feedback, MesProFeedbackWageBreakdown breakdown) {
+        if (CollUtil.isNotEmpty(breakdown.getWarnings())) {
+            log.warn("鎶ュ伐宸ヨ祫璁$畻鎻愮ず锛氭姤宸ュ崟[{}] {}", feedback.getCode(), String.join("锛�", breakdown.getWarnings()));
+        }
+        feedback.setBillingType(breakdown.getBillingType())
+                .setWageRate(breakdown.getWageRate())
+                .setWageRuleId(breakdown.getWageRuleId())
+                .setWorkHours(breakdown.getWorkHours())
+                .setAuxPaidHours(breakdown.getAuxPaidHours())
+                .setAuxWageAmount(breakdown.getAuxWage())
+                .setWageAmount(breakdown.getFinalWage())
+                .setQualityPassRate(breakdown.getQuality() != null ? breakdown.getQuality().getPassRate() : null)
+                .setQualityCoefficient(breakdown.getQuality() != null ? breakdown.getQuality().getCoefficient() : null)
+                .setQualityDeduction(breakdown.getQuality() != null && breakdown.getQuality().getDeduction() != null
+                        ? breakdown.getQuality().getDeduction() : BigDecimal.ZERO)
+                .setWageBreakdown(JsonUtils.toJsonString(breakdown));
+    }
+
+    /**
+     * 鍙栫涓�涓澶囩紪鍙蜂綔涓轰富璁′环璁惧锛堝揩鎹锋姤宸ョ殑 machineryIds 涓洪�楀彿鍒嗛殧锛�
+     */
+    private static Long firstMachineryId(String machineryIds) {
+        if (StrUtil.isBlank(machineryIds)) {
+            return null;
+        }
+        for (String part : machineryIds.split(",")) {
+            String trimmed = part.trim();
+            if (StrUtil.isBlank(trimmed)) {
+                continue;
+            }
+            try {
+                return Long.parseLong(trimmed);
+            } catch (NumberFormatException ex) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 瀹℃壒鏃舵寜鎶ュ伐鏃剁偣鐨勫揩鐓ч噸绠楀伐璧勶紝閬垮厤宸ュ簭閰嶇疆浜嬪悗璋冩暣褰卞搷宸叉彁浜ょ殑鎶ュ伐銆�
+     * 鍘嗗彶鏁版嵁锛堟棤宸ヨ祫鎷嗗垎蹇収锛夊垯鎸夊綋鍓嶅伐搴忛厤缃ˉ榻愩��
+     *
+     * @param qualityResult 杩囩▼妫�楠屾眹鎬伙紱绌�=闈炶川妫�宸ュ簭鎴栬嚜鍔ㄥ鎵癸紝涓嶅仛璐ㄩ噺璋冩暣
+     */
+    private void recalculateWageFromSnapshot(MesProFeedbackDO feedback, MesProFeedbackQualityResult qualityResult) {
+        BigDecimal qualifiedQuantity = qualityResult != null
+                ? qualityResult.qualifiedQuantity() : feedback.getQualifiedQuantity();
+        // 璐ㄦ閮ㄩ棬鍦ㄨ繃绋嬫楠屽崟涓婂~鍐欑殑宸ヨ祫瀛楁鍊硷紝浼樺厛浜庡伐搴忓浐瀹氬��
+        Map<String, String> wageFieldValues = getWageFieldValues(feedback.getId());
+        MesProFeedbackWageBreakdown breakdown;
+        if (StrUtil.isBlank(feedback.getWageBreakdown())) {
+            breakdown = wageCalculator.calculate(processService.getProcess(feedback.getProcessId()),
+                    feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(),
+                    feedback.getFeedbackQuantity(), qualifiedQuantity,
+                    feedback.getId() != null ? feedbackAuxMapper.selectListByFeedbackId(feedback.getId())
+                            : Collections.emptyList(),
+                    feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null,
+                    wageFieldValues);
+        } else {
+            breakdown = wageCalculator.fromSnapshot(feedback, qualifiedQuantity, wageFieldValues);
+        }
+        if (qualityResult != null) {
+            wageCalculator.applyQuality(breakdown, feedback.getProcessId(), feedback.getRouteId(),
+                    qualityResult.passRate(), qualityResult.unqualifiedQuantity());
+        }
+        applyWageBreakdown(feedback, breakdown);
+    }
+
+    /**
+     * 鍙栨姤宸ュ崟鍏宠仈鐨勮繃绋嬫楠屽崟涓婅川妫�濉啓鐨勫伐璧勫瓧娈靛��
+     *
+     * <p>鍚屼竴鎶ュ伐鍗曞瓨鍦ㄥ寮犳楠屽崟鏃讹紝宸插畬鎴愮殑浼樺厛锛涘瓧娈垫湭濉啓鏃朵笉鏀惧叆 Map锛岃绠楁椂鍥為��宸ュ簭鍥哄畾鍊笺��
+     */
+    private Map<String, String> getWageFieldValues(Long feedbackId) {
+        if (feedbackId == null) {
+            return Collections.emptyMap();
+        }
+        List<MesQcIpqcDO> ipqcList = ipqcMapper.selectListBySourceDoc(
+                MesBizTypeConstants.PRO_FEEDBACK, feedbackId);
+        if (CollUtil.isEmpty(ipqcList)) {
+            return Collections.emptyMap();
+        }
+        List<Long> ipqcIds = new ArrayList<>(ipqcList.size());
+        ipqcList.stream()
+                .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
+                .map(MesQcIpqcDO::getId).forEach(ipqcIds::add);
+        ipqcList.stream()
+                .filter(ipqc -> !ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
+                .map(MesQcIpqcDO::getId).forEach(ipqcIds::add);
+        return ipqcWageFieldService.getValueMapByIpqcIds(ipqcIds);
+    }
+
+    /**
+     * 鏍¢獙璐ㄦ宸ュ簭鐨勮繃绋嬫楠屽凡瀹屾垚锛屽苟姹囨�诲悎鏍肩巼涓庝笉鍚堟牸鏁伴噺
+     */
+    private MesProFeedbackQualityResult validateAndGetQualityResult(MesProFeedbackDO feedback) {
+        MesProProcessDO process = processService.getProcess(feedback.getProcessId());
+        String processName = process != null ? process.getName() : "ID=" + feedback.getProcessId();
+        List<MesQcIpqcDO> finishedList = ipqcMapper.selectListBySourceDoc(
+                        MesBizTypeConstants.PRO_FEEDBACK, feedback.getId()).stream()
+                .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
+                .toList();
+        if (CollUtil.isEmpty(finishedList)) {
+            throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(),
+                    StrUtil.format("宸ュ簭[{}]涓鸿川妫�宸ュ簭锛屾姤宸ュ崟[{}]灏氭湭瀹屾垚杩囩▼妫�楠岋紱"
+                                    + "璇峰埌銆岃川閲忕鐞�-杩囩▼妫�楠屻�嶅畬鎴愭楠屽悗鍐嶅鎵�",
+                            processName, feedback.getCode()));
+        }
+        BigDecimal checkQuantity = BigDecimal.ZERO;
+        BigDecimal qualifiedQuantity = BigDecimal.ZERO;
+        BigDecimal unqualifiedQuantity = BigDecimal.ZERO;
+        for (MesQcIpqcDO ipqc : finishedList) {
+            checkQuantity = checkQuantity.add(ObjectUtil.defaultIfNull(ipqc.getCheckQuantity(), BigDecimal.ZERO));
+            qualifiedQuantity = qualifiedQuantity.add(ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO));
+            unqualifiedQuantity = unqualifiedQuantity.add(
+                    ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO));
+        }
+        if (checkQuantity.signum() <= 0) {
+            throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(),
+                    StrUtil.format("宸ュ簭[{}]鐨勮繃绋嬫楠屽崟[{}]妫�娴嬫暟閲忎负 0锛屾棤娉曡绠楀悎鏍肩巼锛�"
+                                    + "璇峰湪銆岃川閲忕鐞�-杩囩▼妫�楠屻�嶈ˉ褰曟娴嬫暟閲忓悗鍐嶅鎵�",
+                            processName, finishedList.get(0).getCode()));
+        }
+        BigDecimal passRate = qualifiedQuantity.multiply(BigDecimal.valueOf(100))
+                .divide(checkQuantity, 2, RoundingMode.HALF_UP);
+        return new MesProFeedbackQualityResult(passRate, checkQuantity, qualifiedQuantity, unqualifiedQuantity);
     }
 
     /**
@@ -582,13 +818,32 @@
                 .setTaskId(reqVO.getTaskId())
                 .setItemId(task.getItemId())
                 .setFeedbackQuantity(reqVO.getFeedbackQuantity())
+                .setWorkHours(reqVO.getWorkHours())
                 .setScheduledQuantity(task.getQuantity())
                 .setFeedbackTime(reqVO.getFeedbackTime() != null ? reqVO.getFeedbackTime() : LocalDateTime.now())
                 .setMachineryIds(reqVO.getMachineryIds())
+                .setMachineryId(firstMachineryId(reqVO.getMachineryIds()))
                 .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                 .setRemark(reqVO.getRemark());
+        // 4.1 蹇嵎鎶ュ伐榛樿娌℃湁宸ユ椂褰曞叆鍏ュ彛锛氳鏃惰钖笖宸插畾浠风殑宸ュ簭锛屽繀椤绘樉寮忎紶宸ユ椂锛�
+        //     鎵归噺鎶ュ伐绛変笉浼犲伐鏃剁殑鍏ュ彛浼氬緱鍒板彲鎵ц鐨勬彁绀猴紝寮曞鍒版姤宸ラ〉闈㈠~鍐�
+        MesProProcessDO process = processService.getProcess(task.getProcessId());
+        if (process == null) {
+            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId());
+        }
+        BigDecimal wageRate = ObjectUtil.defaultIfNull(wageRuleService.resolve(task.getProcessId(), task.getRouteId(),
+                feedback.getMachineryId(), process.getBillingType(), LocalDate.now()).getWageRate(), BigDecimal.ZERO);
+        if (MesProProcessBillingTypeEnum.TIME == MesProProcessBillingTypeEnum.valueOfType(process.getBillingType())
+                && wageRate.signum() > 0 && reqVO.getWorkHours() == null) {
+            throw exception(PRO_FEEDBACK_TIME_BILLING_QUICK_UNSUPPORTED, process.getName());
+        }
+        // 4.2 蹇嵎鎶ュ伐娌℃湁杈呭姪椤瑰叆鍙o紝浠呮寜鐢熶骇闀垮害锛堟姤宸ュ崟浣嶄负绫虫椂锛夎绠楁爣鍑嗗伐鏃跺揩鐓�
+        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, null);
+        // 4.3 鎸夊伐搴忚璐圭被鍨嬨�佸崟浠疯鍒欎笌宸ヨ祫瀛楁蹇収骞惰绠楁湰娆℃姤宸ュ伐璧�
+        applyProcessBilling(feedback, process, auxList);
         equipmentService.validateSubmission(feedback);
         feedbackMapper.insert(feedback);
+        saveFeedbackAux(feedback.getId(), auxList);
         saveParamValues(task.getId(), reqVO.getParamValues());
         // 5. 杩斿洖缁撴灉
         MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId());
@@ -608,6 +863,33 @@
                 .setTaskStatus(task.getStatus())
                 .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null)
                 .setReportableQuantity(reportableQuantity);
+    }
+
+    @Override
+    public MesProFeedbackWageBreakdown previewWage(MesProFeedbackWagePreviewReqVO reqVO) {
+        MesProTaskDO task = taskService.getTask(reqVO.getTaskId());
+        if (task == null) {
+            throw exception(PRO_TASK_NOT_EXISTS);
+        }
+        MesProProcessDO process = processService.getProcess(task.getProcessId());
+        if (process == null) {
+            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId());
+        }
+        // 鍊熺敤涓�涓复鏃舵姤宸ュ崟瀵硅薄澶嶇敤鏍囧噯宸ユ椂/杈呭姪宸ユ椂璁$畻
+        MesProFeedbackDO temp = new MesProFeedbackDO()
+                .setTaskId(task.getId())
+                .setRouteId(task.getRouteId())
+                .setProcessId(task.getProcessId())
+                .setWorkstationId(task.getWorkstationId())
+                .setMachineryId(reqVO.getMachineryId())
+                .setWorkHours(reqVO.getWorkHours())
+                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
+                .setQualifiedQuantity(reqVO.getQualifiedQuantity())
+                .setProductionLength(reqVO.getProductionLength());
+        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(temp, reqVO.getAuxList());
+        return wageCalculator.calculate(process, task.getRouteId(), reqVO.getMachineryId(),
+                reqVO.getWorkHours(), reqVO.getFeedbackQuantity(), reqVO.getQualifiedQuantity(), auxList,
+                reqVO.getFeedbackDate());
     }
 
     @Override
@@ -677,6 +959,23 @@
                 ObjectUtil.defaultIfNull(unqualifiedQty, BigDecimal.ZERO));
         //5.妫�鏌ュ苟鍙戝竷鐢熶骇瀹屾垚浜嬩欢
         checkAndPublishProductionFinished(feedback.getWorkOrderId());
+        checkAndAutoFinishAfterLastQc(feedback);
+    }
+
+    void checkAndAutoFinishAfterLastQc(MesProFeedbackDO feedback) {
+        MesProWorkOrderProcessDO lastProcess = workOrderProcessService
+                .getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
+        if (lastProcess == null || !ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
+            return;
+        }
+        // 鍚屼竴宸ュ崟鐨勬湯閬撴姤宸ュ叏閮ㄥ畬鎴愯川妫�鍚庯紝鎵嶅厑璁哥骇鑱斿畬鎴愬伐鍗曚换鍔°��
+        Long pendingCount = feedbackMapper.selectCount(new LambdaQueryWrapperX<MesProFeedbackDO>()
+                .eq(MesProFeedbackDO::getWorkOrderId, feedback.getWorkOrderId())
+                .eq(MesProFeedbackDO::getProcessId, feedback.getProcessId())
+                .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
+        if (pendingCount == 0) {
+            workOrderService.checkAndAutoFinishWorkOrder(feedback.getWorkOrderId());
+        }
     }
 
     @Override

--
Gitblit v1.9.3