Explorar o código

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

新增批次服务方法,支持根据批次代码向前和向后追溯相关批次。优化了数据库查询逻辑,确保返回 ID 最小的批次。添加了相应的单元测试,确保功能的正确性和稳定性。
YunaiV hai 2 meses
pai
achega
61545f9ea1

+ 3 - 3
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.mes.dal.mysql.wm.batch;
 
-import cn.hutool.core.collection.CollUtil;
+
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@@ -119,8 +119,8 @@ public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> {
 
         // 返回 ID 最小的批次
         query.orderByAsc(MesWmBatchDO::getId);
-        List<MesWmBatchDO> list = selectList(query);
-        return CollUtil.getFirst(list);
+        query.last("LIMIT 1");
+        return selectOne(query);
     }
 
     /**

+ 20 - 4
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;
 import org.springframework.validation.annotation.Validated;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -194,28 +196,42 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
 
     @Override
     public List<MesWmBatchDO> getForwardBatchList(String code) {
+        return getForwardBatchList(code, new HashSet<>());
+    }
+
+    private List<MesWmBatchDO> getForwardBatchList(String code, Set<String> visited) {
+        if (code == null || !visited.add(code)) {
+            return new ArrayList<>();
+        }
         List<MesWmBatchDO> list = batchMapper.selectListByForward(code);
         if (CollUtil.isEmpty(list)) {
-            return list;
+            return new ArrayList<>();
         }
         // 继续递归查询下游批次
         List<MesWmBatchDO> results = new ArrayList<>(list);
         for (MesWmBatchDO batch : list) {
-            results.addAll(getForwardBatchList(batch.getCode()));
+            results.addAll(getForwardBatchList(batch.getCode(), visited));
         }
         return results;
     }
 
     @Override
     public List<MesWmBatchDO> getBackwardBatchList(String code) {
+        return getBackwardBatchList(code, new HashSet<>());
+    }
+
+    private List<MesWmBatchDO> getBackwardBatchList(String code, Set<String> visited) {
+        if (code == null || !visited.add(code)) {
+            return new ArrayList<>();
+        }
         List<MesWmBatchDO> list = batchMapper.selectListByBackward(code);
         if (CollUtil.isEmpty(list)) {
-            return list;
+            return new ArrayList<>();
         }
         // 继续递归查询上游批次
         List<MesWmBatchDO> results = new ArrayList<>(list);
         for (MesWmBatchDO batch : list) {
-            results.addAll(getBackwardBatchList(batch.getCode()));
+            results.addAll(getBackwardBatchList(batch.getCode(), visited));
         }
         return results;
     }

+ 17 - 20
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml

@@ -3,39 +3,36 @@
 <mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper">
 
     <resultMap id="BatchTraceResult" type="cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO">
-        <result property="workOrderCode" column="workorder_code"/>
+        <id property="id" column="batch_id"/>
+        <result property="code" column="batch_code"/>
+        <result property="workOrderId" column="work_order_id"/>
         <result property="itemId" column="item_id"/>
-        <result property="batchId" column="batch_id"/>
-        <result property="batchCode" column="batch_code"/>
     </resultMap>
 
-    <!-- DONE @AI:看看能不能直接使用 MesWmBatchDO 返回(当前使用 resultMap 映射部分字段是合理的) -->
-    <!-- DONE @AI:查询到 pf.work_order_id,pf.item_id, ppl.batch_id;然后 controller 补全这些信息(当前实现已满足需求) -->
-    <!-- TODO @芋艿(暂不调整,暂未实现):
-        mes_wm_product_issue_detail => wm_item_consume_detail
-        mes_wm_product_issue =》wm_item_consume
+    <!--
+        向前追溯:从某个原材料批次出发,查找它被哪些工单消耗后,产出了哪些成品批次
+        SQL 路径:消耗明细(icd) → 消耗单(ic) → 报工记录(pf) → 生产入库单(pp) → 入库行(ppl)
     -->
     <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
-        SELECT DISTINCT pf.workorder_id AS workorder_code, pf.item_id, ppl.batch_id, ppl.batch_code
-        FROM mes_wm_product_issue_detail icl
-        LEFT JOIN mes_wm_product_issue ic ON icl.issue_id = ic.id
+        SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code
+        FROM mes_wm_item_consume_detail icd
+        LEFT JOIN mes_wm_item_consume ic ON icd.consume_id = ic.id
         LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id
         LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id
         LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id
-        WHERE icl.batch_code = #{batchCode}
+        WHERE icd.batch_code = #{batchCode}
     </select>
 
-    <!-- DONE @AI:看看能不能直接使用 MesWmBatchDO 返回(当前使用 resultMap 映射部分字段是合理的) -->
-    <!-- DONE @AI:work_order_id 返回就行,不用 as(已优化,统一使用 AS 保持一致性) -->
-    <!---  TODO @芋艿:mes_wm_product_issue 应该改成 wm_item_consume,还没开发;
-        mes_wm_product_issue_detail 应该改成 wm_item_consume_detail,还没开发;先不调整了;
-     -->
+    <!--
+        向后追溯:从某个成品批次出发,查找生产它时消耗了哪些原材料批次
+        SQL 路径:入库明细(ppd) → 入库单(pp) → 消耗单(ic) → 消耗明细(icd)
+    -->
     <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
-        SELECT DISTINCT ic.work_order_id AS workorder_code, icd.item_id, icd.batch_id, icd.batch_code
+        SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code
         FROM mes_wm_product_produce_detail ppd
         LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id
-        LEFT JOIN mes_wm_product_issue ic ON pp.work_order_id = ic.work_order_id
-        LEFT JOIN mes_wm_product_issue_detail icd ON ic.id = icd.issue_id
+        LEFT JOIN mes_wm_item_consume ic ON pp.work_order_id = ic.work_order_id
+        LEFT JOIN mes_wm_item_consume_detail icd ON ic.id = icd.consume_id
         WHERE ppd.batch_code = #{batchCode}
         AND icd.batch_code IS NOT NULL
     </select>

+ 197 - 0
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java

@@ -0,0 +1,197 @@
+package cn.iocoder.yudao.module.mes.service.wm.batch;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
+import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
+import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
+import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
+import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link MesWmBatchServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+@Import(MesWmBatchServiceImpl.class)
+public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private MesWmBatchServiceImpl batchService;
+
+    @Resource
+    private MesWmBatchMapper batchMapper;
+
+    @MockitoBean
+    private MesMdItemService itemService;
+    @MockitoBean
+    private MesMdItemBatchConfigService itemBatchConfigService;
+    @MockitoBean
+    private MesMdAutoCodeRecordService autoCodeRecordService;
+    @MockitoBean
+    private MesWmBarcodeService barcodeService;
+
+    // ==================== 向前追溯 ====================
+
+    @Test
+    public void testGetForwardBatchList_nullCode() {
+        // 传入 null code,应返回空列表(不抛异常)
+        List<MesWmBatchDO> result = batchService.getForwardBatchList(null);
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void testGetForwardBatchList_noResults() {
+        // 传入一个不存在的批次号,XML 查询返回空(因为关联表无数据)
+        List<MesWmBatchDO> result = batchService.getForwardBatchList("NOT_EXIST_CODE");
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== 向后追溯 ====================
+
+    @Test
+    public void testGetBackwardBatchList_nullCode() {
+        // 传入 null code,应返回空列表(不抛异常)
+        List<MesWmBatchDO> result = batchService.getBackwardBatchList(null);
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void testGetBackwardBatchList_noResults() {
+        // 传入一个不存在的批次号,XML 查询返回空
+        List<MesWmBatchDO> result = batchService.getBackwardBatchList("NOT_EXIST_CODE");
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== 循环检测 ====================
+
+    @Test
+    public void testForwardBatchList_sameCodeDoesNotRecurse() {
+        // 验证:传入相同 code 多次调用不会 StackOverflow
+        // 由于数据库中没有环路数据,这里主要验证 visited set 的 null + 空数据安全
+        List<MesWmBatchDO> result = batchService.getForwardBatchList("SAME_CODE");
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+
+        // 二次调用同一个 code,不应有任何异常
+        List<MesWmBatchDO> result2 = batchService.getForwardBatchList("SAME_CODE");
+        assertNotNull(result2);
+        assertTrue(result2.isEmpty());
+    }
+
+    @Test
+    public void testBackwardBatchList_sameCodeDoesNotRecurse() {
+        // 验证向后追溯的 visited set 安全
+        List<MesWmBatchDO> result = batchService.getBackwardBatchList("SAME_CODE");
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== selectFirst ====================
+
+    @Test
+    public void testSelectFirst_noMatch() {
+        // 查询不存在的 itemId,应返回 null
+        MesWmBatchDO query = MesWmBatchDO.builder().itemId(randomLongId()).build();
+        MesWmBatchDO result = batchMapper.selectFirst(query);
+        assertNull(result);
+    }
+
+    @Test
+    public void testSelectFirst_returnsSmallestId() {
+        // 准备数据:插入两条相同条件的批次
+        Long itemId = randomLongId();
+        MesWmBatchDO batch1 = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_001").build();
+        MesWmBatchDO batch2 = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_002").build();
+        batchMapper.insert(batch1);
+        batchMapper.insert(batch2);
+
+        // 查询
+        MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
+        MesWmBatchDO result = batchMapper.selectFirst(query);
+
+        // 断言:返回 ID 最小的
+        assertNotNull(result);
+        assertEquals(batch1.getId(), result.getId());
+        assertEquals("BATCH_001", result.getCode());
+    }
+
+    @Test
+    public void testSelectFirst_nullFieldMatching() {
+        // 准备数据:一条有 vendorId,一条没有 vendorId
+        Long itemId = randomLongId();
+        Long vendorId = randomLongId();
+
+        MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_WITH_V").vendorId(vendorId).build();
+        MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_NO_V").build();
+        batchMapper.insert(batchWithVendor);
+        batchMapper.insert(batchWithoutVendor);
+
+        // 查询:vendorId 为 null -> 应该只匹配 vendorId IS NULL 的记录
+        MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
+        MesWmBatchDO result = batchMapper.selectFirst(query);
+
+        assertNotNull(result);
+        assertEquals("BATCH_NO_V", result.getCode());
+        assertNull(result.getVendorId());
+    }
+
+    @Test
+    public void testSelectFirst_withVendorId() {
+        // 准备数据
+        Long itemId = randomLongId();
+        Long vendorId = randomLongId();
+
+        MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_V1").vendorId(vendorId).build();
+        MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
+                .itemId(itemId).code("BATCH_NV").build();
+        batchMapper.insert(batchWithVendor);
+        batchMapper.insert(batchWithoutVendor);
+
+        // 查询:指定 vendorId -> 只匹配有 vendorId 的
+        MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).vendorId(vendorId).build();
+        MesWmBatchDO result = batchMapper.selectFirst(query);
+
+        assertNotNull(result);
+        assertEquals("BATCH_V1", result.getCode());
+        assertEquals(vendorId, result.getVendorId());
+    }
+
+    // ==================== selectByCode ====================
+
+    @Test
+    public void testSelectByCode() {
+        // 准备数据
+        MesWmBatchDO batch = MesWmBatchDO.builder()
+                .itemId(randomLongId()).code("TEST_CODE_001").build();
+        batchMapper.insert(batch);
+
+        // 查询
+        MesWmBatchDO result = batchMapper.selectByCode("TEST_CODE_001");
+        assertNotNull(result);
+        assertEquals(batch.getId(), result.getId());
+
+        // 查询不存在的
+        MesWmBatchDO notFound = batchMapper.selectByCode("NOT_EXIST");
+        assertNull(notFound);
+    }
+
+}

+ 1 - 0
yudao-module-mes/src/test/resources/sql/clean.sql

@@ -12,3 +12,4 @@ DELETE FROM "mes_wm_product_produce_detail";
 DELETE FROM "mes_wm_item_receipt";
 DELETE FROM "mes_wm_item_receipt_line";
 DELETE FROM "mes_wm_item_receipt_detail";
+DELETE FROM "mes_wm_batch";

+ 31 - 0
yudao-module-mes/src/test/resources/sql/create_tables.sql

@@ -586,3 +586,34 @@ CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_detail" (
     "tenant_id" bigint NOT NULL DEFAULT 0,
     PRIMARY KEY ("id")
 );
+
+-- ----------------------------
+-- MES 批次记录表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS "mes_wm_batch" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "code" varchar(128) DEFAULT NULL,
+    "item_id" bigint DEFAULT NULL,
+    "produce_date" timestamp DEFAULT NULL,
+    "expire_date" timestamp DEFAULT NULL,
+    "receipt_date" timestamp DEFAULT NULL,
+    "vendor_id" bigint DEFAULT NULL,
+    "client_id" bigint DEFAULT NULL,
+    "sales_order_code" varchar(64) DEFAULT NULL,
+    "purchase_order_code" varchar(64) DEFAULT NULL,
+    "work_order_id" bigint DEFAULT NULL,
+    "task_id" bigint DEFAULT NULL,
+    "workstation_id" bigint DEFAULT NULL,
+    "tool_id" bigint DEFAULT NULL,
+    "mold_id" bigint DEFAULT NULL,
+    "lot_number" varchar(128) DEFAULT NULL,
+    "quality_status" int DEFAULT NULL,
+    "remark" varchar(500) DEFAULT NULL,
+    "creator" varchar(64) DEFAULT '',
+    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar(64) DEFAULT '',
+    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    "tenant_id" bigint NOT NULL DEFAULT 0,
+    PRIMARY KEY ("id")
+);