From 8d211acf5b58213c642605e6d4a3c30a33f5c493 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 26 Oct 2026 15:11:32 +0800 Subject: [PATCH] fix(lastleg): exclude unreceived pallets after inbound completion --- .../mapper/DispatchCandidateMapper.java | 6 ++- .../DispatchCandidateMapperContractTest.java | 50 +++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java index 23968c094..9a356e834 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java @@ -11,6 +11,8 @@ import java.util.List; public interface DispatchCandidateMapper { /** 自动预留托盘不是实收;尚未开始收货时仍以计划来源预排,不生成库存或实托装载事实。 */ String PLAN_ONLY = "(p.warehouse_pallet_id IS NULL OR (COALESCE(p.assignment_status,'')='RESERVED' AND COALESCE(p.actual_package_count,0)=0))"; + /** 完成入库或已确认短少后,不再把未实收的预留托盘当作在途货;真实库存仍可排车。 */ + String OPEN_PLAN = "COALESCE(ip.status,'') NOT IN ('COMPLETED','PARTIAL_INBOUND')"; /** 在途计划的全部真实来源必须已离港;人工节点覆盖后不再借用原柜节点。 */ String DEPARTED_PLAN = "EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation da JOIN ll_inbound_plan_line dl " + @@ -178,7 +180,7 @@ public interface DispatchCandidateMapper { "JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " + "JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " + "WHERE ha.pallet_id=p.id AND ho.detained=1) " + - "AND (NOT " + PLAN_ONLY + " OR (" + DEPARTED_PLAN + ")) " + + "AND (NOT " + PLAN_ONLY + " OR (" + OPEN_PLAN + " AND " + DEPARTED_PLAN + ")) " + "AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " + "AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " + @@ -283,7 +285,7 @@ public interface DispatchCandidateMapper { "JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " + "JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " + "WHERE ha.pallet_id=p.id AND ho.detained=1) " + - "AND (NOT " + PLAN_ONLY + " OR (" + DEPARTED_PLAN + ")) " + + "AND (NOT " + PLAN_ONLY + " OR (" + OPEN_PLAN + " AND " + DEPARTED_PLAN + ")) " + "AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " + "WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " + "AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " + diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java index 5b0cdbba1..5a8e83e91 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java @@ -16,12 +16,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; class DispatchCandidateMapperContractTest { - /** 执行正式计划/实收判定与离港门禁,覆盖自动预留、人工覆盖、混合来源及库存耗尽。 */ + /** 执行正式候选和提交资格,覆盖在途预排、收货结束后的空预留托盘及真实库存。 */ @Test void reservedPalletsCanBePlannedAfterDepartureWithoutInventingReceivedStock() throws Exception { try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-prearrival;MODE=MySQL")) { var db = connection.createStatement(); - db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR)"); + db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR,status VARCHAR DEFAULT 'PDA_PENDING')"); db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT,assignment_status VARCHAR,actual_package_count INT)"); db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)"); db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)"); @@ -29,8 +29,8 @@ class DispatchCandidateMapperContractTest { db.execute("CREATE TABLE ll_order_manual_node(order_id BIGINT,customer_id VARCHAR,node_code VARCHAR,node_at TIMESTAMP)"); db.execute("CREATE TABLE ll_container_shipping_gate(container_booking_id BIGINT,node_sequence INT,actual_at TIMESTAMP)"); db.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR,pallet_id BIGINT,available_packages INT)"); - for (int id = 1; id <= 9; id++) { - db.execute("INSERT INTO ll_inbound_plan VALUES (" + id + ",'A')"); + for (int id = 1; id <= 14; id++) { + db.execute("INSERT INTO ll_inbound_plan(id,customer_id) VALUES (" + id + ",'A')"); db.execute("INSERT INTO ll_inbound_plan_pallet VALUES (" + id + "," + id + "," + (100 + id) + ",'RESERVED',0)"); db.execute("INSERT INTO ll_inbound_plan_line VALUES (" + id + ",'A'," + id + ")"); db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (" + id + "," + id + ")"); @@ -44,24 +44,34 @@ class DispatchCandidateMapperContractTest { db.execute("INSERT INTO ll_order_manual_node VALUES (7,'A','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP),(9,'B','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)"); db.execute("INSERT INTO ll_inbound_plan_line VALUES (20,'A',2)"); db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (8,20)"); + // 同一计划原分两托,最终21箱全收在一托;另一预留托盘不能继续冒充可预排货物。 + db.execute("UPDATE ll_inbound_plan SET status='COMPLETED' WHERE id IN (10,13,14)"); + db.execute("UPDATE ll_inbound_plan_pallet SET plan_id=10,assignment_status='ASSIGNED',actual_package_count=21 WHERE id=11"); + db.execute("INSERT INTO ll_inventory_position VALUES ('A',111,21)"); + db.execute("UPDATE ll_inbound_plan SET status='PARTIAL_INBOUND' WHERE id=12"); + db.execute("UPDATE ll_inbound_plan_pallet SET warehouse_pallet_id=NULL WHERE id=13"); + db.execute("UPDATE ll_inbound_plan_pallet SET assignment_status='ASSIGNED' WHERE id=14"); String plan = DispatchCandidateMapper.PLAN_ONLY; - String eligible = "AND (NOT " + plan + " OR (" + DispatchCandidateMapper.DEPARTED_PLAN + ")) " - + "AND (" + plan + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " - + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0))"; - var ids = new ArrayList(); - try (var rows = db.executeQuery("SELECT p.id,CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS physical," - + "CASE WHEN " + plan + " THEN 'PLAN' ELSE 'RECEIVED' END AS stage " - + "FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE 1=1 " + eligible + " ORDER BY p.id")) { - while (rows.next()) { - long id = rows.getLong("id"); ids.add(id); - assertEquals(id == 3 ? "RECEIVED" : "PLAN", rows.getString("stage")); - assertEquals(id == 3 ? 103L : null, rows.getObject("physical", Long.class)); - } - } - assertEquals(List.of(1L, 3L, 7L), ids); - // 查询和写入选中来源共用同一判定;不允许仅在列表展示在途但提交时又要求实收。 + // 直接执行两条生产SQL中的资格片段,避免仅验证测试自行拼出的规则。 for (String sql : List.of(DispatchCandidateMapper.AVAILABLE_PLAN_BODY, DispatchCandidateMapper.SELECTED_PLAN_SQL)) { - assertTrue(sql.contains(eligible.replace("> 0", "> 0").replace("pos.customer_id=ip.customer_id", sql.contains("WHERE pos.customer_id=#{customerId}") ? "pos.customer_id=#{customerId}" : "pos.customer_id=ip.customer_id"))); + int start = sql.indexOf("AND (NOT " + plan + " OR ("); + int stockGate = sql.indexOf("AND (" + plan + " OR EXISTS", start); + int end = sql.indexOf("AND NOT EXISTS (SELECT 1 FROM ll_", stockGate); + String eligible = sql.substring(start, end).replace(">", ">") + .replace("#{customerId}", "'A'"); + var ids = new ArrayList(); + try (var rows = db.executeQuery("SELECT p.id,p.actual_package_count,CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS physical," + + "CASE WHEN " + plan + " THEN 'PLAN' ELSE 'RECEIVED' END AS stage " + + "FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE 1=1 " + eligible + " ORDER BY p.id")) { + while (rows.next()) { + long id = rows.getLong("id"); ids.add(id); + assertEquals(id == 3 || id == 11 ? "RECEIVED" : "PLAN", rows.getString("stage")); + assertEquals(id == 3 || id == 11 ? 100L + id : null, rows.getObject("physical", Long.class)); + if (id == 11) assertEquals(21, rows.getInt("actual_package_count")); + } + } + // 完成入库或已确认短少后,空预留托盘不可排车,真实实收托盘继续按库存校验。 + assertEquals(List.of(1L, 3L, 7L, 11L), ids); assertTrue(sql.contains("CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS warehousePalletId")); } } -- 2.54.0