yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -12,6 +12,7 @@
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.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
@@ -24,6 +25,7 @@
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.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
@@ -52,6 +54,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -77,6 +80,8 @@
    private MesProWorkOrderParamRecordService paramRecordService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
@@ -104,6 +109,7 @@
    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);
@@ -113,11 +119,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());
@@ -128,11 +136,14 @@
        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,
@@ -178,6 +189,13 @@
        Long id = reqVO.getId();
        // 1. 校验存在 + 草稿状态
        MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id);
        // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许提交
        if (feedback.getTaskId() != null) {
            MesProTaskDO submitTask = taskService.getTask(feedback.getTaskId());
            if (submitTask != null) {
                validatePreviousProcessFinished(submitTask);
            }
        }
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
@@ -273,23 +291,34 @@
                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 非质检工序:合格品/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品
        if (!checkFlag
                && feedback.getQualifiedQuantity() == null
                && feedback.getUnqualifiedQuantity() == null) {
            feedback.setQualifiedQuantity(feedback.getFeedbackQuantity());
            feedback.setUnqualifiedQuantity(BigDecimal.ZERO);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId())
                    .setQualifiedQuantity(feedback.getFeedbackQuantity())
                    .setUnqualifiedQuantity(BigDecimal.ZERO));
        }
        // 2.3 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截)
        if (!checkFlag
                && feedback.getUncheckQuantity() != null
                && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity());
        }
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        // 3. 物料消耗:倒冲工序按本工序投料 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        if (backflushFlag) {
            // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                    feedback.getWorkOrderId());
            // 3.1 校验倒冲工序必须配置本工序的投料 BOM,避免静默不扣料
            //     (只校验当前报工工序的投料,其他工序投料由各自工序报工时扣除)
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
                    feedback.getWorkOrderId(), feedback.getProcessId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
            // 3.2 生成消耗记录并执行扣减(BOM × 报工数量,FIFO 扣线边库)
            // 3.2 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库)
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
@@ -355,6 +384,12 @@
            if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) {
                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. 更新任务的已生产/合格/不合格数量
@@ -455,24 +490,79 @@
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 4.1 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        if (checkFlag) {
            // 需要检验:只需填报工数量,且必须 > 0
            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);
            }
        // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0
        // 合格品/不良品数量由质检确定(非质检工序在审批时默认全部为合格品)
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        }
        return task;
    }
    /**
     * 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报本道工序
     *
     * <p>判定口径:上一道工序的有效任务(未取消)累计审批通过的产量已达到其排产数量,
     * 即任一有效任务 produced_quantity &lt; quantity 都视为未完成并拦截。
     * 之所以不依赖任务 status:任务状态只有整单完工/取消时才置终态
     * (finishTaskByOrderId / cancelTaskByOrderId),无法作为"单道工序已完成"的依据,
     * produced_quantity 是唯一实时可靠的进度信号。
     *
     * @param task 当前待报工任务
     */
    private void validatePreviousProcessFinished(MesProTaskDO task) {
        if (task == null || task.getWorkOrderId() == null || task.getProcessId() == null) {
            return; // 数据不完整,无法推导顺序,放行
        }
        // 1. 当前任务所属工序在工单工序链(按 sort 升序)中的位置
        List<MesProWorkOrderProcessDO> processes = workOrderProcessService
                .getWorkOrderProcessListByWorkOrderId(task.getWorkOrderId());
        if (CollUtil.isEmpty(processes)) {
            return; // 工单未配置工序,放行
        }
        // 拷贝后再排序,避免修改调用方返回的(可能是不可变)集合
        processes = new ArrayList<>(processes);
        processes.sort(Comparator.comparing(MesProWorkOrderProcessDO::getSort,
                Comparator.nullsLast(Comparator.naturalOrder())));
        int index = -1;
        for (int i = 0; i < processes.size(); i++) {
            if (ObjUtil.equal(processes.get(i).getProcessId(), task.getProcessId())) {
                index = i;
                break;
            }
        }
        if (index <= 0) {
            return; // 当前工序不在链中(数据异常,放行)或本身就是第一道工序,无需前置
        }
        Long prevProcessId = processes.get(index - 1).getProcessId();
        if (prevProcessId == null) {
            return;
        }
        // 2. 上一道工序的有效任务(排除已取消)累计待完成量
        List<MesProTaskDO> prevTasks = taskService.getTaskListByWorkOrderIds(
                        Collections.singletonList(task.getWorkOrderId())).stream()
                .filter(t -> ObjUtil.equal(t.getProcessId(), prevProcessId))
                .filter(t -> !ObjUtil.equal(t.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus()))
                .toList();
        if (CollUtil.isEmpty(prevTasks)) {
            return; // 上一道工序无有效生产任务(含全部取消),视为无前置要求,放行
        }
        BigDecimal remaining = BigDecimal.ZERO;
        for (MesProTaskDO prevTask : prevTasks) {
            BigDecimal qty = prevTask.getQuantity() == null ? BigDecimal.ZERO : prevTask.getQuantity();
            BigDecimal produced = prevTask.getProducedQuantity() == null ? BigDecimal.ZERO : prevTask.getProducedQuantity();
            if (produced.compareTo(qty) < 0) {
                remaining = remaining.add(qty.subtract(produced));
            }
        }
        if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
            return; // 上一道工序已全部报满,放行
        }
        // 3. 拦截并提示上一道工序名称与剩余待报数量
        MesProProcessDO prevProcess = processService.getProcess(prevProcessId);
        String prevProcessName = prevProcess != null ? prevProcess.getName() : String.valueOf(prevProcessId);
        throw exception(PRO_FEEDBACK_PREV_PROCESS_NOT_FINISHED, prevProcessName, remaining);
    }
    @Override
@@ -485,6 +575,8 @@
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工
        validatePreviousProcessFinished(task);
        // 2. 校验工艺路线工序配置
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
@@ -492,19 +584,10 @@
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 3. 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        if (checkFlag) {
            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);
            }
        // 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定)
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        }
        // 4. 构建报工DO
        MesProFeedbackDO feedback = new MesProFeedbackDO()
@@ -517,8 +600,6 @@
                .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())
@@ -527,10 +608,21 @@
        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
@@ -544,8 +636,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()