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.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.qc.MesQcTypeEnum; 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.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; import org.springframework.context.annotation.Import; 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.*; /** * {@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 MesQcTemplateService templateService; @MockitoBean private MesQcIpqcLineService ipqcLineService; @MockitoBean private MesQcDefectRecordService defectRecordService; @MockitoBean private AdminUserApi adminUserApi; @MockitoBean private MesProFeedbackService feedbackService; @MockitoBean private MesQcIndicatorResultService indicatorResultService; @MockitoBean private MesWmProductProduceLineService productProduceLineService; @MockitoBean private MesQcNcrService ncrService; @MockitoBean private MdmItemApi mdmItemApi; // ==================== finishIpqc 回写来源单据 ==================== @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); 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(BigDecimal.valueOf(5)); o.setMaterialScrapQuantity(BigDecimal.valueOf(10)); o.setOtherScrapQuantity(BigDecimal.valueOf(5)); }); 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)); // 断言 3:不合格数量 > 0,自动创建 NCR verify(ncrService).createNcr(any()); } @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() { // 准备数据:无来源单据(手工创建的 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); // 调用 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_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())); } // ==================== createIpqc 物料 / 批号 / 模板匹配 ==================== /** * 手工创建:物料来自 MDM,模板按 MDM 物料 ID 匹配;批号、物料均落库 */ @Test public void testCreateIpqc_success() { // 准备参数 Long itemId = randomLongId(); Long templateId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-001"); reqVO.setItemId(itemId); reqVO.setBatchCode("BC20250101"); // mock:按 MDM 物料 ID 匹配到质检方案 mockTemplate(templateId); // 调用 Long ipqcId = ipqcService.createIpqc(reqVO); // 断言 1:批号、物料、方案均落库 MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId); 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); } /** * 物料未匹配到任何质检方案 → 创建失败 */ @Test public void testCreateIpqc_noTemplate() { // 准备参数 MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-TEST-002"); reqVO.setItemId(randomLongId()); // mock:无匹配方案 when(templateItemService.getTemplateItemList(any(), any(), any(), any())) .thenReturn(Collections.emptyList()); // 调用,断言异常 ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO)); assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode()); } // ==================== createIpqc sourceLineId 校验 ==================== /** * sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败 */ @Test public void testCreateIpqc_sourceLineId_required() { // 准备参数 Long feedbackId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-001"); reqVO.setItemId(randomLongId()); reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); reqVO.setSourceDocId(feedbackId); reqVO.setSourceLineId(null); // 不传 sourceLineId // mock mockTemplate(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 feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-002"); reqVO.setItemId(randomLongId()); reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); reqVO.setSourceDocId(feedbackId); reqVO.setSourceLineId(sourceLineId); // mock mockTemplate(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 feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesQcIpqcSaveReqVO reqVO = buildReqVO("IPQC-SL-003"); reqVO.setItemId(randomLongId()); reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); reqVO.setSourceDocId(feedbackId); reqVO.setSourceLineId(sourceLineId); // mock mockTemplate(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(); Long templateId = 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 mockTemplate(templateId); 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); // 断言:创建成功,来源单据字段落库 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 templateItems = Collections.singletonList(templateItem); when(templateItemService.getTemplateItemList(any(), eq(MesQcTypeEnum.IPQC.getType()), isNull(), isNull())) .thenReturn(templateItems); } }