package cn.iocoder.yudao.module.mes.service.qc.ipqc; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; 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.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService; 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.wm.productproduce.MesWmProductProduceLineService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.math.BigDecimal; 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.Mockito.*; /** * {@link MesQcIpqcServiceImpl} 的单元测试 * * @author 超级管理员 */ @Import(MesQcIpqcServiceImpl.class) public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @Resource private MesQcIpqcServiceImpl ipqcService; @Resource private MesQcIpqcMapper ipqcMapper; @MockitoBean private MesQcTemplateItemService templateItemService; @MockitoBean private MesQcIpqcLineService ipqcLineService; @MockitoBean private MdmItemApi mdmItemApi; @MockitoBean private MesQcDefectRecordService defectRecordService; @MockitoBean private AdminUserApi adminUserApi; @MockitoBean private MesProFeedbackService feedbackService; @MockitoBean private MesQcIndicatorResultService indicatorResultService; @MockitoBean private MesWmProductProduceLineService productProduceLineService; @MockitoBean private MesQcNcrService ncrService; @Test public void testFinishIpqc_writeBack_feedback() { // 准备数据:插入一条草稿状态的 IPQC 单,来源为报工单(PRO_FEEDBACK=304) Long sourceDocId = randomLongId(); 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()); o.setCheckResult(1); // 非空,满足 finishIpqc 的校验 o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); o.setSourceDocId(sourceDocId); o.setSourceLineId(sourceLineId); o.setQualifiedQuantity(qualifiedQty); o.setUnqualifiedQuantity(unqualifiedQty); o.setLaborScrapQuantity(laborScrapQty); o.setMaterialScrapQuantity(materialScrapQty); o.setOtherScrapQuantity(otherScrapQty); }); ipqcMapper.insert(ipqc); // 调用 ipqcService.finishIpqc(ipqc.getId()); // 断言 1:状态更新为已完成 MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus()); // 断言 2:调用了 feedbackService,传递 sourceLineId // 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配 verify(feedbackService).updateProFeedbackWhenIpqcFinish( eq(sourceDocId), eq(sourceLineId), any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class)); } @Test public void testFinishIpqc_writeBack_feedback_withNullQuantities() { // 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0 Long sourceDocId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); o.setCheckResult(1); o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); o.setSourceDocId(sourceDocId); o.setSourceLineId(sourceLineId); o.setQualifiedQuantity(null); o.setUnqualifiedQuantity(null); o.setLaborScrapQuantity(null); o.setMaterialScrapQuantity(null); o.setOtherScrapQuantity(null); }); ipqcMapper.insert(ipqc); // 调用 ipqcService.finishIpqc(ipqc.getId()); // 断言:所有数量参数都是 BigDecimal.ZERO(defaultIfNull 处理) verify(feedbackService).updateProFeedbackWhenIpqcFinish( eq(sourceDocId), eq(sourceLineId), eq(BigDecimal.ZERO), eq(BigDecimal.ZERO), eq(BigDecimal.ZERO), eq(BigDecimal.ZERO), eq(BigDecimal.ZERO)); } @Test public void testFinishIpqc_writeBack_noSourceDoc() { // 准备数据:sourceDocType 为空,不需要回写 MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); o.setCheckResult(1); o.setSourceDocType(null); o.setSourceDocId(null); o.setSourceLineId(null); }); ipqcMapper.insert(ipqc); // 调用 ipqcService.finishIpqc(ipqc.getId()); // 断言 1:状态更新成功 MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus()); // 断言 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); o.setUnqualifiedQuantity(BigDecimal.ZERO); }); ipqcMapper.insert(ipqc); // 调用:不抛异常,仅跳过回写 ipqcService.finishIpqc(ipqc.getId()); MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus()); verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish( anyLong(), anyLong(), any(), any(), any(), any(), any()); } @Test public void testFinishIpqc_writeBack_unknownSourceDocType() { // 准备数据:sourceDocType 无法识别,回写阶段不校验类型(仅按 sourceDocId 回写报工) MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); o.setCheckResult(1); o.setSourceDocType(99999); // 无法识别的类型 o.setSourceDocId(randomLongId()); o.setSourceLineId(randomLongId()); o.setUnqualifiedQuantity(BigDecimal.ZERO); }); ipqcMapper.insert(ipqc); // 调用:按 sourceDocId 回写报工 ipqcService.finishIpqc(ipqc.getId()); MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus()); verify(feedbackService).updateProFeedbackWhenIpqcFinish( anyLong(), anyLong(), any(), any(), any(), any(), any()); } @Test public void testFinishIpqc_checkResultEmpty() { // 准备数据:checkResult 为空,应该报错 MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); o.setCheckResult(null); // 检测结论为空 o.setSourceDocType(null); }); ipqcMapper.insert(ipqc); // 调用,应该抛异常(检测结论必填) assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId())); } @Test public void testFinishIpqc_notDraftStatus() { // 准备数据:已完成状态,不能再次完成 MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { o.setStatus(MesQcStatusEnum.FINISHED.getStatus()); o.setCheckResult(1); }); ipqcMapper.insert(ipqc); // 调用,应该抛异常(不是草稿状态) assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId())); } // ==================== 批号必填校验 ==================== /** * 批号为 null → 创建失败 */ @Test public void testCreateIpqc_batchCode_null() { MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); reqVO.setCode("IPQC-BC-001"); reqVO.setBatchCode(null); reqVO.setItemId(randomLongId()); reqVO.setInspectorUserId(randomLongId()); ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode()); } /** * 批号为空白串 → 创建失败(兜底内部/RPC 调用绕过 VO 校验的情况) */ @Test public void testCreateIpqc_batchCode_blank() { MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); reqVO.setCode("IPQC-BC-002"); reqVO.setBatchCode(" "); reqVO.setItemId(randomLongId()); reqVO.setInspectorUserId(randomLongId()); ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode()); } // ==================== 创建成功:批号 / 物料落库 ==================== @Test public void testCreateIpqc_success() { // 准备参数 Long itemId = randomLongId(); Long templateId = randomLongId(); MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); reqVO.setCode("IPQC-OK-001"); reqVO.setName("测试过程检验"); reqVO.setBatchCode("BC-2025-001"); reqVO.setItemId(itemId); reqVO.setInspectorUserId(randomLongId()); mockTemplate(itemId, templateId); // 调用 Long ipqcId = ipqcService.createIpqc(reqVO); // 断言:批号与物料落库,模板按物料+IPQC 类型匹配 MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId); assertNotNull(ipqc); assertEquals("BC-2025-001", ipqc.getBatchCode()); assertEquals(itemId, ipqc.getItemId()); assertEquals(templateId, ipqc.getTemplateId()); verify(mdmItemApi).validateItemExists(itemId); } /** * 传入了不在匹配列表中的 templateId → 拒绝创建 */ @Test public void testCreateIpqc_templateId_notMatched() { Long itemId = randomLongId(); MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); reqVO.setCode("IPQC-TPL-001"); reqVO.setName("指定方案不匹配"); reqVO.setBatchCode("BC-2025-002"); reqVO.setItemId(itemId); reqVO.setInspectorUserId(randomLongId()); reqVO.setTemplateId(randomLongId()); // 与匹配结果不一致 mockTemplate(itemId, randomLongId()); ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode()); } // ==================== sourceLineId 校验测试 ==================== /** * sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败 */ @Test public void testCreateIpqc_sourceLineId_required() { Long itemId = randomLongId(); Long feedbackId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-001", itemId, feedbackId, null); mockTemplate(itemId, randomLongId()); when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO()); // 调用,断言异常 ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_IPQC_SOURCE_LINE_REQUIRED.getCode(), ex.getCode()); } /** * sourceLineId 不属于该报工的产出行 → 创建失败 */ @Test public void testCreateIpqc_sourceLineId_notBelong() { Long itemId = randomLongId(); Long feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-002", itemId, feedbackId, sourceLineId); mockTemplate(itemId, randomLongId()); when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO()); // mock:产出行存在但 feedbackId 不匹配 MesWmProductProduceLineDO line = new MesWmProductProduceLineDO(); line.setId(sourceLineId); line.setFeedbackId(randomLongId()); // 不同的 feedbackId line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()); when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line); // 调用,断言异常 ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_IPQC_SOURCE_LINE_NOT_BELONG.getCode(), ex.getCode()); } /** * sourceLineId 对应的产出行不是待检验状态 → 创建失败 */ @Test public void testCreateIpqc_sourceLineId_notPending() { Long itemId = randomLongId(); Long feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-003", itemId, feedbackId, sourceLineId); mockTemplate(itemId, randomLongId()); when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO()); // mock:产出行存在且归属正确,但不是 PENDING 状态 MesWmProductProduceLineDO line = new MesWmProductProduceLineDO(); line.setId(sourceLineId); line.setFeedbackId(feedbackId); line.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); // 已合格,非待检 when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line); // 调用,断言异常 ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_IPQC_SOURCE_LINE_NOT_PENDING.getCode(), ex.getCode()); } /** * sourceLineId 校验全部通过 → 创建成功 */ @Test public void testCreateIpqc_sourceLineId_success() { Long itemId = randomLongId(); Long feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-004", itemId, feedbackId, sourceLineId); mockTemplate(itemId, randomLongId()); MesProFeedbackDO feedback = new MesProFeedbackDO(); feedback.setId(feedbackId); feedback.setCode("FB-001"); when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(feedback); // mock:产出行存在、归属正确、PENDING 状态 MesWmProductProduceLineDO line = new MesWmProductProduceLineDO(); line.setId(sourceLineId); line.setFeedbackId(feedbackId); line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()); when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line); // 调用 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()); } // ==================== 测试辅助 ==================== private MesQcIpqcSaveReqVO buildFeedbackReqVO(String code, Long itemId, Long feedbackId, Long sourceLineId) { MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); reqVO.setCode(code); reqVO.setName("来源校验过程检验"); reqVO.setBatchCode("BC-SL-001"); reqVO.setItemId(itemId); reqVO.setInspectorUserId(randomLongId()); reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); reqVO.setSourceDocId(feedbackId); reqVO.setSourceLineId(sourceLineId); return reqVO; } private void mockTemplate(Long itemId, Long templateId) { MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO(); templateItem.setTemplateId(templateId); when(templateItemService.getTemplateItemList(eq(itemId), anyInt())) .thenReturn(List.of(templateItem)); } }