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 package cn.iocoder.yudao.module.mes.dal.mysql.wm.batch; 1 package cn.iocoder.yudao.module.mes.dal.mysql.wm.batch;
2 2
3 -import cn.hutool.core.collection.CollUtil; 3 +
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@@ -119,8 +119,8 @@ public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> { @@ -119,8 +119,8 @@ public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> {
119 119
120 // 返回 ID 最小的批次 120 // 返回 ID 最小的批次
121 query.orderByAsc(MesWmBatchDO::getId); 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,7 +22,9 @@ import org.springframework.transaction.annotation.Transactional;
22 import org.springframework.validation.annotation.Validated; 22 import org.springframework.validation.annotation.Validated;
23 23
24 import java.util.ArrayList; 24 import java.util.ArrayList;
  25 +import java.util.HashSet;
25 import java.util.List; 26 import java.util.List;
  27 +import java.util.Set;
26 28
27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
28 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; 30 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -194,28 +196,42 @@ public class MesWmBatchServiceImpl implements MesWmBatchService { @@ -194,28 +196,42 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
194 196
195 @Override 197 @Override
196 public List<MesWmBatchDO> getForwardBatchList(String code) { 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 List<MesWmBatchDO> list = batchMapper.selectListByForward(code); 206 List<MesWmBatchDO> list = batchMapper.selectListByForward(code);
198 if (CollUtil.isEmpty(list)) { 207 if (CollUtil.isEmpty(list)) {
199 - return list; 208 + return new ArrayList<>();
200 } 209 }
201 // 继续递归查询下游批次 210 // 继续递归查询下游批次
202 List<MesWmBatchDO> results = new ArrayList<>(list); 211 List<MesWmBatchDO> results = new ArrayList<>(list);
203 for (MesWmBatchDO batch : list) { 212 for (MesWmBatchDO batch : list) {
204 - results.addAll(getForwardBatchList(batch.getCode())); 213 + results.addAll(getForwardBatchList(batch.getCode(), visited));
205 } 214 }
206 return results; 215 return results;
207 } 216 }
208 217
209 @Override 218 @Override
210 public List<MesWmBatchDO> getBackwardBatchList(String code) { 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 List<MesWmBatchDO> list = batchMapper.selectListByBackward(code); 227 List<MesWmBatchDO> list = batchMapper.selectListByBackward(code);
212 if (CollUtil.isEmpty(list)) { 228 if (CollUtil.isEmpty(list)) {
213 - return list; 229 + return new ArrayList<>();
214 } 230 }
215 // 继续递归查询上游批次 231 // 继续递归查询上游批次
216 List<MesWmBatchDO> results = new ArrayList<>(list); 232 List<MesWmBatchDO> results = new ArrayList<>(list);
217 for (MesWmBatchDO batch : list) { 233 for (MesWmBatchDO batch : list) {
218 - results.addAll(getBackwardBatchList(batch.getCode())); 234 + results.addAll(getBackwardBatchList(batch.getCode(), visited));
219 } 235 }
220 return results; 236 return results;
221 } 237 }
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -3,39 +3,36 @@ @@ -3,39 +3,36 @@
3 <mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper"> 3 <mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper">
4 4
5 <resultMap id="BatchTraceResult" type="cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO"> 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 <result property="itemId" column="item_id"/> 9 <result property="itemId" column="item_id"/>
8 - <result property="batchId" column="batch_id"/>  
9 - <result property="batchCode" column="batch_code"/>  
10 </resultMap> 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 <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult"> 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 LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id 20 LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id
23 LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id 21 LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id
24 LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id 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 </select> 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 <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult"> 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 FROM mes_wm_product_produce_detail ppd 32 FROM mes_wm_product_produce_detail ppd
36 LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id 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 WHERE ppd.batch_code = #{batchCode} 36 WHERE ppd.batch_code = #{batchCode}
40 AND icd.batch_code IS NOT NULL 37 AND icd.batch_code IS NOT NULL
41 </select> 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,3 +12,4 @@ DELETE FROM &quot;mes_wm_product_produce_detail&quot;;
12 DELETE FROM "mes_wm_item_receipt"; 12 DELETE FROM "mes_wm_item_receipt";
13 DELETE FROM "mes_wm_item_receipt_line"; 13 DELETE FROM "mes_wm_item_receipt_line";
14 DELETE FROM "mes_wm_item_receipt_detail"; 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,3 +586,34 @@ CREATE TABLE IF NOT EXISTS &quot;mes_wm_item_consume_detail&quot; (
586 "tenant_id" bigint NOT NULL DEFAULT 0, 586 "tenant_id" bigint NOT NULL DEFAULT 0,
587 PRIMARY KEY ("id") 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 +);