This commit is contained in:
2026-10-15 18:25:53 +08:00
parent caff693ad8
commit cda5f3dc32
10 changed files with 109 additions and 16 deletions
@@ -4528,6 +4528,11 @@ CREATE TABLE IF NOT EXISTS ll_selfop_inventory_daily (
CONSTRAINT chk_selfop_daily_quantity CHECK ((quality_status='VALID' AND physical_quantity IS NOT NULL AND physical_quantity>=0) OR (quality_status='UNKNOWN' AND physical_quantity IS NULL)) CONSTRAINT chk_selfop_daily_quantity CHECK ((quality_status='VALID' AND physical_quantity IS NOT NULL AND physical_quantity>=0) OR (quality_status='UNKNOWN' AND physical_quantity IS NULL))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='历史日末分货主商品品质实物库存'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='历史日末分货主商品品质实物库存';
-- 仓储转运一箱多SKU清点;仅增加结构,不回填历史业务数据。
USE baiyue_lastleg;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_carton' AND column_name='sku_items_json'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_carton ADD COLUMN sku_items_json JSON NULL COMMENT ''实收箱内SKU及逐项件数明细'''));
PREPARE transfer_sku_stmt FROM @ddl; EXECUTE transfer_sku_stmt; DEALLOCATE PREPARE transfer_sku_stmt;
-- Appointment work scope, audit and physical batch allocation. -- Appointment work scope, audit and physical batch allocation.
USE baiyue_lastleg; USE baiyue_lastleg;
@@ -111,10 +111,12 @@ public final class WarehouseTransferRequests {
@NotNull @DecimalMin("0.01") BigDecimal width, @NotNull @DecimalMin("0.01") BigDecimal width,
@NotNull @DecimalMin("0.01") BigDecimal height, @NotNull @DecimalMin("0.01") BigDecimal height,
@NotNull @DecimalMin("0.01") BigDecimal weight, @NotNull @DecimalMin("0.01") BigDecimal weight,
@NotBlank @Size(max = 255) String skuCode, @NotEmpty @Size(max = 200) List<@Valid @NotNull CountSku> items
@Positive @Max(2147483647) long quantity
) {} ) {}
public record CountSku(@NotBlank @Size(max = 255) String skuCode,
@Positive @Max(2147483647) long quantity) {}
public record Palletize( public record Palletize(
@NotBlank @Size(max = 255) String idempotencyKey, @NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds @NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds
@@ -9,7 +9,8 @@ public final class WarehouseTransferResponses {
public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark, Long quantity) {} public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark, Long quantity) {}
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status, public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status,
Long transferItemId, Long transferPalletId, BigDecimal length, BigDecimal width, BigDecimal height, Long transferItemId, Long transferPalletId, BigDecimal length, BigDecimal width, BigDecimal height,
BigDecimal weight, Long quantity, String discrepancyReason, String countedBy, LocalDateTime countedAt) {} BigDecimal weight, Long quantity, String discrepancyReason, String countedBy, LocalDateTime countedAt, List<CartonItem> items) {}
public record CartonItem(Long transferItemId, String skuCode, long quantity) {}
public record Label(String transferNo, List<Carton> cartons, String printableHtml) {} public record Label(String transferNo, List<Carton> cartons, String printableHtml) {}
public record DestinationResult(String customerId, String transferNo) {} public record DestinationResult(String customerId, String transferNo) {}
public record PalletItem(Long transferItemId, String skuCode, int packageCount) {} public record PalletItem(Long transferItemId, String skuCode, int packageCount) {}
@@ -25,6 +25,7 @@ public class WarehouseTransferCarton {
private java.math.BigDecimal heightCm; private java.math.BigDecimal heightCm;
private java.math.BigDecimal grossWeightKg; private java.math.BigDecimal grossWeightKg;
private Long countedQuantity; private Long countedQuantity;
private String skuItemsJson;
private String countIdempotencyKey; private String countIdempotencyKey;
private String countRequestHash; private String countRequestHash;
private String discrepancyReason; private String discrepancyReason;
@@ -535,7 +535,7 @@ public class WarehouseTransferService {
value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList(); value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList();
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()) List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId())
.stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), .stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(),
value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt())).toList(); value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt(), cartonContents(value))).toList();
List<WarehouseTransferPalletItem> palletItemRows = transferPalletItems == null List<WarehouseTransferPalletItem> palletItemRows = transferPalletItems == null
? List.of() : transferPalletItems.listOrder(customerId, transfer.getId()); ? List.of() : transferPalletItems.listOrder(customerId, transfer.getId());
Map<Long, List<WarehouseTransferPalletItem>> palletItemsByPallet = palletItemRows.stream() Map<Long, List<WarehouseTransferPalletItem>> palletItemsByPallet = palletItemRows.stream()
@@ -621,7 +621,7 @@ public class WarehouseTransferService {
if (DRAFT.equals(transfer.getStatus())) throw conflict("Submit the warehouse transfer order before printing carton labels"); if (DRAFT.equals(transfer.getStatus())) throw conflict("Submit the warehouse transfer order before printing carton labels");
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream() List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
.filter(value -> !"NOT_RECEIVED".equals(value.getStatus())) .filter(value -> !"NOT_RECEIVED".equals(value.getStatus()))
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt())).toList(); .map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt(), cartonContents(value))).toList();
StringBuilder labels = new StringBuilder(); StringBuilder labels = new StringBuilder();
for (WarehouseTransferResponses.Carton carton : cartons) { for (WarehouseTransferResponses.Carton carton : cartons) {
labels.append("<section class=label><div class=title>仓储转运箱唛</div><div class=mark>") labels.append("<section class=label><div class=title>仓储转运箱唛</div><div class=mark>")
@@ -829,8 +829,18 @@ public class WarehouseTransferService {
throw badRequest("箱号不属于本单或重复填写"); throw badRequest("箱号不属于本单或重复填写");
if (carton == null && input.transferItemId() == null) throw badRequest("新增实收箱必须选择本单申报明细"); if (carton == null && input.transferItemId() == null) throw badRequest("新增实收箱必须选择本单申报明细");
var item = byItem.get(carton == null ? input.transferItemId() : carton.getTransferItemId()); var item = byItem.get(carton == null ? input.transferItemId() : carton.getTransferItemId());
if (item == null || !item.getSkuCode().equals(input.skuCode())) throw badRequest("箱内 SKU 必须与该箱申报明细一致"); if (item == null) throw badRequest("箱子申报明细不属于本单");
if (input.quantity() <= 0 || input.quantity() > Integer.MAX_VALUE || input.length() == null || input.width() == null if (input.items() == null || input.items().isEmpty() || input.items().size() > 200)
throw badRequest("每箱必须填写 1 至 200 行 SKU");
Set<String> skuCodes = new java.util.HashSet<>();
for (var line : input.items()) {
if (line == null || !StringUtils.hasText(line.skuCode())
|| byItem.values().stream().noneMatch(declared -> declared.getSkuCode().equals(line.skuCode()))
|| !skuCodes.add(line.skuCode())) throw badRequest("箱内 SKU 必须来自本单预报,同箱不能重复");
if (line.quantity() <= 0 || line.quantity() > Integer.MAX_VALUE) throw badRequest("每行 SKU 必须填写有效实点件数");
}
if (!skuCodes.contains(item.getSkuCode())) throw badRequest("请保留箱子原申报 SKU,可添加其他本单 SKU");
if (input.length() == null || input.width() == null
|| input.height() == null || input.weight() == null || input.length().signum() <= 0 || input.width().signum() <= 0 || input.height() == null || input.weight() == null || input.length().signum() <= 0 || input.width().signum() <= 0
|| input.height().signum() <= 0 || input.weight().signum() <= 0) throw badRequest("每箱必须填写有效尺寸、重量和实点件数"); || input.height().signum() <= 0 || input.weight().signum() <= 0) throw badRequest("每箱必须填写有效尺寸、重量和实点件数");
} }
@@ -853,7 +863,14 @@ public class WarehouseTransferService {
} }
carton.setTransferPalletId(input.transferPalletId()); carton.setTransferPalletId(input.transferPalletId());
carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height()); carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height());
carton.setGrossWeightKg(input.weight()); carton.setCountedQuantity(input.quantity()); carton.setGrossWeightKg(input.weight());
carton.setCountedQuantity(input.items().stream().mapToLong(WarehouseTransferRequests.CountSku::quantity).sum());
List<WarehouseTransferResponses.CartonItem> contents = input.items().stream().map(line -> {
var declared = byItem.values().stream().filter(value -> value.getSkuCode().equals(line.skuCode())).findFirst().orElseThrow();
return new WarehouseTransferResponses.CartonItem(declared.getId(), line.skuCode(), line.quantity());
}).toList();
try { carton.setSkuItemsJson(objectMapper.writeValueAsString(contents)); }
catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw new IllegalStateException("Cannot serialize carton contents", e); }
carton.setCountedBy(operatorId); carton.setCountedAt(now); carton.setStatus("COUNTED"); carton.setCountedBy(operatorId); carton.setCountedAt(now); carton.setStatus("COUNTED");
carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash); carton.setUpdatedAt(now); carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash); carton.setUpdatedAt(now);
carton.setDiscrepancyReason(trim(request.discrepancyReason())); carton.setDiscrepancyReason(trim(request.discrepancyReason()));
@@ -871,6 +888,16 @@ public class WarehouseTransferService {
return detail(customerId, transferNo); return detail(customerId, transferNo);
} }
private List<WarehouseTransferResponses.CartonItem> cartonContents(WarehouseTransferCarton carton) {
if (!StringUtils.hasText(carton.getSkuItemsJson())) return List.of();
try {
return objectMapper.readValue(carton.getSkuItemsJson(),
new com.fasterxml.jackson.core.type.TypeReference<List<WarehouseTransferResponses.CartonItem>>() {});
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new IllegalStateException("Invalid persisted carton contents", e);
}
}
private List<WarehouseTransferCarton> requireCountedCartons(WarehouseTransferOrder transfer) { private List<WarehouseTransferCarton> requireCountedCartons(WarehouseTransferOrder transfer) {
var cartons = transferCartons.listOrder(transfer.getCustomerId(), transfer.getId()); var cartons = transferCartons.listOrder(transfer.getCustomerId(), transfer.getId());
if (cartons.isEmpty() || cartons.stream().anyMatch(box -> box.getCountedAt() == null) if (cartons.isEmpty() || cartons.stream().anyMatch(box -> box.getCountedAt() == null)
@@ -4145,6 +4145,11 @@ CREATE TABLE IF NOT EXISTS ll_selfop_inventory_daily (
CONSTRAINT chk_selfop_daily_quantity CHECK ((quality_status='VALID' AND physical_quantity IS NOT NULL AND physical_quantity>=0) OR (quality_status='UNKNOWN' AND physical_quantity IS NULL)) CONSTRAINT chk_selfop_daily_quantity CHECK ((quality_status='VALID' AND physical_quantity IS NOT NULL AND physical_quantity>=0) OR (quality_status='UNKNOWN' AND physical_quantity IS NULL))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='历史日末分货主商品品质实物库存'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='历史日末分货主商品品质实物库存';
-- 仓储转运一箱多SKU清点;仅增加结构,不回填历史业务数据。
USE baiyue_lastleg;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_carton' AND column_name='sku_items_json'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_carton ADD COLUMN sku_items_json JSON NULL COMMENT ''实收箱内SKU及逐项件数明细'''));
PREPARE transfer_sku_stmt FROM @ddl; EXECUTE transfer_sku_stmt; DEALLOCATE PREPARE transfer_sku_stmt;
-- Appointment work scope, audit and physical batch allocation. -- Appointment work scope, audit and physical batch allocation.
USE baiyue_lastleg; USE baiyue_lastleg;
@@ -33,7 +33,7 @@ paths:
/api/pda/v1/last-leg/warehouse-transfers/{transferNo}/cartons/count: /api/pda/v1/last-leg/warehouse-transfers/{transferNo}/cartons/count:
post: post:
summary: 转运逐箱测量与清点 summary: 转运逐箱测量与清点
description: 仅限 lastleg:pda:operate 与 OVERSEAS_WAREHOUSE_PDA_OPERATOR,绑定仓由统一Token确定。先创建本单托盘号,每箱选择所属托盘并按申报SKU记录实点件数,托盘体积重量由分配箱子汇总,全部实收箱子测量清点后才可完成打托入库,打托箱数与已清点实收箱数核平;短收箱保留未收到事实,超收生成系统箱号;差异需原因。事务锁定转运单;原键原载荷重试不重复写入,同键不同载荷拒绝,已确认事实不可覆盖。 description: 仅限 lastleg:pda:operate 与 OVERSEAS_WAREHOUSE_PDA_OPERATOR,绑定仓由统一Token确定。先创建本单托盘号,每箱选择所属托盘,可包含多个本单预报SKU,逐项记录实点件数;同箱SKU不可重复,尺寸重量和实收箱数按实物箱计算一次,托盘体积重量由分配箱子汇总,全部实收箱子测量清点后才可完成打托入库,打托箱数与已清点实收箱数核平;短收箱保留未收到事实,超收生成系统箱号;差异需原因。事务锁定转运单;原键原载荷重试不重复写入,同键不同载荷拒绝,已确认事实不可覆盖。
parameters: parameters:
- {name: transferNo, in: path, required: true, schema: {type: string}} - {name: transferNo, in: path, required: true, schema: {type: string}}
requestBody: requestBody:
@@ -52,7 +52,7 @@ paths:
maxItems: 5000 maxItems: 5000
items: items:
type: object type: object
required: [transferPalletId, length, width, height, weight, skuCode, quantity] required: [transferPalletId, length, width, height, weight, items]
properties: properties:
cartonMark: {type: string, maxLength: 255, nullable: true, description: 已有箱号;新增超收箱不传,由服务端生成} cartonMark: {type: string, maxLength: 255, nullable: true, description: 已有箱号;新增超收箱不传,由服务端生成}
transferItemId: {type: integer, format: int64, nullable: true, description: 新增超收箱所属本单申报明细} transferItemId: {type: integer, format: int64, nullable: true, description: 新增超收箱所属本单申报明细}
@@ -61,10 +61,19 @@ paths:
width: {type: number, minimum: 0.01, description: 箱宽厘米} width: {type: number, minimum: 0.01, description: 箱宽厘米}
height: {type: number, minimum: 0.01, description: 箱高厘米} height: {type: number, minimum: 0.01, description: 箱高厘米}
weight: {type: number, minimum: 0.01, description: 箱重千克} weight: {type: number, minimum: 0.01, description: 箱重千克}
items:
type: array
minItems: 1
maxItems: 200
description: 本箱实际SKU明细,仅限本单预报SKU,同箱不可重复
items:
type: object
required: [skuCode, quantity]
properties:
skuCode: {type: string, maxLength: 255} skuCode: {type: string, maxLength: 255}
quantity: {type: integer, minimum: 1, maximum: 2147483647, description: 箱内实点件数} quantity: {type: integer, minimum: 1, maximum: 2147483647, description: 本SKU箱内实点件数}
responses: responses:
'200': {description: 统一业务响应,成功data为转运详情,cartons含逐箱尺寸重量件数及清点操作人时间} '200': {description: 统一业务响应,成功data为转运详情,cartons含逐箱尺寸重量、quantity合计件数、items逐SKU明细(transferItemId/skuCode/quantity)及清点操作人时间}
/api/platform/v1/last-leg/inventory/balances/{balanceId}: /api/platform/v1/last-leg/inventory/balances/{balanceId}:
get: get:
summary: 查询单条库存余额详情 summary: 查询单条库存余额详情
@@ -42,7 +42,7 @@ class WarehouseTransferCartonCountTest {
when(items.selectList(any())).thenReturn(List.of(ia,ib)); when(items.selectList(any())).thenReturn(List.of(ia,ib));
} }
WarehouseTransferCarton box(String mark, long item) { var box = new WarehouseTransferCarton(); box.setCartonMark(mark); box.setCartonIndex((int)item); box.setTransferItemId(item); return box; } WarehouseTransferCarton box(String mark, long item) { var box = new WarehouseTransferCarton(); box.setCartonMark(mark); box.setCartonIndex((int)item); box.setTransferItemId(item); return box; }
WarehouseTransferRequests.CountCarton input(String mark, String sku, long quantity) { return new WarehouseTransferRequests.CountCarton(mark, null, 200L, new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),sku,quantity); } WarehouseTransferRequests.CountCarton input(String mark, String sku, long quantity) { return new WarehouseTransferRequests.CountCarton(mark, null, 200L, new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),List.of(new WarehouseTransferRequests.CountSku(sku,quantity))); }
WarehouseTransferRequests.CountCartons request(String key, WarehouseTransferRequests.CountCarton... rows) { return new WarehouseTransferRequests.CountCartons(key,List.of(rows),null); } WarehouseTransferRequests.CountCartons request(String key, WarehouseTransferRequests.CountCarton... rows) { return new WarehouseTransferRequests.CountCartons(key,List.of(rows),null); }
@Test void multiSkuBoxesPersistDimensionsPiecesAndReplayWithoutWriting() { @Test void multiSkuBoxesPersistDimensionsPiecesAndReplayWithoutWriting() {
var request = request("K1",input("BOX1","A",12),input("BOX2","B",7)); var request = request("K1",input("BOX1","A",12),input("BOX2","B",7));
@@ -56,10 +56,47 @@ class WarehouseTransferCartonCountTest {
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K1",input("BOX1","A",13),input("BOX2","B",7)))); assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K1",input("BOX1","A",13),input("BOX2","B",7))));
} }
@Test void missingDuplicateForeignSkuAndInvalidQuantityRejectBeforeWrites() { @Test void missingDuplicateForeignSkuAndInvalidQuantityRejectBeforeWrites() {
var invalid = List.of(request("K",input("BOX1","A",1)),request("K",input("BOX1","A",1),input("BOX1","A",2)),request("K",input("OTHER","A",1),input("BOX2","B",2)),request("K",input("BOX1","B",1),input("BOX2","B",2)),request("K",input("BOX1","A",0),input("BOX2","B",2))); var invalid = List.of(request("K",input("BOX1","A",1)),request("K",input("BOX1","A",1),input("BOX1","A",2)),request("K",input("OTHER","A",1),input("BOX2","B",2)),request("K",input("BOX1","FOREIGN",1),input("BOX2","B",2)),request("K",input("BOX1","A",0),input("BOX2","B",2)));
for(var request:invalid) assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request)); for(var request:invalid) assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request));
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class)); verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
} }
@Test void mixedBoxPersistsEverySkuWithoutDuplicatingPhysicalBoxTotals() throws Exception {
var mixed = new WarehouseTransferRequests.CountCarton("BOX1", null, 200L,
new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),
List.of(new WarehouseTransferRequests.CountSku("A",3),new WarehouseTransferRequests.CountSku("B",7)));
var request = new WarehouseTransferRequests.CountCartons("MIX",List.of(mixed),"两种SKU装一箱");
service.countCartons("OP1",77L,"WT1",request);
assertEquals(10L,a.getCountedQuantity());
var contents = new ObjectMapper().readTree(a.getSkuItemsJson());
assertEquals(2,contents.size()); assertEquals("A",contents.get(0).get("skuCode").asText());
assertEquals(7,contents.get(1).get("quantity").asInt());
assertEquals(11L,contents.get(1).get("transferItemId").asLong());
assertEquals(1,pallet.getCartonCount()); assertEquals(new BigDecimal("0.024000"),pallet.getVolumeCbm());
assertEquals(new BigDecimal("2.5"),pallet.getGrossWeightKg());
var allocation = WarehouseTransferService.deriveCountedPalletAllocations(List.of(a),List.of(pallet));
assertEquals(1,allocation.size()); assertEquals(1,allocation.get(0).packageCount());
clearInvocations(cartons);
service.countCartons("OP1",77L,"WT1",request);
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
var response = (java.util.List<?>)ReflectionTestUtils.invokeMethod(service,"cartonContents",a);
assertEquals(2,response.size());
var changed = new WarehouseTransferRequests.CountCarton("BOX1",null,200L,mixed.length(),mixed.width(),mixed.height(),mixed.weight(),
List.of(new WarehouseTransferRequests.CountSku("A",4),new WarehouseTransferRequests.CountSku("B",6)));
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",
new WarehouseTransferRequests.CountCartons("MIX",List.of(changed),"两种SKU装一箱")));
}
@Test void duplicateUnknownEmptyAndInvalidSkuLinesRejectBeforeAnyWrites() {
var invalidLines = List.of(List.<WarehouseTransferRequests.CountSku>of(),
List.of(new WarehouseTransferRequests.CountSku("A",1),new WarehouseTransferRequests.CountSku("A",2)),
List.of(new WarehouseTransferRequests.CountSku("FOREIGN",1)),
List.of(new WarehouseTransferRequests.CountSku("A",0)),
List.of(new WarehouseTransferRequests.CountSku("A",2147483648L)));
for(var lines:invalidLines) {
var mixed = new WarehouseTransferRequests.CountCarton("BOX1",null,200L,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,lines);
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("BAD",mixed,input("BOX2","B",2))));
}
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
}
@Test void shortReceiptNeedsReasonAndPreservesMissingBoxFact() { @Test void shortReceiptNeedsReasonAndPreservesMissingBoxFact() {
var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱"); var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱");
service.countCartons("OP1",77L,"WT1",request); service.countCartons("OP1",77L,"WT1",request);
@@ -67,7 +104,7 @@ class WarehouseTransferCartonCountTest {
assertEquals("少到一箱",b.getDiscrepancyReason()); assertNull(b.getCountedQuantity()); assertEquals("少到一箱",b.getDiscrepancyReason()); assertNull(b.getCountedQuantity());
} }
@Test void extraReceiptGeneratesOwnedBoxAndReconcilesToDeclaredSku() { @Test void extraReceiptGeneratesOwnedBoxAndReconcilesToDeclaredSku() {
var extra = new WarehouseTransferRequests.CountCarton(null,11L,200L,new BigDecimal("10"),new BigDecimal("20"),new BigDecimal("30"),BigDecimal.ONE,"B",4); var extra = new WarehouseTransferRequests.CountCarton(null,11L,200L,new BigDecimal("10"),new BigDecimal("20"),new BigDecimal("30"),BigDecimal.ONE,List.of(new WarehouseTransferRequests.CountSku("B",4)));
service.countCartons("OP1",77L,"WT1",new WarehouseTransferRequests.CountCartons("EXTRA",List.of(input("BOX1","A",1),input("BOX2","B",2),extra),"多到一箱")); service.countCartons("OP1",77L,"WT1",new WarehouseTransferRequests.CountCartons("EXTRA",List.of(input("BOX1","A",1),input("BOX2","B",2),extra),"多到一箱"));
var captured = org.mockito.ArgumentCaptor.forClass(WarehouseTransferCarton.class); var captured = org.mockito.ArgumentCaptor.forClass(WarehouseTransferCarton.class);
verify(cartons).insert(captured.capture()); verify(cartons).insert(captured.capture());
+2
View File
@@ -523,3 +523,5 @@ Admin 全量中的 `phase4-cross-region-events` 使用内部 `test-event-fault-e
`db/warehouse-transfer-consolidated-incremental-20261015.sql`:仓储转运近期本地增量汇总,固定 `USE baiyue_lastleg`,适用已有 Lastleg 基线的本地/开发 MySQL 8。依次合入 `warehouse-transfer-outbound-incremental.sql` 和 `warehouse-transfer-carton-counting-incremental.sql`,前者已覆盖旧 `dev-warehouse-transfer-pallet-lifecycle-incremental.sql`,无须重复跑旧生命周期补丁。包含审核、打托/照片、换标、目的地/出库/派送结构、实收箱数,以及逐箱尺寸、重量、实点件数、清点幂等、差异原因、箱所属托盘和独立 SKU 申报件数。只执行 DDL/元数据查询,无业务 DML;不回填单据或修改库存。实例和凭据由操作者在数据库客户端外部提供,无内置连接信息或报告文件。需支持 DELIMITER 和 CREATE ROUTINE/ALTER/INDEX 权限,整文件串行执行,遇错停止,不使用 --force;可按缺失结构检查重复执行,不修复任意同名异构旧表。末尾 `missing_carton_and_quantity_columns=0`、`inbound_pallet_unique_index=READY`、`migration_result=WAREHOUSE_TRANSFER_RECENT_SCHEMA_READY` 表示本轮列/索引核对通过;同客户重复托盘号会报告 INDEX_BLOCKED,不擅自更改旧托盘号。仅整理文件不表示目标库已经执行;连接必须由操作者明确选中,不重放 `deploy/sql/海外区域统一增量-MySQL8.sql` 空库基线。 `db/warehouse-transfer-consolidated-incremental-20261015.sql`:仓储转运近期本地增量汇总,固定 `USE baiyue_lastleg`,适用已有 Lastleg 基线的本地/开发 MySQL 8。依次合入 `warehouse-transfer-outbound-incremental.sql` 和 `warehouse-transfer-carton-counting-incremental.sql`,前者已覆盖旧 `dev-warehouse-transfer-pallet-lifecycle-incremental.sql`,无须重复跑旧生命周期补丁。包含审核、打托/照片、换标、目的地/出库/派送结构、实收箱数,以及逐箱尺寸、重量、实点件数、清点幂等、差异原因、箱所属托盘和独立 SKU 申报件数。只执行 DDL/元数据查询,无业务 DML;不回填单据或修改库存。实例和凭据由操作者在数据库客户端外部提供,无内置连接信息或报告文件。需支持 DELIMITER 和 CREATE ROUTINE/ALTER/INDEX 权限,整文件串行执行,遇错停止,不使用 --force;可按缺失结构检查重复执行,不修复任意同名异构旧表。末尾 `missing_carton_and_quantity_columns=0`、`inbound_pallet_unique_index=READY`、`migration_result=WAREHOUSE_TRANSFER_RECENT_SCHEMA_READY` 表示本轮列/索引核对通过;同客户重复托盘号会报告 INDEX_BLOCKED,不擅自更改旧托盘号。仅整理文件不表示目标库已经执行;连接必须由操作者明确选中,不重放 `deploy/sql/海外区域统一增量-MySQL8.sql` 空库基线。
`db/warehouse-transfer-carton-multi-sku-incremental.sql`:固定指向 `baiyue_lastleg`,按列存在性检查新增 `ll_warehouse_transfer_carton.sku_items_json`(JSON),保存同一实收箱的多个预报 SKU 及逐项件数。可重复执行,不更改或回填既有单据;需在启用新版转运清点后端前执行。
@@ -0,0 +1,4 @@
-- 仓储转运一箱多SKU清点;仅增加结构,不回填历史业务数据。
USE baiyue_lastleg;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_carton' AND column_name='sku_items_json'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_carton ADD COLUMN sku_items_json JSON NULL COMMENT ''实收箱内SKU及逐项件数明细'''));
PREPARE transfer_sku_stmt FROM @ddl; EXECUTE transfer_sku_stmt; DEALLOCATE PREPARE transfer_sku_stmt;