生产更新:同步已提交的海外仓分析及业务修复(2026-10-16) #27
@@ -2414,6 +2414,7 @@ CREATE TABLE ll_return_inventory_balance (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退货库存按客户海外仓SKU汇总余额';
|
||||
|
||||
CREATE TABLE ll_return_inventory_carton (
|
||||
original_packaging TINYINT(1) NULL COMMENT '是否使用原箱原包装',
|
||||
label_status varchar(32) DEFAULT NULL COMMENT 'WAIT_LABEL待贴标 CONFIRMED已扫码确认标签',
|
||||
new_carton_mark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '换标后新箱唛',
|
||||
id bigint NOT NULL COMMENT '退货库存箱ID',
|
||||
@@ -2565,7 +2566,9 @@ CREATE TABLE ll_return_receipt (
|
||||
length DECIMAL(12,3) NULL COMMENT '实测尺寸cm',
|
||||
width DECIMAL(12,3) NULL COMMENT '实测尺寸cm',
|
||||
height DECIMAL(12,3) NULL COMMENT '实测尺寸cm',
|
||||
weight DECIMAL(12,3) NULL COMMENT '实测重量kg'
|
||||
weight DECIMAL(12,3) NULL COMMENT '实测重量kg',
|
||||
original_packaging TINYINT(1) NULL COMMENT '是否使用原箱原包装',
|
||||
measurement_source VARCHAR(32) NULL COMMENT '尺寸重量来源'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PDA退货实收签收事实';
|
||||
|
||||
CREATE TABLE ll_return_receipt_batch (
|
||||
|
||||
+3
-1
@@ -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 " +
|
||||
|
||||
+8
-1
@@ -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) { }
|
||||
|
||||
+19
-4
@@ -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) {
|
||||
|
||||
+12
-1
@@ -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,
|
||||
|
||||
+12
-1
@@ -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();
|
||||
|
||||
+9
-2
@@ -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,
|
||||
|
||||
+8
-2
@@ -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) { }
|
||||
}
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+4
@@ -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}")
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
+11
@@ -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(),
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
-17
@@ -151,12 +151,6 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
// A split relabel transfer may intentionally defer the whole-order
|
||||
// destination: relabeled cartons go through relabel dispatch while the
|
||||
// remaining cartons are bound later from warehouse-transfer outbound.
|
||||
// All other transfer types must still select FBA or PRIVATE up front.
|
||||
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
|
||||
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
|
||||
@@ -292,8 +286,6 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
|
||||
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -1508,15 +1500,6 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode,
|
||||
boolean allowDeferredForRelabel) {
|
||||
String type = trim(destinationType);
|
||||
if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA";
|
||||
if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) {
|
||||
throw badRequest("仓储转运必须选择 FBA 仓或私人仓");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) {
|
||||
String type = trim(request.destinationType());
|
||||
if (type == null && StringUtils.hasText(request.fbaWarehouseCode())) type = "FBA";
|
||||
|
||||
@@ -2978,6 +2978,7 @@ CREATE TABLE IF NOT EXISTS ll_return_order_item (
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_item_order_sku(return_order_id,sku_code), KEY idx_ll_return_item_sku(sku_code,received_quantity), CONSTRAINT chk_ll_return_item_quantity CHECK(expected_quantity>0 AND received_quantity>=0 AND received_quantity<=expected_quantity)
|
||||
) ENGINE=InnoDB COMMENT='客户退货订单的SKU预报与实收明细';
|
||||
CREATE TABLE IF NOT EXISTS ll_return_receipt (
|
||||
original_packaging TINYINT(1) NULL COMMENT '是否使用原箱原包装', measurement_source VARCHAR(32) NULL COMMENT '尺寸重量来源',
|
||||
id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', return_order_id BIGINT NOT NULL COMMENT '退货订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', receipt_no VARCHAR(128) NOT NULL COMMENT '签收单号', receipt_batch_no VARCHAR(128) NOT NULL COMMENT '签收批次号', tracking_no VARCHAR(128) NULL COMMENT '本次签收包裹的可选快递单号', bound_by VARCHAR(16) NOT NULL COMMENT '开始签收方式,RETURN_NO退货单号或SKU候选', status VARCHAR(32) NOT NULL COMMENT '签收状态,当前SIGNED', received_quantity BIGINT NOT NULL COMMENT '本次实收数量', operator_id VARCHAR(128) NOT NULL COMMENT 'PDA操作人ID', operator_name VARCHAR(128) NULL COMMENT 'PDA操作人名称', idempotency_key VARCHAR(255) NOT NULL COMMENT 'PDA确认幂等键', request_hash CHAR(64) NOT NULL COMMENT 'PDA确认请求SHA-256', signed_at DATETIME(3) NOT NULL COMMENT '签收时间', created_at DATETIME(3) NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_receipt_no(receipt_no), UNIQUE KEY uk_ll_return_receipt_batch(receipt_batch_no), UNIQUE KEY uk_ll_return_receipt_operator_key(operator_id,idempotency_key), KEY idx_ll_return_receipt_order(return_order_id,signed_at), CONSTRAINT chk_ll_return_receipt_quantity CHECK(received_quantity>0), CONSTRAINT chk_ll_return_receipt_bound CHECK(bound_by IN('RETURN_NO','SKU'))
|
||||
) ENGINE=InnoDB COMMENT='PDA退货实收签收事实';
|
||||
@@ -3018,6 +3019,10 @@ 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 ''是否使用原箱原包装'' 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;
|
||||
SET @ll_return_receipt_add_measurement_facts = (SELECT IF(COUNT(*)=0, 'ALTER TABLE ll_return_receipt ADD COLUMN measurement_source VARCHAR(32) NULL COMMENT ''尺寸重量来源'' AFTER original_packaging', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='measurement_source');
|
||||
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)
|
||||
@@ -3031,12 +3036,15 @@ CREATE TABLE IF NOT EXISTS ll_return_receipt_batch (
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='carton_mark'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN carton_mark VARCHAR(128) NULL COMMENT ''签收生成或绑定的退货箱号'' AFTER receipt_batch_no'));
|
||||
PREPARE return_receipt_carton_mark_stmt FROM @ddl; EXECUTE return_receipt_carton_mark_stmt; DEALLOCATE PREPARE return_receipt_carton_mark_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
|
||||
original_packaging TINYINT(1) NULL COMMENT '是否使用原箱原包装',
|
||||
new_carton_mark VARCHAR(255) NULL COMMENT '换标后新箱唛',
|
||||
id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', source_receipt_id BIGINT NULL COMMENT '来源PDA签收单ID', source_batch_no VARCHAR(128) NULL COMMENT '来源签收批次号', carton_mark VARCHAR(128) NOT NULL COMMENT '退货箱号和箱唛编码', status VARCHAR(32) NOT NULL COMMENT 'AVAILABLE可用或MERGED已合箱', sku_types INT NOT NULL COMMENT '箱内SKU种类数', total_quantity BIGINT NOT NULL COMMENT '箱内总件数', length DECIMAL(12,3) NULL COMMENT '箱长CM', width DECIMAL(12,3) NULL COMMENT '箱宽CM', height DECIMAL(12,3) NULL COMMENT '箱高CM', weight DECIMAL(12,3) NULL COMMENT '箱重KG', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', merged_at DATETIME(3) NULL COMMENT '合箱完成时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'))
|
||||
) 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;
|
||||
|
||||
@@ -1214,7 +1214,7 @@ paths:
|
||||
responses: { '200': { description: 当前客户仓储转运单分页 }, '403': { description: 缺少仓储转运查看权限 } }
|
||||
post:
|
||||
summary: 客户创建仓储转运单
|
||||
description: 权限 merchant:overseas-warehouse:transfer:create;客户范围只取认证 Token。
|
||||
description: 权限 merchant:overseas-warehouse:transfer:create;客户范围只取认证 Token。目的去向可暂未确定(destinationType 为 NONE 或不传),允许保存草稿、编辑和提交;入库确认后可在出库时绑定 FBA 仓或私人仓,也可选择入库库存发起贴标申请出库;普通转运出库前必须补齐目的仓。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 创建后的仓储转运单 }, '403': { description: 缺少仓储转运创建权限 }, '409': { description: 幂等、库存或状态冲突 } }
|
||||
/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}:
|
||||
|
||||
+82
@@ -92,6 +92,88 @@ 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.resolveLocation(88L, 1020L)).thenReturn(
|
||||
new PalletPhysicalGateway.LocationReference(1020L, "SOURCE-LOC", "STORAGE", "ACTIVE"));
|
||||
when(physical.resolveLocation(88L, 1010L)).thenReturn(
|
||||
new PalletPhysicalGateway.LocationReference(1010L, "TARGET-LOC", "STORAGE", "ACTIVE"));
|
||||
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");
|
||||
|
||||
+25
@@ -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);
|
||||
|
||||
+4
-3
@@ -65,8 +65,9 @@ class ReturnCartonCountServiceTest {
|
||||
when(cartons.lockReceipt(1L)).thenReturn(List.of(a,b));
|
||||
when(receiptItems.listReceipt(1L)).thenReturn(expected);
|
||||
}
|
||||
private CountBox measuredBox(String mark, List<CountItem> lines) { return new CountBox(mark, lines, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE); }
|
||||
private Confirm unequal(long first, long second) {
|
||||
return new Confirm("retry", List.of(new CountBox(a.getCartonMark(),List.of(new CountItem("SKU1",first))),new CountBox(b.getCartonMark(),List.of(new CountItem("SKU1",second)))));
|
||||
return new Confirm("retry", List.of(measuredBox(a.getCartonMark(),List.of(new CountItem("SKU1",first))),measuredBox(b.getCartonMark(),List.of(new CountItem("SKU1",second)))));
|
||||
}
|
||||
@Test void unequalBoxesAreConfirmedWithoutChangingSkuUnitBalanceAndRetryIsStable() {
|
||||
prepare(List.of(expected("SKU1",10)));
|
||||
@@ -97,7 +98,7 @@ class ReturnCartonCountServiceTest {
|
||||
when(items.lockCarton(3L)).thenReturn(List.of(provisional(3,"SKU2",4)));
|
||||
when(balances.adjustCartonQuantity("C1","123","SKU1",1)).thenReturn(1);
|
||||
when(balances.adjustCartonQuantity("C1","123","SKU2",1)).thenReturn(1);
|
||||
var request = new Confirm("mixed",List.of(new CountBox(a.getCartonMark(),List.of(new CountItem("SKU1",2),new CountItem("SKU2",1))),new CountBox(b.getCartonMark(),List.of(new CountItem("SKU1",4),new CountItem("SKU2",3)))));
|
||||
var request = new Confirm("mixed",List.of(measuredBox(a.getCartonMark(),List.of(new CountItem("SKU1",2),new CountItem("SKU2",1))),measuredBox(b.getCartonMark(),List.of(new CountItem("SKU1",4),new CountItem("SKU2",3)))));
|
||||
service.confirm(identity,"RR-1",request);
|
||||
verify(balances).adjustCartonQuantity("C1","123","SKU1",1); verify(balances).adjustCartonQuantity("C1","123","SKU2",1);
|
||||
verifyNoMoreInteractions(balances); assertThat(a.getSkuTypes()).isEqualTo(2);
|
||||
@@ -110,7 +111,7 @@ class ReturnCartonCountServiceTest {
|
||||
}
|
||||
@Test void duplicateOrForeignBoxesCannotBeSubmitted() {
|
||||
prepare(List.of(expected("SKU1",10)));
|
||||
var request = new Confirm("dup",List.of(new CountBox(a.getCartonMark(),List.of(new CountItem("SKU1",5))),new CountBox(a.getCartonMark(),List.of(new CountItem("SKU1",5)))));
|
||||
var request = new Confirm("dup",List.of(measuredBox(a.getCartonMark(),List.of(new CountItem("SKU1",5))),measuredBox(a.getCartonMark(),List.of(new CountItem("SKU1",5)))));
|
||||
assertThatThrownBy(() -> service.confirm(identity,"RR-1",request)).isInstanceOf(ResponseStatusException.class).hasMessageContaining("重复");
|
||||
verify(items,never()).deleteCarton(anyLong());
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-10
@@ -47,6 +47,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
@@ -438,20 +441,65 @@ class WarehouseTransferServiceTest {
|
||||
verifyNoInteractions(transfers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRejectsRequestWithoutDestination() {
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = {"NONE"})
|
||||
void transferWithoutDestinationCanBeCreatedAndSubmitted(String destinationType) {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
"IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5,
|
||||
"IN-1", List.of(11L), "WH-1", null, destinationType, null, null, null,
|
||||
null, null, null, null, "123 Pickup Road", null, null, 5,
|
||||
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
|
||||
null, null, null, "FBA destination will be supplied later", false, "key-no-fba",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")));
|
||||
null, null, null, null, "Destination will be supplied before outbound", false,
|
||||
null, null, null, null, null, "key-no-fba",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")), null);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.create("TENANT-A", "USER-1", request));
|
||||
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
|
||||
when(orders.selectList(any())).thenReturn(List.of(historicalOrder()));
|
||||
WarehouseTransferOrder draft = transfer(WarehouseTransferService.DRAFT);
|
||||
draft.setFbaWarehouseCode(null);
|
||||
when(transfers.findNo(eq("TENANT-A"), anyString())).thenReturn(draft);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(draft);
|
||||
when(orders.selectById(draft.getLastlegOrderId())).thenReturn(tailOrder());
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
assertEquals(400, exception.getStatusCode().value());
|
||||
assertEquals("仓储转运必须选择 FBA 仓或私人仓", exception.getReason());
|
||||
verifyNoInteractions(transfers);
|
||||
service.create("TENANT-A", "USER-1", request);
|
||||
var result = service.submit("TENANT-A", "WTR-1", "USER-1");
|
||||
|
||||
ArgumentCaptor<WarehouseTransferOrder> created = ArgumentCaptor.forClass(WarehouseTransferOrder.class);
|
||||
verify(transfers).insert(created.capture());
|
||||
assertNull(created.getValue().getDestinationType());
|
||||
assertNull(created.getValue().getFbaWarehouseCode());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status());
|
||||
verify(relabelApplications, never()).insert(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCanClearDestinationWhenEditingDraft() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.DRAFT);
|
||||
transfer.setDestinationType("FBA");
|
||||
transfer.setDestinationWarehouseCode("ONT8");
|
||||
LastlegOrder order = tailOrder();
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(transfer.getLastlegOrderId())).thenReturn(order);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
"IN-1", List.of(11L), "WH-1", null, "NONE", null, null, null,
|
||||
null, null, null, null, "123 Pickup Road", null, null, 5,
|
||||
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
|
||||
null, null, null, null, null, false, null, null, null, null, null, "edit-no-fba",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")), null);
|
||||
|
||||
var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request);
|
||||
|
||||
assertEquals(WarehouseTransferService.DRAFT, result.status());
|
||||
assertNull(transfer.getDestinationType());
|
||||
assertNull(transfer.getDestinationWarehouseCode());
|
||||
assertNull(transfer.getFbaWarehouseCode());
|
||||
assertNull(order.getFbaWarehouseCode());
|
||||
verify(transfers).updateById(transfer);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 后端脚本管理与全量 HTTP 验收索引
|
||||
|
||||
`db/return-consolidated-incremental-20261012.sql`:10月10日/12日退货、签收、清点、换标与按件出库增量汇总,显式 `USE baiyue_lastleg`,MySQL 8.0.16+ 已有业务库可重复执行。本轮补齐签收原包装、尺寸重量来源及库存箱原包装三个字段;逐列判断存在性,不回填签收/清点事实,不改库存数量。连接实例及凭据由执行者明确指定,整份执行、遇错停止;保留执行错误,修复后可重跑,末尾输出字段、索引与约束供核对。托盘重量、体积汇总是应用逻辑,不新增资源库字段;不自动执行到测试或生产环境。
|
||||
|
||||
`procurement-sample-test-env.mjs` 是采购样品正常全链路 TEST 专项入口,默认或 `--plan` 零网络。真实执行使用 `node --env-file="<授权目录>/test-acceptance.env" scripts/procurement-sample-test-env.mjs --execute --environment TEST --run-id <唯一短批次> --warehouse-id <已核对有效海外收货仓>`。只允许当前 TEST 工作区及指定配置;由正式 API 创建独立客户并完成资质,人工充值10元,上传凭证/面单,调用内部 `procurement-sample-fulfillment.mjs` 完成两箱正常履约。充值、文件和采购请求前落盘,结果未知停写;报告独占 `reports/procurement-sample-<批次>/report.json`,不覆盖旧报告,不自动重跑,不执行 SQL/部署/删除。结束退出自身平台与客户会话,保留样本。通过只表示该正常路径,不替代异常、退款、权限、真实页面和请求量验收。离线入口合同为 `procurement-sample-test-env.test.mjs`;内部模块安全合同见模块索引。
|
||||
|
||||
|
||||
|
||||
@@ -101,13 +101,23 @@ PREPARE return_index_stmt FROM @return_index_ddl; EXECUTE return_index_stmt; DEA
|
||||
SET @return_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='idx_ll_return_outbound_carton_sku'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_outbound_movement ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)');
|
||||
PREPARE return_index_stmt FROM @return_index_ddl; EXECUTE return_index_stmt; DEALLOCATE PREPARE return_index_stmt;
|
||||
|
||||
-- 五、原包装及尺寸重量来源。逐列检查,支持部分执行后重跑。
|
||||
-- 托盘重量/体积由当前绑定箱的实测尺寸重量汇总,无需新增托盘字段。
|
||||
SET @return_measurement_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='original_packaging'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'' AFTER weight');
|
||||
PREPARE return_measurement_stmt FROM @return_measurement_ddl; EXECUTE return_measurement_stmt; DEALLOCATE PREPARE return_measurement_stmt;
|
||||
SET @return_measurement_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='measurement_source'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN measurement_source VARCHAR(32) NULL COMMENT ''尺寸重量来源 PALLET_MEASURED托盘实测 UNIT_SUM子箱汇总'' AFTER original_packaging');
|
||||
PREPARE return_measurement_stmt FROM @return_measurement_ddl; EXECUTE return_measurement_stmt; DEALLOCATE PREPARE return_measurement_stmt;
|
||||
SET @return_measurement_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='original_packaging'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'' AFTER weight');
|
||||
PREPARE return_measurement_stmt FROM @return_measurement_ddl; EXECUTE return_measurement_stmt; DEALLOCATE PREPARE return_measurement_stmt;
|
||||
|
||||
-- 校验输出:核对新增字段、索引和守恒约束。
|
||||
SELECT table_name,column_name,column_type,is_nullable,column_default FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND (
|
||||
(table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity') OR
|
||||
(table_name='ll_return_inventory_carton' AND column_name IN('label_status','count_status','counted_at','counted_by')) OR
|
||||
(table_name='ll_return_inventory_carton' AND column_name IN('label_status','count_status','counted_at','counted_by','original_packaging')) OR
|
||||
(table_name='ll_return_receipt' AND column_name IN('original_packaging','measurement_source')) OR
|
||||
(table_name='ll_relabel_pda_unit_fact' AND column_name='quantity')) ORDER BY table_name,column_name;
|
||||
SELECT table_name,index_name,non_unique,GROUP_CONCAT(column_name ORDER BY seq_in_index) AS indexed_columns
|
||||
FROM information_schema.statistics WHERE table_schema=DATABASE() AND index_name IN
|
||||
('idx_ll_return_outbound_carton_sku','uk_ll_return_outbound_pick_sku','idx_ll_relabel_application_order','uk_ll_return_count_session_receipt','idx_ll_return_count') GROUP BY table_name,index_name,non_unique;
|
||||
SELECT constraint_name,check_clause FROM information_schema.check_constraints WHERE constraint_schema=DATABASE() AND constraint_name='chk_ll_return_outbound_movement_delta';
|
||||
SELECT constraint_name,check_clause FROM information_schema.check_constraints WHERE constraint_schema=DATABASE() AND constraint_name='chk_ll_return_outbound_movement_delta';
|
||||
|
||||
Reference in New Issue
Block a user