From 87733eb3623593da247c051593d64b8ff9e1d3a4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 06 十一月 2026 19:00:39 +0800
Subject: [PATCH] feat(crm,mes): 扩展客户字段与重构批次号生成逻辑

---
 yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java |  174 +++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 141 insertions(+), 33 deletions(-)

diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
index 6ccfae7..50aff13 100644
--- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
@@ -4,11 +4,25 @@
 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.MesQcSeedIndicatorRangeDO;
 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.mdm.MesMdmItemService;
+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;
@@ -21,58 +35,120 @@
     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 MesQcSeedIndicatorRangeService ranges=mock(MesQcSeedIndicatorRangeService.class);
+    private final MesMdmItemService items=mock(MesMdmItemService.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<MesQcSeedQualitySubmitReqVO.Indicator> validIndicators() {
+        return List.of(indicator("purity"), indicator("cleanliness"), indicator("germination_rate"), indicator("moisture"));
+    }
+    private MesQcSeedIndicatorRangeDO range(String code) {
+        var range = new MesQcSeedIndicatorRangeDO();
+        range.setIndicatorCode(code); range.setIndicatorName(code); range.setUnit("%");
+        range.setMinValue(java.math.BigDecimal.ZERO); range.setMaxValue(java.math.BigDecimal.TEN);
+        return range;
+    }
+    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<MesQcSeedQualityIndicatorDO> storedIndicators(Long... qualityIds) {
+        var rows = new java.util.ArrayList<MesQcSeedQualityIndicatorDO>(); 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);
-        var batch=new MesProBatchDO().setId(10L).setCode("BATCH").setBusinessCode("BATCH-BUS").setStatus(30);
+        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);
+        ReflectionTestUtils.setField(service,"rangeService",ranges);
+        ReflectionTestUtils.setField(service,"mdmItemService",items);
+        when(ranges.getEffectiveRanges(any())).thenReturn(List.of(range("purity"),range("cleanliness"),range("germination_rate"),range("moisture")));
+        when(ranges.getEffectiveMap(any())).thenReturn(java.util.Map.of());
+        when(items.getItem(any())).thenReturn(null);
+        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<MesQcSeedQualityDO>) 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())).thenReturn(List.of(first,second));
-        req=new MesQcSeedQualityGroupSubmitReqVO();req.setCode("BATCH-BUS");req.setScanType("BATCH");req.setBagIds(List.of(1L,2L));
-        req.setQualifiedWarehouseId(8L);req.setIndicators(List.of());
-        doReturn(101L).when(service).submit(any(MesQcSeedQualitySubmitReqVO.class));
+        when(bags.selectListByIdsForUpdate(any())).thenAnswer(invocation -> {
+            java.util.Collection<Long> 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.setQcLine(1);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()));
+        when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").qcLine(1).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(service,never()).submit(any());
+        verify(qualities,never()).insertBatch(anyCollection(),anyInt());
     }
     @Test void palletProcessesOnlyConfirmedUninspectedBags() {
         req.setScanType("PALLET");req.setCode("PALLET-BUS");req.setBagIds(List.of(2L));
-        when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build()));
+        when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").qcLine(1).build()));
+        when(qualities.selectListByCodesForUpdate(List.of("B2"))).thenReturn(List.of());
         assertEquals(1,service.submitGroup(req).get("processedCount"));
-        var capture=org.mockito.ArgumentCaptor.forClass(MesQcSeedQualitySubmitReqVO.class);verify(service).submit(capture.capture());
-        assertEquals("B2",capture.getValue().getCode());assertEquals(8L,capture.getValue().getQualifiedWarehouseId());
+        // 涓�娆℃彁浜ゅ悓鏃剁敓鎴愬姞宸ラ儴銆侀攢鍞儴涓ゆ潯璁板綍锛屾寚鏍囦笌缁撹涓�鑷淬��
+        verify(qualities).insertBatch(argThat(rows -> rows.size() == 2 && rows.stream().allMatch(row -> row.getCode().equals("B2")
+                && row.getQualifiedWarehouseId().equals(8L))
+                && rows.stream().map(MesQcSeedQualityDO::getQcLine).collect(java.util.stream.Collectors.toSet())
+                .equals(java.util.Set.of(1, 2))),eq(100));
+        verify(indicators).insertBatch(argThat(rows -> rows.size() == 8),eq(400));
+        verify(actions).insertBatch(argThat(rows -> rows.size() == 2),eq(100));
     }
     @Test void changedConfirmedListDoesNotSubmitAnyBag() {
-        req.setBagIds(List.of(1L));assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any());
+        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(service,never()).submit(any());
+        second.setInboundEventId(null);assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt());
     }
     @Test void wrongTypeAndCanceledBatchCannotSubmit() {
-        req.setScanType("OTHER");assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));
-        req.setScanType("BATCH");when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(new MesProBatchDO().setId(10L).setCode("BATCH").setStatus(40));
-        assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any());
+        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);when(bags.selectListByIdsForUpdate(any())).thenReturn(List.of(first));
-        assertEquals(1,service.submitGroup(req).get("processedCount"));verify(service).submit(any());
+        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->{var single=(MesQcSeedQualitySubmitReqVO)invocation.getArgument(0);jdbc.update("INSERT INTO qc_result(id) VALUES(?)",single.getCode().equals("B1")?1:2);if(single.getCode().equals("B2"))throw new IllegalStateException("搴撳瓨娴佽浆澶辫触");return 101L;}).when(service).submit(any());
+        doAnswer(invocation->{long id=1;for(var quality:(java.util.Collection<MesQcSeedQualityDO>)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();
@@ -80,31 +156,63 @@
     }
     @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()));
+                MesQcSeedQualityDO.builder().id(11L).code("B1").qcLine(1).workflowStatus(10).build(),
+                MesQcSeedQualityDO.builder().id(12L).code("B2").qcLine(1).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").qcLine(1).workflowStatus(20).build(),
+                MesQcSeedQualityDO.builder().id(12L).code("B2").qcLine(1).workflowStatus(25).build(),
+                MesQcSeedQualityDO.builder().id(13L).code("B3").qcLine(1).workflowStatus(30).build(),
+                MesQcSeedQualityDO.builder().id(14L).code("B4").qcLine(1).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.selectListByCodesForUpdate(any())).thenReturn(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(List.of(
+                MesQcSeedQualityDO.builder().id(11L).code("B1").qcLine(1).workflowStatus(10).build(),
+                MesQcSeedQualityDO.builder().id(12L).code("B2").qcLine(1).workflowStatus(10).build()));
         var request = new MesQcSeedQualityScopeRetestReqVO();
-        request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10);
-        request.setQualityIds(List.of(11L)); request.setIndicators(List.of());
+        request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setQcLine(1); request.setWorkflowStatus(10);
+        request.setQualityIds(List.of(11L)); request.setIndicators(validIndicators());
         assertThrows(IllegalArgumentException.class, () -> service.retestScope(request));
-        verify(service, never()).retest(anyLong(), any());
+        verify(indicators, never()).updateBatch(anyCollection(), anyInt());
     }
     @Test void retestScopeProcessesAllConfirmedBags() {
-        when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(
-                MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
-                MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()));
-        doNothing().when(service).retest(anyLong(), any());
+        var pending = List.of(MesQcSeedQualityDO.builder().id(11L).code("B1").qcLine(1).workflowStatus(10).build(),
+                MesQcSeedQualityDO.builder().id(12L).code("B2").qcLine(1).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(List.of());
+        request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setQcLine(1); request.setWorkflowStatus(10);
+        request.setQualityIds(List.of(11L, 12L)); request.setIndicators(validIndicators());
         assertEquals(2, service.retestScope(request).get("processedCount"));
-        verify(service, times(2)).retest(anyLong(), any());
+        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());
     }
 }

--
Gitblit v1.9.3