11 小时以前 74ad846852f42b4e2258bfddd5c2a33cc1ce9847
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -4,6 +4,7 @@
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.*;
@@ -11,23 +12,34 @@
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;
@@ -39,7 +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;
@@ -50,10 +64,11 @@
/**
 * MES 生产报工 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@lombok.extern.slf4j.Slf4j
public class MesProFeedbackServiceImpl implements MesProFeedbackService {
    @Resource
@@ -62,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;
@@ -73,6 +95,9 @@
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
    @Lazy // 避免循环依赖:产品收货单自动生成逻辑依赖报工 Service
    private MesWmProductReceiptService productReceiptService;
    @Resource
    private MesWmProductProduceLineService produceLineService;
    @Resource
    private MesProMpsApi mpsApi;
@@ -80,8 +105,15 @@
    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);
@@ -91,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());
@@ -105,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
@@ -140,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
@@ -161,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) {
@@ -185,11 +311,12 @@
        }
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        //    报工数量为空/0 时跳过倒冲扣料,避免空数量误扣线边库
        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);
            }
@@ -201,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));
        // 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) {
            updateTaskAndWorkOrderByFeedback(feedback);
        } else {
            taskService.updateProducedQuantity(feedback.getTaskId(),
                    feedback.getFeedbackQuantity(),
                    ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
                    ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
                checkAndPublishProductionFinished(feedback.getWorkOrderId());
        }
        if (checkFlag) {
            // 6.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
            productProduceService.generateProductProduce(feedback, true);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
            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);
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
            // 关键工序:更新任务 + 工单的已生产数量;非关键工序:只更新任务的已生产数量(工单只看关键工序产出)
            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());
            }
            // 6.3 报工产出定稿后自动生成产品入库单(一次报工=一条,幂等)
            productReceiptService.createProductReceiptByFeedback(feedback.getId());
        }
        return true; // 已完成
@@ -239,6 +379,7 @@
     *   <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li>
     *   <li>使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量
     *       (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)</li>
     *      isPush 是否推送生产完成事件
     * </ul>
     *
     * @param feedback 报工单
@@ -256,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());
@@ -333,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());
@@ -346,24 +502,47 @@
        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
@@ -371,27 +550,28 @@
    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. 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        if (checkFlag) {
        // 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);
            }
        } else {
            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);
            }
        }
        // 4. 构建报工DO
        // 5. 构建报工DO
        MesProFeedbackDO feedback = new MesProFeedbackDO()
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
                .setType(MesProFeedbackTypeEnum.SELF.getType())
@@ -402,19 +582,29 @@
                .setTaskId(reqVO.getTaskId())
                .setItemId(task.getItemId())
                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
                .setQualifiedQuantity(reqVO.getQualifiedQuantity())
                .setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity())
                .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);
                .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null)
                .setReportableQuantity(reportableQuantity);
    }
    @Override
@@ -428,8 +618,6 @@
                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
                singleReq.setTaskId(taskId);
                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
                singleReq.setQualifiedQuantity(reqVO.getQualifiedQuantity());
                singleReq.setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity());
                singleReq.setRemark(reqVO.getRemark());
                MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
@@ -477,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;
    }
}