From 22133fe6041d3509b90fe60f2e225598026f4c5a Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 4 Oct 2026 18:04:21 +0800
Subject: [PATCH] fix(firstleg): project completed loading lists from actual
picked cargo
---
.../mapper/LoadingListQueryMapper.java | 53 ++++-
.../LoadingListActualCargoTest.java | 183 ++++++++++++++++++
2 files changed, 231 insertions(+), 5 deletions(-)
create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListActualCargoTest.java
diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java
index 1ebb4d447..22a4f2fbc 100644
--- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java
+++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java
@@ -19,6 +19,35 @@ import java.util.List;
/** 基于共享装柜计划表提供清单查询投影和状态推进持久化操作。 */
@Mapper
public interface LoadingListQueryMapper {
+ // Completed projections use the same settled physical scope as LoadingCompletedEventMapper.
+ // Keep skipped plan rows for history and for the unfinished preload projection.
+ String ITEM_SCOPE = """
+ AND (p.plan_status != 'COMPLETED' OR EXISTS (
+ SELECT 1 FROM fl_loading_pick_item actual_pick
+ JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
+ WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=i.id
+ AND actual_pick.item_status IN ('PICKED','PARTIAL')
+ AND actual_pick.picked_package_count > 0
+ AND actual_pick.picked_package_count=i.package_count))
+ """;
+ String PALLET_ITEM_SCOPE = """
+ AND (p.plan_status != 'COMPLETED' OR EXISTS (
+ SELECT 1 FROM fl_loading_pick_item actual_pick
+ JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
+ WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=pallet_item.id
+ AND actual_pick.item_status IN ('PICKED','PARTIAL')
+ AND actual_pick.picked_package_count > 0
+ AND actual_pick.picked_package_count=pallet_item.package_count))
+ """;
+ String CUSTOMER_ACTUAL_SCOPE = """
+ AND EXISTS (
+ SELECT 1 FROM fl_loading_pick_item actual_pick
+ JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
+ WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=customer_item.id
+ AND actual_pick.item_status IN ('PICKED','PARTIAL')
+ AND actual_pick.picked_package_count > 0
+ AND actual_pick.picked_package_count=customer_item.package_count)
+ """;
String AGGREGATE_COLUMNS = """
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
p.created_by, p.created_by_name, p.created_at, p.version,
@@ -50,28 +79,36 @@ public interface LoadingListQueryMapper {
FROM fl_dropship_pallet_carton carton
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
- AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_package_count,
+ AND pallet_item.pallet_id=carton.pallet_id
+ """ + PALLET_ITEM_SCOPE + """
+ )), 0) AS total_package_count,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.gross_weight_kg ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_gross_weight_kg/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
- AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_gross_weight_kg,
+ AND pallet_item.pallet_id=carton.pallet_id
+ """ + PALLET_ITEM_SCOPE + """
+ )), 0) AS total_gross_weight_kg,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.net_weight_kg ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_net_weight_kg/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
- AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_net_weight_kg,
+ AND pallet_item.pallet_id=carton.pallet_id
+ """ + PALLET_ITEM_SCOPE + """
+ )), 0) AS total_net_weight_kg,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.volume_cbm ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_volume_cbm/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
- AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_volume_cbm,
+ AND pallet_item.pallet_id=carton.pallet_id
+ """ + PALLET_ITEM_SCOPE + """
+ )), 0) AS total_volume_cbm,
COALESCE(SUM(i.product_quantity), 0) AS total_quantity
""";
@@ -115,8 +152,11 @@ public interface LoadingListQueryMapper {
FROM fl_loading_plan p
JOIN fl_container_booking b ON b.id = p.container_booking_id
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
+ """ + ITEM_SCOPE + """
WHERE p.plan_status = 'COMPLETED'
- AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId})
+ AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId}
+ """ + CUSTOMER_ACTUAL_SCOPE + """
+ )
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
@@ -138,6 +178,7 @@ public interface LoadingListQueryMapper {
FROM fl_loading_plan p
JOIN fl_container_booking b ON b.id = p.container_booking_id
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
+ """ + ITEM_SCOPE + """
WHERE p.id = #{planId}
GROUP BY
""" + AGGREGATE_GROUP)
@@ -149,6 +190,7 @@ public interface LoadingListQueryMapper {
FROM fl_loading_plan p
JOIN fl_container_booking b ON b.id = p.container_booking_id
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
+ """ + ITEM_SCOPE + """
WHERE p.container_booking_id = #{bookingId}
GROUP BY
""" + AGGREGATE_GROUP)
@@ -224,6 +266,7 @@ public interface LoadingListQueryMapper {
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
LEFT JOIN fl_inbound_order inbound ON inbound.id=i.inbound_id AND inbound.tenant_id=i.account_id
WHERE i.loading_plan_id = #{planId} AND i.deleted = 0
+ """ + ITEM_SCOPE + """
ORDER BY i.load_sequence, i.id
""")
List selectItems(@Param("planId") long planId);
diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListActualCargoTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListActualCargoTest.java
new file mode 100644
index 000000000..f58575bb2
--- /dev/null
+++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListActualCargoTest.java
@@ -0,0 +1,183 @@
+package com.baiyue.firstleg.repository;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
+import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
+import com.baiyue.firstleg.entity.ContainerBookingEntity;
+import com.baiyue.firstleg.entity.InboundOrderEntity;
+import com.baiyue.firstleg.entity.LoadingPlanEntity;
+import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
+import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Executes the real SQL, including pagination COUNT and correlated physical-pallet totals. */
+class LoadingListActualCargoTest {
+ private JdbcTemplate jdbc;
+ private LoadingListQueryMapper mapper;
+
+ @BeforeEach
+ void setup() throws Exception {
+ var ds = new JdbcDataSource();
+ ds.setURL("jdbc:h2:mem:loaded-list-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
+ jdbc = new JdbcTemplate(ds);
+ table("fl_loading_plan", LoadingPlanEntity.class);
+ table("fl_loading_plan_item", LoadingPlanItemEntity.class);
+ table("fl_container_booking", ContainerBookingEntity.class);
+ table("fl_inbound_order", InboundOrderEntity.class);
+ jdbc.execute("CREATE TABLE fl_outbound_dispatch(container_booking_id BIGINT, confirmed_at TIMESTAMP, reversed_at TIMESTAMP, status VARCHAR)");
+ jdbc.execute("CREATE TABLE fl_preload_loading_plan_link(loading_plan_id BIGINT, preload_plan_id BIGINT)");
+ jdbc.execute("CREATE TABLE fl_preload_plan(id BIGINT, status VARCHAR)");
+ jdbc.execute("CREATE TABLE fl_ocean_booking_container_allocation(container_booking_id BIGINT, ocean_booking_id BIGINT)");
+ jdbc.execute("CREATE TABLE fl_ocean_booking(id BIGINT, status VARCHAR)");
+ jdbc.execute("CREATE TABLE fl_loading_pick_task(id BIGINT, loading_plan_id BIGINT, task_status VARCHAR)");
+ jdbc.execute("CREATE TABLE fl_loading_pick_item(loading_plan_item_id BIGINT, pick_task_id BIGINT, item_status VARCHAR, picked_package_count INT)");
+ jdbc.execute("CREATE TABLE fl_dropship_pallet_carton(id BIGINT, pallet_id BIGINT, inbound_id BIGINT, tenant_id BIGINT)");
+ jdbc.execute("CREATE TABLE fl_inbound_weight_result(tenant_id BIGINT, inbound_id BIGINT, deleted INT, recognition_status VARCHAR)");
+ jdbc.update("INSERT INTO fl_container_booking(id,warehouse_id,record_status) VALUES(10,1,'ACTIVE')");
+ jdbc.update("INSERT INTO fl_loading_plan(id,container_booking_id,plan_status,created_at) VALUES(1,10,'COMPLETED',CURRENT_TIMESTAMP)");
+ jdbc.update("INSERT INTO fl_loading_pick_task VALUES(100,1,'COMPLETED'),(200,2,'COMPLETED')");
+ var configuration = new MybatisConfiguration();
+ configuration.setMapUnderscoreToCamelCase(true);
+ configuration.addMapper(LoadingListQueryMapper.class);
+ var pagination = new MybatisPlusInterceptor();
+ pagination.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
+ var factory = new MybatisSqlSessionFactoryBean();
+ factory.setDataSource(ds);
+ factory.setConfiguration(configuration);
+ factory.setPlugins(pagination);
+ mapper = new SqlSessionTemplate(factory.getObject()).getMapper(LoadingListQueryMapper.class);
+ }
+
+ @Test
+ void completedDetailAndListExcludeSkippedAndUnsettledRowsFromAllMeasures() {
+ ordinaryCargo();
+ assertActualTotals(mapper.selectAggregate(1));
+ assertActualTotals(mapper.selectAggregateByBooking(10));
+ assertThat(mapper.selectItems(1)).extracting(LoadingListQueryMapper.ItemRow::getItemId)
+ .containsExactly(1L, 2L);
+ var page = mapper.selectConfirmedPage(new Page<>(1, 1), query(null));
+ assertThat(page.getTotal()).isEqualTo(1);
+ assertThat(page.getRecords()).singleElement().satisfies(this::assertActualTotals);
+ }
+
+ @Test
+ void customerWithOnlySkippedCargoCannotMatchCompletedListCountOrRows() {
+ ordinaryCargo();
+ var skipped = mapper.selectConfirmedPage(new Page<>(1, 1), query(22L));
+ assertThat(skipped.getTotal()).isZero();
+ assertThat(skipped.getRecords()).isEmpty();
+ var loaded = mapper.selectConfirmedPage(new Page<>(1, 1), query(11L));
+ assertThat(loaded.getTotal()).isEqualTo(1);
+ assertThat(loaded.getRecords()).singleElement().satisfies(this::assertActualTotals);
+ }
+
+ @Test
+ void emptyActualScopeDoesNotHideThePlanIdentityOrFallBackToPlannedTotals() {
+ item(1, 22, 1, null, "SKIPPED", 0, 100);
+ var row = mapper.selectAggregate(1);
+ assertThat(row.getPlanId()).isEqualTo(1);
+ assertThat(row.getPlanStatus()).isEqualTo("COMPLETED");
+ assertThat(row.getTotalPackageCount()).isZero();
+ assertThat(row.getTotalGrossWeightKg()).isEqualByComparingTo("0");
+ assertThat(mapper.selectItems(1)).isEmpty();
+ }
+
+ @Test
+ void unfinishedPreloadKeepsPlannedCargoIncludingPendingAndSkippedItems() {
+ ordinaryCargo();
+ jdbc.update("UPDATE fl_loading_plan SET plan_status='LOADING' WHERE id=1");
+ assertThat(mapper.selectItems(1)).hasSize(6);
+ assertThat(mapper.selectAggregate(1).getTotalPackageCount()).isEqualTo(8);
+ var page = mapper.selectPreloadPage(new Page<>(1, 1),
+ new PreloadQuery(null, null, 22L, null, null, null, 1, 1));
+ assertThat(page.getTotal()).isEqualTo(1);
+ assertThat(page.getRecords()).singleElement().satisfies(row ->
+ assertThat(row.getTotalPackageCount()).isEqualTo(8));
+ }
+
+ @Test
+ void physicalPalletTotalsIgnoreUnloadedPalletsAndDoNotCountSkuFragmentsTwice() {
+ item(1, 11, 2, 501L, "PICKED", 2, 100);
+ item(2, 11, 2, 501L, "PICKED", 2, 100);
+ item(3, 22, 1, 502L, "SKIPPED", 0, 100);
+ jdbc.update("INSERT INTO fl_inbound_order(id,tenant_id,total_package_count,actual_gross_weight_kg,actual_net_weight_kg,actual_volume_cbm) VALUES(700,11,2,20,16,4),(701,22,1,999,999,999)");
+ jdbc.update("INSERT INTO fl_dropship_pallet_carton VALUES(1,501,700,11),(2,501,700,11),(3,502,701,22)");
+ var actual = mapper.selectAggregate(1);
+ assertThat(actual.getTotalPackageCount()).isEqualTo(2);
+ assertThat(actual.getTotalGrossWeightKg()).isEqualByComparingTo("20");
+ assertThat(actual.getTotalNetWeightKg()).isEqualByComparingTo("16");
+ assertThat(actual.getTotalVolumeCbm()).isEqualByComparingTo("4");
+ assertThat(actual.getTotalQuantity()).isEqualTo(40);
+ assertThat(mapper.selectItems(1)).hasSize(2);
+ assertThat(mapper.selectConfirmedPage(new Page<>(1, 1), query(22L)).getTotal()).isZero();
+ jdbc.update("UPDATE fl_loading_plan SET plan_status='EDITING' WHERE id=1");
+ assertThat(mapper.selectAggregate(1).getTotalPackageCount()).isEqualTo(3);
+ assertThat(mapper.selectAggregate(1).getTotalGrossWeightKg()).isEqualByComparingTo("1019");
+ assertThat(mapper.selectItems(1)).hasSize(3);
+ }
+
+ private void ordinaryCargo() {
+ item(1, 11, 2, null, "PICKED", 2, 100);
+ item(2, 11, 1, null, "PARTIAL", 1, 100);
+ item(3, 22, 1, null, "SKIPPED", 0, 100);
+ item(4, 22, 1, null, "PENDING", 0, 100);
+ item(5, 22, 1, null, "PICKED", 1, 200); // wrong task belongs to a different plan
+ item(6, 22, 2, null, "PARTIAL", 1, 100); // not reconciled to the final fragment
+ item(7, 11, 9, null, "PICKED", 9, 100);
+ jdbc.update("UPDATE fl_loading_plan_item SET deleted=1 WHERE id=7");
+ }
+
+ private void item(long id, long customer, int packages, Long pallet, String status, int picked, long task) {
+ jdbc.update("INSERT INTO fl_loading_plan_item(id,loading_plan_id,account_id,sku_code,package_count,product_quantity,gross_weight_kg,net_weight_kg,volume_cbm,pallet_id,pallet_count,deleted,load_sequence) VALUES(?,1,?,?,?,?,?,?,?,?,1,0,?)",
+ id, customer, "SKU-" + id, packages, packages * 10, packages * 5, packages * 4, packages, pallet, id);
+ jdbc.update("INSERT INTO fl_loading_pick_item VALUES(?,?,?,?)", id, task, status, picked);
+ }
+
+ private ConfirmedQuery query(Long customer) {
+ return new ConfirmedQuery(null, customer, null, null, null, 1, 1);
+ }
+
+ private void assertActualTotals(LoadingListQueryMapper.AggregateRow row) {
+ assertThat(row.getTotalPackageCount()).isEqualTo(3);
+ assertThat(row.getTotalQuantity()).isEqualTo(30);
+ assertThat(row.getSkuCount()).isEqualTo(2);
+ assertThat(row.getTotalGrossWeightKg()).isEqualByComparingTo("15");
+ assertThat(row.getTotalNetWeightKg()).isEqualByComparingTo("12");
+ assertThat(row.getTotalVolumeCbm()).isEqualByComparingTo("3");
+ }
+
+ private void table(String name, Class> entity) {
+ var columns = new ArrayList();
+ for (var type = entity; type != Object.class; type = type.getSuperclass()) {
+ for (var field : type.getDeclaredFields()) {
+ var annotation = field.getAnnotation(TableField.class);
+ if (Modifier.isStatic(field.getModifiers()) || annotation != null && !annotation.exist()) continue;
+ String column = field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
+ Class> value = field.getType();
+ String sqlType = value == Long.class || value == long.class ? "BIGINT"
+ : value == Integer.class || value == int.class || value == Boolean.class || value == boolean.class ? "INT"
+ : value == BigDecimal.class ? "DECIMAL(20,6)"
+ : value == java.time.LocalDateTime.class ? "TIMESTAMP" : "VARCHAR(2000)";
+ columns.add(column + " " + sqlType);
+ }
+ }
+ jdbc.execute("CREATE TABLE " + name + " (" + String.join(",", columns) + ")");
+ }
+}