From 74ad846852f42b4e2258bfddd5c2a33cc1ce9847 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 29 八月 2026 18:03:46 +0800
Subject: [PATCH] feat(mes): 新增工序等待时长和报工数量配置功能

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java |  465 ++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 405 insertions(+), 60 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 7e1868e..489c6b4 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
@@ -4,28 +4,42 @@
 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.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackPageReqVO;
-import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
+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.MesProFeedbackDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
-import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
 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.mysql.pro.feedback.MesProFeedbackMapper;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
+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.route.MesProRouteProductBomService;
 import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
+import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
+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.wm.itemconsume.MesWmItemConsumeService;
 import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
 import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
+import cn.iocoder.yudao.module.mes.service.wm.productreceipt.MesWmProductReceiptService;
+import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
+import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
 import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
 import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
 import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
@@ -37,6 +51,9 @@
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -47,10 +64,11 @@
 /**
  * MES 鐢熶骇鎶ュ伐 Service 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
+@lombok.extern.slf4j.Slf4j
 public class MesProFeedbackServiceImpl implements MesProFeedbackService {
 
     @Resource
@@ -59,9 +77,16 @@
     @Resource
     private MesProWorkOrderService workOrderService;
     @Resource
+    @Lazy
+    private MesProWorkOrderParamRecordService paramRecordService;
+    @Resource
     private MesProRouteProcessService routeProcessService;
     @Resource
-    private MesProRouteProductBomService routeProductBomService;
+    private MesProProcessService processService;
+    @Resource
+    private MesProWorkOrderBomService workOrderBomService;
+    @Resource
+    private MesProWorkOrderProcessService workOrderProcessService;
     @Resource
     @Lazy // 閬垮厤寰幆渚濊禆
     private MesProTaskService taskService;
@@ -70,13 +95,25 @@
     @Resource
     private MesWmProductProduceService productProduceService;
     @Resource
+    @Lazy // 閬垮厤寰幆渚濊禆锛氫骇鍝佹敹璐у崟鑷姩鐢熸垚閫昏緫渚濊禆鎶ュ伐 Service
+    private MesWmProductReceiptService productReceiptService;
+    @Resource
     private MesWmProductProduceLineService produceLineService;
     @Resource
     private MesProMpsApi mpsApi;
     @Resource
+    private MesMdAutoCodeRecordService autoCodeRecordService;
+    @Resource
     private ApplicationEventPublisher eventPublisher;
+    @Resource
+    private MesMdmItemService mdmItemService;
+    @Resource
+    private MdmItemBatchConfigApi itemBatchConfigApi;
+    @Resource
+    private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
         // 1. 鏍¢獙
         MesProTaskDO task = validateFeedbackData(createReqVO);
@@ -86,10 +123,13 @@
                 .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                 .setItemId(task.getItemId());
         feedbackMapper.insert(feedback);
+        // 3. 淇濆瓨鍙傛暟鍊硷紱蹇呭~鍙傛暟鏍¢獙澶辫触鏃舵暣浣撳洖婊氾紝閬垮厤娈嬬暀鑽夌
+        saveParamValues(task.getId(), createReqVO.getParamValues());
         return feedback.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
         // 1.1 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
         validateFeedbackStatusPrepare(updateReqVO.getId());
@@ -100,6 +140,19 @@
         MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                 .setItemId(task.getItemId());
         feedbackMapper.updateById(updateObj);
+        // 3. 淇濆瓨鍙傛暟鍊硷紱蹇呭~鍙傛暟鏍¢獙澶辫触鏃舵暣浣撳洖婊�
+        saveParamValues(task.getId(), updateReqVO.getParamValues());
+    }
+
+    private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) {
+        // 鍗充娇鍙傛暟涓虹┖涔熻皟鐢ㄦ洿鏂帮紝浠ヤ究瑙﹀彂蹇呭~鍙傛暟鏍¢獙锛堜换鍔$粦瀹氫簡鍙傛暟鏃跺繀椤诲~鍐欙級
+        if (CollUtil.isEmpty(paramValues)) {
+            paramRecordService.updateTaskParamValues(taskId, null, null);
+            return;
+        }
+        paramRecordService.updateTaskParamValues(taskId,
+                paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getDetailId).toList(),
+                paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getValue).toList());
     }
 
     @Override
@@ -135,13 +188,76 @@
     }
 
     @Override
-    public void submitFeedback(Long id) {
+    @Transactional(rollbackFor = Exception.class)
+    public void submitFeedback(MesProFeedbackSubmitReqVO reqVO) {
+        Long id = reqVO.getId();
         // 1. 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        validateFeedbackStatusPrepare(id);
+        MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id);
 
-        // 2. 鏇存柊鐘舵�佷负瀹℃壒涓紙鎶ュ伐浜哄拰鎶ュ伐鏃堕棿鐢辫〃鍗曚繚瀛樻椂纭畾锛屾彁浜や笉瑕嗙洊锛�
-        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
-                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
+        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
+
+        if (workOrder != null && Boolean.TRUE.equals(workOrder.getFeedbackApprove()) && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
+            // 2. 鏇存柊鐘舵�佷负瀹℃壒涓紝骞跺惎鍔ㄦ祦绋�
+            String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(),
+                    new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO()
+                            .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
+                            .setBusinessKey(String.valueOf(id)));
+
+            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
+                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
+                    .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
+                    .setProcessInstanceId(processInstanceId));
+        } else {
+            // 2. 鍒ゆ柇杩欎釜鎶ュ伐闇�涓嶉渶瑕佹壒娆″彿
+            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
+            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()) {
+                if (StrUtil.isBlank(reqVO.getLotNumber())) {
+                    throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
+                }
+            }
+
+            // 3. 鏇存柊鐘舵�佷负瀹℃壒涓紝鐒跺悗鐩存帴閫氳繃瀹℃壒
+            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);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handleBpmProcessResult(Long id, Integer bpmResult,String reason) {
+        // 1. 鏌旀�ф牎楠岋細鑾峰彇鎶ュ伐鍗曞苟鍒ゆ柇鐘舵��
+        MesProFeedbackDO feedback = feedbackMapper.selectById(id);
+        if (feedback == null) {
+            log.warn("[handleBpmProcessResult] 鐢熶骇鎶ュ伐({}) 涓嶅瓨鍦�", id);
+            return;
+        }
+        if (!MesProFeedbackStatusEnum.APPROVING.getStatus().equals(feedback.getStatus())) {
+            log.warn("[handleBpmProcessResult] 鐢熶骇鎶ュ伐({}) 涓嶅浜庡鎵逛腑鐘舵�侊紝褰撳墠鐘舵�侊細{}", id, feedback.getStatus());
+            return;
+        }
+
+        // 2. 鏍规嵁瀹℃壒缁撴灉杩涜澶勭悊
+        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
+            MesProFeedbackApproveReqVO  reqVO = new MesProFeedbackApproveReqVO();
+            reqVO.setId(id);
+            //  鍒ゆ柇杩欎釜鎶ュ伐闇�涓嶉渶瑕佹壒娆″彿
+            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
+            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()){
+               if (StrUtil.isBlank(reason)){
+                   //  鎶ラ敊 璇峰湪瀹℃壒鎰忚閲屽~鍐欐壒娆″彿
+                   throw  exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
+               }else {
+                 BeanUtils.copyProperties(mesProFeedbackBatchCheckRespVO,reqVO);
+                 reqVO.setLotNumber(reason);
+               }
+            }
+            approveFeedback(reqVO);
+        } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) ||
+                   cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
+            rejectFeedback(id);
+        }
     }
 
     @Override
@@ -156,23 +272,38 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean approveFeedback(Long id) {
+    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
         // 1.1 鏍¢獙瀛樺湪 + 瀹℃壒涓姸鎬�
-        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
-        // 1.2 鏍¢獙鎶ュ伐鏁伴噺 > 0
-        if (feedback.getFeedbackQuantity() == null
-                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
-            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
+        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
+        // 1.2 鏍¢獙浠诲姟鏈畬鎴� + 绛夊緟鏃堕暱鍊掕鏃�
+        MesProTaskDO task = taskService.validateTaskNotFinished(feedback.getTaskId());
+        MesProProcessDO process = validateWaitDuration(task);
+        BigDecimal fbQty = feedback.getFeedbackQuantity() == null
+                ? BigDecimal.ZERO : feedback.getFeedbackQuantity();
+        // 1.3 鏍¢獙鎶ュ伐鏁伴噺锛氶粯璁ゅ繀椤� > 0锛涘伐搴忛厤缃�"涓嶉渶瑕佸~鍐欐姤宸ユ暟閲�"鏃跺厑璁镐负绌�
+        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
+            if (fbQty.compareTo(BigDecimal.ZERO) <= 0) {
+                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
+            }
         }
-        // 1.3 鏍¢獙浠诲姟鏈畬鎴�
-        taskService.validateTaskNotFinished(feedback.getTaskId());
 
         // 2.1 鏌ヨ宸ュ簭鐨勫叧閿伐搴忔爣璇� + 妫�楠屾爣璇嗭紙闇�鍦� uncheckQuantity 鏍¢獙涔嬪墠锛屽洜涓鸿川妫�宸ュ簭鍏佽 uncheckQuantity > 0锛�
         MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                 feedback.getRouteId(), feedback.getProcessId());
         boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag());
         boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag());
-        // 2.2 闈炶川妫�宸ュ簭锛氫粛鏈夊緟妫�鏁伴噺鏃朵笉鑳藉鎵癸紙璐ㄦ宸ュ簭鐨� uncheckQuantity > 0 鏄甯哥姸鎬侊紝涓嶅仛鎷︽埅锛�
+        // 2.2 闈炶川妫�宸ュ簭锛氬悎鏍煎搧/涓嶈壇鍝佹暟閲忕敱璐ㄦ纭畾锛涙湰宸ュ簭涓嶇粡杩囪川妫�锛屾姤宸ユ湭濉啓鏃堕粯璁ゅ叏閮ㄤ负鍚堟牸鍝�
+        //     锛堟暟閲忎负绌�/0 鏃舵寜 0 澶勭悊锛岄伩鍏嶅啓鍏� null锛�
+        if (!checkFlag
+                && feedback.getQualifiedQuantity() == null
+                && feedback.getUnqualifiedQuantity() == null) {
+            feedback.setQualifiedQuantity(fbQty);
+            feedback.setUnqualifiedQuantity(BigDecimal.ZERO);
+            feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId())
+                    .setQualifiedQuantity(fbQty)
+                    .setUnqualifiedQuantity(BigDecimal.ZERO));
+        }
+        // 2.3 闈炶川妫�宸ュ簭锛氫粛鏈夊緟妫�鏁伴噺鏃朵笉鑳藉鎵癸紙璐ㄦ宸ュ簭鐨� uncheckQuantity > 0 鏄甯哥姸鎬侊紝涓嶅仛鎷︽埅锛�
         if (!checkFlag
                 && feedback.getUncheckQuantity() != null
                 && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) {
@@ -180,11 +311,12 @@
         }
 
         // 3. 鐗╂枡娑堣�楋細鍊掑啿宸ュ簭鎸� BOM 鑷姩鎵f枡锛涢潪鍊掑啿宸ュ簭璺宠繃锛堢墿鏂欒蛋鎵嬪伐棰嗘枡/鎶曟枡锛�
+        //    鎶ュ伐鏁伴噺涓虹┖/0 鏃惰烦杩囧�掑啿鎵f枡锛岄伩鍏嶇┖鏁伴噺璇墸绾胯竟搴�
         boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
-        if (backflushFlag) {
+        if (backflushFlag && fbQty.compareTo(BigDecimal.ZERO) > 0) {
             // 3.1 鏍¢獙鍊掑啿宸ュ簭蹇呴』閰嶇疆 BOM 娑堣�楋紝閬垮厤闈欓粯涓嶆墸鏂�
-            List<MesProRouteProductBomDO> boms = routeProductBomService.getRouteProductBomList(
-                    feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId());
+            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
+                    feedback.getWorkOrderId());
             if (CollUtil.isEmpty(boms)) {
                 throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
             }
@@ -196,32 +328,45 @@
         }
 
         // 5. 鍏堟洿鏂版姤宸ョ姸鎬佷负宸插畬鎴愶紙纭繚 checkAndFinishMps 妫�鏌ユ椂褰撳墠鎶ュ伐宸叉爣璁板畬鎴愶級
-        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
+        feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                 .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus())
                 .setUncheckQuantity(BigDecimal.ZERO));
 
-        // 6. 鍏抽敭宸ュ簭锛氱敓鎴愪骇鍑哄崟锛屽苟鏍规嵁鏄惁闇�瑕佹楠屽喅瀹氬叆搴撴柟寮�
+        // 5.1 鎶ュ伐鏁伴噺涓虹┖/0锛�"涓嶉渶瑕佸~鍐欐姤宸ユ暟閲�"鐨勫伐搴忥級锛氭棤浜у嚭鍙叆搴擄紝浠呮洿鏂颁换鍔¤繘搴﹀苟缁撴潫
+        if (fbQty.compareTo(BigDecimal.ZERO) <= 0) {
+            taskService.updateProducedQuantity(feedback.getTaskId(),
+                    BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
+            checkAndPublishProductionFinished(feedback.getWorkOrderId());
+            return true;
+        }
+
+        // 6. 鏍规嵁鏄惁闇�瑕佹楠岋紝鍐冲畾浜у嚭鍜屽叆搴撴柟寮�
+        // 6.2 璋冩暣 鍙瀹℃壒鎴愬姛鐩存帴鏇存柊
+        // 鍏抽敭宸ュ簭锛氭洿鏂颁换鍔� + 宸ュ崟鐨勫凡鐢熶骇鏁伴噺锛涢潪鍏抽敭宸ュ簭锛氬彧鏇存柊浠诲姟鐨勫凡鐢熶骇鏁伴噺锛堝伐鍗曞彧鐪嬪叧閿伐搴忎骇鍑猴級
         if (keyFlag) {
-            // 6.1 闇�瑕佹楠岋細鐢熸垚浜у嚭鍗曪紙璐ㄩ噺鐘舵��=寰呮楠岋級锛屾洿鏂版姤宸ョ姸鎬佷负寰呮楠岋紝绛夎川妫�瀹屾垚鍥炶皟鍚庡啀鍏ュ簱
-            if (checkFlag) {
-                // 瀹屾垚鏃跺洖璋冭锛歁esQcIpqcServiceImpl#finishIpqc 鈫� splitPendingAndFinishProduce + completeFeedbackFromIpqc
-                productProduceService.generateProductProduce(feedback, true);
-                feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
-                        .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
-                return false;
-            }
-            // 6.2 鏃犻渶妫�楠岋細鐢熸垚浜у嚭鍗曪紙鎸夊悎鏍�/涓嶅悎鏍兼媶琛岋級锛岀洿鎺ュ畬鎴愬叆搴擄紝骞舵洿鏂颁换鍔�/宸ュ崟鐨勫凡鐢熶骇鏁伴噺
-            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false);
-            productProduceService.finishProductProduce(produce.getId());
             updateTaskAndWorkOrderByFeedback(feedback);
         } else {
-            // 6.3 闈炲叧閿伐搴忥細鍙洿鏂颁换鍔$殑宸茬敓浜ф暟閲忥紙宸ュ崟涓嶆洿鏂帮紝宸ュ崟鍙湅鍏抽敭宸ュ簭浜у嚭锛�
             taskService.updateProducedQuantity(feedback.getTaskId(),
                     feedback.getFeedbackQuantity(),
                     ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
                     ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
-            // 6.4 闈炲叧閿伐搴忎篃闇�瑕佹鏌� MPS 瀹屾垚鐘舵�侊紙鎵�鏈夊伐搴忔姤宸ュ畬鎴愭墠绠楀畬鎴愶級
-            checkAndPublishProductionFinished(feedback.getWorkOrderId());
+                checkAndPublishProductionFinished(feedback.getWorkOrderId());
+
+        }
+        if (checkFlag) {
+            // 6.1 闇�瑕佹楠岋細鐢熸垚浜у嚭鍗曪紙璐ㄩ噺鐘舵��=寰呮楠岋級锛屾洿鏂版姤宸ョ姸鎬佷负寰呮楠岋紝绛夎川妫�瀹屾垚鍥炶皟鍚庡啀鍏ュ簱
+            // 瀹屾垚鏃跺洖璋冭锛歁esQcIpqcServiceImpl#finishIpqc 鈫� splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
+            productProduceService.generateProductProduce(feedback, true, reqVO);
+            feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
+                    .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
+            return false;
+        } else {
+            // 6.2 鏃犻渶妫�楠岋細鐢熸垚浜у嚭鍗曪紙鎸夊悎鏍�/涓嶅悎鏍兼媶琛岋級锛岀洿鎺ュ畬鎴愬叆搴�
+            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
+            productProduceService.finishProductProduce(produce.getId());
+            // 6.3 鎶ュ伐浜у嚭瀹氱鍚庤嚜鍔ㄧ敓鎴愪骇鍝佸叆搴撳崟锛堜竴娆℃姤宸�=涓�鏉★紝骞傜瓑锛�
+            productReceiptService.createProductReceiptByFeedback(feedback.getId());
+
         }
 
         return true; // 宸插畬鎴�
@@ -234,6 +379,7 @@
      *   <li>浣跨敤鎶ュ伐鏁伴噺锛坒eedbackQuantity锛夛紝绱姞浠诲姟鍜屽伐鍗曠殑宸茬敓浜ф暟閲�</li>
      *   <li>浣跨敤浜у嚭鍗曡鎸� qualityStatus 鑱氬悎鐨勫悎鏍煎搧/涓嶅悎鏍煎搧鏁伴噺锛岀疮鍔犱换鍔$殑鍚堟牸鍝�/涓嶅悎鏍煎搧鏁伴噺
      *       锛堜笉鐩存帴鐢� feedback 涓婄殑鏁伴噺锛岀‘淇濊川妫�鍥炶皟鍦烘櫙涓嬫暟閲忔潵婧愭纭級</li>
+     *      isPush 鏄惁鎺ㄩ�佺敓浜у畬鎴愪簨浠�
      * </ul>
      *
      * @param feedback 鎶ュ伐鍗�
@@ -251,19 +397,29 @@
                 qualifiedQty = qualifiedQty.add(line.getQuantity());
             }
         }
+        // 1.1 鍏滃簳锛氬叧閿潪璐ㄦ宸ュ簭瀹℃壒鏃朵骇鍑哄崟琛屽皻鏈敓鎴愶紝鐩存帴閲囩敤鎶ュ伐鍗曚笂鐨勫悎鏍�/涓嶈壇鏁伴噺
+        //     锛堥潪璐ㄦ宸ュ簭榛樿鍏ㄩ儴涓哄悎鏍煎搧锛涜川妫�宸ュ簭鐨勫悎鏍�/涓嶈壇鍦ㄨ川妫�鍥炶皟鍚庢寜浜у嚭琛岃仛鍚堬級
+        if (CollUtil.isEmpty(lines)) {
+            qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO);
+            unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO);
+        }
 
-        // 2. 鏇存柊浠诲姟鐨勫凡鐢熶骇/鍚堟牸/涓嶅悎鏍兼暟閲�
+        //   2. 鏇存柊浠诲姟鐨勫凡鐢熶骇/鍚堟牸/涓嶅悎鏍兼暟閲�
         taskService.updateProducedQuantity(feedback.getTaskId(),
                 feedback.getFeedbackQuantity(), qualifiedQty, unqualifiedQty);
-        // 3. 鏇存柊宸ュ崟鐨勫凡鐢熶骇鏁伴噺
-        workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
-                feedback.getFeedbackQuantity());
-
-        // 4. 鏇存柊 MPS 鐨勫凡鐢熶骇鏁伴噺锛堝鏋滄槸閿�鍞鍗曟潵婧愶級
-        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId());
-        if (mps != null) {
-            mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity());
+        // 3. 鏇存柊璁㈠崟鐨勫凡鐢熶骇鏁伴噺
+        // 鏂板閫昏緫锛氬彧鏈夊湪璇ョ瑪璁㈠崟鏈�鍚庝竴涓伐搴忔墠鏇存柊鐢熶骇璁㈠崟鐨勫凡鐢熶骇鏁伴噺
+        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());
+            }
         }
+
 
         // 5. 妫�鏌ュ苟鍙戝竷鐢熶骇瀹屾垚浜嬩欢
         checkAndPublishProductionFinished(feedback.getWorkOrderId());
@@ -328,6 +484,11 @@
         // 1. 鏍¢獙浠诲姟瀛樺湪涓旀湭缁堟��
         MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
 
+        // 鏍¢獙鍋滃伐鐘舵�佷笉鑳芥姤宸�
+        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
+            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
+        }
+
         // 2. 浠庝换鍔¤嚜鍔ㄥ~鍏呭叧鑱斿瓧娈碉紙鍓嶇鏃犻渶浼犲叆锛屽悗绔粺涓�浠庝换鍔¤幏鍙栵級
         reqVO.setWorkOrderId(task.getWorkOrderId());
         reqVO.setRouteId(task.getRouteId());
@@ -341,24 +502,141 @@
         if (routeProcess == null) {
             throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
         }
-        // 4.1 鏍¢獙鏁伴噺
-        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
-        if (checkFlag) {
-            // 闇�瑕佹楠岋細鍙渶濉姤宸ユ暟閲忥紝涓斿繀椤� > 0
+        // 4. 鏍¢獙绛夊緟鏃堕暱鍊掕鏃讹細宸ュ簭閰嶇疆浜嗙瓑寰呮椂闀匡紙waitDuration > 0锛夋椂锛岄渶鍏堝紑濮嬪�掕鏃跺苟绛夊緟缁撴潫
+        MesProProcessDO process = validateWaitDuration(task);
+        // 4.1 鏍¢獙鏁伴噺锛氶粯璁ゅ繀椤� > 0锛涘伐搴忛厤缃�"涓嶉渶瑕佸~鍐欐姤宸ユ暟閲�"鏃跺厑璁镐负绌�
+        // 鍚堟牸鍝�/涓嶈壇鍝佹暟閲忕敱璐ㄦ纭畾锛堥潪璐ㄦ宸ュ簭鍦ㄥ鎵规椂榛樿鍏ㄩ儴涓哄悎鏍煎搧锛�
+        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
             if (reqVO.getFeedbackQuantity() == null
                     || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                 throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
             }
-        } else {
-            // 涓嶉渶妫�楠岋細闇�濉悎鏍煎搧 + 涓嶈壇鍝佹暟閲忥紝鍚堣 > 0
-            BigDecimal qualified = ObjectUtil.defaultIfNull(reqVO.getQualifiedQuantity(), BigDecimal.ZERO);
-            BigDecimal unqualified = ObjectUtil.defaultIfNull(reqVO.getUnqualifiedQuantity(), BigDecimal.ZERO);
-            if (qualified.add(unqualified).compareTo(BigDecimal.ZERO) <= 0) {
-                throw exception(PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED);
-            }
         }
 
         return task;
+    }
+
+    /**
+     * 鏍¢獙宸ュ簭绛夊緟鏃堕暱鍊掕鏃讹紙鎶ュ伐鍓嶇疆鏉′欢锛�
+     *
+     * <p>璇诲彇浠诲姟鍏宠仈鐨勫伐搴忎富鏁版嵁 waitDuration锛堝皬鏃讹級锛氳嫢鏈厤缃垨涓� 0 鍒欑洿鎺ヨ繑鍥烇紱閰嶇疆浜嗗垯闇�瑕�
+     * 浠诲姟宸茬偣鍑�"寮�濮嬪�掕鏃�"涓斿�掕鏃跺凡缁撴潫锛屽惁鍒欐姏鍑哄搴斿紓甯搞�傝繑鍥炲伐搴忎富鏁版嵁锛屼緵璋冪敤鏂瑰鐢�
+     * needReportQuantity 绛夊瓧娈点��
+     *
+     * @param task 鐢熶骇浠诲姟
+     * @return 宸ュ簭涓绘暟鎹�
+     */
+    private MesProProcessDO validateWaitDuration(MesProTaskDO task) {
+        MesProProcessDO process = processService.getProcess(task.getProcessId());
+        if (process == null || process.getWaitDuration() == null
+                || process.getWaitDuration().compareTo(BigDecimal.ZERO) <= 0) {
+            return process; // 鏈厤缃瓑寰呮椂闀匡紝鏃犻渶鍊掕鏃�
+        }
+        if (task.getWaitStartTime() == null) {
+            throw exception(PRO_TASK_WAIT_NOT_STARTED); // 璇峰厛鐐瑰嚮"寮�濮嬪�掕鏃�"
+        }
+        LocalDateTime readyTime = task.getWaitStartTime()
+                .plusMinutes((long) (process.getWaitDuration().doubleValue() * 60));
+        if (LocalDateTime.now().isBefore(readyTime)) {
+            Duration remaining = Duration.between(LocalDateTime.now(), readyTime);
+            throw exception(PRO_TASK_WAIT_NOT_ELAPSED,
+                    remaining.toHours() + "灏忔椂" + remaining.toMinutesPart() + "鍒嗛挓");
+        }
+        return process;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
+        // 1. 鏍¢獙浠诲姟瀛樺湪涓旈潪缁堟��
+        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
+
+        // 鏍¢獙鍋滃伐鐘舵�佷笉鑳芥姤宸�
+        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
+            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
+        }
+
+        // 2. 鏍¢獙宸ヨ壓璺嚎宸ュ簭閰嶇疆
+        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
+                task.getRouteId(), task.getProcessId());
+        if (routeProcess == null) {
+            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
+        }
+        // 3. 鏍¢獙绛夊緟鏃堕暱鍊掕鏃讹細宸ュ簭閰嶇疆浜嗙瓑寰呮椂闀匡紙waitDuration > 0锛夋椂锛岄渶鍏堝紑濮嬪�掕鏃跺苟绛夊緟缁撴潫
+        MesProProcessDO process = validateWaitDuration(task);
+        // 4. 鏍¢獙鏁伴噺锛氶粯璁ゅ繀椤� > 0锛涘伐搴忛厤缃�"涓嶉渶瑕佸~鍐欐姤宸ユ暟閲�"鏃跺厑璁镐负绌猴紙鍚堟牸鍝�/涓嶈壇鍝佹暟閲忕敱璐ㄦ纭畾锛�
+        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
+            if (reqVO.getFeedbackQuantity() == null
+                    || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
+            }
+        }
+        // 5. 鏋勫缓鎶ュ伐DO
+        MesProFeedbackDO feedback = new MesProFeedbackDO()
+                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
+                .setType(MesProFeedbackTypeEnum.SELF.getType())
+                .setWorkstationId(task.getWorkstationId())
+                .setRouteId(task.getRouteId())
+                .setProcessId(task.getProcessId())
+                .setWorkOrderId(task.getWorkOrderId())
+                .setTaskId(reqVO.getTaskId())
+                .setItemId(task.getItemId())
+                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
+                .setScheduledQuantity(task.getQuantity())
+                .setFeedbackTime(LocalDateTime.now())
+                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
+                .setRemark(reqVO.getRemark());
+        feedbackMapper.insert(feedback);
+        saveParamValues(task.getId(), reqVO.getParamValues());
+        // 5. 杩斿洖缁撴灉
+        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId());
+        // 5.1 鎶ュ伐鍚庡墿浣欏彲鎶ュ伐鏁伴噺 = 鎺掍骇 - 宸茬敓浜� - 鍦ㄩ�旓紙鍚湰娆℃姤宸ワ級
+        BigDecimal reportable = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity();
+        BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
+        BigDecimal inTransit = BigDecimal.ZERO;
+        for (MesProFeedbackDO fb : getFeedbackListByTaskId(task.getId())) {
+            if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())) {
+                inTransit = inTransit.add(fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity());
+            }
+        }
+        BigDecimal reportableQuantity = reportable.subtract(produced).subtract(inTransit).max(BigDecimal.ZERO);
+        return new MesProFeedbackQuickCreateRespVO()
+                .setFeedbackId(feedback.getId())
+                .setTaskStatus(task.getStatus())
+                .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null)
+                .setReportableQuantity(reportableQuantity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) {
+        List<MesProFeedbackBatchCreateRespVO.BatchResult> results = new java.util.ArrayList<>();
+        int successCount = 0;
+        int failCount = 0;
+        for (Long taskId : reqVO.getTaskIds()) {
+            try {
+                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
+                singleReq.setTaskId(taskId);
+                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
+                singleReq.setRemark(reqVO.getRemark());
+                MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
+                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
+                        .setTaskId(taskId)
+                        .setSuccess(true)
+                        .setFeedbackId(resp.getFeedbackId()));
+                successCount++;
+            } catch (Exception e) {
+                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
+                        .setTaskId(taskId)
+                        .setSuccess(false)
+                        .setErrorMsg(e.getMessage()));
+                failCount++;
+            }
+        }
+        return new MesProFeedbackBatchCreateRespVO()
+                .setSuccessCount(successCount)
+                .setFailCount(failCount)
+                .setResults(results);
     }
 
     @Override
@@ -387,7 +665,74 @@
 
         // 4. 鏇存柊浠诲姟/宸ュ崟鐨勫凡鐢熶骇鏁伴噺
         feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
-        updateTaskAndWorkOrderByFeedback(feedback);
+        //5.妫�鏌ュ苟鍙戝竷鐢熶骇瀹屾垚浜嬩欢
+        checkAndPublishProductionFinished(feedback.getWorkOrderId());
+    }
+
+    @Override
+    public MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId) {
+        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
+        MesProFeedbackBatchCheckRespVO respVO = new MesProFeedbackBatchCheckRespVO();
+
+        // 1. 妫�鏌ョ墿鏂欐槸鍚﹀惎鐢ㄦ壒娆$鐞�
+        MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
+        if (item == null || Boolean.FALSE.equals(item.getIsBatchManaged())) {
+            respVO.setNeedBatchInput(false);
+            respVO.setIsBatchManaged(false);
+            return respVO;
+        }
+        respVO.setIsBatchManaged(true);
+
+        // 2. 鑾峰彇鎵规閰嶇疆
+        MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(feedback.getItemId());
+        if (config == null) {
+            respVO.setNeedBatchInput(false);
+            return respVO;
+        }
+
+        // 3. 濉厖閰嶇疆鏍囧織
+        respVO.setProduceDateFlag(config.getProduceDateFlag());
+        respVO.setExpireDateFlag(config.getExpireDateFlag());
+        respVO.setReceiptDateFlag(config.getReceiptDateFlag());
+        respVO.setVendorFlag(config.getVendorFlag());
+        respVO.setClientFlag(config.getClientFlag());
+        respVO.setSalesOrderCodeFlag(config.getSalesOrderCodeFlag());
+        respVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag());
+        respVO.setWorkOrderFlag(config.getWorkOrderFlag());
+        respVO.setTaskFlag(config.getTaskFlag());
+        respVO.setWorkstationFlag(config.getWorkstationFlag());
+        respVO.setToolFlag(config.getToolFlag());
+        respVO.setMoldFlag(config.getMoldFlag());
+        respVO.setLotNumberFlag(config.getLotNumberFlag());
+        respVO.setQualityStatusFlag(config.getQualityStatusFlag());
+
+        // 4. 鍒ゆ柇鍝簺瀛楁宸茶嚜鍔ㄥ~鍏咃紙鎶ュ伐鍜岀敓浜ц繃绋嬩腑蹇呯劧鏈夊�肩殑瀛楁锛�
+        List<String> autoFilled = new ArrayList<>();
+        // 鐢熶骇鏃ユ湡锛氬鎵规椂鑷姩鍙栧綋澶�
+        autoFilled.add("produceDate");
+        // 鐢熶骇宸ュ崟锛氫粠鎶ュ伐璁板綍鑾峰彇
+        autoFilled.add("workOrderId");
+        // 鐢熶骇浠诲姟锛氫粠鎶ュ伐璁板綍鑾峰彇
+        autoFilled.add("taskId");
+        // 宸ヤ綔绔欙細浠庢姤宸ヨ褰曡幏鍙�
+        autoFilled.add("workstationId");
+        respVO.setAutoFilledFields(autoFilled);
+
+        // 5. 鍒ゆ柇鏄惁闇�瑕佸脊绐楋細瀛樺湪蹇呭~浣嗘棤娉曡嚜鍔ㄥ~鍏呯殑瀛楁
+        boolean needBatchInput = false;
+        if (Boolean.TRUE.equals(config.getExpireDateFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getReceiptDateFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getVendorFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getClientFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getToolFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getMoldFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getLotNumberFlag())) needBatchInput = true;
+        if (Boolean.TRUE.equals(config.getQualityStatusFlag())) needBatchInput = true;
+        respVO.setNeedBatchInput(needBatchInput);
+
+        return respVO;
     }
 
 }

--
Gitblit v1.9.3