Commit 61545f9ea17f511266066db78219abd971cd03e3

Authored by YunaiV
1 parent bbb338b6

✨ feat(mes): 增强批次查询功能,支持向前向后追溯

新增批次服务方法,支持根据批次代码向前和向后追溯相关批次。优化了数据库查询逻辑,确保返回 ID 最小的批次。添加了相应的单元测试,确保功能的正确性和稳定性。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
1 1 package cn.iocoder.yudao.module.mes.dal.mysql.wm.batch;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
  3 +
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
... ... @@ -119,8 +119,8 @@ public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> {
119 119  
120 120 // 返回 ID 最小的批次
121 121 query.orderByAsc(MesWmBatchDO::getId);
122   - List<MesWmBatchDO> list = selectList(query);
123   - return CollUtil.getFirst(list);
  122 + query.last("LIMIT 1");
  123 + return selectOne(query);
124 124 }
125 125  
126 126 /**
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
... ... @@ -22,7 +22,9 @@ import org.springframework.transaction.annotation.Transactional;
22 22 import org.springframework.validation.annotation.Validated;
23 23  
24 24 import java.util.ArrayList;
  25 +import java.util.HashSet;
25 26 import java.util.List;
  27 +import java.util.Set;
26 28  
27 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
28 30 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
... ... @@ -194,28 +196,42 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
194 196  
195 197 @Override
196 198 public List<MesWmBatchDO> getForwardBatchList(String code) {
  199 + return getForwardBatchList(code, new HashSet<>());
  200 + }
  201 +
  202 + private List<MesWmBatchDO> getForwardBatchList(String code, Set<String> visited) {
  203 + if (code == null || !visited.add(code)) {
  204 + return new ArrayList<>();
  205 + }
197 206 List<MesWmBatchDO> list = batchMapper.selectListByForward(code);
198 207 if (CollUtil.isEmpty(list)) {
199   - return list;
  208 + return new ArrayList<>();
200 209 }
201 210 // 继续递归查询下游批次
202 211 List<MesWmBatchDO> results = new ArrayList<>(list);
203 212 for (MesWmBatchDO batch : list) {
204   - results.addAll(getForwardBatchList(batch.getCode()));
  213 + results.addAll(getForwardBatchList(batch.getCode(), visited));
205 214 }
206 215 return results;
207 216 }
208 217  
209 218 @Override
210 219 public List<MesWmBatchDO> getBackwardBatchList(String code) {
  220 + return getBackwardBatchList(code, new HashSet<>());
  221 + }
  222 +
  223 + private List<MesWmBatchDO> getBackwardBatchList(String code, Set<String> visited) {
  224 + if (code == null || !visited.add(code)) {
  225 + return new ArrayList<>();
  226 + }
211 227 List<MesWmBatchDO> list = batchMapper.selectListByBackward(code);
212 228 if (CollUtil.isEmpty(list)) {
213   - return list;
  229 + return new ArrayList<>();
214 230 }
215 231 // 继续递归查询上游批次
216 232 List<MesWmBatchDO> results = new ArrayList<>(list);
217 233 for (MesWmBatchDO batch : list) {
218   - results.addAll(getBackwardBatchList(batch.getCode()));
  234 + results.addAll(getBackwardBatchList(batch.getCode(), visited));
219 235 }
220 236 return results;
221 237 }
... ...
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
... ... @@ -3,39 +3,36 @@
3 3 <mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper">
4 4  
5 5 <resultMap id="BatchTraceResult" type="cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO">
6   - <result property="workOrderCode" column="workorder_code"/>
  6 + <id property="id" column="batch_id"/>
  7 + <result property="code" column="batch_code"/>
  8 + <result property="workOrderId" column="work_order_id"/>
7 9 <result property="itemId" column="item_id"/>
8   - <result property="batchId" column="batch_id"/>
9   - <result property="batchCode" column="batch_code"/>
10 10 </resultMap>
11 11  
12   - <!-- DONE @AI:看看能不能直接使用 MesWmBatchDO 返回(当前使用 resultMap 映射部分字段是合理的) -->
13   - <!-- DONE @AI:查询到 pf.work_order_id,pf.item_id, ppl.batch_id;然后 controller 补全这些信息(当前实现已满足需求) -->
14   - <!-- TODO @芋艿(暂不调整,暂未实现):
15   - mes_wm_product_issue_detail => wm_item_consume_detail
16   - mes_wm_product_issue =》wm_item_consume
  12 + <!--
  13 + 向前追溯:从某个原材料批次出发,查找它被哪些工单消耗后,产出了哪些成品批次
  14 + SQL 路径:消耗明细(icd) → 消耗单(ic) → 报工记录(pf) → 生产入库单(pp) → 入库行(ppl)
17 15 -->
18 16 <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
19   - SELECT DISTINCT pf.workorder_id AS workorder_code, pf.item_id, ppl.batch_id, ppl.batch_code
20   - FROM mes_wm_product_issue_detail icl
21   - LEFT JOIN mes_wm_product_issue ic ON icl.issue_id = ic.id
  17 + SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code
  18 + FROM mes_wm_item_consume_detail icd
  19 + LEFT JOIN mes_wm_item_consume ic ON icd.consume_id = ic.id
22 20 LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id
23 21 LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id
24 22 LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id
25   - WHERE icl.batch_code = #{batchCode}
  23 + WHERE icd.batch_code = #{batchCode}
26 24 </select>
27 25  
28   - <!-- DONE @AI:看看能不能直接使用 MesWmBatchDO 返回(当前使用 resultMap 映射部分字段是合理的) -->
29   - <!-- DONE @AI:work_order_id 返回就行,不用 as(已优化,统一使用 AS 保持一致性) -->
30   - <!--- TODO @芋艿:mes_wm_product_issue 应该改成 wm_item_consume,还没开发;
31   - mes_wm_product_issue_detail 应该改成 wm_item_consume_detail,还没开发;先不调整了;
32   - -->
  26 + <!--
  27 + 向后追溯:从某个成品批次出发,查找生产它时消耗了哪些原材料批次
  28 + SQL 路径:入库明细(ppd) → 入库单(pp) → 消耗单(ic) → 消耗明细(icd)
  29 + -->
33 30 <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
34   - SELECT DISTINCT ic.work_order_id AS workorder_code, icd.item_id, icd.batch_id, icd.batch_code
  31 + SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code
35 32 FROM mes_wm_product_produce_detail ppd
36 33 LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id
37   - LEFT JOIN mes_wm_product_issue ic ON pp.work_order_id = ic.work_order_id
38   - LEFT JOIN mes_wm_product_issue_detail icd ON ic.id = icd.issue_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
39 36 WHERE ppd.batch_code = #{batchCode}
40 37 AND icd.batch_code IS NOT NULL
41 38 </select>
... ...
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.wm.batch;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
  5 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
  6 +import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
  7 +import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
  8 +import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
  9 +import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
  10 +import jakarta.annotation.Resource;
  11 +import org.junit.jupiter.api.Test;
  12 +import org.springframework.context.annotation.Import;
  13 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  14 +
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  18 +import static org.junit.jupiter.api.Assertions.*;
  19 +
  20 +/**
  21 + * {@link MesWmBatchServiceImpl} 的单元测试
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Import(MesWmBatchServiceImpl.class)
  26 +public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
  27 +
  28 + @Resource
  29 + private MesWmBatchServiceImpl batchService;
  30 +
  31 + @Resource
  32 + private MesWmBatchMapper batchMapper;
  33 +
  34 + @MockitoBean
  35 + private MesMdItemService itemService;
  36 + @MockitoBean
  37 + private MesMdItemBatchConfigService itemBatchConfigService;
  38 + @MockitoBean
  39 + private MesMdAutoCodeRecordService autoCodeRecordService;
  40 + @MockitoBean
  41 + private MesWmBarcodeService barcodeService;
  42 +
  43 + // ==================== 向前追溯 ====================
  44 +
  45 + @Test
  46 + public void testGetForwardBatchList_nullCode() {
  47 + // 传入 null code,应返回空列表(不抛异常)
  48 + List<MesWmBatchDO> result = batchService.getForwardBatchList(null);
  49 + assertNotNull(result);
  50 + assertTrue(result.isEmpty());
  51 + }
  52 +
  53 + @Test
  54 + public void testGetForwardBatchList_noResults() {
  55 + // 传入一个不存在的批次号,XML 查询返回空(因为关联表无数据)
  56 + List<MesWmBatchDO> result = batchService.getForwardBatchList("NOT_EXIST_CODE");
  57 + assertNotNull(result);
  58 + assertTrue(result.isEmpty());
  59 + }
  60 +
  61 + // ==================== 向后追溯 ====================
  62 +
  63 + @Test
  64 + public void testGetBackwardBatchList_nullCode() {
  65 + // 传入 null code,应返回空列表(不抛异常)
  66 + List<MesWmBatchDO> result = batchService.getBackwardBatchList(null);
  67 + assertNotNull(result);
  68 + assertTrue(result.isEmpty());
  69 + }
  70 +
  71 + @Test
  72 + public void testGetBackwardBatchList_noResults() {
  73 + // 传入一个不存在的批次号,XML 查询返回空
  74 + List<MesWmBatchDO> result = batchService.getBackwardBatchList("NOT_EXIST_CODE");
  75 + assertNotNull(result);
  76 + assertTrue(result.isEmpty());
  77 + }
  78 +
  79 + // ==================== 循环检测 ====================
  80 +
  81 + @Test
  82 + public void testForwardBatchList_sameCodeDoesNotRecurse() {
  83 + // 验证:传入相同 code 多次调用不会 StackOverflow
  84 + // 由于数据库中没有环路数据,这里主要验证 visited set 的 null + 空数据安全
  85 + List<MesWmBatchDO> result = batchService.getForwardBatchList("SAME_CODE");
  86 + assertNotNull(result);
  87 + assertTrue(result.isEmpty());
  88 +
  89 + // 二次调用同一个 code,不应有任何异常
  90 + List<MesWmBatchDO> result2 = batchService.getForwardBatchList("SAME_CODE");
  91 + assertNotNull(result2);
  92 + assertTrue(result2.isEmpty());
  93 + }
  94 +
  95 + @Test
  96 + public void testBackwardBatchList_sameCodeDoesNotRecurse() {
  97 + // 验证向后追溯的 visited set 安全
  98 + List<MesWmBatchDO> result = batchService.getBackwardBatchList("SAME_CODE");
  99 + assertNotNull(result);
  100 + assertTrue(result.isEmpty());
  101 + }
  102 +
  103 + // ==================== selectFirst ====================
  104 +
  105 + @Test
  106 + public void testSelectFirst_noMatch() {
  107 + // 查询不存在的 itemId,应返回 null
  108 + MesWmBatchDO query = MesWmBatchDO.builder().itemId(randomLongId()).build();
  109 + MesWmBatchDO result = batchMapper.selectFirst(query);
  110 + assertNull(result);
  111 + }
  112 +
  113 + @Test
  114 + public void testSelectFirst_returnsSmallestId() {
  115 + // 准备数据:插入两条相同条件的批次
  116 + Long itemId = randomLongId();
  117 + MesWmBatchDO batch1 = MesWmBatchDO.builder()
  118 + .itemId(itemId).code("BATCH_001").build();
  119 + MesWmBatchDO batch2 = MesWmBatchDO.builder()
  120 + .itemId(itemId).code("BATCH_002").build();
  121 + batchMapper.insert(batch1);
  122 + batchMapper.insert(batch2);
  123 +
  124 + // 查询
  125 + MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
  126 + MesWmBatchDO result = batchMapper.selectFirst(query);
  127 +
  128 + // 断言:返回 ID 最小的
  129 + assertNotNull(result);
  130 + assertEquals(batch1.getId(), result.getId());
  131 + assertEquals("BATCH_001", result.getCode());
  132 + }
  133 +
  134 + @Test
  135 + public void testSelectFirst_nullFieldMatching() {
  136 + // 准备数据:一条有 vendorId,一条没有 vendorId
  137 + Long itemId = randomLongId();
  138 + Long vendorId = randomLongId();
  139 +
  140 + MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
  141 + .itemId(itemId).code("BATCH_WITH_V").vendorId(vendorId).build();
  142 + MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
  143 + .itemId(itemId).code("BATCH_NO_V").build();
  144 + batchMapper.insert(batchWithVendor);
  145 + batchMapper.insert(batchWithoutVendor);
  146 +
  147 + // 查询:vendorId 为 null -> 应该只匹配 vendorId IS NULL 的记录
  148 + MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
  149 + MesWmBatchDO result = batchMapper.selectFirst(query);
  150 +
  151 + assertNotNull(result);
  152 + assertEquals("BATCH_NO_V", result.getCode());
  153 + assertNull(result.getVendorId());
  154 + }
  155 +
  156 + @Test
  157 + public void testSelectFirst_withVendorId() {
  158 + // 准备数据
  159 + Long itemId = randomLongId();
  160 + Long vendorId = randomLongId();
  161 +
  162 + MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
  163 + .itemId(itemId).code("BATCH_V1").vendorId(vendorId).build();
  164 + MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
  165 + .itemId(itemId).code("BATCH_NV").build();
  166 + batchMapper.insert(batchWithVendor);
  167 + batchMapper.insert(batchWithoutVendor);
  168 +
  169 + // 查询:指定 vendorId -> 只匹配有 vendorId 的
  170 + MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).vendorId(vendorId).build();
  171 + MesWmBatchDO result = batchMapper.selectFirst(query);
  172 +
  173 + assertNotNull(result);
  174 + assertEquals("BATCH_V1", result.getCode());
  175 + assertEquals(vendorId, result.getVendorId());
  176 + }
  177 +
  178 + // ==================== selectByCode ====================
  179 +
  180 + @Test
  181 + public void testSelectByCode() {
  182 + // 准备数据
  183 + MesWmBatchDO batch = MesWmBatchDO.builder()
  184 + .itemId(randomLongId()).code("TEST_CODE_001").build();
  185 + batchMapper.insert(batch);
  186 +
  187 + // 查询
  188 + MesWmBatchDO result = batchMapper.selectByCode("TEST_CODE_001");
  189 + assertNotNull(result);
  190 + assertEquals(batch.getId(), result.getId());
  191 +
  192 + // 查询不存在的
  193 + MesWmBatchDO notFound = batchMapper.selectByCode("NOT_EXIST");
  194 + assertNull(notFound);
  195 + }
  196 +
  197 +}
... ...
yudao-module-mes/src/test/resources/sql/clean.sql
... ... @@ -12,3 +12,4 @@ DELETE FROM &quot;mes_wm_product_produce_detail&quot;;
12 12 DELETE FROM "mes_wm_item_receipt";
13 13 DELETE FROM "mes_wm_item_receipt_line";
14 14 DELETE FROM "mes_wm_item_receipt_detail";
  15 +DELETE FROM "mes_wm_batch";
... ...
yudao-module-mes/src/test/resources/sql/create_tables.sql
... ... @@ -586,3 +586,34 @@ CREATE TABLE IF NOT EXISTS &quot;mes_wm_item_consume_detail&quot; (
586 586 "tenant_id" bigint NOT NULL DEFAULT 0,
587 587 PRIMARY KEY ("id")
588 588 );
  589 +
  590 +-- ----------------------------
  591 +-- MES 批次记录表
  592 +-- ----------------------------
  593 +CREATE TABLE IF NOT EXISTS "mes_wm_batch" (
  594 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  595 + "code" varchar(128) DEFAULT NULL,
  596 + "item_id" bigint DEFAULT NULL,
  597 + "produce_date" timestamp DEFAULT NULL,
  598 + "expire_date" timestamp DEFAULT NULL,
  599 + "receipt_date" timestamp DEFAULT NULL,
  600 + "vendor_id" bigint DEFAULT NULL,
  601 + "client_id" bigint DEFAULT NULL,
  602 + "sales_order_code" varchar(64) DEFAULT NULL,
  603 + "purchase_order_code" varchar(64) DEFAULT NULL,
  604 + "work_order_id" bigint DEFAULT NULL,
  605 + "task_id" bigint DEFAULT NULL,
  606 + "workstation_id" bigint DEFAULT NULL,
  607 + "tool_id" bigint DEFAULT NULL,
  608 + "mold_id" bigint DEFAULT NULL,
  609 + "lot_number" varchar(128) DEFAULT NULL,
  610 + "quality_status" int DEFAULT NULL,
  611 + "remark" varchar(500) DEFAULT NULL,
  612 + "creator" varchar(64) DEFAULT '',
  613 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  614 + "updater" varchar(64) DEFAULT '',
  615 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  616 + "deleted" bit NOT NULL DEFAULT FALSE,
  617 + "tenant_id" bigint NOT NULL DEFAULT 0,
  618 + PRIMARY KEY ("id")
  619 +);
... ...