Commit 61545f9ea17f511266066db78219abd971cd03e3
1 parent
bbb338b6
✨ feat(mes): 增强批次查询功能,支持向前向后追溯
新增批次服务方法,支持根据批次代码向前和向后追溯相关批次。优化了数据库查询逻辑,确保返回 ID 最小的批次。添加了相应的单元测试,确保功能的正确性和稳定性。
Showing
6 changed files
with
269 additions
and
27 deletions
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
yudao-module-mes/src/test/resources/sql/create_tables.sql
| ... | ... | @@ -586,3 +586,34 @@ CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_detail" ( |
| 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 | +); | ... | ... |