✨ 运次级下发计重合计与货物明细行

This commit is contained in:
2026-10-20 22:20:43 +08:00
parent e39251eba5
commit 142eee384b
8 changed files with 232 additions and 30 deletions
@@ -0,0 +1,48 @@
/**
* 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.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 货物明细行:一条货物一行
* <p>
* 详情页逐行展示「货物名称 + 数量单位」,名称与数量分开给,前端不需要再从汇总串里拆。
*/
@Data
@Schema(description = "货物明细行")
public class CargoLineVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "货物名称")
private String cargoName;
@Schema(description = "数量(含单位,如 1000吨);录入时只填了名称则为空")
private String quantity;
}
@@ -67,6 +67,9 @@ public class DriverWaybillCardVO implements Serializable {
@Schema(description = "货物类别")
private String cargoCategory;
@Schema(description = "货物明细(详情返回):一条一行,同名同单位的货物已合并数量")
private List<CargoLineVO> cargos;
@Schema(description = "重量(带单位)")
private String weight;
@@ -129,12 +129,21 @@ public class TripVO implements Serializable {
@Schema(description = "货物汇总:按货物名合并数量,同名同单位取合计")
private String cargoSummary;
@Schema(description = "货物明细:一条货物一行,同名同单位的货物已合并数量")
private List<CargoLineVO> cargos;
@Schema(description = "合计重量:全部子运单同单位时为数量合计,单位不一致时为空")
private BigDecimal totalQuantity;
@Schema(description = "合计重量的单位,与 totalQuantity 同时为空或同时有值")
private String quantityUnit;
@Schema(description = "计重合计:全部货物同单位时取该单位合计,单位不一致时只合计「吨」项(其余单位不参与);无可用合计时为空")
private BigDecimal weighingQuantity;
@Schema(description = "计重合计的单位,与 weighingQuantity 同时为空或同时有值")
private String weighingUnit;
@Schema(description = "计划发货日期(配载单,未配载为空)")
private LocalDate estimatedStartDate;
@@ -58,6 +58,7 @@ import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO;
import org.springblade.transport.pojo.vo.DriverNodePunchVO;
import org.springblade.transport.pojo.vo.DriverPunchNodeVO;
import org.springblade.transport.pojo.vo.DriverPunchPhotoVO;
import org.springblade.transport.pojo.vo.CargoLineVO;
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
@@ -981,7 +982,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return toCard(waybill, false);
}
private DriverWaybillCardVO toCard(Waybill waybill, boolean withEnrouteRecords) {
private DriverWaybillCardVO toCard(Waybill waybill, boolean withDetail) {
DriverWaybillCardVO card = new DriverWaybillCardVO();
card.setId(waybill.getId());
card.setWaybillNo(waybill.getWaybillNo());
@@ -1000,7 +1001,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
.filter(Func::isNotEmpty)
.distinct()
.collect(Collectors.joining("、")));
card.setWeight(formatCargoWeight(cargos, formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())));
// 计重合计:全部货物同单位时就是该单位合计,单位不一致时只有吨项参与(口径见 WaybillCargoSupport#weighing)
WaybillCargoSupport.Quantity weighing = WaybillCargoSupport.weighing(cargos);
card.setWeight(weighing == null ? "" : formatWeight(weighing.value(), weighing.unit()));
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
String createTime = formatDateTime(waybill.getCreateTime());
card.setCreateTime(createTime);
@@ -1024,13 +1027,14 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
card.setUnloadAddress(card.getToAddress());
card.setCargoQuantity(weightText);
card.setTotalWeight(weightText);
card.setCargos(withDetail ? cargoLines(cargos) : null);
card.setTransportType(toTransportTypeLabel(waybill.getTransportType()));
card.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime()));
card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime()));
card.setRemark(Func.toStr(waybill.getRemark(), ""));
if (withEnrouteRecords) {
if (withDetail) {
Date lastPunchAt = findLastPunchTime(waybill.getId());
WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin(
processJson, waybill.getBusinessStatus(), lastPunchAt, LocalDateTime.now());
@@ -1152,17 +1156,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
card.setCargoCategory(String.join("、", cargoTypes));
card.setCargoSummary(Func.toStr(trip.getCargoSummary(), ""));
// 合计重量只在全部货物同单位时才有值;单位不一致时留空,卡片上已经由货物汇总逐条列出,
// 拿汇总串冒充重量会让「货名 + 重量」两栏把同一段文字打印两遍
String weight = trip.getTotalQuantity() == null
// 计重合计:同单位时就是该单位合计,单位不一致时只有吨项参与(见 TripQueryServiceImpl#fillWeighing);
// 都没有就留空,卡片上只剩货名,不拿汇总串冒充重量
String weight = trip.getWeighingQuantity() == null
? ""
: formatWeight(trip.getTotalQuantity(), trip.getQuantityUnit());
: formatWeight(trip.getWeighingQuantity(), trip.getWeighingUnit());
card.setWeight(weight);
card.setCargoQuantity(weight);
card.setTotalWeight(weight);
if (withDetail) {
card.setStations(trip.getStations());
card.setRoutePoints(toTripRoutePoints(trip.getStations()));
card.setCargos(trip.getCargos());
}
}
@@ -2008,31 +2013,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
* 重量文案:全部货物同单位时按货物行相加,单位不一致时不出合计
* <p>
* 主表 quantity 是各行数量的合计、quantity_unit 只是首行的单位,「20堆 + 50方」会变成「70堆」。
* 拿 goodsJson 算不出合计(有货物没填数量或单位)时,退回主表那份汇总值——宁可显示一个来源不明的数,
* 也不要让卡片上的重量凭空消失。
* 货物明细行:一条货物一行,数量带单位
*/
private String formatCargoWeight(List<WaybillCargoSupport.Cargo> cargos, String fallback) {
if (cargos.isEmpty()) {
return fallback;
private List<CargoLineVO> cargoLines(List<WaybillCargoSupport.Cargo> cargos) {
List<CargoLineVO> lines = new ArrayList<>();
for (WaybillCargoSupport.Line line : WaybillCargoSupport.lines(cargos)) {
CargoLineVO vo = new CargoLineVO();
vo.setCargoName(line.name());
vo.setQuantity(line.quantity() == null ? "" : formatWeight(line.quantity(), line.unit()));
lines.add(vo);
}
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);
return lines;
}
private String formatWeight(BigDecimal quantity, String unit) {
@@ -31,6 +31,7 @@ import org.springblade.core.log.exception.ServiceException;
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.CargoLineVO;
import org.springblade.transport.pojo.vo.TripQueryVO;
import org.springblade.transport.pojo.vo.TripStationVO;
import org.springblade.transport.pojo.vo.TripTabCountsVO;
@@ -610,7 +611,9 @@ public class TripQueryServiceImpl implements ITripQueryService {
trip.setCargoType(String.join("、", cargoTypes));
}
trip.setCargoSummary(cargoSummary(children));
trip.setCargos(cargoLines(cargos));
fillTotalQuantity(trip, children);
fillWeighing(trip, cargos);
}
/**
@@ -639,6 +642,20 @@ public class TripQueryServiceImpl implements ITripQueryService {
return String.join("、", parts);
}
/**
* 货物明细行:与 cargoSummary 同一套合并口径,但数量与单位分开留,供详情页逐行展示
*/
private List<CargoLineVO> cargoLines(List<WaybillCargoSupport.Cargo> cargos) {
List<CargoLineVO> lines = new ArrayList<>();
for (WaybillCargoSupport.Line line : WaybillCargoSupport.lines(cargos)) {
CargoLineVO vo = new CargoLineVO();
vo.setCargoName(line.name());
vo.setQuantity(line.quantity() == null ? "" : formatQuantity(line.quantity(), line.unit()));
lines.add(vo);
}
return lines;
}
/**
* 合计重量:只有全部货物同单位时才能相加,单位不一致时留空(汇总文案里仍分单位列出)
* <p>
@@ -671,6 +688,18 @@ public class TripQueryServiceImpl implements ITripQueryService {
trip.setQuantityUnit(unit);
}
/**
* 计重合计:卡片上那个「货物重量」,口径见 WaybillCargoSupport#weighing
*/
private void fillWeighing(TripVO trip, List<WaybillCargoSupport.Cargo> cargos) {
WaybillCargoSupport.Quantity weighing = WaybillCargoSupport.weighing(cargos);
if (weighing == null) {
return;
}
trip.setWeighingQuantity(weighing.value());
trip.setWeighingUnit(weighing.unit());
}
private String formatQuantity(BigDecimal quantity, String unit) {
String value = quantity.stripTrailingZeros().toPlainString();
return Func.isEmpty(unit) ? value : value + unit;
@@ -28,8 +28,11 @@ import org.springblade.transport.pojo.entity.Waybill;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 运单货物明细的取法,只此一处。
@@ -42,6 +45,9 @@ import java.util.Map;
*/
public final class WaybillCargoSupport {
/** 计重时唯一允许跨货物相加的单位:其他单位只在「全部货物同单位」时才合计 */
private static final String UNIT_TON = "吨";
/** 货物没有填名称时的展示名:宁可让司机看到「货物」,也不要出现空行 */
private static final String FALLBACK_NAME = "货物";
@@ -146,6 +152,83 @@ public final class WaybillCargoSupport {
return cargos;
}
/**
* 计重合计:卡片上那个「货物重量」
* <p>
* 全部货物同单位时就是该单位合计;单位不一致时只合计「吨」项,其余单位不参与——
* 「100吨 + 20堆」报 100 吨,「20堆 + 50方」没有吨项就不出合计。
* 有货物缺数量时该单位合计不完整,宁可不报也不报一个偏小的数。
*
* @return 有可用合计时返回数量与单位,否则 null
*/
public static Quantity weighing(List<Cargo> cargos) {
Map<String, BigDecimal> totals = new LinkedHashMap<>();
Set<String> incomplete = new HashSet<>();
for (Cargo cargo : cargos) {
String unit = Func.toStr(cargo.quantityUnit(), "");
if (Func.isEmpty(unit)) {
continue;
}
if (cargo.quantity() == null) {
incomplete.add(unit);
continue;
}
totals.merge(unit, cargo.quantity(), BigDecimal::add);
}
if (totals.isEmpty()) {
return null;
}
String unit;
if (totals.size() == 1) {
unit = totals.keySet().iterator().next();
} else {
unit = totals.containsKey(UNIT_TON) ? UNIT_TON : null;
}
if (unit == null || incomplete.contains(unit)) {
return null;
}
return new Quantity(totals.get(unit), unit);
}
/** 计重合计的数量与单位 */
public record Quantity(BigDecimal value, String unit) {
}
/**
* 货物明细行:一条货物一行,同名同单位的合并数量,按录入顺序
* <p>
* 与 {@link #cargoSummary(List)} 的合并口径一致,但数量与单位保持分开,供详情页逐行展示。
* 数量为空的货物只留名称,不按 0 参与合并。
*/
public static List<Line> lines(List<Cargo> cargos) {
Map<String, Integer> index = new LinkedHashMap<>();
List<Line> lines = new ArrayList<>();
for (Cargo cargo : cargos) {
String name = cargo.displayName();
String unit = Func.toStr(cargo.quantityUnit(), "");
String key = name + '\u0000' + unit;
Integer at = index.get(key);
if (at == null) {
index.put(key, lines.size());
lines.add(new Line(name, cargo.quantity(), unit));
continue;
}
Line merged = lines.get(at);
if (cargo.quantity() == null) {
continue;
}
BigDecimal quantity = merged.quantity() == null
? cargo.quantity()
: merged.quantity().add(cargo.quantity());
lines.set(at, new Line(name, quantity, unit));
}
return lines;
}
/** 一条货物明细:数量可能为空(录入时只填了名称) */
public record Line(String name, BigDecimal quantity, String unit) {
}
/**
* 主表货名按顿号、逗号摊平;空格与空段去掉,全空时给空集合
*/
@@ -383,6 +383,8 @@ class DriverTripCardTest {
trip.setCargoSummary("石英砂A 131.84吨、技嘉4070显卡 30吨");
trip.setTotalQuantity(new BigDecimal("161.84"));
trip.setQuantityUnit("吨");
trip.setWeighingQuantity(new BigDecimal("161.84"));
trip.setWeighingUnit("吨");
trip.setDriverName("黄宇富");
trip.setDriverPhone(PHONE);
trip.setVehicleNo("桂A32523");
@@ -410,6 +412,8 @@ class DriverTripCardTest {
trip.setCargoSummary("石灰石 30吨");
trip.setTotalQuantity(new BigDecimal("30"));
trip.setQuantityUnit("吨");
trip.setWeighingQuantity(new BigDecimal("30"));
trip.setWeighingUnit("吨");
trip.setDriverName("黄宇富");
trip.setDriverPhone(PHONE);
trip.setVehicleNo("桂A32523");
@@ -309,6 +309,41 @@ class TripReadServiceBoundaryTest {
assertNull(trip.getQuantityUnit());
}
@Test
@DisplayName("计重合计:单位不一致时只合计吨项,堆方不参与;全是吨时就是吨合计")
void weighingOnlyCountsTonsWhenUnitsDiffer() {
Waybill mixed = child(CHILD_A, "running", null, ACCEPT_NO, at(1787657033000L));
mixed.setCargoName("阿什顿");
mixed.setQuantity(new BigDecimal("100"));
mixed.setQuantityUnit("吨");
mixed.setGoodsJson("[{\"cargoName\":\"阿什顿\",\"quantity\":\"100\",\"quantityUnit\":\"吨\"},"
+ "{\"cargoName\":\"显卡050\",\"quantity\":\"20\",\"quantityUnit\":\"堆\"}]");
stubLoadedTrip(mixed);
TripVO trip = onlyTrip();
assertNull(trip.getTotalQuantity(), "单位不一致,合计重量仍然为空");
assertEquals(0, new BigDecimal("100").compareTo(trip.getWeighingQuantity()), "计重合计只算吨项");
assertEquals("吨", trip.getWeighingUnit());
}
@Test
@DisplayName("计重合计:没有吨项且单位不一致时不出合计,合计重量同样为空")
void weighingEmptyWhenNoTonAndUnitsDiffer() {
Waybill mixed = child(CHILD_A, "running", null, ACCEPT_NO, at(1787657033000L));
mixed.setCargoName("阿什顿");
mixed.setQuantity(new BigDecimal("70.00"));
mixed.setQuantityUnit("堆");
mixed.setGoodsJson("[{\"cargoName\":\"阿什顿\",\"quantity\":\"20\",\"quantityUnit\":\"堆\"},"
+ "{\"cargoName\":\"显卡050\",\"quantity\":\"50\",\"quantityUnit\":\"方\"}]");
stubLoadedTrip(mixed);
TripVO trip = onlyTrip();
assertNull(trip.getWeighingQuantity(), "没有吨项就不出计重合计");
assertNull(trip.getWeighingUnit());
}
// ------------------------------------------------------------ 夹具
@SuppressWarnings("unchecked")