From c95dccc8712059e447132aa7db8d0c5674c4307c Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Sun, 25 Oct 2026 21:57:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20fix(=E6=89=93=E5=8D=A1?= =?UTF-8?q?=E9=87=8C=E7=A8=8B):=20=E5=9B=9E=E6=98=BE=E9=87=8C=E7=A8=8B?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E4=B8=A4=E4=BD=8D=E5=B0=8F=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运单里程列是 decimal(18,2),BigDecimal 经全局 ToStringSerializer 序列化后 下发成 "200.00",而客户端按正整数校验,原样回填直接提交会被拒。 - DriverPunchNodeVO / WaybillPunchRecordItemVO 的 mileage 改 String(正整数文本) - 新增 TransportBusinessSupport.mileageText,去尾随零后再下发 - 打卡记录单测断言同步为字符串 --- .../springblade/transport/pojo/vo/DriverPunchNodeVO.java | 8 +++++--- .../transport/pojo/vo/WaybillPunchRecordItemVO.java | 8 +++++--- .../transport/service/impl/DriverWaybillServiceImpl.java | 2 +- .../transport/service/impl/WaybillServiceImpl.java | 2 +- .../transport/support/TransportBusinessSupport.java | 9 +++++++++ .../service/impl/WaybillPunchRecordListTest.java | 2 +- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java index 7533ce2e..5674f7e7 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java @@ -11,7 +11,6 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -103,8 +102,11 @@ public class DriverPunchNodeVO implements Serializable { @Schema(description = "本节点是否需要采集里程(末位可打卡节点且计费规则含里程)") private Boolean needMileage; - @Schema(description = "运单里程(公里),needMileage 为真时用于回显") - private BigDecimal mileage; + /** + * 里程只有正整数口径(ADR-0023 决策 6),下发的是去尾随零的文本,如 "200" 而非 "200.00"。 + */ + @Schema(description = "运单里程(公里,正整数文本),needMileage 为真时用于回显") + private String mileage; @Schema(description = "已上传凭证图(已打卡回显)") private List photos = new ArrayList<>(); diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java index 62f089ba..4d003d5f 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java @@ -9,7 +9,6 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -62,8 +61,11 @@ public class WaybillPunchRecordItemVO implements Serializable { @Schema(description = "本条是否需要采集里程(末位打卡节点且计费规则含里程)") private Boolean needMileage; - @Schema(description = "运单里程(公里),needMileage 为真时用于回显") - private BigDecimal mileage; + /** + * 里程只有正整数口径(ADR-0023 决策 6),下发的是去尾随零的文本,如 "200" 而非 "200.00"。 + */ + @Schema(description = "运单里程(公里,正整数文本),needMileage 为真时用于回显") + private String mileage; @Schema(description = "里程是否已冻结(运单已生成结算单,只能看不能改)") private Boolean mileageLocked; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 25b6bd4a..22bab55a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -1649,7 +1649,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { // 里程同样只在末位卡采集:末位判定与上面同一个变量,避免两处各算一次走偏。 if (lastPunchNode && lastNodeNeedsMileage) { vo.setNeedMileage(true); - vo.setMileage(waybill.getMileage()); + vo.setMileage(TransportBusinessSupport.mileageText(waybill.getMileage())); } if (done) { if (punched.getPunchTime() != null) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 45a9ffa8..98aff41c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -442,7 +442,7 @@ public class WaybillServiceImpl extends BaseServiceImpl item.setType(isTransit ? "enroute" : "node"); if (lastNodeNeedsMileage && !isTransit && lastPunchKey.equals(nodeCode)) { item.setNeedMileage(true); - item.setMileage(waybill.getMileage()); + item.setMileage(TransportBusinessSupport.mileageText(waybill.getMileage())); item.setMileageLocked(punchMileageLocked); } item.setExceptionFlag(false); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java index 5f1e8efd..1a17390d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/TransportBusinessSupport.java @@ -282,4 +282,13 @@ public final class TransportBusinessSupport { return mileage; } + /** + * 里程回显文本:去掉列上 decimal(18,2) 带来的尾随零,使 "200.00" 下发为 "200"。 + *

+ * 客户端按正整数校验里程,回显成两位小数会被当成非法值拒掉——不清零就等于「原样回填不能提交」。 + */ + public static String mileageText(BigDecimal mileage) { + return mileage == null ? null : mileage.stripTrailingZeros().toPlainString(); + } + } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillPunchRecordListTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillPunchRecordListTest.java index d9e7e76b..baef2cae 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillPunchRecordListTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillPunchRecordListTest.java @@ -211,7 +211,7 @@ class WaybillPunchRecordListTest { // 接单不参与司机打卡序列,末位是装货 assertFalse(Boolean.TRUE.equals(accept.getNeedMileage())); assertTrue(Boolean.TRUE.equals(load.getNeedMileage())); - assertEquals(0, new BigDecimal("128").compareTo(load.getMileage())); + assertEquals("128", load.getMileage()); assertFalse(Boolean.TRUE.equals(load.getMileageLocked())); } From 05aaa5396b15218a6ee3fed2eb81da4ccc2be99c Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 00:45:54 +0800 Subject: [PATCH 2/3] =?UTF-8?q?:sparkles:=20=E8=BF=90=E6=AC=A1=E4=B8=8E?= =?UTF-8?q?=E8=BF=90=E5=8D=95=E4=B8=8B=E5=8F=91=E5=A4=9A=E5=BC=8F=E8=81=94?= =?UTF-8?q?=E8=BF=90=E6=A0=87=E5=BF=97=EF=BC=8C=E8=BF=90=E8=BE=93=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=AD=9B=E9=80=89=E5=88=86=E4=B8=89=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transportOrgType 仍是 common/load 二值(组织形态维),另加 Boolean multimodal 表示挂了多联总单;两者可叠加,文案由前端组合。visibleRows 补选 master_no,否则派生永远为空。配载档筛选排掉挂总单的行,多式联运档只看 master_no。 --- .../pojo/vo/AdminWaybillDetailVO.java | 5 +- .../pojo/vo/DriverWaybillCardVO.java | 5 +- .../springblade/transport/pojo/vo/TripVO.java | 5 +- .../impl/DriverWaybillServiceImpl.java | 2 + .../impl/ManageWaybillServiceImpl.java | 4 ++ .../service/impl/TripQueryServiceImpl.java | 24 ++++++-- .../service/impl/DriverTripCardTest.java | 13 ++++ .../impl/ManageTripListDetailTest.java | 13 ++++ .../impl/TripQueryServiceImplTest.java | 61 ++++++++++++++++++- 9 files changed, 121 insertions(+), 11 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java index e874df68..0a95b2a1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java @@ -90,9 +90,12 @@ public class AdminWaybillDetailVO implements Serializable { @Schema(description = "运输方式字典值:road 等") private String transportMode; - @Schema(description = "运输组织:common普通 / load配载") + @Schema(description = "运输类型:common普通运输 / load配载运输") private String transportOrgType; + @Schema(description = "是否挂多联总单:为真表示这趟含多式联运,与配载运输可叠加(不与 transportOrgType 互斥)") + private Boolean multimodal; + @Schema(description = "计划发货时间") private String planShipTime; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java index a4be3625..2b4c2914 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -156,9 +156,12 @@ public class DriverWaybillCardVO implements Serializable { @Schema(description = "运输方式文案:公路运输等") private String transportType; - @Schema(description = "运输组织:common普通 / load配载") + @Schema(description = "运输类型:common普通运输 / load配载运输") private String transportOrgType; + @Schema(description = "是否挂多联总单:为真表示这趟含多式联运,与配载运输可叠加(不与 transportOrgType 互斥)") + private Boolean multimodal; + /* ===== 运次(一趟车)字段:运次接口返回,旧运单接口不填 ===== */ @Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripVO.java index a6523dfb..304addeb 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripVO.java @@ -54,9 +54,12 @@ public class TripVO implements Serializable { @Schema(description = "配载单号,未配载为空") private String loadingNo; - @Schema(description = "运输组织类型:load 公路配载 / common 普通运输") + @Schema(description = "运输类型:load 配载运输 / common 普通运输") private String transportOrgType; + @Schema(description = "是否挂多联总单:为真表示这趟含多式联运,与配载运输可叠加(不与 transportOrgType 互斥)") + private Boolean multimodal; + @Schema(description = "状态:0待接单/1运输中/2已完成/3已取消,子单集合为空时为空") private Integer status; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 22bab55a..afd82e56 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -1055,6 +1055,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setCargos(withDetail ? cargoLines(cargos) : null); card.setTransportType(toTransportTypeLabel(waybill.getTransportType())); card.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON); + card.setMultimodal(Func.isNotBlank(waybill.getMasterNo())); card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime())); card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime())); card.setRemark(Func.toStr(waybill.getRemark(), "")); @@ -1141,6 +1142,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setRequireAccept(trip.getRequireAccept()); card.setAcceptStatus(trip.getAcceptStatus()); card.setTransportOrgType(trip.getTransportOrgType()); + card.setMultimodal(trip.getMultimodal()); card.setTransportType(toTransportTypeLabel(trip.getTransportType())); card.setDriverName(Func.toStr(trip.getDriverName(), "")); card.setDriverPhone(Func.toStr(trip.getDriverPhone(), "")); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java index 8b82d417..3707fc75 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java @@ -445,6 +445,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setTransportMode(Func.toStr(trip.getTransportType(), "")); detail.setTransportType(toTransportTypeLabel(Func.toStr(trip.getTransportType(), ""))); detail.setTransportOrgType(Func.isEmpty(trip.getLoadingNo()) ? TRANSPORT_COMMON : TRANSPORT_LOAD); + detail.setMultimodal(Boolean.TRUE.equals(trip.getMultimodal())); detail.setTripId(trip.getId()); detail.setLoadingNo(Func.toStr(trip.getLoadingNo(), "")); detail.setTripNo(Func.toStr(trip.getTripNo(), "")); @@ -504,6 +505,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setLoadingNo(loading.getLoadingNo()); detail.setTripNo(loading.getLoadingNo()); detail.setTransportOrgType(TRANSPORT_LOAD); + detail.setMultimodal(children.stream().anyMatch(child -> Func.isNotBlank(child.getMasterNo()))); detail.setViaName(TripStationAssembler.transitName(stations)); detail.setStationCount(stations.size()); detail.setStations(stations); @@ -538,6 +540,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setTransportMode(Func.toStr(loading.getTransportType(), "")); detail.setTransportType(toTransportTypeLabel(Func.toStr(loading.getTransportType(), ""))); detail.setTransportOrgType(TRANSPORT_LOAD); + detail.setMultimodal(children.stream().anyMatch(child -> Func.isNotBlank(child.getMasterNo()))); detail.setTripId(String.valueOf(loading.getId())); detail.setLoadingNo(loading.getLoadingNo()); detail.setTripNo(loading.getLoadingNo()); @@ -777,6 +780,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setTransportMode(mode); detail.setTransportType(toTransportTypeLabel(mode)); detail.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON); + detail.setMultimodal(Func.isNotBlank(waybill.getMasterNo())); detail.setFromName(formatPlaceName(waybill.getDepartureName(), mode)); detail.setToName(formatPlaceName(waybill.getArrivalName(), mode)); 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 56a2a5d3..892d0d00 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 @@ -79,9 +79,10 @@ public class TripQueryServiceImpl implements ITripQueryService { private static final int DEFAULT_PAGE_SIZE = 10; private static final int MAX_PAGE_SIZE = 50; - /** 运输组织类型:有配载单为公路配载,否则普通运输 */ + /** 运输类型:有配载单为配载运输,挂多联总单为多式联运,两者都没有为普通运输 */ private static final String TRANSPORT_LOAD = "load"; private static final String TRANSPORT_COMMON = "common"; + private static final String TRANSPORT_MULTIMODAL = "multimodal"; /** 未配载运次的键前缀:与 TripAggregation.SQL_TRIP_KEY 的 W 前缀同一个口径 */ private static final String LONE_KEY_PREFIX = "W"; @@ -244,6 +245,8 @@ public class TripQueryServiceImpl implements ITripQueryService { Waybill::getDepartureName, Waybill::getDepartureAddress, Waybill::getArrivalName, Waybill::getArrivalAddress, Waybill::getDriverName, Waybill::getDriverPhone, Waybill::getVehicleNo, Waybill::getCustomerName, Waybill::getTransportType, Waybill::getTaskRemark, + // 运输类型的三态靠它派生,行级筛选也按它判,必须随行取回 + Waybill::getMasterNo, // 未配载运次没有配载单可依,计划时间要取运单自己的,故必须随行取回 Waybill::getEstimatedStartTime, Waybill::getEstimatedEndTime, Waybill::getCreateTime) .in(Waybill::getBusinessStatus, TripAggregation.CHILD_STATUSES) @@ -285,14 +288,20 @@ public class TripQueryServiceImpl implements ITripQueryService { } /** - * 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号, - * 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。 + * 运输类型按子单行筛,三态分区且不重叠:普通 = 两列都空,配载 = 挂配载单且未挂总单, + * 多式联运 = 挂多联总单(不问有没有配载)。配载这一档必须排掉挂总单的行, + * 否则叠加态的行会同时落进两档。 + *

+ * 与配载/普通两档不同,多式联运在同一趟车里不保证是同一个值:一张配载单可以混装来自不同总单的运单, + * 这一档按行筛会把那趟车缩小成只命中总单的那几张子单。混装是少数,此处不为它另走整趟车判据。 */ private void applyOrgTypeFilter(LambdaQueryWrapper wrapper, TripQueryVO condition) { - if (TRANSPORT_LOAD.equals(condition.getTransportOrgType())) { - wrapper.apply("IFNULL(loading_no, '') <> ''"); + 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, '') = ''"); + wrapper.apply("IFNULL(loading_no, '') = '' AND IFNULL(master_no, '') = ''"); } } @@ -530,6 +539,8 @@ public class TripQueryServiceImpl implements ITripQueryService { trip.setLoadingNo(loading.getLoadingNo()); trip.setTripNo(loading.getLoadingNo()); trip.setTransportOrgType(TRANSPORT_LOAD); + // 多式联运与配载可叠加:任一子单挂总单即算这趟车含多式联运 + trip.setMultimodal(children.stream().anyMatch(child -> Func.isNotBlank(child.getMasterNo()))); trip.setDriverName(Func.toStr(loading.getDriverName(), "")); trip.setDriverPhone(Func.toStr(loading.getDriverPhone(), "")); trip.setVehicleNo(Func.toStr(loading.getVehicleNo(), "")); @@ -562,6 +573,7 @@ public class TripQueryServiceImpl implements ITripQueryService { trip.setId(tripIdOf(waybill.getId())); trip.setTripNo(waybill.getWaybillNo()); trip.setTransportOrgType(TRANSPORT_COMMON); + trip.setMultimodal(Func.isNotBlank(waybill.getMasterNo())); trip.setDriverName(Func.toStr(waybill.getDriverName(), "")); trip.setDriverPhone(Func.toStr(waybill.getDriverPhone(), "")); trip.setVehicleNo(Func.toStr(waybill.getVehicleNo(), "")); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java index c34e0441..03a0eb47 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java @@ -149,6 +149,19 @@ class DriverTripCardTest { assertEquals("桂A32523", card.getVehicleNo()); } + @Test + @DisplayName("运次卡片带多式联运:与配载两个事实并存,不被合并成一个单选") + void tripCardCarriesMultimodalFlag() { + TripVO trip = loadingTrip(); + trip.setMultimodal(true); + stubTripPage(1, trip); + + DriverWaybillCardVO card = runAsDriver(() -> service.tripPage(1, 10, null, null)).getRecords().get(0); + + assertEquals("load", card.getTransportOrgType()); + assertEquals(Boolean.TRUE, card.getMultimodal()); + } + @Test @DisplayName("运次卡片带唯一原始业务状态:子单都在待回单确认时下发 pending_return_confirm,数字态仍是 1") void tripCardCarriesUniqueChildBusinessStatus() { diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java index 8f3f26d6..f815220a 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java @@ -216,6 +216,19 @@ class ManageTripListDetailTest { assertEquals("2131150784070275011", second.getCargos().get(1).getWaybillId()); } + @Test + @DisplayName("详情带多式联运:运次上派生的标志原样下发,与配载并存") + void detailCarriesMultimodalFlag() { + TripVO trip = trip(); + trip.setMultimodal(true); + when(tripQueryService.detail(any())).thenReturn(trip); + + AdminWaybillDetailVO detail = service.detail(String.valueOf(LOADING_ID)); + + assertEquals("load", detail.getTransportOrgType()); + assertEquals(Boolean.TRUE, detail.getMultimodal(), "运输类型两个事实都下发,文案由前端组合"); + } + @Test @DisplayName("取不到运次时按运单主键与配载单主键回落:整单被拒的配载单仍能打开并重新派单") void detailFallsBackWhenTripIsNotReachable() { 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 c2b11f0c..c7c47dcf 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 @@ -249,6 +249,35 @@ class TripQueryServiceImplTest { verify(waybillService, times(1)).list(anyWaybillWrapper()); } + @Test + @DisplayName("未配载但挂总单:含多式联运,组织类型仍是普通运输") + void unloadedWaybillOnMasterOrderIsMultimodal() { + Waybill row = waybill(WAYBILL_ID, "YD-20260920-0003", null, "running", null, at(1000L), ACCEPT_NO); + row.setMasterNo("DL-20261001-0001"); + stubRows(row); + + TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0); + + assertEquals(Boolean.TRUE, trip.getMultimodal(), "挂总单即含多式联运"); + assertEquals("common", trip.getTransportOrgType(), "运输类型是组织形态维,与多式联运各算一个值"); + } + + @Test + @DisplayName("配载运次混装总单运单:只要有一张挂总单,整趟车就含多式联运,且配载事实仍在") + void loadingTripIsMultimodalWhenAnyChildIsOnMasterOrder() { + Waybill onMasterOrder = waybill(2092186187239780353L, "YD-1", LOADING_NO, "running", null, at(2000L), ACCEPT_NO); + onMasterOrder.setMasterNo("DL-20261001-0001"); + stubRows( + onMasterOrder, + waybill(2092186187470467073L, "YD-2", LOADING_NO, "running", null, at(1000L), ACCEPT_NO)); + when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading(PHONE))); + + TripVO trip = service.page(TripQueryVO.forDriver(PHONE)).getRecords().get(0); + + assertEquals("load", trip.getTransportOrgType()); + assertEquals(Boolean.TRUE, trip.getMultimodal(), "两个事实叠加,不做互斥单选"); + } + @Test @DisplayName("未配载运次的状态与接单需求来自段一取回的那张运单") void unloadedTripStateComesFromFetchedWaybill() { @@ -461,7 +490,7 @@ class TripQueryServiceImplTest { } @Test - @DisplayName("管理端筛选:组织类型按子单行筛(同一趟车里是同一个值),异常与创建时间不进 SQL") + @DisplayName("管理端筛选:运输类型三档按子单行筛,异常与创建时间不进 SQL") void adminFiltersStayAtTripGranularity() { TripQueryVO query = TripQueryVO.forAdmin(); query.setTransportOrgType("common"); @@ -473,12 +502,40 @@ class TripQueryServiceImplTest { service.page(query); String sql = capturedRowSql(); - assertTrue(sql.contains("IFNULL(loading_no, '') = ''"), "普通运输在库层按子单行筛:" + sql); + 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); } + @Test + @DisplayName("管理端筛选:配载档排掉挂总单的行,叠加的行只落多式联运档") + void loadFilterExcludesMasterOrderRows() { + TripQueryVO query = TripQueryVO.forAdmin(); + query.setTransportOrgType("load"); + + service.page(query); + + String sql = capturedRowSql(); + assertTrue(sql.contains("IFNULL(loading_no, '') <> ''"), sql); + assertTrue(sql.contains("IFNULL(master_no, '') = ''"), + "同时挂配载与总单的行必须排掉,否则它会在两个档里各出现一次:" + sql); + } + + @Test + @DisplayName("管理端筛选:多式联运档只看挂总单,不问有没有配载") + void multimodalFilterMatchesMasterOrderRows() { + TripQueryVO query = TripQueryVO.forAdmin(); + query.setTransportOrgType("multimodal"); + + service.page(query); + + String sql = capturedRowSql(); + assertTrue(sql.contains("IFNULL(master_no, '') <> ''"), sql); + assertFalse(sql.contains("loading_no"), "多式联运与配载可叠加,这一档不能按配载号排除:" + sql); + } + @Test @DisplayName("创建时间按子单 create_time 的最大值(卡片上显示的就是它)判区间") void createdRangeUsesTripSortKey() { 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 3/3] =?UTF-8?q?:bug:=20=E8=BF=90=E8=BE=93=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=AD=9B=E9=80=89=E6=94=B9=E6=8C=89=E6=95=B4=E8=B6=9F?= =?UTF-8?q?=E8=BD=A6=E5=88=A4=EF=BC=8C=E4=BF=AE=E6=AD=A3=E9=85=8D=E8=BD=BD?= =?UTF-8?q?=E4=B8=8E=E5=A4=9A=E5=BC=8F=E8=81=94=E8=BF=90=E5=8F=8C=E9=87=8D?= =?UTF-8?q?=E8=AE=A1=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);