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 b799765c..ff601f14 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 @@ -66,9 +66,12 @@ public class AdminWaybillDetailVO implements Serializable { @Schema(description = "卸货地址") private String unloadAddress; - @Schema(description = "货物名称") + @Schema(description = "货物名称:运单上录入的全部货名以「、」连接") private String cargoName; + @Schema(description = "货物名称清单:运单上录入的全部货物,同名的只留一次") + private List cargoNames; + @Schema(description = "货物数量(带单位)") private String cargoQuantity; 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 9e52e08d..e3c388a2 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 @@ -93,12 +93,18 @@ public class TripVO implements Serializable { @Schema(description = "运输类型") private String transportType; - @Schema(description = "货物名称") + @Schema(description = "货物名称:整趟车全部货名以「、」连接,一格多名的历史写法已摊平") private String cargoName; - @Schema(description = "货物类型") + @Schema(description = "货物名称清单:整趟车全部货物,同名的只留一次,保持子单与录入顺序") + private List cargoNames; + + @Schema(description = "货物类型:整趟车全部类型以「、」连接") private String cargoType; + @Schema(description = "货物类型清单:整趟车全部货物类型,同类型的只留一次") + private List cargoTypes; + @Schema(description = "发货地址") private String departureAddress; @@ -183,12 +189,18 @@ public class TripVO implements Serializable { @Schema(description = "项目名称") private String projectName; - @Schema(description = "货物名称") + @Schema(description = "货物名称:运单上录入的全部货名以「、」连接,一格多名的历史写法已摊平") private String cargoName; - @Schema(description = "货物类型") + @Schema(description = "货物名称清单:运单上录入的全部货物,同名的只留一次") + private List cargoNames; + + @Schema(description = "货物类型:主表首行的类型,完整清单见 cargoTypes") private String cargoType; + @Schema(description = "货物类型清单:运单上录入的全部货物类型,同类型的只留一次") + private List cargoTypes; + @Schema(description = "数量") private BigDecimal quantity; 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 350c0e92..59e02bb0 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 @@ -46,13 +46,14 @@ import org.springblade.transport.pojo.dto.PunchPhotoDTO; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.entity.TransportVehicle; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillEnroutePunch; +import org.springblade.transport.pojo.entity.WaybillNodePunch; import org.springblade.transport.service.ITransportVehicleService; import org.springblade.transport.support.CertificationGate; import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.VehiclePlateSupport; -import org.springblade.transport.pojo.entity.Waybill; -import org.springblade.transport.pojo.entity.WaybillEnroutePunch; -import org.springblade.transport.pojo.entity.WaybillNodePunch; +import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO; import org.springblade.transport.pojo.vo.DriverNodePunchVO; import org.springblade.transport.pojo.vo.DriverPunchNodeVO; @@ -973,9 +974,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setToName(Func.toStrWithEmpty(waybill.getArrivalName(), Func.toStr(waybill.getArrivalAddress(), ""))); card.setFromAddress(Func.toStrWithEmpty(waybill.getDepartureAddress(), card.getFromName())); card.setToAddress(Func.toStrWithEmpty(waybill.getArrivalAddress(), card.getToName())); - card.setCargoNames(splitCargoNames(waybill.getCargoName())); - card.setCargoCategory(Func.toStr(waybill.getCargoType(), "")); - card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); + // 货名取运单上录入的全部货物:主表 cargo_name 只落了第一行,直接拿它会漏掉这张票的其余货物 + List cargos = WaybillCargoSupport.of(waybill); + List cargoNames = WaybillCargoSupport.cargoNames(cargos); + card.setCargoNames(cargoNames); + card.setCargoCategory(cargos.stream() + .map(WaybillCargoSupport.Cargo::cargoType) + .filter(Func::isNotEmpty) + .distinct() + .collect(Collectors.joining("、"))); + card.setWeight(formatCargoWeight(cargos, formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()))); card.setStatus(toAppStatus(waybill.getBusinessStatus())); String createTime = formatDateTime(waybill.getCreateTime()); card.setCreateTime(createTime); @@ -992,7 +1000,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setRejectReason(waybill.getDriverRejectReason()); // 详情页字段(列表也可带上,体积很小) - String cargoName = Func.toStr(waybill.getCargoName(), ""); + String cargoName = String.join("、", cargoNames); String weightText = card.getWeight(); card.setCargoName(cargoName); card.setPickupAddress(card.getFromAddress()); @@ -1106,21 +1114,31 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setStationCount(trip.getStationCount()); card.setPickupAddress(card.getFromAddress()); card.setUnloadAddress(card.getToAddress()); - // 货物名可能是一格里的多个名字,按既有拆分口径摊平后再去重 - List cargoNames = children.stream() - .flatMap(child -> splitCargoNames(child.getCargoName()).stream()) - .distinct() - .collect(Collectors.toList()); + // 货名取运次上算好的整趟车清单:它按子单录入的货物明细摊平去重,主表 cargo_name 只是首行。 + // 运次清单为空时按子单卡片自己的货名兜底,不让卡片上的货物行整个消失 + List cargoNames = trip.getCargoNames() == null ? List.of() : trip.getCargoNames(); + if (cargoNames.isEmpty()) { + cargoNames = children.stream() + .flatMap(child -> splitCargoNames(child.getCargoName()).stream()) + .distinct() + .collect(Collectors.toList()); + } card.setCargoNames(cargoNames); card.setCargoName(String.join("、", cargoNames)); - card.setCargoCategory(children.stream() - .map(TripVO.TripWaybillVO::getCargoType) - .filter(Func::isNotEmpty) - .distinct() - .collect(Collectors.joining("、"))); + List cargoTypes = trip.getCargoTypes() == null ? List.of() : trip.getCargoTypes(); + if (cargoTypes.isEmpty()) { + cargoTypes = children.stream() + .map(TripVO.TripWaybillVO::getCargoType) + .filter(Func::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + } + card.setCargoCategory(String.join("、", cargoTypes)); card.setCargoSummary(Func.toStr(trip.getCargoSummary(), "")); + // 合计重量只在全部货物同单位时才有值;单位不一致时留空,卡片上已经由货物汇总逐条列出, + // 拿汇总串冒充重量会让「货名 + 重量」两栏把同一段文字打印两遍 String weight = trip.getTotalQuantity() == null - ? card.getCargoSummary() + ? "" : formatWeight(trip.getTotalQuantity(), trip.getQuantityUnit()); card.setWeight(weight); card.setCargoQuantity(weight); @@ -1895,16 +1913,40 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { }; } + /** + * 一格多名的历史写法(顿号、逗号分隔)摊平成货名清单,与货物明细同一个拆分口径 + */ private List splitCargoNames(String cargoName) { - if (Func.isEmpty(cargoName)) { - return Collections.emptyList(); + List names = WaybillCargoSupport.splitCargoNames(cargoName); + return names.isEmpty() ? List.of() : names; + } + + /** + * 重量文案:全部货物同单位时按货物行相加,单位不一致时不出合计 + *

+ * 主表 quantity 是各行数量的合计、quantity_unit 只是首行的单位,「20堆 + 50方」会变成「70堆」。 + * 拿 goodsJson 算不出合计(有货物没填数量或单位)时,退回主表那份汇总值——宁可显示一个来源不明的数, + * 也不要让卡片上的重量凭空消失。 + */ + private String formatCargoWeight(List cargos, String fallback) { + if (cargos.isEmpty()) { + return fallback; } - String normalized = cargoName.replace(",", ",").replace("、", ","); - List names = Func.toStrList(",", normalized).stream() - .map(String::trim) - .filter(Func::isNotEmpty) - .collect(Collectors.toList()); - return names.isEmpty() ? List.of(cargoName.trim()) : names; + String unit = null; + BigDecimal total = BigDecimal.ZERO; + for (WaybillCargoSupport.Cargo cargo : cargos) { + String cargoUnit = Func.toStr(cargo.quantityUnit(), ""); + if (cargo.quantity() == null || Func.isEmpty(cargoUnit)) { + return fallback; + } + if (unit == null) { + unit = cargoUnit; + } else if (!unit.equals(cargoUnit)) { + return ""; + } + total = total.add(cargo.quantity()); + } + return formatWeight(total, unit); } private String formatWeight(BigDecimal quantity, String unit) { 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 7cab655b..f8701641 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 @@ -60,6 +60,7 @@ import org.springblade.transport.service.IRiskDisposalService; import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springframework.stereotype.Service; @@ -428,6 +429,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setCargoSummary(Func.toStr(trip.getCargoSummary(), "")); String weight = formatWeight(trip.getTotalQuantity(), trip.getQuantityUnit()); detail.setCargoName(Func.toStr(trip.getCargoName(), "")); + detail.setCargoNames(trip.getCargoNames() == null ? List.of() : trip.getCargoNames()); detail.setCargoQuantity(weight); detail.setWeight(weight); detail.setTotalWeight(weight); @@ -757,9 +759,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { detail.setPickupAddress(fromAddr); detail.setUnloadAddress(toAddr); - String cargo = Func.toStr(waybill.getCargoName(), ""); - String weight = formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()); - detail.setCargoName(cargo); + // 货名与合计重量取运单上录入的全部货物:主表 cargo_name 只是首行、quantity 是各行合计而单位只是首行的 + List cargos = WaybillCargoSupport.of(waybill); + List cargoNames = WaybillCargoSupport.cargoNames(cargos); + detail.setCargoName(String.join("、", cargoNames)); + detail.setCargoNames(cargoNames); + String weight = cargoWeight(cargos, formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); detail.setCargoQuantity(weight); detail.setWeight(weight); detail.setTotalWeight(weight); @@ -997,6 +1002,33 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { return Func.isBlank(unit) ? qty : qty + unit; } + /** + * 重量文案:全部货物同单位时按货物行相加,单位不一致时不出合计 + *

+ * 主表 quantity 是各行数量的合计、quantity_unit 只是首行的单位,「20堆 + 50方」会变成「70堆」。 + * 拿明细算不出合计(有货物没填数量或单位)时退回主表那份汇总值,不让重量凭空消失。 + */ + private String cargoWeight(List cargos, String fallback) { + if (cargos.isEmpty()) { + return fallback; + } + String unit = null; + BigDecimal total = BigDecimal.ZERO; + for (WaybillCargoSupport.Cargo cargo : cargos) { + String cargoUnit = Func.toStr(cargo.quantityUnit(), ""); + if (cargo.quantity() == null || Func.isEmpty(cargoUnit)) { + return fallback; + } + if (unit == null) { + unit = cargoUnit; + } else if (!unit.equals(cargoUnit)) { + return ""; + } + total = total.add(cargo.quantity()); + } + return formatWeight(total, unit); + } + private String formatLocalDate(LocalDate date) { if (date == null) { return ""; 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 d4c45658..8a831a4e 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 @@ -40,6 +40,7 @@ import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.LiveProcessConfigSupport; +import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springframework.stereotype.Service; @@ -222,13 +223,16 @@ public class TripQueryServiceImpl implements ITripQueryService { * 段一:一次取回该收口与行级筛选命中的可见运单行 *

* 只 select 拼装与分档用得上的列:运单表上有几张大 JSON 列,整行拖回不值得。 - * 可见口径与运单列表同一条:状态进运次的才取,被该司机拒单的不取。 + * goods_json 必须随行取回——卡片上的货名、货物汇总与合计重量都以它为准,一张运单可以录多条货物, + * 主表 cargo_name 只落了第一行。可见口径与运单列表同一条:状态进运次的才取,被该司机拒单的不取。 */ private List visibleRows(TripQueryVO condition) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .select(Waybill::getId, Waybill::getLoadingNo, Waybill::getWaybillNo, Waybill::getBusinessStatus, Waybill::getDriverAcceptStatus, Waybill::getProcessJson, Waybill::getProjectId, Waybill::getProjectName, Waybill::getCargoName, Waybill::getCargoType, Waybill::getQuantity, Waybill::getQuantityUnit, + // 多货物运单的主表字段只有首行,货物明细只能从 goods_json 拿 + Waybill::getGoodsJson, Waybill::getDepartureName, Waybill::getDepartureAddress, Waybill::getArrivalName, Waybill::getArrivalAddress, Waybill::getDriverName, Waybill::getDriverPhone, Waybill::getVehicleNo, Waybill::getCustomerName, Waybill::getTransportType, Waybill::getTaskRemark, @@ -587,21 +591,47 @@ public class TripQueryServiceImpl implements ITripQueryService { if (withPayload) { trip.setStations(stations); } + List cargos = children.stream() + .flatMap(child -> WaybillCargoSupport.of(child).stream()) + .toList(); + List cargoNames = WaybillCargoSupport.cargoNames(cargos); + // 运次级货名覆盖整趟车:主表 cargo_name 只是首行,配载单的 cargo_name 也是派单时的快照 + trip.setCargoNames(cargoNames); + if (!cargoNames.isEmpty()) { + trip.setCargoName(String.join("、", cargoNames)); + } + List cargoTypes = cargos.stream() + .map(WaybillCargoSupport.Cargo::cargoType) + .filter(Func::isNotEmpty) + .distinct() + .toList(); + trip.setCargoTypes(cargoTypes); + if (!cargoTypes.isEmpty()) { + trip.setCargoType(String.join("、", cargoTypes)); + } trip.setCargoSummary(cargoSummary(children)); fillTotalQuantity(trip, children); } /** * 货物汇总:同名货物合并且单位一致时取数量合计,没有数量的只列货名 + *

+ * 一条运单可以有多条货物,明细在 goods_json;主表 cargo_name 只落了第一行,拿它汇总等于把其余货物丢掉。 */ private String cargoSummary(List children) { Map> grouped = new LinkedHashMap<>(); for (Waybill child : children) { - String name = Func.toStrWithEmpty(child.getCargoName(), "货物"); - String unit = Func.toStr(child.getQuantityUnit(), ""); - Map byUnit = grouped.computeIfAbsent(name, key -> new LinkedHashMap<>()); - BigDecimal quantity = child.getQuantity() == null ? BigDecimal.ZERO : child.getQuantity(); - byUnit.merge(unit, quantity, BigDecimal::add); + List cargos = WaybillCargoSupport.of(child); + if (cargos.isEmpty()) { + continue; + } + for (WaybillCargoSupport.Cargo cargo : cargos) { + String name = cargo.displayName(); + String unit = Func.toStr(cargo.quantityUnit(), ""); + Map byUnit = grouped.computeIfAbsent(name, key -> new LinkedHashMap<>()); + BigDecimal quantity = cargo.quantity() == null ? BigDecimal.ZERO : cargo.quantity(); + byUnit.merge(unit, quantity, BigDecimal::add); + } } List parts = new ArrayList<>(); grouped.forEach((name, byUnit) -> byUnit.forEach((unit, quantity) -> @@ -610,24 +640,31 @@ public class TripQueryServiceImpl implements ITripQueryService { } /** - * 合计重量:只有全部子运单同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出) + * 合计重量:只有全部货物同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出) + *

+ * 按货物行算而不是按运单行算:一张运单里的多条货物可能是不同的单位与数量, + * 主表 quantity 是这些行的合计、quantity_unit 只是首行的单位,拿它当合计会把「20堆 + 50方」说成「70堆」。 */ private void fillTotalQuantity(TripVO trip, List children) { String unit = null; BigDecimal total = BigDecimal.ZERO; + boolean any = false; for (Waybill child : children) { - String childUnit = Func.toStr(child.getQuantityUnit(), ""); - if (child.getQuantity() == null || Func.isEmpty(childUnit)) { - return; + for (WaybillCargoSupport.Cargo cargo : WaybillCargoSupport.of(child)) { + String cargoUnit = Func.toStr(cargo.quantityUnit(), ""); + if (cargo.quantity() == null || Func.isEmpty(cargoUnit)) { + return; + } + if (unit == null) { + unit = cargoUnit; + } else if (!unit.equals(cargoUnit)) { + return; + } + total = total.add(cargo.quantity()); + any = true; } - if (unit == null) { - unit = childUnit; - } else if (!unit.equals(childUnit)) { - return; - } - total = total.add(child.getQuantity()); } - if (unit == null) { + if (!any || unit == null) { return; } trip.setTotalQuantity(total); @@ -648,7 +685,15 @@ public class TripQueryServiceImpl implements ITripQueryService { child.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(waybill.getProcessJson())); child.setProjectId(waybill.getProjectId()); child.setProjectName(waybill.getProjectName()); - child.setCargoName(waybill.getCargoName()); + // 货名按运单上实际录入的货物给全:主表 cargo_name 只是首行,多货物运单会漏掉其余货物 + List cargos = WaybillCargoSupport.of(waybill); + child.setCargoName(String.join("、", WaybillCargoSupport.cargoNames(cargos))); + child.setCargoNames(WaybillCargoSupport.cargoNames(cargos)); + child.setCargoTypes(cargos.stream() + .map(WaybillCargoSupport.Cargo::cargoType) + .filter(Func::isNotEmpty) + .distinct() + .toList()); child.setCargoType(waybill.getCargoType()); child.setQuantity(waybill.getQuantity()); child.setQuantityUnit(waybill.getQuantityUnit()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripStationAssembler.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripStationAssembler.java index 62735c22..134e2a34 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripStationAssembler.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripStationAssembler.java @@ -27,6 +27,7 @@ import org.springblade.core.tool.utils.Func; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.TripStationVO; +import org.springblade.transport.support.WaybillCargoSupport; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -238,26 +239,51 @@ final class TripStationAssembler { return byId.get(waybillIds.get(order - 1)); } + /** + * 一条子运单在该站的货物条目:运单上录了几条货物就出几条,同一张运单的多条货物不能被折成一行, + * 否则站点上看不出这票到底装/卸了哪几种货。去重键含货名,站点标记重复出现时不会重复列出。 + *

+ * 一条货物都没录的运单仍出一行(货名为空):站点上的「这站有这张票」由这一行承载, + * 只按货名判断有没有货物会让没填货名的单在任务安排里凭空消失。 + */ private static void addCargo(List cargos, Waybill child, String operation) { if (child == null) { return; } String waybillId = child.getId() == null ? null : String.valueOf(child.getId()); - boolean exists = cargos.stream().anyMatch(cargo -> - Objects.equals(cargo.getWaybillId(), waybillId) && Objects.equals(cargo.getOperation(), operation)); - if (exists) { + List items = WaybillCargoSupport.of(child); + if (items.isEmpty()) { + // 一条货物都没录:站点条目仍要有这一行,否则任务安排里这票凭空消失 + TripStationVO.TripCargoVO cargo = new TripStationVO.TripCargoVO(); + cargo.setWaybillId(waybillId); + cargo.setWaybillNo(child.getWaybillNo()); + cargo.setCargoName(""); + cargo.setCustomerName(child.getCustomerName()); + cargo.setOperation(operation); + cargo.setProjectName(child.getProjectName()); + cargos.add(cargo); return; } - TripStationVO.TripCargoVO cargo = new TripStationVO.TripCargoVO(); - cargo.setWaybillId(waybillId); - cargo.setWaybillNo(child.getWaybillNo()); - cargo.setCargoName(child.getCargoName()); - cargo.setCustomerName(child.getCustomerName()); - cargo.setQuantity(child.getQuantity()); - cargo.setQuantityUnit(child.getQuantityUnit()); - cargo.setOperation(operation); - cargo.setProjectName(child.getProjectName()); - cargos.add(cargo); + for (WaybillCargoSupport.Cargo item : items) { + String cargoName = item.displayName(); + boolean exists = cargos.stream().anyMatch(cargo -> + Objects.equals(cargo.getWaybillId(), waybillId) + && Objects.equals(cargo.getOperation(), operation) + && Objects.equals(cargo.getCargoName(), cargoName)); + if (exists) { + continue; + } + TripStationVO.TripCargoVO cargo = new TripStationVO.TripCargoVO(); + cargo.setWaybillId(waybillId); + cargo.setWaybillNo(child.getWaybillNo()); + cargo.setCargoName(cargoName); + cargo.setCustomerName(child.getCustomerName()); + cargo.setQuantity(item.quantity()); + cargo.setQuantityUnit(item.quantityUnit()); + cargo.setOperation(operation); + cargo.setProjectName(child.getProjectName()); + cargos.add(cargo); + } } private static void fillOperation(TripStationVO station, boolean load, boolean unload) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillCargoSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillCargoSupport.java new file mode 100644 index 00000000..afe71bc6 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillCargoSupport.java @@ -0,0 +1,219 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.pojo.entity.Waybill; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 运单货物明细的取法,只此一处。 + *

+ * 一张运单可以录多条货物,明细落在 {@code goods_json};主表的 {@code cargo_name} / {@code cargo_type} / + * {@code quantity} 只是给列表与报表用的首行汇总(数量是各行合计),拿它当货物清单会把其余货物丢掉。 + * 所以「这张运单装了什么」一律以 goods_json 为准,主表字段只在 goods_json 缺失或解析不出任何货名时兜底。 + *

+ * 货名也可能是主表里一格多名的历史写法(顿号、逗号分隔),按同一口径摊平。 + */ +public final class WaybillCargoSupport { + + /** 货物没有填名称时的展示名:宁可让司机看到「货物」,也不要出现空行 */ + private static final String FALLBACK_NAME = "货物"; + + /** 货名分隔符:历史数据里顿号、中英文逗号都出现过 */ + private static final String NAME_SEPARATOR_REGEX = "[、,,]"; + + /** 明细里可能承载数量的字段,按优先级取第一个有值的 */ + private static final String[] QUANTITY_KEYS = {"quantity", "cargoQuantity", "goodsQuantity"}; + + /** 明细里可能承载单位的字段,按优先级取第一个有值的 */ + private static final String[] UNIT_KEYS = {"quantityUnit", "goodsQuantityUnit", "unit"}; + + /** 明细里可能承载名称的字段,按优先级取第一个有值的 */ + private static final String[] NAME_KEYS = {"cargoName", "goodsName"}; + + /** 明细里可能承载类型的字段,按优先级取第一个有值的 */ + private static final String[] TYPE_KEYS = {"cargoType", "goodsType"}; + + private WaybillCargoSupport() { + } + + /** + * 一张运单的货物条目:goods_json 能解析出货物就用它,否则回退到主表的货物字段。 + *

+ * 每条明细至少出一条:货名、数量都为空的行也保留,让「这张票录了一条空货物」与「这条货物没填数量」可区分。 + */ + public static List of(Waybill waybill) { + if (waybill == null) { + return List.of(); + } + List detail = parseDetail(waybill.getGoodsJson()); + if (!detail.isEmpty()) { + return detail; + } + return fallback(waybill); + } + + /** + * 货名清单:同名的只留一次,保持录入顺序;档口与报表需要横向摊平时用它 + */ + public static List cargoNames(List cargos) { + List names = new ArrayList<>(); + for (Cargo cargo : cargos) { + String name = Func.toStr(cargo.cargoName(), "").trim(); + if (Func.isNotEmpty(name) && !names.contains(name)) { + names.add(name); + } + } + return names; + } + + /** + * 解析 goods_json 为货物条目;字段缺失、JSON 非法或解析不出任何条目都返回空集合,由调用方兜底 + */ + private static List parseDetail(String goodsJson) { + if (Func.isEmpty(goodsJson)) { + return List.of(); + } + List rows; + try { + rows = JsonUtil.parseArray(goodsJson, Map.class); + } catch (Exception exception) { + // 历史脏数据:JSON 坏了就当没有明细,回退主表字段,不让列表与详情因一条脏数据打不开 + return List.of(); + } + if (Func.isEmpty(rows)) { + return List.of(); + } + List cargos = new ArrayList<>(rows.size()); + for (Map row : rows) { + if (row == null) { + continue; + } + cargos.add(new Cargo( + firstText(row, NAME_KEYS), + firstText(row, TYPE_KEYS), + quantityOf(row), + firstText(row, UNIT_KEYS))); + } + return cargos; + } + + /** + * 回退:主表货名按历史分隔符摊平,每一段配同一个数量与单位(主表数量是各行合计,摊到哪一段都不准, + * 故只给一段配数量;多段说明主表数量不可拆分,宁可只展示货名也不重复同一份合计) + */ + private static List fallback(Waybill waybill) { + List names = splitCargoNames(waybill.getCargoName()); + if (names.isEmpty()) { + return List.of(); + } + List cargos = new ArrayList<>(names.size()); + boolean first = true; + for (String name : names) { + cargos.add(new Cargo( + name, + Func.toStr(waybill.getCargoType(), ""), + first ? waybill.getQuantity() : null, + first ? Func.toStr(waybill.getQuantityUnit(), "") : "")); + first = false; + } + return cargos; + } + + /** + * 主表货名按顿号、逗号摊平;空格与空段去掉,全空时给空集合 + */ + public static List splitCargoNames(String cargoName) { + if (Func.isEmpty(cargoName)) { + return List.of(); + } + List names = new ArrayList<>(); + for (String part : cargoName.split(NAME_SEPARATOR_REGEX)) { + String name = part.trim(); + if (Func.isNotEmpty(name)) { + names.add(name); + } + } + return names; + } + + private static String firstText(Map row, String... keys) { + for (String key : keys) { + Object value = row.get(key); + if (value == null) { + continue; + } + String text = String.valueOf(value).trim(); + if (Func.isNotEmpty(text)) { + return text; + } + } + return ""; + } + + private static BigDecimal quantityOf(Map row) { + for (String key : QUANTITY_KEYS) { + Object value = row.get(key); + if (value == null) { + continue; + } + String text = String.valueOf(value).trim(); + if (Func.isEmpty(text)) { + continue; + } + try { + BigDecimal quantity = new BigDecimal(text); + // 录入端用 -1 表示「未填」,落到汇总里会把合计算成负数 + if (quantity.compareTo(BigDecimal.ONE.negate()) == 0) { + continue; + } + return quantity; + } catch (NumberFormatException exception) { + // 这一字段不是数字:继续试下一个候选字段 + } + } + return null; + } + + /** + * 运单上的一条货物 + *

+ * 数量与单位可能为空:录货物时允许只填名称。展示端要把「没填数量」与「数量为 0」区分开。 + */ + public record Cargo(String cargoName, String cargoType, BigDecimal quantity, String quantityUnit) { + + /** + * 展示用货名:空名称回退成「货物」 + */ + public String displayName() { + return Func.toStrWithEmpty(cargoName, FALLBACK_NAME); + } + } + +} 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 7c1a9420..8679be08 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 @@ -116,7 +116,7 @@ class TripQueryServiceImplTest { assertTrue(select.contains("estimated_start_time"), "未配载运次的计划时间取自运单,必须取:" + select); assertTrue(select.contains("estimated_end_time"), select); assertFalse(select.contains("route_json"), "运单表上的大 JSON 列不必拖回:" + select); - assertFalse(select.contains("goods_json"), select); + assertTrue(select.contains("goods_json"), "货物明细只能从 goods_json 拿,必须随行取回:" + select); assertFalse(select.contains("task_info_json"), select); assertFalse(select.contains(String.valueOf((Object) null)) && select.contains("*"), "不得整行拖回:" + select); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java index 0f194a94..b4aac8d2 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java @@ -287,6 +287,28 @@ class TripReadServiceBoundaryTest { assertEquals("吨", trip.getQuantityUnit()); } + @Test + @DisplayName("一张运单录了多条货物时,货名与汇总都要全带上,不能只取主表首行") + void multiCargoWaybillKeepsEveryCargo() { + Waybill multi = child(CHILD_A, "running", null, ACCEPT_NO, at(1787657033000L)); + // 主表只落了首行:货名与单位都是第一行的,数量却是两行的合计 + multi.setCargoName("阿什顿"); + multi.setCargoType("服饰"); + multi.setQuantity(new BigDecimal("70.00")); + multi.setQuantityUnit("堆"); + multi.setGoodsJson("[{\"cargoName\":\"阿什顿\",\"cargoType\":\"服饰\",\"quantity\":\"20\",\"quantityUnit\":\"堆\"}," + + "{\"cargoName\":\"显卡050\",\"cargoType\":\"显卡\",\"quantity\":\"50\",\"quantityUnit\":\"方\"}]"); + stubLoadedTrip(multi); + + TripVO trip = onlyTrip(); + + assertEquals(List.of("阿什顿", "显卡050"), trip.getCargoNames(), "两种货物都要在,不能只剩主表首行"); + assertEquals(List.of("阿什顿", "显卡050"), trip.getWaybills().get(0).getCargoNames(), "子单也要带上完整货名"); + assertEquals("阿什顿 20堆、显卡050 50方", trip.getCargoSummary()); + assertNull(trip.getTotalQuantity(), "两种货物单位不同,不能把 20堆+50方 说成 70堆"); + assertNull(trip.getQuantityUnit()); + } + // ------------------------------------------------------------ 夹具 @SuppressWarnings("unchecked")