11 小时以前 3ce57820e618b704ce61487b7cdb38fdd6b0f44a
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java
@@ -3,30 +3,23 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
@@ -34,11 +27,16 @@
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;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
/**
@@ -58,13 +56,9 @@
    @MockitoBean
    private MesQcTemplateItemService templateItemService;
    @MockitoBean
    private MesQcTemplateService templateService;
    @MockitoBean
    private MesQcIpqcLineService ipqcLineService;
    @MockitoBean
    private MesProWorkOrderService workOrderService;
    @MockitoBean
    private MesMdWorkstationService workstationService;
    @MockitoBean
    private MesMdItemService itemService;
    @MockitoBean
    private MesQcDefectRecordService defectRecordService;
    @MockitoBean
@@ -72,15 +66,15 @@
    @MockitoBean
    private MesProFeedbackService feedbackService;
    @MockitoBean
    private MesProRouteProductService routeProductService;
    @MockitoBean
    private MesProRouteProcessService routeProcessService;
    @MockitoBean
    private MesProTaskService taskService;
    @MockitoBean
    private MesQcIndicatorResultService indicatorResultService;
    @MockitoBean
    private MesWmProductProduceLineService productProduceLineService;
    @MockitoBean
    private MesQcNcrService ncrService;
    @MockitoBean
    private MdmItemApi mdmItemApi;
    // ==================== finishIpqc 回写来源单据 ====================
    @Test
    public void testFinishIpqc_writeBack_feedback() {
@@ -89,9 +83,6 @@
        Long sourceLineId = randomLongId();
        BigDecimal qualifiedQty = BigDecimal.valueOf(80);
        BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
        BigDecimal laborScrapQty = BigDecimal.valueOf(5);
        BigDecimal materialScrapQty = BigDecimal.valueOf(10);
        BigDecimal otherScrapQty = BigDecimal.valueOf(5);
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
@@ -101,9 +92,9 @@
            o.setSourceLineId(sourceLineId);
            o.setQualifiedQuantity(qualifiedQty);
            o.setUnqualifiedQuantity(unqualifiedQty);
            o.setLaborScrapQuantity(laborScrapQty);
            o.setMaterialScrapQuantity(materialScrapQty);
            o.setOtherScrapQuantity(otherScrapQty);
            o.setLaborScrapQuantity(BigDecimal.valueOf(5));
            o.setMaterialScrapQuantity(BigDecimal.valueOf(10));
            o.setOtherScrapQuantity(BigDecimal.valueOf(5));
        });
        ipqcMapper.insert(ipqc);
@@ -124,6 +115,8 @@
                any(BigDecimal.class),
                any(BigDecimal.class),
                any(BigDecimal.class));
        // 断言 3:不合格数量 > 0,自动创建 NCR
        verify(ncrService).createNcr(any());
    }
    @Test
@@ -161,13 +154,14 @@
    @Test
    public void testFinishIpqc_writeBack_noSourceDoc() {
        // 准备数据:sourceDocType 为空,不需要回写
        // 准备数据:无来源单据(手工创建的 IPQC),直接完成,不回写
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(null);
            o.setSourceDocId(null);
            o.setSourceLineId(null);
            o.setUnqualifiedQuantity(BigDecimal.ZERO);
        });
        ipqcMapper.insert(ipqc);
@@ -181,38 +175,6 @@
        // 断言 2:不应该调用 feedbackService
        verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
                anyLong(), anyLong(), any(), any(), any(), any(), any());
    }
    @Test
    public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
        // 准备数据:sourceDocType 非空但 sourceDocId 为空,应该抛异常
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
            o.setSourceDocId(null); // 脏数据
            o.setSourceLineId(null);
        });
        ipqcMapper.insert(ipqc);
        // 调用,并断言异常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    @Test
    public void testFinishIpqc_writeBack_unknownSourceDocType() {
        // 准备数据:sourceDocType 无法识别
        MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setSourceDocType(99999); // 无法识别的类型
            o.setSourceDocId(randomLongId());
            o.setSourceLineId(null);
        });
        ipqcMapper.insert(ipqc);
        // 调用,并断言异常
        assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    @Test
@@ -242,162 +204,61 @@
        assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
    }
    // ==================== processId 推导行为测试 ====================
    // ==================== createIpqc 物料 / 批号 / 模板匹配 ====================
    /**
     * 工位有工序 + 该工序在产品工艺路线中 → processId 应该被设置为工位工序
     * 手工创建:物料来自 MDM,模板按 MDM 物料 ID 匹配;批号、物料均落库
     */
    @Test
    public void testCreateIpqc_processId_workstationHasProcess() {
    public void testCreateIpqc_success() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long itemId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-001");
        reqVO.setItemId(itemId);
        reqVO.setBatchCode("BC20250101");
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-001");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        // mock 工位返回有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(processId);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        // mock 工艺路线
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
        when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
        // mock 工序在路线中
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(new MesProRouteProcessDO());
        // mock:按 MDM 物料 ID 匹配到质检方案
        mockTemplate(templateId);
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是工位工序
        // 断言 1:批号、物料、方案均落库
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertEquals(processId, ipqc.getProcessId());
        assertNotNull(ipqc);
        assertEquals("BC20250101", ipqc.getBatchCode());
        assertEquals(itemId, ipqc.getItemId());
        assertEquals(templateId, ipqc.getTemplateId());
        assertEquals(MesQcStatusEnum.DRAFT.getStatus(), ipqc.getStatus());
        assertNull(ipqc.getSourceDocCode());
        // 断言 2:模板按 MDM 物料 ID 匹配,不再依赖工单/任务
        verify(templateItemService).getTemplateItemList(
                eq(itemId), eq(MesQcTypeEnum.IPQC.getType()), isNull(), isNull());
        // 断言 3:物料存在性校验走 MDM
        verify(mdmItemApi).validateItemExists(itemId);
    }
    /**
     * 工位没有工序(processId=null)→ processId 应该留 null(不回退到任务工序)
     * 物料未匹配到任何质检方案 → 创建失败
     */
    @Test
    public void testCreateIpqc_processId_workstationNoProcess_shouldBeNull() {
    public void testCreateIpqc_noTemplate() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long taskId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-002");
        reqVO.setItemId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-002");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setTaskId(taskId); // 故意传入 taskId,验证不会回退
        reqVO.setInspectorUserId(randomLongId());
        // mock:无匹配方案
        when(templateItemService.getTemplateItemList(any(), any(), any(), any()))
                .thenReturn(Collections.emptyList());
        // mock 工位没有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null); // 无工序
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 任务
        MesProTaskDO task = new MesProTaskDO();
        task.setId(taskId);
        task.setWorkOrderId(workOrderId);
        task.setWorkstationId(workstationId);
        task.setItemId(productId);
        when(taskService.validateTaskNotFinished(taskId)).thenReturn(task);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是 null,即使传入了 taskId 也不应该回退
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId(),
                "工位无工序时 processId 应为 null,不应回退到任务工序");
        // 断言:不应该读取任务的 processId
        verify(routeProductService, never()).getRouteProductByItemId(any());
        // 调用,断言异常
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode());
    }
    /**
     * 工位有工序但该工序不在产品工艺路线中 → processId 应该留 null
     */
    @Test
    public void testCreateIpqc_processId_processNotInRoute() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long processId = randomLongId();
        Long routeId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-TEST-003");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        // mock 工位有工序
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(processId);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        // mock 工单
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock 模板匹配
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        // mock 工艺路线存在
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
        when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
        // mock 工序不在路线中
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(null);
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:processId 应该是 null
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNull(ipqc.getProcessId());
    }
    // ==================== sourceLineId 校验测试 ====================
    // ==================== createIpqc sourceLineId 校验 ====================
    /**
     * sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败
@@ -405,34 +266,15 @@
    @Test
    public void testCreateIpqc_sourceLineId_required() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long feedbackId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-001");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-001");
        reqVO.setItemId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(null); // 不传 sourceLineId
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // 调用,断言异常
@@ -446,35 +288,16 @@
    @Test
    public void testCreateIpqc_sourceLineId_notBelong() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-002");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-002");
        reqVO.setItemId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在但 feedbackId 不匹配
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -494,35 +317,16 @@
    @Test
    public void testCreateIpqc_sourceLineId_notPending() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-003");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-003");
        reqVO.setItemId(randomLongId());
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        mockTemplate(randomLongId());
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在且归属正确,但不是 PENDING 状态
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -537,40 +341,24 @@
    }
    /**
     * sourceLineId 校验全部通过 → 创建成功
     * sourceLineId 校验全部通过 → 创建成功,来源单据编号回写
     */
    @Test
    public void testCreateIpqc_sourceLineId_success() {
        // 准备参数
        Long workstationId = randomLongId();
        Long workOrderId = randomLongId();
        Long productId = randomLongId();
        Long itemId = randomLongId();
        Long feedbackId = randomLongId();
        Long sourceLineId = randomLongId();
        Long templateId = randomLongId();
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode("IPQC-SL-004");
        reqVO.setWorkstationId(workstationId);
        reqVO.setWorkOrderId(workOrderId);
        reqVO.setInspectorUserId(randomLongId());
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-004");
        reqVO.setItemId(itemId);
        reqVO.setBatchCode("BC20250101");
        reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
        reqVO.setSourceDocId(feedbackId);
        reqVO.setSourceLineId(sourceLineId);
        // mock
        MesMdWorkstationDO workstation = new MesMdWorkstationDO();
        workstation.setId(workstationId);
        workstation.setProcessId(null);
        when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        mockTemplate(templateId);
        MesProFeedbackDO feedback = new MesProFeedbackDO();
        feedback.setId(feedbackId);
        feedback.setCode("FB-001");
@@ -585,12 +373,65 @@
        // 调用
        Long ipqcId = ipqcService.createIpqc(reqVO);
        // 断言:创建成功,sourceDocCode 回写
        // 断言:创建成功,来源单据字段落库
        MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
        assertNotNull(ipqc);
        assertEquals(feedbackId, ipqc.getSourceDocId());
        assertEquals(sourceLineId, ipqc.getSourceLineId());
        assertEquals("FB-001", ipqc.getSourceDocCode());
        assertEquals("BC20250101", ipqc.getBatchCode());
        assertEquals(itemId, ipqc.getItemId());
    }
    /**
     * 未知来源单据类型 → 创建失败
     */
    @Test
    public void testCreateIpqc_sourceDocTypeUnknown() {
        // 准备参数
        MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-005");
        reqVO.setItemId(randomLongId());
        reqVO.setSourceDocType(99999); // 无法识别的类型
        reqVO.setSourceDocId(randomLongId());
        // mock
        mockTemplate(randomLongId());
        // 调用,断言异常
        ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
        assertEquals(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN.getCode(), ex.getCode());
    }
    // ==================== 辅助方法 ====================
    /**
     * 构造一个满足必填项的过程检验单请求(不含来源单据)
     */
    private MesQcIpqcSaveReqVO buildReqVO(String code) {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        reqVO.setCode(code);
        reqVO.setName(code + "-名称");
        reqVO.setType(1);
        reqVO.setBatchCode("BC-" + code);
        reqVO.setCheckQuantity(BigDecimal.valueOf(100));
        reqVO.setQualifiedQuantity(BigDecimal.valueOf(95));
        reqVO.setUnqualifiedQuantity(BigDecimal.valueOf(5));
        reqVO.setLaborScrapQuantity(BigDecimal.ZERO);
        reqVO.setMaterialScrapQuantity(BigDecimal.ZERO);
        reqVO.setOtherScrapQuantity(BigDecimal.ZERO);
        reqVO.setInspectorUserId(randomLongId());
        return reqVO;
    }
    /**
     * mock:按 MDM 物料 ID + IPQC 类型匹配到指定质检方案
     */
    private void mockTemplate(Long templateId) {
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        List<MesQcTemplateItemDO> templateItems = Collections.singletonList(templateItem);
        when(templateItemService.getTemplateItemList(any(), eq(MesQcTypeEnum.IPQC.getType()), isNull(), isNull()))
                .thenReturn(templateItems);
    }
}