package cn.iocoder.yudao.module.mes.service.qc.seedquality; import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.*; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityActionMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityIndicatorMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum; import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import cn.iocoder.yudao.framework.common.exception.ServiceException; import org.junit.jupiter.api.*; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class SeedQualityGroupTest { private final MesQcSeedQualityServiceImpl service=spy(new MesQcSeedQualityServiceImpl()); private final MesProBagCodeMapper bags=mock(MesProBagCodeMapper.class); private final MesProBatchMapper batches=mock(MesProBatchMapper.class); private final MesProPalletMapper pallets=mock(MesProPalletMapper.class); private final MesQcSeedQualityMapper qualities=mock(MesQcSeedQualityMapper.class); private final MesQcSeedQualityIndicatorMapper indicators=mock(MesQcSeedQualityIndicatorMapper.class); private final MesQcSeedQualityActionMapper actions=mock(MesQcSeedQualityActionMapper.class); private final MesProBatchService batchService=mock(MesProBatchService.class); private final MesWmBatchMapper wmBatches=mock(MesWmBatchMapper.class); private final MesWmMaterialStockMapper stocks=mock(MesWmMaterialStockMapper.class); private final MesWmWarehouseService warehouses=mock(MesWmWarehouseService.class); private final MesQcApproverService approvers=mock(MesQcApproverService.class); private final MesProBagCodeDO first=bag(1L),second=bag(2L),canceled=bag(3L).setStatus(30); private MesQcSeedQualityGroupSubmitReqVO req; private MesProBagCodeDO bag(Long id) {return new MesProBagCodeDO().setId(id).setCode("B"+id).setBatchId(10L).setPalletId(20L).setStatus(20).setInboundEventId(9L);} private List validIndicators() { return List.of(indicator("purity"), indicator("cleanliness"), indicator("germination_rate"), indicator("moisture")); } private MesQcSeedQualitySubmitReqVO.Indicator indicator(String code) { var item = new MesQcSeedQualitySubmitReqVO.Indicator(); item.setCode(code); item.setValue(java.math.BigDecimal.ONE); item.setMinValue(java.math.BigDecimal.ZERO); item.setMaxValue(java.math.BigDecimal.TEN); return item; } private List storedIndicators(Long... qualityIds) { var rows = new java.util.ArrayList(); long id = 100; for (Long qualityId : qualityIds) for (var item : validIndicators()) rows.add(MesQcSeedQualityIndicatorDO.builder() .id(id++).qualityId(qualityId).indicatorCode(item.getCode()).value(item.getValue()) .minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); return rows; } @BeforeEach void setup() { ReflectionTestUtils.setField(service,"bagCodeMapper",bags);ReflectionTestUtils.setField(service,"batchMapper",batches); ReflectionTestUtils.setField(service,"palletMapper",pallets);ReflectionTestUtils.setField(service,"qualityMapper",qualities); ReflectionTestUtils.setField(service,"indicatorMapper",indicators);ReflectionTestUtils.setField(service,"actionMapper",actions); ReflectionTestUtils.setField(service,"proBatchService",batchService);ReflectionTestUtils.setField(service,"mesWmBatchMapper",wmBatches); ReflectionTestUtils.setField(service,"materialStockMapper",stocks);ReflectionTestUtils.setField(service,"warehouseService",warehouses); ReflectionTestUtils.setField(service,"approverService",approvers); var batch=new MesProBatchDO().setId(10L).setCode("BATCH").setBusinessCode("BATCH-BUS").setItemId(99L).setStatus(30); when(batches.selectByScanCode(anyString())).thenReturn(batch);when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(batch); when(batchService.getBatch(10L)).thenReturn(batch); when(wmBatches.selectByCode("BATCH")).thenReturn(new MesWmBatchDO().setId(30L).setCode("BATCH")); when(stocks.selectTempStockCountByBatchId(30L)).thenReturn(2L); var warehouse = new MesWmWarehouseDO().setId(8L).setType(MesWmWarehouseTypeEnum.QUALIFIED.getType()); when(warehouses.getFirstQualifiedWarehouse()).thenReturn(warehouse); when(warehouses.getWarehouse(8L)).thenReturn(warehouse); doAnswer(invocation -> { long id = 1000; for (var quality : (java.util.Collection) invocation.getArgument(0)) quality.setId(id++); return true; }) .when(qualities).insertBatch(anyCollection(), anyInt()); var pallet=new MesProPalletDO().setId(20L).setCode("PALLET").setBusinessCode("PALLET-BUS").setBatchCode("BATCH").setBatchId(10L).setStatus(10); when(pallets.selectByScanCode(anyString())).thenReturn(pallet);when(pallets.selectByScanCodeForUpdate(anyString())).thenReturn(pallet); when(bags.selectListByBatchId(10L)).thenReturn(List.of(first,second,canceled)); when(bags.selectListByBatchIdForUpdate(10L)).thenReturn(List.of(first,second,canceled)); when(bags.selectListByPalletId(20L)).thenReturn(List.of(first,second,canceled)); when(bags.selectListByPalletIdForUpdate(20L)).thenReturn(List.of(first,second,canceled)); when(bags.selectListByIdsForUpdate(any())).thenAnswer(invocation -> { java.util.Collection ids = invocation.getArgument(0); return List.of(first, second).stream().filter(bag -> ids.contains(bag.getId())).toList(); }); req=new MesQcSeedQualityGroupSubmitReqVO();req.setCode("BATCH-BUS");req.setScanType("BATCH");req.setBagIds(List.of(1L,2L)); req.setQualifiedWarehouseId(8L);req.setIndicators(validIndicators()); } @Test void batchPreviewCountsEffectiveInspectedAndPendingBags() { when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build())); var result=service.resolveScan("BATCH-BUS","BATCH");assertEquals(2,result.get("totalCount")); assertEquals(List.of(1L),result.get("completedBagIds"));assertEquals(List.of(2L),result.get("pendingBagIds")); verify(qualities,never()).insertBatch(anyCollection(),anyInt()); } @Test void palletProcessesOnlyConfirmedUninspectedBags() { req.setScanType("PALLET");req.setCode("PALLET-BUS");req.setBagIds(List.of(2L)); when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build())); when(qualities.selectListByCodesForUpdate(List.of("B2"))).thenReturn(List.of()); assertEquals(1,service.submitGroup(req).get("processedCount")); verify(qualities).insertBatch(argThat(rows -> rows.size() == 1 && rows.iterator().next().getCode().equals("B2") && rows.iterator().next().getQualifiedWarehouseId().equals(8L)),eq(100)); verify(indicators).insertBatch(argThat(rows -> rows.size() == 4),eq(400)); verify(actions).insertBatch(argThat(rows -> rows.size() == 1),eq(100)); } @Test void changedConfirmedListDoesNotSubmitAnyBag() { req.setBagIds(List.of(1L));assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt()); } @Test void unenteredBagBlocksWholeGroup() { second.setInboundEventId(null);assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt()); } @Test void wrongTypeAndCanceledBatchCannotSubmit() { req.setScanType("OTHER");assertThrows(ServiceException.class,()->service.submitGroup(req)); req.setScanType("BATCH");when(batches.selectByScanCode(anyString())).thenReturn(new MesProBatchDO().setId(10L).setCode("BATCH").setStatus(40)); assertThrows(ServiceException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt()); } @Test void unknownPalletReturnsActionableBadRequest() { when(pallets.selectByScanCode("UNKNOWN")).thenReturn(null); var error = assertThrows(ServiceException.class, () -> service.resolveRetestScope("UNKNOWN", "PALLET")); assertEquals(400, error.getCode()); assertTrue(error.getMessage().contains("托盘码")); } @Test void singleBagUsesSameConfirmedGroupProtocol() { req.setScanType("BAG");req.setCode("B1");req.setBagIds(List.of(1L)); when(bags.selectByScanCode("B1")).thenReturn(first); assertEquals(1,service.submitGroup(req).get("processedCount"));verify(qualities).insertBatch(anyCollection(),eq(100)); } @Test void failingSecondBagRollsBackWholeTransaction() { var source=new org.h2.jdbcx.JdbcDataSource();source.setURL("jdbc:h2:mem:qcgroup"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1"); var jdbc=new org.springframework.jdbc.core.JdbcTemplate(source);jdbc.execute("CREATE TABLE qc_result(id BIGINT)"); doAnswer(invocation->{long id=1;for(var quality:(java.util.Collection)invocation.getArgument(0)){jdbc.update("INSERT INTO qc_result(id) VALUES(?)",id);quality.setId(id++);}return true;}) .when(qualities).insertBatch(anyCollection(),anyInt()); doThrow(new IllegalStateException("日志批量写入失败")).when(actions).insertBatch(anyCollection(),anyInt()); var factory=new org.springframework.aop.framework.ProxyFactory(service); factory.addAdvice(new org.springframework.transaction.interceptor.TransactionInterceptor(new org.springframework.jdbc.datasource.DataSourceTransactionManager(source),new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource())); var proxy=(MesQcSeedQualityService)factory.getProxy(); assertThrows(IllegalStateException.class,()->proxy.submitGroup(req));assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM qc_result",Integer.class)); } @Test void retestScopePreviewSeparatesWorkflowStages() { when(qualities.selectListByCodes(any())).thenReturn(List.of( MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(), MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(50).build())); var result = service.resolveRetestScope("PALLET-BUS", "PALLET"); assertEquals(List.of(11L), result.get("retestIds")); assertEquals(List.of(12L), result.get("initialRecheckIds")); } @Test void retestScopePreviewExposesPendingReviewGroups() { var third = bag(3L); var fourth = bag(4L); when(bags.selectListByPalletId(20L)).thenReturn(List.of(first, second, third, fourth)); when(qualities.selectListByCodes(any())).thenReturn(List.of( MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(20).build(), MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(25).build(), MesQcSeedQualityDO.builder().id(13L).code("B3").workflowStatus(30).build(), MesQcSeedQualityDO.builder().id(14L).code("B4").workflowStatus(60).build())); var result = service.resolveRetestScope("PALLET-BUS", "PALLET"); assertEquals(List.of(11L), result.get("pendingProcessIds")); assertEquals(List.of(12L), result.get("pendingInitialFailIds")); assertEquals(List.of(13L), result.get("pendingCompanyIds")); assertEquals(List.of(14L), result.get("pendingCompanyFailIds")); assertEquals(2, result.get("pendingProcessCount")); assertEquals(2, result.get("pendingCompanyCount")); } @Test void retestScopeRejectsStaleConfirmedList() { when(qualities.selectListByCodes(any())).thenReturn(List.of( MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(), MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build())); var request = new MesQcSeedQualityScopeRetestReqVO(); request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10); request.setQualityIds(List.of(11L)); request.setIndicators(validIndicators()); assertThrows(IllegalArgumentException.class, () -> service.retestScope(request)); verify(indicators, never()).updateBatch(anyCollection(), anyInt()); } @Test void retestScopeProcessesAllConfirmedBags() { var pending = List.of(MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(), MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()); when(qualities.selectListByCodes(any())).thenReturn(pending); when(qualities.selectListByIdsForUpdate(List.of(11L, 12L))).thenReturn(pending); when(indicators.selectListByQualityIds(any())).thenReturn(storedIndicators(11L, 12L)); var request = new MesQcSeedQualityScopeRetestReqVO(); request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10); request.setQualityIds(List.of(11L, 12L)); request.setIndicators(validIndicators()); assertEquals(2, service.retestScope(request).get("processedCount")); verify(indicators).selectListByQualityIds(List.of(11L, 12L)); verify(qualities).updateBatch(argThat(rows -> rows.size() == 2), eq(100)); verify(indicators).updateBatch(argThat(rows -> rows.size() == 8), eq(400)); verify(actions).insertBatch(argThat(rows -> rows.size() == 2), eq(100)); } @Test void processApprovalLocksAndWritesWholeBatchOnce() { when(qualities.selectListByIdsForUpdate(List.of(11L, 12L))).thenReturn(List.of( MesQcSeedQualityDO.builder().id(11L).result(1).workflowStatus(20).build(), MesQcSeedQualityDO.builder().id(12L).result(2).workflowStatus(25).build())); service.processApproveBatch(List.of(12L, 11L), true, "批量通过"); verify(qualities).selectListByIdsForUpdate(List.of(11L, 12L)); verify(approvers, times(1)).validateApprover(anyInt(), anyInt(), nullable(Long.class)); verify(qualities).updateBatch(argThat(rows -> rows.size() == 2 && rows.stream().allMatch(row -> row.getWorkflowStatus() == 10)), eq(100)); verify(actions).insertBatch(argThat(rows -> rows.size() == 2), eq(100)); verify(qualities, never()).selectByIdForUpdate(anyLong()); } }