feat(mes): 生产报工按工单工序顺序控制,上一道报满后才能报下一道

- 新增校验 validatePreviousProcessFinished:同一生产工单内上一道工序
produced_quantity >= quantity(报满)后才允许报本道工序
- 拦截入口:submitFeedback、quickCreateFeedback(批量报工经 quickCreate 自动覆盖);
草稿保存不拦
- 判定用产量口径而非任务状态(任务状态仅整单完工/取消才置终态);
质检工序待 IPQC 回调产量计入后才放行下一道
- 新增错误码 1040506016:提示上一道工序名称与剩余待报数量
- 补充 6 个顺序报工单元测试

Co-Authored-By: Claude <noreply@anthropic.com>
已修改3个文件
390 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -328,6 +328,10 @@
    ErrorCode PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS = new ErrorCode(1_040_506_012, "当前报工单未完成检验(待检数量:{}),无法执行报工");
    ErrorCode PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED = new ErrorCode(1_040_506_013, "当前工序为倒冲工序,但未配置BOM消耗,无法报工");
    ErrorCode PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED = new ErrorCode(1_040_506_014, "生产任务处于停工状态,无法报工");
    ErrorCode PRO_FEEDBACK_PENDING_PRODUCE_UNREPAIRABLE = new ErrorCode(1_040_506_015,
            "报工对应的产出入库单已存在处理结果,无法自动补生成待检行,请运行质检报工数据修复脚本核对");
    ErrorCode PRO_FEEDBACK_PREV_PROCESS_NOT_FINISHED = new ErrorCode(1_040_506_016,
            "上一道工序「{}」尚未报工完成(剩余 {}),请先完成上一道工序的报工");
    // ========== MES 生产管理-生产流转卡(1-040-507-000) ==========
    ErrorCode PRO_CARD_NOT_EXISTS = new ErrorCode(1_040_507_000, "生产流转卡不存在");
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
@@ -184,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());
@@ -488,6 +500,71 @@
        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
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
@@ -498,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(
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java
@@ -1,18 +1,35 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackApproveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackQuickCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackQuickCreateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSubmitReqVO;
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.MesProWorkOrderProcessDO;
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.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
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;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService;
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.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
@@ -23,6 +40,8 @@
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -57,6 +76,24 @@
    private MesWmProductProduceService productProduceService;
    @MockitoBean
    private MesWmProductProduceLineService produceLineService;
    @MockitoBean
    private MesProWorkOrderParamRecordService paramRecordService;
    @MockitoBean
    private MesProWorkOrderBomService workOrderBomService;
    @MockitoBean
    private MesProWorkOrderProcessService workOrderProcessService;
    @MockitoBean
    private MesProProcessService processService;
    @MockitoBean
    private MesProMpsApi mpsApi;
    @MockitoBean
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @MockitoBean
    private MesMdmItemService mdmItemService;
    @MockitoBean
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @MockitoBean
    private BpmProcessInstanceApi processInstanceApi;
    @Test
    public void testUpdateProFeedbackWhenIpqcFinish_success_withUnqualified() {
@@ -405,4 +442,274 @@
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean(), any());
    }
    // ==================== 顺序报工(上一道工序完成后才能报下一道)测试 ====================
    /**
     * 构造"上道工序(prev)+ 当前工序(cur)"的工单工序链(故意乱序传入,验证按 sort 排序后取前驱)
     */
    private List<MesProWorkOrderProcessDO> buildTwoProcessChain(Long workOrderId, Long prevProcessId,
                                                                Long curProcessId) {
        return ListUtil.of(
                MesProWorkOrderProcessDO.builder().workOrderId(workOrderId).processId(curProcessId).sort(2).build(),
                MesProWorkOrderProcessDO.builder().workOrderId(workOrderId).processId(prevProcessId).sort(1).build());
    }
    @Test
    public void testQuickCreateFeedback_blockedWhenPreviousProcessNotFinished() {
        // 准备:当前任务在第 2 道工序,上道工序任务只做了 60/100
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(60));
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        when(processService.getProcess(prevProcessId)).thenReturn(
                randomPojo(MesProProcessDO.class, o -> o.setName("CNC加工")));
        // 调用:应被拦截
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.quickCreateFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
        assertTrue(ex.getMessage().contains("CNC加工"));
        // 未真正生成报工单
        verify(autoCodeRecordService, never()).generateAutoCode(anyString());
    }
    @Test
    public void testQuickCreateFeedback_blockedWhenPreviousMultiTaskAnyNotFinished() {
        // 准备:上道工序有 2 个任务,一个做满、一个未做满,仍应拦截
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO finishedTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(50));
            o.setProducedQuantity(BigDecimal.valueOf(50)); // 已做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        MesProTaskDO partialTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(50));
            o.setProducedQuantity(BigDecimal.valueOf(20)); // 未做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(ListUtil.of(finishedTask, partialTask));
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.quickCreateFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenPreviousProcessFinished() {
        // 准备:上道工序任务已做满(100/100),当前为第 2 道工序,应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(100)); // 已做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        // 走通后续逻辑的桩
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-1");
        // 调用:应成功生成报工单
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
        verify(autoCodeRecordService).generateAutoCode(anyString());
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenPreviousProcessAllCancelled() {
        // 准备:上道工序任务全部取消,视为无前置要求,当前第 2 道工序应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO cancelledTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.CANCELED.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(cancelledTask));
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-2");
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenCurrentIsFirstProcess() {
        // 准备:当前任务就是第 1 道工序(链中只有一道工序),无前置,应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(ListUtil.of(MesProWorkOrderProcessDO.builder()
                        .workOrderId(workOrderId).processId(curProcessId).sort(1).build()));
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-3");
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
    }
    @Test
    public void testSubmitFeedback_blockedWhenPreviousProcessNotFinished() {
        // 准备:已存在草稿报工单(第 2 道工序),上道工序未做满,提交应被拦截
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
            o.setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus());
            o.setTaskId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
        });
        feedbackMapper.insert(feedback);
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTask(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(30));
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        MesProFeedbackSubmitReqVO req = new MesProFeedbackSubmitReqVO();
        req.setId(feedback.getId());
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.submitFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
    }
}