From 2e3853db186a1fc258ca77ad2caf61656a43d53a Mon Sep 17 00:00:00 2001
From: weicw <594098497@qq.com>
Date: Mon, 26 Oct 2026 00:56:16 +0800
Subject: [PATCH] =?UTF-8?q?:bug:=20=E8=BF=90=E8=BE=93=E7=B1=BB=E5=9E=8B?=
=?UTF-8?q?=E7=AD=9B=E9=80=89=E6=94=B9=E6=8C=89=E6=95=B4=E8=B6=9F=E8=BD=A6?=
=?UTF-8?q?=E5=88=A4=EF=BC=8C=E4=BF=AE=E6=AD=A3=E9=85=8D=E8=BD=BD=E4=B8=8E?=
=?UTF-8?q?=E5=A4=9A=E5=BC=8F=E8=81=94=E8=BF=90=E5=8F=8C=E9=87=8D=E8=AE=A1?=
=?UTF-8?q?=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
行级 SQL 筛会把混装挂总单与不挂总单的配载单劈成两档:同一趟车同时出现在配载运输与多式联运,两档各只剩命中的子单,卡片上的运单数、货物与重量缩水,三档数目之和大于全部。判据移到服务层 matchesOrgType,按整趟车的子单集合分档。
---
.../service/impl/TripQueryServiceImpl.java | 35 +++---
.../impl/TripQueryServiceImplTest.java | 102 ++++++++++++++----
2 files changed, 101 insertions(+), 36 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
index 892d0d00..fcff3fdc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
@@ -186,8 +186,8 @@ public class TripQueryServiceImpl implements ITripQueryService {
/**
* 全部可见运次:一次取回运单行后在服务层分组、分档、筛选并按排序键倒序
*
- * 分档用过程配置与接单态推导,于是 Tab 与卡片必然同档;行级筛选(组织类型 / 异常 / 创建时间)落在库层,
- * 命中的行同时决定这一趟车的子单集合与分档。
+ * 分档用过程配置与接单态推导,于是 Tab 与卡片必然同档;异常与创建时间按整趟车的子单集合判,
+ * 运输类型也在这里判——三者都不在库层筛行,否则一趟车会被劈成两半、三档数目加起来对不上全部。
*/
private List trips(TripQueryVO condition) {
Map> grouped = new LinkedHashMap<>();
@@ -203,7 +203,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
return;
}
List children = sortedChildren(rows);
- if (!inScope(children, condition)) {
+ if (!matchesOrgType(children, condition.getTransportOrgType()) || !inScope(children, condition)) {
return;
}
boolean requireAccept = TripAggregation.requiresAccept(children);
@@ -255,7 +255,6 @@ public class TripQueryServiceImpl implements ITripQueryService {
applyScope(wrapper, condition);
applyDeptScope(wrapper, condition);
applyProjectScope(wrapper, condition);
- applyOrgTypeFilter(wrapper, condition);
applyKeyword(wrapper, condition.getKeyword());
List rows = waybillService.list(wrapper);
applyLiveProcessConfig(rows);
@@ -288,21 +287,25 @@ public class TripQueryServiceImpl implements ITripQueryService {
}
/**
- * 运输类型按子单行筛,三态分区且不重叠:普通 = 两列都空,配载 = 挂配载单且未挂总单,
- * 多式联运 = 挂多联总单(不问有没有配载)。配载这一档必须排掉挂总单的行,
- * 否则叠加态的行会同时落进两档。
+ * 运输类型按整趟车判,三档互斥且覆盖全部运次:普通 = 不挂配载单也不挂总单,
+ * 配载 = 挂配载单且整趟车没挂总单,多式联运 = 任一子单挂总单(不问有没有配载)。
*
- * 与配载/普通两档不同,多式联运在同一趟车里不保证是同一个值:一张配载单可以混装来自不同总单的运单,
- * 这一档按行筛会把那趟车缩小成只命中总单的那几张子单。混装是少数,此处不为它另走整趟车判据。
+ * 不能按子单行筛:一张配载单可以混装挂总单与不挂总单的运单,行级筛会让同一趟车同时落进
+ * 配载与多式联运两档,而每档里只剩命中的那部分子单——卡片上的运单数、货物与重量都会缩水,
+ * 三个档的数目加起来也对不上「全部」。判据只能落在取回后的整趟车集合上。
*/
- private void applyOrgTypeFilter(LambdaQueryWrapper wrapper, TripQueryVO condition) {
- if (TRANSPORT_MULTIMODAL.equals(condition.getTransportOrgType())) {
- wrapper.apply("IFNULL(master_no, '') <> ''");
- } else if (TRANSPORT_LOAD.equals(condition.getTransportOrgType())) {
- wrapper.apply("IFNULL(loading_no, '') <> '' AND IFNULL(master_no, '') = ''");
- } else if (TRANSPORT_COMMON.equals(condition.getTransportOrgType())) {
- wrapper.apply("IFNULL(loading_no, '') = '' AND IFNULL(master_no, '') = ''");
+ private static boolean matchesOrgType(List children, String transportOrgType) {
+ if (Func.isBlank(transportOrgType)) {
+ return true;
}
+ boolean multimodal = children.stream().anyMatch(child -> Func.isNotBlank(child.getMasterNo()));
+ boolean loaded = children.stream().anyMatch(child -> Func.isNotBlank(child.getLoadingNo()));
+ return switch (transportOrgType) {
+ case TRANSPORT_MULTIMODAL -> multimodal;
+ case TRANSPORT_LOAD -> loaded && !multimodal;
+ case TRANSPORT_COMMON -> !loaded && !multimodal;
+ default -> true;
+ };
}
/**
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
index c7c47dcf..d717921a 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripQueryServiceImplTest.java
@@ -490,7 +490,7 @@ class TripQueryServiceImplTest {
}
@Test
- @DisplayName("管理端筛选:运输类型三档按子单行筛,异常与创建时间不进 SQL")
+ @DisplayName("管理端筛选:异常与创建时间不进 SQL,运输类型也不按子单行筛")
void adminFiltersStayAtTripGranularity() {
TripQueryVO query = TripQueryVO.forAdmin();
query.setTransportOrgType("common");
@@ -502,38 +502,52 @@ class TripQueryServiceImplTest {
service.page(query);
String sql = capturedRowSql();
- assertTrue(sql.contains("IFNULL(loading_no, '') = '' AND IFNULL(master_no, '') = ''"),
- "普通运输两列都空,挂总单的行不落这一档:" + sql);
assertFalse(sql.contains("id NOT IN ("), "异常要看整趟车,不能在库层缩小子单集合:" + sql);
assertFalse(sql.contains("create_time"), "创建时间按运次排序键在服务层判:" + sql);
assertFalse(sql.contains("blade_loading_manage"), "管理端不按司机收口:" + sql);
+ assertFalse(sql.contains("IFNULL(loading_no, '') = ''"), "运输类型按整趟车判,行级筛会把叠加的运次劈成两半:" + sql);
}
@Test
- @DisplayName("管理端筛选:配载档排掉挂总单的行,叠加的行只落多式联运档")
- void loadFilterExcludesMasterOrderRows() {
- TripQueryVO query = TripQueryVO.forAdmin();
- query.setTransportOrgType("load");
+ @DisplayName("运输类型三档是整趟车的分区:互不重叠,合起来正好是全部运次")
+ void orgTypeTabsPartitionTrips() {
+ stubFourTrips();
- service.page(query);
+ Set common = tripNos("common");
+ Set load = tripNos("load");
+ Set multimodal = tripNos("multimodal");
+ Set all = tripNos(null);
- String sql = capturedRowSql();
- assertTrue(sql.contains("IFNULL(loading_no, '') <> ''"), sql);
- assertTrue(sql.contains("IFNULL(master_no, '') = ''"),
- "同时挂配载与总单的行必须排掉,否则它会在两个档里各出现一次:" + sql);
+ assertEquals(Set.of("YD-PT"), common, "两列都空的整张运单才是普通运输");
+ assertEquals(Set.of("PZ-1"), load, "叠加配载与总单的那趟车只落多式联运档,不能两档都算");
+ assertEquals(Set.of("YD-DS", "PZ-2"), multimodal, "任一子单挂总单即含水陆联运之外的多式联运");
+ assertEquals(all, union(common, load, multimodal), "三档计数加起来必须等于全部,否则用户看到的数目对不上");
+ assertEquals(4, all.size());
}
@Test
- @DisplayName("管理端筛选:多式联运档只看挂总单,不问有没有配载")
- void multimodalFilterMatchesMasterOrderRows() {
- TripQueryVO query = TripQueryVO.forAdmin();
- query.setTransportOrgType("multimodal");
+ @DisplayName("按规定判:叠加的那趟车在多式联运档里保留整趟车的子单,不被子单行筛劈成一半")
+ void multimodalTabKeepsWholesaleTrip() {
+ stubFourTrips();
- service.page(query);
+ TripVO mixed = orgTypeTrips("multimodal").stream()
+ .filter(trip -> "PZ-2".equals(trip.getTripNo()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("叠加配载与总单的那趟车必须出现在多式联运档"));
- String sql = capturedRowSql();
- assertTrue(sql.contains("IFNULL(master_no, '') <> ''"), sql);
- assertFalse(sql.contains("loading_no"), "多式联运与配载可叠加,这一档不能按配载号排除:" + sql);
+ assertEquals(2, mixed.getWaybillCount(), "两柱子单都要数进去,否则卡片上的运单数与列表总数对不上");
+ assertEquals(Boolean.TRUE, mixed.getMultimodal());
+ assertEquals("load", mixed.getTransportOrgType(), "组织形态仍是配载,多式联运是叠加出来的第二个事实");
+ }
+
+ @Test
+ @DisplayName("运输类型没给或不认识时不筛:四趟车一辆不少")
+ void unknownOrgTypeKeepsEveryTrip() {
+ stubFourTrips();
+
+ assertEquals(4, orgTypeTrips(null).size());
+ assertEquals(4, orgTypeTrips("").size());
+ assertEquals(4, orgTypeTrips("whatever").size());
}
@Test
@@ -678,6 +692,47 @@ class TripQueryServiceImplTest {
});
}
+ /**
+ * 四趟车:普通(两列都空)、配载(只挂配载单)、未配载但挂总单、配载与总单叠加。
+ * 叠加那趟车混装一张挂总单的子单和一张不挂的,正是行级筛会劈开它的场景。
+ */
+ private void stubFourTrips() {
+ Waybill multimodalLone = waybill(3001L, "YD-DS", null, "running", null, at(3000L), ACCEPT_NO);
+ multimodalLone.setMasterNo("DL-20261001-0001");
+ Waybill mixedOnMaster = waybill(4001L, "YD-HH-1", "PZ-2", "running", null, at(4000L), ACCEPT_NO);
+ mixedOnMaster.setMasterNo("DL-20261001-0002");
+ stubRows(
+ waybill(1001L, "YD-PT", null, "running", null, at(1000L), ACCEPT_NO),
+ waybill(2001L, "YD-PZ-1", "PZ-1", "running", null, at(2000L), ACCEPT_NO),
+ waybill(2002L, "YD-PZ-2", "PZ-1", "running", null, at(2001L), ACCEPT_NO),
+ multimodalLone,
+ mixedOnMaster,
+ waybill(4002L, "YD-HH-2", "PZ-2", "running", null, at(4001L), ACCEPT_NO));
+ when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading("PZ-1", 2000L), loading("PZ-2", 4000L)));
+ }
+
+ private List orgTypeTrips(String orgType) {
+ TripQueryVO query = TripQueryVO.forDriver(PHONE);
+ query.setTransportOrgType(orgType);
+ query.setCurrent(1);
+ query.setSize(50);
+ return service.page(query).getRecords();
+ }
+
+ private Set tripNos(String orgType) {
+ return orgTypeTrips(orgType).stream().map(TripVO::getTripNo)
+ .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
+ }
+
+ @SafeVarargs
+ private static Set union(Set... parts) {
+ Set union = new java.util.LinkedHashSet<>();
+ for (Set part : parts) {
+ union.addAll(part);
+ }
+ return union;
+ }
+
private void stubOneLoadedTrip() {
stubRows(
waybill(2092186187239780353L, "YD-20260825-0006", LOADING_NO, "running", null, at(2000L), ACCEPT_YES),
@@ -706,6 +761,13 @@ class TripQueryServiceImplTest {
return query;
}
+ private LoadingManage loading(String loadingNo, Long loadingId) {
+ LoadingManage loading = loading(PHONE);
+ loading.setId(loadingId);
+ loading.setLoadingNo(loadingNo);
+ return loading;
+ }
+
private LoadingManage loading(String driverPhone) {
LoadingManage loading = new LoadingManage();
loading.setId(LOADING_ID);