Commit 80f7f51cff7a5cbf2d73d5fe6c22a354b5627877
1 parent
67801112
✨ feat(mes): 增强批次追溯功能,添加删除标记过滤
更新 SQL 查询以过滤已删除的记录,确保批次追溯时只返回有效数据。同时,增加了集成测试用例以验证新逻辑的正确性。
Showing
2 changed files
with
219 additions
and
7 deletions
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
| ... | ... | @@ -16,11 +16,12 @@ |
| 16 | 16 | <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult"> |
| 17 | 17 | SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code |
| 18 | 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 | 23 | WHERE icd.batch_code = #{batchCode} |
| 24 | + AND icd.deleted = 0 | |
| 24 | 25 | </select> |
| 25 | 26 | |
| 26 | 27 | <!-- |
| ... | ... | @@ -30,10 +31,11 @@ |
| 30 | 31 | <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult"> |
| 31 | 32 | SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code |
| 32 | 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 | 37 | WHERE ppd.batch_code = #{batchCode} |
| 38 | + AND ppd.deleted = 0 | |
| 37 | 39 | AND icd.batch_code IS NOT NULL |
| 38 | 40 | </select> |
| 39 | 41 | ... | ... |
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java
| 1 | 1 | package cn.iocoder.yudao.module.mes.service.wm.batch; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 4 | +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; | |
| 4 | 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 | 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 | 18 | import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService; |
| 7 | 19 | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService; |
| 8 | 20 | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| ... | ... | @@ -12,6 +24,8 @@ import org.junit.jupiter.api.Test; |
| 12 | 24 | import org.springframework.context.annotation.Import; |
| 13 | 25 | import org.springframework.test.context.bean.override.mockito.MockitoBean; |
| 14 | 26 | |
| 27 | +import java.math.BigDecimal; | |
| 28 | +import java.time.LocalDateTime; | |
| 15 | 29 | import java.util.List; |
| 16 | 30 | |
| 17 | 31 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| ... | ... | @@ -31,6 +45,19 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest { |
| 31 | 45 | @Resource |
| 32 | 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 | 61 | @MockitoBean |
| 35 | 62 | private MesMdItemService itemService; |
| 36 | 63 | @MockitoBean |
| ... | ... | @@ -194,4 +221,187 @@ public class MesWmBatchServiceImplTest extends BaseDbUnitTest { |
| 194 | 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 | } | ... | ... |