贴标申请,仓储转运

This commit is contained in:
2026-10-12 18:01:16 +08:00
parent c06e788b0f
commit 46507e41c6
17 changed files with 214 additions and 16 deletions
@@ -258,11 +258,13 @@ public interface DispatchCandidateMapper {
@Param("relabelNo") String relabelNo);
@Select("<script>SELECT a.customer_id AS customerId,a.lastleg_order_id AS orderId,a.overseas_warehouse_id AS overseasWarehouseId," +
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,'RETURN_RELABEL' AS orderType,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
"GROUP_CONCAT(DISTINCT c.source_return_no ORDER BY c.source_return_no SEPARATOR ',') AS inboundCode," +
"c.pallet_code AS palletNo,COUNT(DISTINCT c.id) AS packageCount,'RELABEL' AS sourceStage,1 AS returnCartonSource," +
"GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') AS relabelCartonMarks," +
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.weight) THEN SUM(x.weight) END FROM (SELECT DISTINCT c2.id,c2.weight FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.weight FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS grossWeightKg," +
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.volume) THEN SUM(x.volume) END FROM (SELECT DISTINCT c2.id,(c2.length*c2.width*c2.height)/1000000 AS volume FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.length*rr.width*rr.height/1000000 FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS volumeCbm," +
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode " +
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
@@ -25,7 +25,14 @@ public final class PalletOperationResponses {
String palletCode, String status, int palletVersion,
@JsonSerialize(using = ToStringSerializer.class) Long locationId,
String locationCode, String destinationWarehouseCode,
long totalPackages, List<InventoryLine> inventory) { }
long totalPackages, List<InventoryLine> inventory,
java.math.BigDecimal grossWeightKg, java.math.BigDecimal volumeCbm) {
public PalletContent(long palletId, String palletCode, String status, int palletVersion, Long locationId,
String locationCode, String destinationWarehouseCode, long totalPackages, List<InventoryLine> inventory) {
this(palletId, palletCode, status, palletVersion, locationId, locationCode, destinationWarehouseCode,
totalPackages, inventory, null, null);
}
}
public record PrintablePallet(@JsonSerialize(using = ToStringSerializer.class) long palletId,
String palletCode, int palletVersion,
String relabelNo, String printableHtml) { }
@@ -316,7 +316,8 @@ public class PdaPalletOperationService {
PalletOperationRequests.RelabelDisassemble request) {
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
Set<String> alreadyBound = boundRelabelMarks(application);
if (selected.stream().map(RelabelItem::getCartonMark).anyMatch(alreadyBound::contains))
boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark()));
if (!rebind && selected.stream().map(RelabelItem::getCartonMark).anyMatch(alreadyBound::contains))
throw conflict("退货换标箱已在本轮完成组托,请刷新后重试");
long quantity = 0;
List<ReturnInventoryCarton> cartons = new ArrayList<>();
@@ -330,8 +331,8 @@ public class PdaPalletOperationService {
quantity += value(item.getNotifiedPackageCount());
cartons.add(carton);
}
if (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|| request.actualSource().packages() != quantity)
if (!rebind && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|| request.actualSource().packages() != quantity))
throw conflict("退货换标拆出数量必须等于本批已完成贴标数量");
String key = required(request.idempotencyKey(), "幂等键不能为空");
String requestHash = hash(request);
@@ -836,6 +837,8 @@ public class PdaPalletOperationService {
Set<String> destinations = new HashSet<>();
selected.forEach(item -> destinations.add(text(item.getDestinationFbaWarehouseCode())));
if (destinations.size() != 1 || destinations.contains("")) throw conflict("请按已维护的新目的仓分批组托");
returnCartons.palletCartons(String.valueOf(warehouseId), target.palletCode())
.forEach(carton -> destinations.add(text(carton.getDestinationWarehouseCode())));
for (PalletInventoryRow row : mapper.content(String.valueOf(warehouseId), target.palletId())) {
var bound = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
.eq("customer_id", row.getCustomerId()).eq("source_inventory_position_id", row.getPositionId()));
@@ -898,10 +901,22 @@ public class PdaPalletOperationService {
value(row.getReceivingPackages()), value(row.getVersion()));
}).toList();
long total = rows.stream().mapToLong(PdaPalletOperationService::rowPackages).sum();
var boxes = returnCartons.palletCartons(String.valueOf(warehouseId), pallet.palletCode());
boxes.forEach(box -> destinations.add(text(box.getDestinationWarehouseCode())));
total += boxes.size();
// Derive totals from current physical bindings, so retries and rebinds cannot double count.
java.math.BigDecimal weight = rows.isEmpty() ? java.math.BigDecimal.ZERO : null;
java.math.BigDecimal volume = rows.isEmpty() ? java.math.BigDecimal.ZERO : null;
for (var box : boxes) {
if (weight != null) weight = box.getWeight() == null || box.getWeight().signum() <= 0 ? null : weight.add(box.getWeight());
if (volume != null) volume = box.getLength() == null || box.getWidth() == null || box.getHeight() == null
|| box.getLength().signum() <= 0 || box.getWidth().signum() <= 0 || box.getHeight().signum() <= 0 ? null
: volume.add(box.getLength().multiply(box.getWidth()).multiply(box.getHeight()).movePointLeft(6));
}
String locationCode = locationCode(warehouseId, pallet.locationId());
return new PalletOperationResponses.PalletContent(pallet.palletId(), pallet.palletCode(), pallet.status(),
pallet.version(), pallet.locationId(), locationCode, destinations.size() == 1 ? destinations.iterator().next() : null,
total, inventory);
total, inventory, weight, volume);
}
private PdaPalletOperation begin(PdaWarehouseIdentity identity, String type, String key, String requestHash) {
@@ -42,7 +42,18 @@ public final class PdaRelabelResponses {
List<SkuItem> skuItems,
int expectedPackages, int relabelledPackages, int skippedPackages,
int exceptionPackages, int cartonProcessedPackages, int skuProcessedPackages,
String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity) {
String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity,
String sourcePalletCode, String sourcePalletBasis) {
public Item(Long itemId, String skuCode, String productNameZh, String shipmentNo, String trackingCode,
String newSkuCode, String labelRequirement, String relabelFileId, String relabelFileName, String relabelFileUrl,
String cartonMark, String fbaCartonMark, String operationType, List<SkuItem> skuItems, int expectedPackages,
int relabelledPackages, int skippedPackages, int exceptionPackages, int cartonProcessedPackages,
int skuProcessedPackages, String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity) {
this(itemId, skuCode, productNameZh, shipmentNo, trackingCode, newSkuCode, labelRequirement,
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark, operationType, skuItems,
expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, cartonProcessedPackages,
skuProcessedPackages, destinationFbaWarehouseCode, returnCartonId, returnQuantity, null, null);
}
public Item(Long itemId, String skuCode, String productNameZh, String shipmentNo, String trackingCode,
String newSkuCode, String labelRequirement, String relabelFileId, String relabelFileName, String relabelFileUrl,
String cartonMark, String fbaCartonMark, String operationType, List<SkuItem> skuItems, int expectedPackages,
@@ -92,6 +92,7 @@ public class PdaRelabelService {
@Resource private PdaRelabelTaskMapper tasks;
@Resource private PdaRelabelUnitFactMapper unitFacts;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper sourcePositions;
@Resource private PdaRelabelOperationMapper operations;
@Resource private PdaRelabelHistoryMapper histories;
@Resource private RelabelApplicationService applicationService;
@@ -786,7 +787,7 @@ public class PdaRelabelService {
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "CARTON") ? 1 : 0),
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "SKU") ? 1 : 0),
item.getDestinationFbaWarehouseCode(), item.getReturnCartonId(),
returnQuantity(item));
returnQuantity(item), sourcePalletCode(item), item.getReturnCartonId() != null ? "RETURN_CARTON" : "INVENTORY_REFERENCE");
}).toList();
List<PdaRelabelResponses.UnitFact> factResponses = facts.stream().map(fact ->
new PdaRelabelResponses.UnitFact(fact.getId(), fact.getItemId(), fact.getOldLabel(), fact.getNewLabel(),
@@ -881,6 +882,16 @@ public class PdaRelabelService {
List.class, PdaRelabelResponses.SkuItem.class));
} catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
}
private String sourcePalletCode(RelabelItem item) {
if (item.getReturnCartonId() != null) {
var carton = returnCartons == null ? null : returnCartons.selectById(item.getReturnCartonId());
return carton != null && Objects.equals(item.getCustomerId(), carton.getCustomerId()) ? carton.getPalletCode() : null;
}
var position = sourcePositions == null || item.getSourceInventoryPositionId() == null ? null
: sourcePositions.findTenantPosition(item.getCustomerId(), item.getSourceInventoryPositionId());
return position != null ? position.getPalletNo() : null;
}
private long returnQuantity(RelabelItem item) {
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
return carton == null || carton.getTotalQuantity() == null ? 0L : carton.getTotalQuantity();
@@ -25,11 +25,18 @@ public final class PdaReturnReceiptRequests {
@Size(max = 16) String unitType,
@Size(max = 128) String unitCode,
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
Long totalBoxCount) {
Long totalBoxCount, Boolean originalPackaging) {
public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
List<@Valid Item> items, String idempotencyKey) {
this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey,
null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null);
}
public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
List<@Valid Item> items, String idempotencyKey, String unitType, String unitCode,
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
Long totalBoxCount) {
this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey, unitType, unitCode,
length, width, height, weight, totalBoxCount, null);
}
}
public record Item(@NotBlank @Size(max = 128) String skuCode,
@@ -3,14 +3,20 @@ package com.baiyue.lastleg.returnreceipt.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
import java.math.BigDecimal;
public final class ReturnCartonCountDtos {
private ReturnCartonCountDtos() { }
public record Query(@Min(1) long page, @Min(1) @Max(50) long size, @Size(max=128) String keyword) { }
public record Sku(String skuCode, String productNameZh, long quantity) { }
public record Box(String cartonMark, List<Sku> items) { }
public record Box(String cartonMark, List<Sku> items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) {
public Box(String cartonMark, List<Sku> items) { this(cartonMark, items, null, null, null, null); }
}
public record Task(String receiptNo, String returnNo, String palletCode, String status, List<Sku> expectedItems, List<Box> boxes) { }
public record CountItem(@NotBlank @Size(max=128) String skuCode, @Min(1) long quantity) { }
public record CountBox(@NotBlank @Size(max=128) String cartonMark, @NotEmpty @Size(max=100) List<@NotNull @Valid CountItem> items) { }
public record CountBox(@NotBlank @Size(max=128) String cartonMark, @NotEmpty @Size(max=100) List<@NotNull @Valid CountItem> items,
@Positive BigDecimal length, @Positive BigDecimal width, @Positive BigDecimal height, @Positive BigDecimal weight) {
public CountBox(String cartonMark, List<@NotNull @Valid CountItem> items) { this(cartonMark, items, null, null, null, null); }
}
public record Confirm(@NotBlank @Size(max=128) String idempotencyKey, @NotEmpty @Size(max=500) List<@NotNull @Valid CountBox> boxes) { }
}
@@ -41,6 +41,7 @@ public class ReturnInventoryCarton {
private BigDecimal width;
private BigDecimal height;
private BigDecimal weight;
private Boolean originalPackaging;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime mergedAt;
@@ -23,6 +23,10 @@ public class ReturnReceipt {
private BigDecimal width;
private BigDecimal height;
private BigDecimal weight;
/** Whether the signed physical unit reused its original box/packaging. */
private Boolean originalPackaging;
/** PALLET_MEASURED for a measured carrier, UNIT_SUM for child-unit aggregation. */
private String measurementSource;
private String palletCode;
private String cartonMark;
private String trackingNo;
@@ -10,6 +10,10 @@ import java.util.List;
@Mapper
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} " +
"AND pallet_code=#{palletCode} AND total_quantity>0 " +
"AND status IN ('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','DISCARD_PENDING') ORDER BY id")
List<ReturnInventoryCarton> palletCartons(@Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
@Select("SELECT EXISTS(SELECT 1 FROM ll_return_inventory_outbound_movement WHERE carton_id=#{cartonId} AND quantity>0)")
boolean hasOutbound(@Param("cartonId") long cartonId);
@Update("UPDATE ll_return_inventory_carton SET relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL WHERE id=#{id}")
@@ -129,6 +129,7 @@ public class ReturnCartonCountService {
Set<String> marks = new HashSet<>();
for (CountBox box : request.boxes()) {
if (!byMark.containsKey(box.cartonMark()) || !marks.add(box.cartonMark())) throw error(HttpStatus.BAD_REQUEST, "箱号不属于本次签收或重复填写");
if (box.length() == null || box.width() == null || box.height() == null || box.weight() == null || box.length().signum() <= 0 || box.width().signum() <= 0 || box.height().signum() <= 0 || box.weight().signum() <= 0) throw error(HttpStatus.BAD_REQUEST, "每个箱子必须填写有效的长、宽、高和重量");
Set<String> skus = new HashSet<>();
for (CountItem line : box.items()) {
if (!expected.containsKey(line.skuCode()) || !skus.add(line.skuCode()) || line.quantity() <= 0) throw error(HttpStatus.BAD_REQUEST, "SKU 不属于本次签收、数量无效或同箱重复");
@@ -144,7 +145,7 @@ public class ReturnCartonCountService {
item.setCustomerId(box.getCustomerId()); item.setSkuCode(line.skuCode()); item.setProductNameZh(names.get(line.skuCode())); item.setQuantity(line.quantity());
item.setPackingReservedQuantity(0L); items.insert(item);
}
box.setSkuTypes(input.items().size()); box.setTotalQuantity(input.items().stream().mapToLong(CountItem::quantity).sum());
box.setSkuTypes(input.items().size()); box.setTotalQuantity(input.items().stream().mapToLong(CountItem::quantity).sum()); box.setLength(input.length()); box.setWidth(input.width()); box.setHeight(input.height()); box.setWeight(input.weight());
box.setCountStatus("CONFIRMED"); box.setCountedAt(now); box.setCountedBy(identity.operatorKey()); box.setUpdatedAt(now); cartons.updateById(box);
}
for (String sku : expected.keySet()) {
@@ -167,7 +168,8 @@ public class ReturnCartonCountService {
private Task task(ReturnReceipt receipt, List<ReturnInventoryCarton> boxes) {
List<Sku> expected = receiptItems.listReceipt(receipt.getId()).stream().map(line -> new Sku(line.getSkuCode(), null, line.getReceivedQuantity())).toList();
List<Box> contents = boxes.stream().map(box -> new Box(box.getCartonMark(), items.listCarton(box.getId()).stream()
.map(line -> new Sku(line.getSkuCode(), line.getProductNameZh(), "PENDING".equals(box.getCountStatus()) ? 0 : line.getQuantity())).toList())).toList();
.map(line -> new Sku(line.getSkuCode(), line.getProductNameZh(), "PENDING".equals(box.getCountStatus()) ? 0 : line.getQuantity())).toList(),
box.getLength(), box.getWidth(), box.getHeight(), box.getWeight())).toList();
return new Task(receipt.getReceiptNo(), boxes.isEmpty() ? null : boxes.get(0).getSourceReturnNo(), receipt.getPalletCode(),
boxes.stream().allMatch(box -> "CONFIRMED".equals(box.getCountStatus())) ? "CONFIRMED" : sessions.findReceipt(receipt.getId()) != null ? "COUNTING" : "PENDING", expected, contents);
}
@@ -509,6 +509,7 @@ public class ReturnInventoryService {
// Pallet measurements describe the carrier, never each child carton.
if ("BOX".equals(receipt.getUnitType()) || "PARCEL".equals(receipt.getUnitType())) {
carton.setLength(receipt.getLength()); carton.setWidth(receipt.getWidth()); carton.setHeight(receipt.getHeight()); carton.setWeight(receipt.getWeight());
carton.setOriginalPackaging(receipt.getOriginalPackaging());
}
carton.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
cartons.insert(carton);
@@ -1086,6 +1087,16 @@ public class ReturnInventoryService {
}
if (existingTarget != null) {
target.setTotalQuantity(cartonItems.totalQuantity(targetId)); target.setSkuTypes(cartonItems.skuTypes(targetId));
if (request.weight() != null) target.setWeight(request.weight());
else {
BigDecimal combinedWeight = target.getWeight();
for (var source : sources) combinedWeight = combinedWeight == null || source.getWeight() == null
? null : combinedWeight.add(source.getWeight());
target.setWeight(combinedWeight);
}
if (request.length() != null) target.setLength(request.length());
if (request.width() != null) target.setWidth(request.width());
if (request.height() != null) target.setHeight(request.height());
target.setUpdatedAt(now); target.setLocationCode(null); target.setPutawayAt(null); cartons.updateById(target); cartons.clearPutaway(targetId);
}
for (TraceTransfer transfer : transfers) packingTrace.insert(IdWorker.getId(), customerId, operation.getId(),
@@ -618,6 +618,14 @@ public class ReturnReceiptService {
|| request.height().signum() <= 0 || request.weight().signum() <= 0)) {
throw badRequest("托盘签收必须填写有效的长、宽、高和重量");
}
if ("BOX".equals(unitType) || "PARCEL".equals(unitType)) {
if (request.originalPackaging() == null) throw badRequest("箱子或包裹必须选择是否使用原箱原包装");
if (request.length() == null || request.width() == null || request.height() == null || request.weight() == null
|| request.length().signum() <= 0 || request.width().signum() <= 0
|| request.height().signum() <= 0 || request.weight().signum() <= 0) {
throw badRequest("箱子或包裹必须填写有效的长、宽、高和重量");
}
}
LocalDateTime now = LocalDateTime.now();
long receiptId = IdWorker.getId();
ReturnReceipt receipt = new ReturnReceipt();
@@ -629,6 +637,8 @@ public class ReturnReceiptService {
receipt.setTotalBoxCount(totalBoxCount);
receipt.setPalletCode("PALLET".equals(unitType) ? unitCode : ("BOX".equals(unitType) ? text(request.palletCode()) : null));
receipt.setLength(request.length()); receipt.setWidth(request.width()); receipt.setHeight(request.height()); receipt.setWeight(request.weight());
receipt.setOriginalPackaging(request.originalPackaging());
receipt.setMeasurementSource("PALLET".equals(unitType) ? "PALLET_MEASURED" : "UNIT_MEASURED");
receipt.setReceivedQuantity(receiptQuantity); receipt.setOperatorId(identity.operatorKey());
receipt.setOperatorName(identity.operatorName()); receipt.setIdempotencyKey(key); receipt.setRequestHash(requestHash);
receipt.setSignedAt(now); receipt.setCreatedAt(now);
@@ -3018,6 +3018,8 @@ EXECUTE ll_return_receipt_alter_stmt;
DEALLOCATE PREPARE ll_return_receipt_alter_stmt;
SET @ll_return_receipt_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''本次签收绑定的托盘码'' AFTER receipt_batch_no'));
PREPARE ll_return_receipt_add_pallet_stmt FROM @ll_return_receipt_add_pallet; EXECUTE ll_return_receipt_add_pallet_stmt; DEALLOCATE PREPARE ll_return_receipt_add_pallet_stmt;
SET @ll_return_receipt_add_measurement_facts = (SELECT IF(COUNT(*)=0, 'ALTER TABLE ll_return_receipt ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'', ADD COLUMN measurement_source VARCHAR(32) NULL COMMENT ''尺寸重量来源'' AFTER weight', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='original_packaging');
PREPARE ll_return_receipt_add_measurement_facts_stmt FROM @ll_return_receipt_add_measurement_facts; EXECUTE ll_return_receipt_add_measurement_facts_stmt; DEALLOCATE PREPARE ll_return_receipt_add_measurement_facts_stmt;
CREATE TABLE IF NOT EXISTS ll_return_receipt_item (
id BIGINT NOT NULL COMMENT '签收SKU明细ID', receipt_id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', return_order_item_id BIGINT NOT NULL COMMENT '退货预报明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', received_quantity BIGINT NOT NULL COMMENT '本次SKU实收数量',
PRIMARY KEY(id), KEY idx_ll_return_receipt_item_receipt(receipt_id), KEY idx_ll_return_receipt_item_order_item(return_order_item_id), CONSTRAINT chk_ll_return_receipt_item_quantity CHECK(received_quantity>0)
@@ -3037,6 +3039,8 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛';
SET @ll_return_carton_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER source_batch_no'));
PREPARE ll_return_carton_add_pallet_stmt FROM @ll_return_carton_add_pallet; EXECUTE ll_return_carton_add_pallet_stmt; DEALLOCATE PREPARE ll_return_carton_add_pallet_stmt;
SET @ll_return_carton_add_original_packaging = (SELECT IF(COUNT(*)=0, 'ALTER TABLE ll_return_inventory_carton ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'' AFTER weight', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='original_packaging');
PREPARE ll_return_carton_add_original_packaging_stmt FROM @ll_return_carton_add_original_packaging; EXECUTE ll_return_carton_add_original_packaging_stmt; DEALLOCATE PREPARE ll_return_carton_add_original_packaging_stmt;
-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt;
@@ -92,6 +92,84 @@ class PdaPalletOperationServiceTest {
service.configureTransactions(new NoOpTransactionManager());
}
@Test void returnPalletTotalsFollowCurrentBindingsWithoutCountingRetriesTwice() {
var first = new ReturnInventoryCarton(); first.setWeight(new java.math.BigDecimal("5"));
first.setLength(new java.math.BigDecimal("30")); first.setWidth(new java.math.BigDecimal("20"));
first.setHeight(new java.math.BigDecimal("20")); first.setDestinationWarehouseCode("SCK8");
var second = new ReturnInventoryCarton(); second.setWeight(new java.math.BigDecimal("11"));
second.setLength(new java.math.BigDecimal("40")); second.setWidth(new java.math.BigDecimal("30"));
second.setHeight(new java.math.BigDecimal("30")); second.setDestinationWarehouseCode("SCK8");
var pallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "OCCUPIED", "SCK8", 2);
when(returnCartons.palletCartons("88", "TARGET")).thenReturn(List.of(first), List.of(first, second), List.of(first, second), List.of(first));
var before = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
assertEquals(0, before.grossWeightKg().compareTo(new java.math.BigDecimal("5")));
var added = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
assertEquals(2, added.totalPackages());
assertEquals(0, added.grossWeightKg().compareTo(new java.math.BigDecimal("16")));
assertEquals(0, added.volumeCbm().compareTo(new java.math.BigDecimal("0.048")));
assertEquals(added, ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet));
assertEquals(before, ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet));
}
@Test void returnRebindingRequiresCurrentRoundBindingAndAddsToExistingPallet() {
var workTasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
var workHistory = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", workTasks);
ReflectionTestUtils.setField(service, "relabelWorkHistory", workHistory);
var app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(700L);
when(workTasks.find("100", 600L)).thenReturn(task);
var item = new RelabelItem(); item.setReturnCartonId(801L); item.setCartonMark("BOX-1");
item.setDestinationFbaWarehouseCode("SCK8"); item.setNotifiedPackageCount(1);
var box = new ReturnInventoryCarton(); box.setId(801L); box.setCustomerId("100");
box.setPalletCode("SOURCE"); box.setRelabelApplicationId(600L); box.setStatus("RELABEL_PENDING");
box.setTotalQuantity(10L); box.setWeight(new java.math.BigDecimal("11"));
box.setLength(new java.math.BigDecimal("40")); box.setWidth(new java.math.BigDecimal("30"));
box.setHeight(new java.math.BigDecimal("30")); box.setDestinationWarehouseCode("SCK8");
var existing = new ReturnInventoryCarton(); existing.setWeight(new java.math.BigDecimal("5"));
existing.setLength(new java.math.BigDecimal("30")); existing.setWidth(new java.math.BigDecimal("20"));
existing.setHeight(new java.math.BigDecimal("20")); existing.setDestinationWarehouseCode("SCK8");
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L, "OCCUPIED", "SCK8", 1);
var target = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L, "OCCUPIED", "SCK8", 2);
when(returnCartons.lockId(801L)).thenReturn(box);
when(returnCartons.palletCartons("88", "TARGET")).thenAnswer(call ->
"TARGET".equals(box.getPalletCode()) ? List.of(existing, box) : List.of(existing));
var request = new PalletOperationRequests.RelabelDisassemble("rebind-current", "RL-1",
new PalletOperationRequests.PalletExpectation("SOURCE", 1),
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("BOX-1"), "rebind");
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
verifyNoInteractions(physical);
var binding = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
binding.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"BOX-1\"]}");
when(workHistory.selectList(any())).thenReturn(List.of(binding));
task.setReviewedAt(LocalDateTime.of(2026, 10, 12, 12, 0));
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
task.setReviewedAt(null);
when(physical.resolve("SOURCE")).thenReturn(source);
when(physical.resolve("TARGET")).thenReturn(target);
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation)
ReflectionTestUtils.invokeMethod(service, "bindReturnRelabelPallet", identity(), app,
List.of(item), source, target, request);
assertEquals("TARGET", box.getPalletCode());
assertEquals(0, result.pallets().get(0).totalPackages());
assertEquals(2, result.pallets().get(1).totalPackages());
assertEquals(0, result.pallets().get(1).grossWeightKg().compareTo(new java.math.BigDecimal("16")));
assertEquals(0, result.pallets().get(1).volumeCbm().compareTo(new java.math.BigDecimal("0.048")));
verify(returnCartons).updateById(box);
verify(workHistory).insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class));
}
@Test void missingCartonMeasurementDoesNotBecomeZeroWeight() {
var box = new ReturnInventoryCarton(); box.setWeight(new java.math.BigDecimal("5"));
when(returnCartons.palletCartons("88", "TARGET")).thenReturn(List.of(box));
var pallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "OCCUPIED", "SCK8", 2);
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
assertNull(result.volumeCbm());
assertEquals(0, result.grossWeightKg().compareTo(new java.math.BigDecimal("5")));
}
@Test void partialReturnRelabelKeepsUnlabelledRemainderOnOriginalPallet() {
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100"); app.setRelabelNo("RL-1");
RelabelItem item = new RelabelItem(); item.setId(701L); item.setSkuCode("SKU-1");
@@ -82,6 +82,31 @@ class PdaRelabelServiceTest {
lenient().when(tasks.updateWithVersion(any(), anyInt())).thenReturn(1);
}
@Test
void detailDistinguishesInventoryReferenceFromRecordedReturnPallet() {
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
var positions = org.mockito.Mockito.mock(com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper.class);
ReflectionTestUtils.setField(service, "sourcePositions", positions);
item.setSourceInventoryPositionId(30L);
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition();
position.setPalletNo("PLT-REFERENCE");
when(positions.findTenantPosition("100", 30L)).thenReturn(position);
var ordinary = service.detail("7", 900L, "FBR-1").items().get(0);
assertEquals("PLT-REFERENCE", ordinary.sourcePalletCode());
assertEquals("INVENTORY_REFERENCE", ordinary.sourcePalletBasis());
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
ReflectionTestUtils.setField(service, "returnCartons", cartons);
item.setReturnCartonId(40L);
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
carton.setCustomerId("100"); carton.setPalletCode("PLT-RETURN"); carton.setTotalQuantity(20L);
when(cartons.selectById(40L)).thenReturn(carton);
var returned = service.detail("7", 900L, "FBR-1").items().get(0);
assertEquals("PLT-RETURN", returned.sourcePalletCode());
assertEquals("RETURN_CARTON", returned.sourcePalletBasis());
carton.setCustomerId("OTHER");
assertEquals(null, service.detail("7", 900L, "FBR-1").items().get(0).sourcePalletCode());
}
@Test
void candidatesUseCustomerNameOnlyFromMatchingCustomerOrder() {
var orders = org.mockito.Mockito.mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);
@@ -454,7 +454,7 @@ class ReturnReceiptServiceTest {
stubNewReceipt(order, List.of(line), "SP-SOURCE");
var request = new PdaReturnReceiptRequests.Confirm("RT-1", null, "SKU", null,
List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)), "parcel-key",
"PARCEL", null, null, null, null, null, null);
"PARCEL", null, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, null, true);
var result = service.confirm(identity, request);
var capture = ArgumentCaptor.forClass(ReturnReceipt.class);
verify(receipts).insert(capture.capture());
@@ -611,6 +611,6 @@ class ReturnReceiptServiceTest {
// These focused tests exercise receipt/idempotency behavior at the
// carton level. Pallet receipts have a separate multi-carton contract.
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, palletCode, boxed, key,
"BOX", palletCode, null, null, null, null, 1L);
"BOX", palletCode, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, 1L, true);
}
}