diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 1867ced47..41a092247 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -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)) ) 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. USE baiyue_lastleg; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java index 4758b6fff..ebdbcf71d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java @@ -111,10 +111,12 @@ public final class WarehouseTransferRequests { @NotNull @DecimalMin("0.01") BigDecimal width, @NotNull @DecimalMin("0.01") BigDecimal height, @NotNull @DecimalMin("0.01") BigDecimal weight, - @NotBlank @Size(max = 255) String skuCode, - @Positive @Max(2147483647) long quantity + @NotEmpty @Size(max = 200) List<@Valid @NotNull CountSku> items ) {} + public record CountSku(@NotBlank @Size(max = 255) String skuCode, + @Positive @Max(2147483647) long quantity) {} + public record Palletize( @NotBlank @Size(max = 255) String idempotencyKey, @NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java index 00c20ef04..ca60a8991 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java @@ -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 Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status, 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 items) {} + public record CartonItem(Long transferItemId, String skuCode, long quantity) {} public record Label(String transferNo, List cartons, String printableHtml) {} public record DestinationResult(String customerId, String transferNo) {} public record PalletItem(Long transferItemId, String skuCode, int packageCount) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java index 62d303ed2..3bbba0377 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java @@ -25,6 +25,7 @@ public class WarehouseTransferCarton { private java.math.BigDecimal heightCm; private java.math.BigDecimal grossWeightKg; private Long countedQuantity; + private String skuItemsJson; private String countIdempotencyKey; private String countRequestHash; private String discrepancyReason; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index fae47ed6d..0b6df860c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -535,7 +535,7 @@ public class WarehouseTransferService { value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList(); List cartons = transferCartons.listOrder(customerId, transfer.getId()) .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 palletItemRows = transferPalletItems == null ? List.of() : transferPalletItems.listOrder(customerId, transfer.getId()); Map> 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"); List cartons = transferCartons.listOrder(customerId, transfer.getId()).stream() .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(); for (WarehouseTransferResponses.Carton carton : cartons) { labels.append("
仓储转运箱唛
") @@ -829,8 +829,18 @@ public class WarehouseTransferService { throw badRequest("箱号不属于本单或重复填写"); if (carton == null && input.transferItemId() == null) throw badRequest("新增实收箱必须选择本单申报明细"); var item = byItem.get(carton == null ? input.transferItemId() : carton.getTransferItemId()); - if (item == null || !item.getSkuCode().equals(input.skuCode())) throw badRequest("箱内 SKU 必须与该箱申报明细一致"); - if (input.quantity() <= 0 || input.quantity() > Integer.MAX_VALUE || input.length() == null || input.width() == null + if (item == null) throw badRequest("箱子申报明细不属于本单"); + if (input.items() == null || input.items().isEmpty() || input.items().size() > 200) + throw badRequest("每箱必须填写 1 至 200 行 SKU"); + Set 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().signum() <= 0 || input.weight().signum() <= 0) throw badRequest("每箱必须填写有效尺寸、重量和实点件数"); } @@ -853,7 +863,14 @@ public class WarehouseTransferService { } carton.setTransferPalletId(input.transferPalletId()); 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 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.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash); carton.setUpdatedAt(now); carton.setDiscrepancyReason(trim(request.discrepancyReason())); @@ -871,6 +888,16 @@ public class WarehouseTransferService { return detail(customerId, transferNo); } + private List cartonContents(WarehouseTransferCarton carton) { + if (!StringUtils.hasText(carton.getSkuItemsJson())) return List.of(); + try { + return objectMapper.readValue(carton.getSkuItemsJson(), + new com.fasterxml.jackson.core.type.TypeReference>() {}); + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + throw new IllegalStateException("Invalid persisted carton contents", e); + } + } + private List requireCountedCartons(WarehouseTransferOrder transfer) { var cartons = transferCartons.listOrder(transfer.getCustomerId(), transfer.getId()); if (cartons.isEmpty() || cartons.stream().anyMatch(box -> box.getCountedAt() == null) diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index e3b0fdecd..f4053b04e 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -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)) ) 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. USE baiyue_lastleg; diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index d91e3ccd6..fd13c94b2 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -33,7 +33,7 @@ paths: /api/pda/v1/last-leg/warehouse-transfers/{transferNo}/cartons/count: post: summary: 转运逐箱测量与清点 - description: 仅限 lastleg:pda:operate 与 OVERSEAS_WAREHOUSE_PDA_OPERATOR,绑定仓由统一Token确定。先创建本单托盘号,每箱选择所属托盘并按申报SKU记录实点件数,托盘体积重量由分配箱子汇总,全部实收箱子测量清点后才可完成打托入库,打托箱数与已清点实收箱数核平;短收箱保留未收到事实,超收生成系统箱号;差异需原因。事务锁定转运单;原键原载荷重试不重复写入,同键不同载荷拒绝,已确认事实不可覆盖。 + description: 仅限 lastleg:pda:operate 与 OVERSEAS_WAREHOUSE_PDA_OPERATOR,绑定仓由统一Token确定。先创建本单托盘号,每箱选择所属托盘,可包含多个本单预报SKU,逐项记录实点件数;同箱SKU不可重复,尺寸重量和实收箱数按实物箱计算一次,托盘体积重量由分配箱子汇总,全部实收箱子测量清点后才可完成打托入库,打托箱数与已清点实收箱数核平;短收箱保留未收到事实,超收生成系统箱号;差异需原因。事务锁定转运单;原键原载荷重试不重复写入,同键不同载荷拒绝,已确认事实不可覆盖。 parameters: - {name: transferNo, in: path, required: true, schema: {type: string}} requestBody: @@ -52,7 +52,7 @@ paths: maxItems: 5000 items: type: object - required: [transferPalletId, length, width, height, weight, skuCode, quantity] + required: [transferPalletId, length, width, height, weight, items] properties: cartonMark: {type: string, maxLength: 255, nullable: true, description: 已有箱号;新增超收箱不传,由服务端生成} transferItemId: {type: integer, format: int64, nullable: true, description: 新增超收箱所属本单申报明细} @@ -61,10 +61,19 @@ paths: width: {type: number, minimum: 0.01, description: 箱宽厘米} height: {type: number, minimum: 0.01, description: 箱高厘米} weight: {type: number, minimum: 0.01, description: 箱重千克} - skuCode: {type: string, maxLength: 255} - quantity: {type: integer, minimum: 1, maximum: 2147483647, description: 箱内实点件数} + items: + type: array + minItems: 1 + maxItems: 200 + description: 本箱实际SKU明细,仅限本单预报SKU,同箱不可重复 + items: + type: object + required: [skuCode, quantity] + properties: + skuCode: {type: string, maxLength: 255} + quantity: {type: integer, minimum: 1, maximum: 2147483647, description: 本SKU箱内实点件数} responses: - '200': {description: 统一业务响应,成功data为转运详情,cartons含逐箱尺寸重量件数及清点操作人时间} + '200': {description: 统一业务响应,成功data为转运详情,cartons含逐箱尺寸重量、quantity合计件数、items逐SKU明细(transferItemId/skuCode/quantity)及清点操作人时间} /api/platform/v1/last-leg/inventory/balances/{balanceId}: get: summary: 查询单条库存余额详情 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java index f2739a2af..9f3329051 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java @@ -42,7 +42,7 @@ class WarehouseTransferCartonCountTest { 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; } - 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); } @Test void multiSkuBoxesPersistDimensionsPiecesAndReplayWithoutWriting() { 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)))); } @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)); 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.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() { var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱"); service.countCartons("OP1",77L,"WT1",request); @@ -67,7 +104,7 @@ class WarehouseTransferCartonCountTest { assertEquals("少到一箱",b.getDiscrepancyReason()); assertNull(b.getCountedQuantity()); } @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),"多到一箱")); var captured = org.mockito.ArgumentCaptor.forClass(WarehouseTransferCarton.class); verify(cartons).insert(captured.capture()); diff --git a/scripts/README.md b/scripts/README.md index 95430a35a..a78fb315a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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-carton-multi-sku-incremental.sql`:固定指向 `baiyue_lastleg`,按列存在性检查新增 `ll_warehouse_transfer_carton.sku_items_json`(JSON),保存同一实收箱的多个预报 SKU 及逐项件数。可重复执行,不更改或回填既有单据;需在启用新版转运清点后端前执行。 diff --git a/scripts/db/warehouse-transfer-carton-multi-sku-incremental.sql b/scripts/db/warehouse-transfer-carton-multi-sku-incremental.sql new file mode 100644 index 000000000..6a3be22a6 --- /dev/null +++ b/scripts/db/warehouse-transfer-carton-multi-sku-incremental.sql @@ -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;