fix(lastleg): preserve return relabel delivery metadata and measurements
This commit is contained in:
+9
-2
@@ -1478,10 +1478,17 @@ public class DeliveryTransferService {
|
||||
&& !Objects.equals(delivery.getDestinationWarehouseCode(), item.getDestinationWarehouseCode())) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.findCustomerMark(customerId, item.getCartonMarks());
|
||||
if (carton == null) throw conflict("退货派送单来源箱记录缺失");
|
||||
for (var line : returnCartonItems.listCarton(carton.getId()))
|
||||
boolean relabelCarton = "RELABEL_OUTBOUND".equals(sourceOutbound.getOutboundType());
|
||||
boolean firstSku = true;
|
||||
// Match receipt cargo semantics: stable first SKU carries the physical box once, never per piece.
|
||||
for (var line : returnCartonItems.listCarton(carton.getId())) {
|
||||
cargo.add(new DeliveryResponses.DeliverySku(item.getInboundCode(), delivery.getCustomerName(),
|
||||
carton.getDestinationWarehouseCode(), delivery.getDeliveryAddress(), null, null,
|
||||
line.getSkuCode(), line.getProductNameZh(), 1, null, null, line.getQuantity()));
|
||||
line.getSkuCode(), line.getProductNameZh(), !relabelCarton || firstSku ? 1 : 0,
|
||||
relabelCarton && firstSku ? item.getGrossWeightKg() : null,
|
||||
relabelCarton && firstSku ? item.getVolumeCbm() : null, line.getQuantity()));
|
||||
firstSku = false;
|
||||
}
|
||||
}
|
||||
return new DeliveryResponses.DeliveryDetail(deliveryRow(delivery), cargo,
|
||||
feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId()).stream().map(this::feeRow).toList(),
|
||||
|
||||
+49
-1
@@ -105,6 +105,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private OutboundPalletItemMapper outboundItems;
|
||||
@Resource private OutboundPdaTaskMapper outboundTasks;
|
||||
@Resource private com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper deliveryOrders;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.fleet.driver.FleetWarehouseDirectory warehouseDirectory;
|
||||
@Resource private OverseasFeeSourceService feeSources;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferRelabelCompletionService transferRelabelCompletion;
|
||||
@@ -1089,6 +1091,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return;
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
Map<Long, ReturnInventoryCarton> confirmedReturnCartons = new LinkedHashMap<>();
|
||||
for (RelabelItem source : relabelItems) {
|
||||
if (source.getReturnCartonId() == null) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(source.getReturnCartonId());
|
||||
@@ -1099,6 +1102,22 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|| intValue(source.getNotifiedPackageCount()) != 1)
|
||||
throw conflict("退货整箱换标出库必须覆盖该箱全部 SKU 数量:" + source.getCartonMark());
|
||||
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
|
||||
confirmedReturnCartons.put(carton.getId(), carton);
|
||||
}
|
||||
boolean returnOnly = relabelItems.stream().allMatch(item -> item.getReturnCartonId() != null);
|
||||
String customerName = null;
|
||||
String warehouseName = null;
|
||||
if (returnOnly) {
|
||||
// Resolve once per new outbound, before any insert; every destination shares these snapshots.
|
||||
var customer = customerDirectory.find(application.getCustomerId());
|
||||
var warehouse = warehouseDirectory.required(Long.parseLong(application.getOverseasWarehouseId()), "OVERSEAS");
|
||||
if (customer == null || !application.getCustomerId().equals(customer.id()) || !StringUtils.hasText(customer.name())
|
||||
|| warehouse == null || !application.getOverseasWarehouseId().equals(warehouse.id())
|
||||
|| !"OVERSEAS".equals(warehouse.region()) || !StringUtils.hasText(warehouse.name())) {
|
||||
throw conflict("客户或作业仓库名称未完整返回,请核实主档后重试");
|
||||
}
|
||||
customerName = customer.name().trim();
|
||||
warehouseName = warehouse.name().trim();
|
||||
}
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
|
||||
@@ -1107,6 +1126,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> item.getReturnCartonId() == null ? intValue(item.getNotifiedPackageCount()) : 1).sum());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(this::outboundSourceKey).distinct().count());
|
||||
outbound.setRecipientInfo(application.getDeliveryMethod()); outbound.setIdempotencyKey(key);
|
||||
if (returnOnly) {
|
||||
var measurement = returnMeasurement(relabelItems, confirmedReturnCartons);
|
||||
outbound.setGrossWeightKg(measurement.weightKg()); outbound.setVolumeCbm(measurement.volumeCbm());
|
||||
}
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
Map<Long, List<RelabelItem>> byPosition = relabelItems.stream().collect(Collectors.groupingBy(
|
||||
this::outboundSourceKey, LinkedHashMap::new, Collectors.toList()));
|
||||
@@ -1132,12 +1155,16 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setInboundCode(group.stream().map(RelabelItem::getSourceInboundCode).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setOrderRefs(writeOutboundRefs(application.getRelabelNo(), position == null ? null : entry.getKey(), group));
|
||||
if (position == null) {
|
||||
var measurement = returnMeasurement(group, confirmedReturnCartons);
|
||||
item.setGrossWeightKg(measurement.weightKg()); item.setVolumeCbm(measurement.volumeCbm());
|
||||
}
|
||||
item.setCreatedAt(now); outboundItems.insert(item);
|
||||
}
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(application.getCustomerId());
|
||||
task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId());
|
||||
task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); outboundTasks.insert(task);
|
||||
if (relabelItems.stream().allMatch(item -> item.getReturnCartonId() != null)) {
|
||||
if (returnOnly) {
|
||||
Map<String, List<RelabelItem>> destinations = relabelItems.stream().collect(Collectors.groupingBy(
|
||||
item -> Objects.toString(item.getDestinationFbaWarehouseCode(), ""), LinkedHashMap::new, Collectors.toList()));
|
||||
int index = 0;
|
||||
@@ -1147,12 +1174,15 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", application.getOverseasWarehouseId(), delivery.getId()));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(application.getOverseasWarehouseId());
|
||||
delivery.setCustomerName(customerName); delivery.setOverseasWarehouseName(warehouseName);
|
||||
delivery.setDeliveryGroupKey("RETURN_RELABEL:" + destination.getKey());
|
||||
delivery.setDestinationWarehouseCode(destination.getKey());
|
||||
delivery.setDestinationType(StringUtils.hasText(destination.getKey()) ? "FBA" : null);
|
||||
delivery.setContactName(application.getContactName()); delivery.setContactPhone(application.getContactPhone());
|
||||
delivery.setDeliveryMethod(application.getDeliveryMethod()); delivery.setStatus("WAIT_DELIVERY");
|
||||
delivery.setPackageCount(destination.getValue().size()); delivery.setPalletCount(0);
|
||||
var measurement = returnMeasurement(destination.getValue(), confirmedReturnCartons);
|
||||
delivery.setTotalWeightKg(measurement.weightKg()); delivery.setTotalVolumeCbm(measurement.volumeCbm());
|
||||
delivery.setIdempotencyKey(key + ":DELIVERY:" + index++); delivery.setVersion(0L);
|
||||
delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
|
||||
deliveryOrders.insert(delivery);
|
||||
@@ -1161,6 +1191,24 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setOutboundOrderNo(outbound.getOutboundNo());
|
||||
}
|
||||
|
||||
private record ReturnMeasurement(BigDecimal weightKg, BigDecimal volumeCbm) { }
|
||||
|
||||
private ReturnMeasurement returnMeasurement(List<RelabelItem> sources, Map<Long, ReturnInventoryCarton> cartons) {
|
||||
BigDecimal weight = BigDecimal.ZERO;
|
||||
BigDecimal volume = BigDecimal.ZERO;
|
||||
// The selected physical carton contributes once, regardless of its SKU count or piece quantities.
|
||||
for (Long cartonId : sources.stream().map(RelabelItem::getReturnCartonId).distinct().toList()) {
|
||||
ReturnInventoryCarton carton = cartons.get(cartonId);
|
||||
if (carton == null) return new ReturnMeasurement(null, null);
|
||||
weight = weight == null || !positive(carton.getWeight()) ? null : weight.add(carton.getWeight());
|
||||
volume = volume == null || !positive(carton.getLength()) || !positive(carton.getWidth()) || !positive(carton.getHeight())
|
||||
? null : volume.add(carton.getLength().multiply(carton.getWidth()).multiply(carton.getHeight()).movePointLeft(6));
|
||||
}
|
||||
return new ReturnMeasurement(weight, volume);
|
||||
}
|
||||
|
||||
private static boolean positive(BigDecimal value) { return value != null && value.signum() > 0; }
|
||||
|
||||
private String writeOutboundRefs(String relabelNo, Long positionId, List<RelabelItem> values) {
|
||||
try {
|
||||
Map<String, Object> refs = new LinkedHashMap<>();
|
||||
|
||||
@@ -300,7 +300,7 @@ CREATE TABLE IF NOT EXISTS ll_delivery_order (
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '派送箱数',
|
||||
pallet_count INT NOT NULL DEFAULT 0 COMMENT '派送托盘数',
|
||||
total_weight_kg DECIMAL(18,4) NULL COMMENT '总重量(千克),任一来源未确认时为空',
|
||||
total_volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '总体积(立方米)',
|
||||
total_volume_cbm DECIMAL(18,4) NULL COMMENT '体积(立方米),来源未确认时为空',
|
||||
planned_delivery_at DATETIME(3) NULL COMMENT '预计派送时间',
|
||||
dispatched_at DATETIME(3) NULL COMMENT '实际派送时间',
|
||||
expected_arrival_at DATETIME(3) NULL COMMENT '预计到达时间',
|
||||
@@ -2053,7 +2053,7 @@ CREATE TABLE IF NOT EXISTS ll_outbound_order (
|
||||
overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外仓ID', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_OUTBOUND' COMMENT '业务状态',
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', pallet_count INT NOT NULL DEFAULT 0 COMMENT '托盘数',
|
||||
gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),来源未确认时为空', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)',
|
||||
volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)',
|
||||
volume_cbm DECIMAL(18,4) NULL COMMENT '体积(立方米),来源未确认时为空',
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', created_by VARCHAR(128) NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
outbound_type VARCHAR(32) NOT NULL DEFAULT 'DISPATCH_DELIVERY' COMMENT '出库类型:派送转出库、一件代发', lx_codes VARCHAR(1000) NULL COMMENT '关联一件代发领星入库单代码汇总',
|
||||
@@ -2069,7 +2069,7 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pallet_item (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', outbound_order_id BIGINT NOT NULL COMMENT '出库单ID',
|
||||
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', pallet_no VARCHAR(128) NULL COMMENT '托盘号',
|
||||
destination_warehouse_id VARCHAR(128) NULL COMMENT '目的仓ID', destination_warehouse_code VARCHAR(128) NULL COMMENT '目的仓编码快照', destination_warehouse_name VARCHAR(255) NULL COMMENT '目的仓名称快照',
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),来源未确认时为空', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', return_sku_quantity BIGINT NULL COMMENT '退货单SKU本来源计划出库件数快照,NULL为历史整箱出库', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照',
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),来源未确认时为空', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NULL COMMENT '体积(立方米),来源未确认时为空', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', return_sku_quantity BIGINT NULL COMMENT '退货单SKU本来源计划出库件数快照,NULL为历史整箱出库', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code),
|
||||
KEY idx_ll_outbound_pallet_list (customer_id,outbound_order_id), KEY idx_ll_outbound_pallet_destination (customer_id,destination_warehouse_code)
|
||||
|
||||
@@ -2054,6 +2054,10 @@ components:
|
||||
required: [deliveryNo, packageCount, signedPackages]
|
||||
properties:
|
||||
deliveryNo: { type: string }
|
||||
customerName: { type: string, nullable: true, description: 客户名称快照;退货换标直出创建时从权威客户目录获取,查询不按ID猜名 }
|
||||
overseasWarehouseName: { type: string, nullable: true, description: 作业海外仓名称快照;退货换标直出创建时从权威仓库目录获取 }
|
||||
totalWeightKg: { type: number, nullable: true, description: 实物毛重千克;退货换标直出按选中实际箱每箱一次汇总,任一箱缺测则为空,不按SKU件数倍乘 }
|
||||
totalVolumeCbm: { type: number, nullable: true, description: 实物体积立方米;退货换标直出按每箱实测长宽高厘米相乘除以1000000,任一箱缺尺寸则为空 }
|
||||
packageCount: { type: integer, minimum: 0 }
|
||||
signedPackages:
|
||||
type: integer
|
||||
@@ -2080,7 +2084,7 @@ components:
|
||||
order: { $ref: '#/components/schemas/DeliveryOrderRow' }
|
||||
skus:
|
||||
type: array
|
||||
description: 当前派送目的地的本次排车货物投影;数量按冻结排车与本次PDA实拣归属,不返回整单申报箱数。未实拣或MIXED库存没有单SKU归属时保留数量,SKU及品名为null;没有本次SKU测量事实时重量与体积为null,不按比例分摊整单测量。
|
||||
description: 当前派送目的地的本次排车货物投影;数量按冻结排车与本次PDA实拣归属,不返回整单申报箱数。未实拣或MIXED库存没有单SKU归属时保留数量,SKU及品名为null;没有本次SKU测量事实时重量与体积为null,不按比例分摊整单测量。退货换标直接出库沿签收货物约定,按每实物箱的sku_code排序,仅首SKU行承载1箱及冻结的整箱重量/体积,其余SKU行承载0箱、物理量为空;每行productQuantity仍是对应SKU件数,整箱物理量不是单SKU净重。
|
||||
items: { $ref: '#/components/schemas/DeliveryCargoSku' }
|
||||
DeliveryCargoSku:
|
||||
type: object
|
||||
@@ -2094,9 +2098,10 @@ components:
|
||||
fbaTrackingNo: { type: string, nullable: true }
|
||||
skuCode: { type: string, nullable: true }
|
||||
productName: { type: string, nullable: true }
|
||||
packageCount: { type: integer, minimum: 0, description: 本次排车/实拣的箱数,不是整单申报箱数 }
|
||||
weightKg: { type: number, nullable: true }
|
||||
volumeCbm: { type: number, nullable: true }
|
||||
packageCount: { type: integer, minimum: 0, description: 本次实物箱贡献数,不是件数或整单申报箱数;退货换标直出同箱仅首SKU行计1,其余为0 }
|
||||
productQuantity: { type: string, nullable: true, pattern: '^[0-9]+$', description: 退货箱中本行SKU件数,long按字符串返回;不用于乘算箱级重量或体积 }
|
||||
weightKg: { type: number, nullable: true, description: 千克;退货换标直出同箱首SKU行承载冻结整箱毛重,其余行或缺测为null }
|
||||
volumeCbm: { type: number, nullable: true, description: 立方米;退货换标直出同箱首SKU行承载冻结整箱体积,其余行或缺测为null }
|
||||
ReturnOrderCreate:
|
||||
type: object
|
||||
required: [overseasWarehouseId, needRelabel, idempotencyKey, items]
|
||||
|
||||
Reference in New Issue
Block a user