Commit 80f7f51cff7a5cbf2d73d5fe6c22a354b5627877

Authored by YunaiV
1 parent 67801112

✨ feat(mes): 增强批次追溯功能,添加删除标记过滤

更新 SQL 查询以过滤已删除的记录,确保批次追溯时只返回有效数据。同时,增加了集成测试用例以验证新逻辑的正确性。
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -16,11 +16,12 @@ @@ -16,11 +16,12 @@
16 <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult"> 16 <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
17 SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code 17 SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code
18 FROM mes_wm_item_consume_detail icd 18 FROM mes_wm_item_consume_detail icd
19 - LEFT JOIN mes_wm_item_consume ic ON icd.consume_id = ic.id  
20 - LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id  
21 - LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id  
22 - LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id 19 + LEFT JOIN mes_wm_item_consume ic ON icd.consume_id = ic.id AND ic.deleted = 0
  20 + LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id AND pf.deleted = 0
  21 + LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id AND pp.deleted = 0
  22 + LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id AND ppl.deleted = 0
23 WHERE icd.batch_code = #{batchCode} 23 WHERE icd.batch_code = #{batchCode}
  24 + AND icd.deleted = 0
24 </select> 25 </select>
25 26
26 <!-- 27 <!--
@@ -30,10 +31,11 @@ @@ -30,10 +31,11 @@
30 <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult"> 31 <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
31 SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code 32 SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code
32 FROM mes_wm_product_produce_detail ppd 33 FROM mes_wm_product_produce_detail ppd
33 - LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id  
34 - LEFT JOIN mes_wm_item_consume ic ON pp.work_order_id = ic.work_order_id  
35 - LEFT JOIN mes_wm_item_consume_detail icd ON ic.id = icd.consume_id 34 + LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id AND pp.deleted = 0
  35 + LEFT JOIN mes_wm_item_consume ic ON pp.work_order_id = ic.work_order_id AND ic.deleted = 0
  36 + LEFT JOIN mes_wm_item_consume_detail icd ON ic.id = icd.consume_id AND icd.deleted = 0
36 WHERE ppd.batch_code = #{batchCode} 37 WHERE ppd.batch_code = #{batchCode}
  38 + AND ppd.deleted = 0
37 AND icd.batch_code IS NOT NULL 39 AND icd.batch_code IS NOT NULL
38 </select> 40 </select>
39 41
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java
1 package cn.iocoder.yudao.module.mes.service.wm.batch; 1 package cn.iocoder.yudao.module.mes.service.wm.batch;
2 2
3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
4 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; 5 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO;
  8 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
  9 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
  10 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
  11 +import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
5 import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper; 12 import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
  13 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeDetailMapper;
  14 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper;
  15 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceDetailMapper;
  16 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
  17 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
6 import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService; 18 import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
7 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService; 19 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
8 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; 20 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
@@ -12,6 +24,8 @@ import org.junit.jupiter.api.Test; @@ -12,6 +24,8 @@ import org.junit.jupiter.api.Test;
12 import org.springframework.context.annotation.Import; 24 import org.springframework.context.annotation.Import;
13 import org.springframework.test.context.bean.override.mockito.MockitoBean; 25 import org.springframework.test.context.bean.override.mockito.MockitoBean;
14 26
  27 +import java.math.BigDecimal;
  28 +import java.time.LocalDateTime;
15 import java.util.List; 29 import java.util.List;
16 30
17 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 31 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
@@ -31,6 +45,19 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest { @@ -31,6 +45,19 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
31 @Resource 45 @Resource
32 private MesWmBatchMapper batchMapper; 46 private MesWmBatchMapper batchMapper;
33 47
  48 + @Resource
  49 + private MesWmItemConsumeMapper consumeMapper;
  50 + @Resource
  51 + private MesWmItemConsumeDetailMapper consumeDetailMapper;
  52 + @Resource
  53 + private MesProFeedbackMapper feedbackMapper;
  54 + @Resource
  55 + private MesWmProductProduceMapper produceMapper;
  56 + @Resource
  57 + private MesWmProductProduceLineMapper produceLineMapper;
  58 + @Resource
  59 + private MesWmProductProduceDetailMapper produceDetailMapper;
  60 +
34 @MockitoBean 61 @MockitoBean
35 private MesMdItemService itemService; 62 private MesMdItemService itemService;
36 @MockitoBean 63 @MockitoBean
@@ -194,4 +221,187 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest { @@ -194,4 +221,187 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
194 assertNull(notFound); 221 assertNull(notFound);
195 } 222 }
196 223
  224 + // ==================== 向前追溯(集成测试)====================
  225 +
  226 + /**
  227 + * 向前追溯集成测试:构造完整链路数据
  228 + * 链路:消耗明细(icd, batchCode=RAW_BATCH) → 消耗单(ic) → 报工(pf) → 入库单(pp) → 入库行(ppl, batchCode=PRODUCT_BATCH)
  229 + * 预期:查 RAW_BATCH 能找到 PRODUCT_BATCH
  230 + */
  231 + @Test
  232 + public void testSelectListByForward_withData() {
  233 + // 准备:公共 ID
  234 + Long workOrderId = randomLongId();
  235 + Long rawItemId = randomLongId();
  236 + Long productItemId = randomLongId();
  237 + Long productBatchId = randomLongId();
  238 + LocalDateTime now = LocalDateTime.now();
  239 +
  240 + // 1. 报工记录
  241 + MesProFeedbackDO feedback = MesProFeedbackDO.builder()
  242 + .workOrderId(workOrderId).itemId(productItemId).build();
  243 + feedbackMapper.insert(feedback);
  244 +
  245 + // 2. 消耗单
  246 + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
  247 + .workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
  248 + .processId(randomLongId()).feedbackId(feedback.getId()).consumeDate(now).build();
  249 + consumeMapper.insert(consume);
  250 +
  251 + // 3. 消耗明细(关联原材料批次 RAW_BATCH)
  252 + MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
  253 + .consumeId(consume.getId()).lineId(randomLongId()).itemId(rawItemId)
  254 + .quantity(BigDecimal.TEN).batchCode("RAW_BATCH")
  255 + .warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
  256 + consumeDetailMapper.insert(consumeDetail);
  257 +
  258 + // 4. 生产入库单
  259 + MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
  260 + .workOrderId(workOrderId).build();
  261 + produceMapper.insert(produce);
  262 +
  263 + // 5. 入库行(关联成品批次 PRODUCT_BATCH)
  264 + MesWmProductProduceLineDO produceLine = MesWmProductProduceLineDO.builder()
  265 + .produceId(produce.getId()).itemId(productItemId)
  266 + .batchId(productBatchId).batchCode("PRODUCT_BATCH").build();
  267 + produceLineMapper.insert(produceLine);
  268 +
  269 + // 执行向前追溯
  270 + List<MesWmBatchDO> result = batchMapper.selectListByForward("RAW_BATCH");
  271 +
  272 + // 断言
  273 + assertNotNull(result);
  274 + assertEquals(1, result.size());
  275 + assertEquals("PRODUCT_BATCH", result.get(0).getCode());
  276 + assertEquals(productBatchId, result.get(0).getId());
  277 + assertEquals(workOrderId, result.get(0).getWorkOrderId());
  278 + assertEquals(productItemId, result.get(0).getItemId());
  279 + }
  280 +
  281 + /**
  282 + * 向前追溯:已删除的消耗明细应被过滤(icd.deleted = 1)
  283 + */
  284 + @Test
  285 + public void testSelectListByForward_deletedDataFiltered() {
  286 + Long workOrderId = randomLongId();
  287 + LocalDateTime now = LocalDateTime.now();
  288 +
  289 + // 1. 报工记录
  290 + MesProFeedbackDO feedback = MesProFeedbackDO.builder()
  291 + .workOrderId(workOrderId).itemId(randomLongId()).build();
  292 + feedbackMapper.insert(feedback);
  293 +
  294 + // 2. 消耗单
  295 + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
  296 + .workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
  297 + .processId(randomLongId()).feedbackId(feedback.getId()).consumeDate(now).build();
  298 + consumeMapper.insert(consume);
  299 +
  300 + // 3. 消耗明细(逻辑删除)
  301 + MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
  302 + .consumeId(consume.getId()).lineId(randomLongId()).itemId(randomLongId())
  303 + .quantity(BigDecimal.TEN).batchCode("RAW_DEL_TEST")
  304 + .warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
  305 + consumeDetailMapper.insert(consumeDetail);
  306 + consumeDetailMapper.deleteById(consumeDetail.getId()); // 逻辑删除消耗明细
  307 +
  308 + // 4. 生产入库单 + 入库行
  309 + MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
  310 + .workOrderId(workOrderId).build();
  311 + produceMapper.insert(produce);
  312 + MesWmProductProduceLineDO produceLine = MesWmProductProduceLineDO.builder()
  313 + .produceId(produce.getId()).itemId(randomLongId())
  314 + .batchId(randomLongId()).batchCode("PRODUCT_DEL_TEST").build();
  315 + produceLineMapper.insert(produceLine);
  316 +
  317 + // 执行:消耗明细已删除,应该追溯不到
  318 + List<MesWmBatchDO> result = batchMapper.selectListByForward("RAW_DEL_TEST");
  319 + assertNotNull(result);
  320 + assertTrue(result.isEmpty());
  321 + }
  322 +
  323 + // ==================== 向后追溯(集成测试)====================
  324 +
  325 + /**
  326 + * 向后追溯集成测试:构造完整链路数据
  327 + * 链路:入库明细(ppd, batchCode=PRODUCT_BATCH) → 入库单(pp) → 消耗单(ic) → 消耗明细(icd, batchCode=RAW_BATCH)
  328 + * 预期:查 PRODUCT_BATCH 能找到 RAW_BATCH
  329 + */
  330 + @Test
  331 + public void testSelectListByBackward_withData() {
  332 + Long workOrderId = randomLongId();
  333 + Long rawItemId = randomLongId();
  334 + Long rawBatchId = randomLongId();
  335 + LocalDateTime now = LocalDateTime.now();
  336 +
  337 + // 1. 生产入库单
  338 + MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
  339 + .workOrderId(workOrderId).build();
  340 + produceMapper.insert(produce);
  341 +
  342 + // 2. 入库明细(关联成品批次 PRODUCT_BACK_BATCH)
  343 + MesWmProductProduceDetailDO produceDetail = MesWmProductProduceDetailDO.builder()
  344 + .produceId(produce.getId()).itemId(randomLongId())
  345 + .batchCode("PRODUCT_BACK_BATCH").build();
  346 + produceDetailMapper.insert(produceDetail);
  347 +
  348 + // 3. 消耗单
  349 + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
  350 + .workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
  351 + .processId(randomLongId()).feedbackId(randomLongId()).consumeDate(now).build();
  352 + consumeMapper.insert(consume);
  353 +
  354 + // 4. 消耗明细(关联原材料批次 RAW_BACK_BATCH)
  355 + MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
  356 + .consumeId(consume.getId()).lineId(randomLongId()).itemId(rawItemId)
  357 + .quantity(BigDecimal.ONE).batchId(rawBatchId).batchCode("RAW_BACK_BATCH")
  358 + .warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
  359 + consumeDetailMapper.insert(consumeDetail);
  360 +
  361 + // 执行向后追溯
  362 + List<MesWmBatchDO> result = batchMapper.selectListByBackward("PRODUCT_BACK_BATCH");
  363 +
  364 + // 断言
  365 + assertNotNull(result);
  366 + assertEquals(1, result.size());
  367 + assertEquals("RAW_BACK_BATCH", result.get(0).getCode());
  368 + assertEquals(rawBatchId, result.get(0).getId());
  369 + assertEquals(workOrderId, result.get(0).getWorkOrderId());
  370 + assertEquals(rawItemId, result.get(0).getItemId());
  371 + }
  372 +
  373 + /**
  374 + * 向后追溯:消耗明细 batchCode 为 NULL 的应被过滤
  375 + */
  376 + @Test
  377 + public void testSelectListByBackward_nullBatchCodeFiltered() {
  378 + Long workOrderId = randomLongId();
  379 + LocalDateTime now = LocalDateTime.now();
  380 +
  381 + // 生产入库单 + 入库明细
  382 + MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
  383 + .workOrderId(workOrderId).build();
  384 + produceMapper.insert(produce);
  385 + MesWmProductProduceDetailDO produceDetail = MesWmProductProduceDetailDO.builder()
  386 + .produceId(produce.getId()).itemId(randomLongId())
  387 + .batchCode("PRODUCT_NULL_TEST").build();
  388 + produceDetailMapper.insert(produceDetail);
  389 +
  390 + // 消耗单 + 消耗明细(batchCode 为 null)
  391 + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
  392 + .workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
  393 + .processId(randomLongId()).feedbackId(randomLongId()).consumeDate(now).build();
  394 + consumeMapper.insert(consume);
  395 + MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
  396 + .consumeId(consume.getId()).lineId(randomLongId()).itemId(randomLongId())
  397 + .quantity(BigDecimal.ONE).batchCode(null) // 无批次
  398 + .warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
  399 + consumeDetailMapper.insert(consumeDetail);
  400 +
  401 + // 执行:应过滤掉 batchCode IS NULL 的消耗明细
  402 + List<MesWmBatchDO> result = batchMapper.selectListByBackward("PRODUCT_NULL_TEST");
  403 + assertNotNull(result);
  404 + assertTrue(result.isEmpty());
  405 + }
  406 +
197 } 407 }