🐛 多货物运单的货名、汇总与合计重量按货物明细算
一张运单可以录多条货物,明细落在 goods_json,主表 cargo_name / cargo_type / quantity_unit 只是首行、quantity 是各行合计。列表与详情原先只读主表字段,第二种及以后的货物整条丢失 (如 YD-20261018-0005 只显示「阿什顿」,漏掉「显卡050」),并把「20堆 + 50方」算成「70堆」。 货物明细的取法收进 WaybillCargoSupport 一处:goods_json 能解析出货物就以它为准, 解析不出再回退主表(一格多名的历史写法按同一口径摊平)。 - 运次读服务的段一补取 goods_json:卡片货名与汇总都靠它,少取就只能拿首行 - 货名、货物汇总、合计重量改按货物行算;单位不一致时合计留空,不把汇总串冒充成重量 - 站点货物条目一张子单可出多条,并带上各自的货名与数量单位 - 运次级与子单级都补 cargoNames / cargoTypes 清单,供小程序缩写与展示
This commit is contained in:
+4
-1
@@ -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<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物数量(带单位)")
|
||||
private String cargoQuantity;
|
||||
|
||||
|
||||
+16
-4
@@ -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<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物类型:整趟车全部类型以「、」连接")
|
||||
private String cargoType;
|
||||
|
||||
@Schema(description = "货物类型清单:整趟车全部货物类型,同类型的只留一次")
|
||||
private List<String> 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<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物类型:主表首行的类型,完整清单见 cargoTypes")
|
||||
private String cargoType;
|
||||
|
||||
@Schema(description = "货物类型清单:运单上录入的全部货物类型,同类型的只留一次")
|
||||
private List<String> cargoTypes;
|
||||
|
||||
@Schema(description = "数量")
|
||||
private BigDecimal quantity;
|
||||
|
||||
|
||||
+68
-26
@@ -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<WaybillCargoSupport.Cargo> cargos = WaybillCargoSupport.of(waybill);
|
||||
List<String> 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<String> cargoNames = children.stream()
|
||||
.flatMap(child -> splitCargoNames(child.getCargoName()).stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
// 货名取运次上算好的整趟车清单:它按子单录入的货物明细摊平去重,主表 cargo_name 只是首行。
|
||||
// 运次清单为空时按子单卡片自己的货名兜底,不让卡片上的货物行整个消失
|
||||
List<String> 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<String> 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<String> splitCargoNames(String cargoName) {
|
||||
if (Func.isEmpty(cargoName)) {
|
||||
return Collections.emptyList();
|
||||
List<String> names = WaybillCargoSupport.splitCargoNames(cargoName);
|
||||
return names.isEmpty() ? List.of() : names;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重量文案:全部货物同单位时按货物行相加,单位不一致时不出合计
|
||||
* <p>
|
||||
* 主表 quantity 是各行数量的合计、quantity_unit 只是首行的单位,「20堆 + 50方」会变成「70堆」。
|
||||
* 拿 goodsJson 算不出合计(有货物没填数量或单位)时,退回主表那份汇总值——宁可显示一个来源不明的数,
|
||||
* 也不要让卡片上的重量凭空消失。
|
||||
*/
|
||||
private String formatCargoWeight(List<WaybillCargoSupport.Cargo> cargos, String fallback) {
|
||||
if (cargos.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
String normalized = cargoName.replace(",", ",").replace("、", ",");
|
||||
List<String> 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) {
|
||||
|
||||
+35
-3
@@ -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<WaybillCargoSupport.Cargo> cargos = WaybillCargoSupport.of(waybill);
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重量文案:全部货物同单位时按货物行相加,单位不一致时不出合计
|
||||
* <p>
|
||||
* 主表 quantity 是各行数量的合计、quantity_unit 只是首行的单位,「20堆 + 50方」会变成「70堆」。
|
||||
* 拿明细算不出合计(有货物没填数量或单位)时退回主表那份汇总值,不让重量凭空消失。
|
||||
*/
|
||||
private String cargoWeight(List<WaybillCargoSupport.Cargo> 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 "";
|
||||
|
||||
+63
-18
@@ -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 {
|
||||
* 段一:一次取回该收口与行级筛选命中的可见运单行
|
||||
* <p>
|
||||
* 只 select 拼装与分档用得上的列:运单表上有几张大 JSON 列,整行拖回不值得。
|
||||
* 可见口径与运单列表同一条:状态进运次的才取,被该司机拒单的不取。
|
||||
* goods_json 必须随行取回——卡片上的货名、货物汇总与合计重量都以它为准,一张运单可以录多条货物,
|
||||
* 主表 cargo_name 只落了第一行。可见口径与运单列表同一条:状态进运次的才取,被该司机拒单的不取。
|
||||
*/
|
||||
private List<Waybill> visibleRows(TripQueryVO condition) {
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>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<WaybillCargoSupport.Cargo> cargos = children.stream()
|
||||
.flatMap(child -> WaybillCargoSupport.of(child).stream())
|
||||
.toList();
|
||||
List<String> cargoNames = WaybillCargoSupport.cargoNames(cargos);
|
||||
// 运次级货名覆盖整趟车:主表 cargo_name 只是首行,配载单的 cargo_name 也是派单时的快照
|
||||
trip.setCargoNames(cargoNames);
|
||||
if (!cargoNames.isEmpty()) {
|
||||
trip.setCargoName(String.join("、", cargoNames));
|
||||
}
|
||||
List<String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 货物汇总:同名货物合并且单位一致时取数量合计,没有数量的只列货名
|
||||
* <p>
|
||||
* 一条运单可以有多条货物,明细在 goods_json;主表 cargo_name 只落了第一行,拿它汇总等于把其余货物丢掉。
|
||||
*/
|
||||
private String cargoSummary(List<Waybill> children) {
|
||||
Map<String, Map<String, BigDecimal>> grouped = new LinkedHashMap<>();
|
||||
for (Waybill child : children) {
|
||||
String name = Func.toStrWithEmpty(child.getCargoName(), "货物");
|
||||
String unit = Func.toStr(child.getQuantityUnit(), "");
|
||||
Map<String, BigDecimal> byUnit = grouped.computeIfAbsent(name, key -> new LinkedHashMap<>());
|
||||
BigDecimal quantity = child.getQuantity() == null ? BigDecimal.ZERO : child.getQuantity();
|
||||
byUnit.merge(unit, quantity, BigDecimal::add);
|
||||
List<WaybillCargoSupport.Cargo> cargos = WaybillCargoSupport.of(child);
|
||||
if (cargos.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (WaybillCargoSupport.Cargo cargo : cargos) {
|
||||
String name = cargo.displayName();
|
||||
String unit = Func.toStr(cargo.quantityUnit(), "");
|
||||
Map<String, BigDecimal> byUnit = grouped.computeIfAbsent(name, key -> new LinkedHashMap<>());
|
||||
BigDecimal quantity = cargo.quantity() == null ? BigDecimal.ZERO : cargo.quantity();
|
||||
byUnit.merge(unit, quantity, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
grouped.forEach((name, byUnit) -> byUnit.forEach((unit, quantity) ->
|
||||
@@ -610,24 +640,31 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合计重量:只有全部子运单同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出)
|
||||
* 合计重量:只有全部货物同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出)
|
||||
* <p>
|
||||
* 按货物行算而不是按运单行算:一张运单里的多条货物可能是不同的单位与数量,
|
||||
* 主表 quantity 是这些行的合计、quantity_unit 只是首行的单位,拿它当合计会把「20堆 + 50方」说成「70堆」。
|
||||
*/
|
||||
private void fillTotalQuantity(TripVO trip, List<Waybill> 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<WaybillCargoSupport.Cargo> 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());
|
||||
|
||||
+39
-13
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条子运单在该站的货物条目:运单上录了几条货物就出几条,同一张运单的多条货物不能被折成一行,
|
||||
* 否则站点上看不出这票到底装/卸了哪几种货。去重键含货名,站点标记重复出现时不会重复列出。
|
||||
* <p>
|
||||
* 一条货物都没录的运单仍出一行(货名为空):站点上的「这站有这张票」由这一行承载,
|
||||
* 只按货名判断有没有货物会让没填货名的单在任务安排里凭空消失。
|
||||
*/
|
||||
private static void addCargo(List<TripStationVO.TripCargoVO> 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<WaybillCargoSupport.Cargo> 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) {
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 运单货物明细的取法,只此一处。
|
||||
* <p>
|
||||
* 一张运单可以录多条货物,明细落在 {@code goods_json};主表的 {@code cargo_name} / {@code cargo_type} /
|
||||
* {@code quantity} 只是给列表与报表用的首行汇总(数量是各行合计),拿它当货物清单会把其余货物丢掉。
|
||||
* 所以「这张运单装了什么」一律以 goods_json 为准,主表字段只在 goods_json 缺失或解析不出任何货名时兜底。
|
||||
* <p>
|
||||
* 货名也可能是主表里一格多名的历史写法(顿号、逗号分隔),按同一口径摊平。
|
||||
*/
|
||||
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 能解析出货物就用它,否则回退到主表的货物字段。
|
||||
* <p>
|
||||
* 每条明细至少出一条:货名、数量都为空的行也保留,让「这张票录了一条空货物」与「这条货物没填数量」可区分。
|
||||
*/
|
||||
public static List<Cargo> of(Waybill waybill) {
|
||||
if (waybill == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<Cargo> detail = parseDetail(waybill.getGoodsJson());
|
||||
if (!detail.isEmpty()) {
|
||||
return detail;
|
||||
}
|
||||
return fallback(waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 货名清单:同名的只留一次,保持录入顺序;档口与报表需要横向摊平时用它
|
||||
*/
|
||||
public static List<String> cargoNames(List<Cargo> cargos) {
|
||||
List<String> 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<Cargo> parseDetail(String goodsJson) {
|
||||
if (Func.isEmpty(goodsJson)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map> rows;
|
||||
try {
|
||||
rows = JsonUtil.parseArray(goodsJson, Map.class);
|
||||
} catch (Exception exception) {
|
||||
// 历史脏数据:JSON 坏了就当没有明细,回退主表字段,不让列表与详情因一条脏数据打不开
|
||||
return List.of();
|
||||
}
|
||||
if (Func.isEmpty(rows)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Cargo> 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<Cargo> fallback(Waybill waybill) {
|
||||
List<String> names = splitCargoNames(waybill.getCargoName());
|
||||
if (names.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Cargo> 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<String> splitCargoNames(String cargoName) {
|
||||
if (Func.isEmpty(cargoName)) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运单上的一条货物
|
||||
* <p>
|
||||
* 数量与单位可能为空:录货物时允许只填名称。展示端要把「没填数量」与「数量为 0」区分开。
|
||||
*/
|
||||
public record Cargo(String cargoName, String cargoType, BigDecimal quantity, String quantityUnit) {
|
||||
|
||||
/**
|
||||
* 展示用货名:空名称回退成「货物」
|
||||
*/
|
||||
public String displayName() {
|
||||
return Func.toStrWithEmpty(cargoName, FALLBACK_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+22
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user