✨ 运次与运单下发多式联运标志,运输类型筛选分三档

transportOrgType 仍是 common/load 二值(组织形态维),另加 Boolean multimodal 表示挂了多联总单;两者可叠加,文案由前端组合。visibleRows 补选 master_no,否则派生永远为空。配载档筛选排掉挂总单的行,多式联运档只看 master_no。
This commit is contained in:
2026-10-26 00:45:54 +08:00
parent c95dccc871
commit 05aaa5396b
9 changed files with 121 additions and 11 deletions
@@ -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;
@@ -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 位雪花号按数字下发会被前端取整)")
@@ -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;
@@ -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(), ""));
@@ -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));
@@ -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 {
}
/**
* 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号,
* 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。
* 运输类型按子单行筛,三态分区且不重叠:普通 = 两列都空,配载 = 挂配载单且未挂总单,
* 多式联运 = 挂多联总单(不问有没有配载)。配载这一档必须排掉挂总单的行,
* 否则叠加态的行会同时落进两档。
* <p>
* 与配载/普通两档不同,多式联运在同一趟车里不保证是同一个值:一张配载单可以混装来自不同总单的运单,
* 这一档按行筛会把那趟车缩小成只命中总单的那几张子单。混装是少数,此处不为它另走整趟车判据。
*/
private void applyOrgTypeFilter(LambdaQueryWrapper<Waybill> 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(), ""));
@@ -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() {
@@ -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() {
@@ -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() {