From 249bf0ab4b447a6de4d7ef17a8926d16f4723f2d Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 8 Sep 2026 20:25:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RelabelApplicationService.java | 8 ++-- .../PlatformReturnInventoryController.java | 6 +++ .../dto/ReturnInventoryResponses.java | 9 +++-- .../entity/ReturnInventoryBalance.java | 2 + .../mapper/ReturnInventoryBalanceMapper.java | 9 +++-- .../mapper/ReturnInventoryMovementMapper.java | 33 +++++++++++++++ .../service/ReturnInventoryService.java | 21 +++++++--- .../src/main/resources/db/lastleg.sql | 15 +++++-- .../RelabelApplicationServiceTest.java | 40 +++++++++++++++++++ scripts/db/incremental.sql | 13 ++++-- 10 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryMovementMapper.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index b07dcec99..f8795ab49 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -126,7 +126,7 @@ public class RelabelApplicationService implements WalletResultHandler { || !Objects.equals(balance.getInboundOrderId(), item.sourceInboundOrderId())) { throw conflict("Selected inventory does not belong to this customer, operation warehouse and inbound order"); } - if (value(position.getAvailablePackages()) < item.totalPackageCount()) { + if (value(position.getAvailablePackages()) < item.notifiedPackageCount()) { throw conflict("Available inventory changed for inbound " + balance.getInboundOrderNo()); } sources.add(new SourceInventory(item, balance)); @@ -137,7 +137,7 @@ public class RelabelApplicationService implements WalletResultHandler { String relabelNo = orderNoGenerator.next("RELABEL", operationWarehouseId, applicationId); for (SourceInventory source : sources) { inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move( - source.request().sourceInventoryPositionId(), source.request().totalPackageCount(), relabelNo, + source.request().sourceInventoryPositionId(), source.request().notifiedPackageCount(), relabelNo, null, "RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":HOLD:" + source.request().sourceInventoryPositionId(), "贴标申请冻结原入仓单库存")); } @@ -398,9 +398,9 @@ public class RelabelApplicationService implements WalletResultHandler { private void releaseInventory(String customerId, String operatorId, RelabelApplication application) { for (RelabelItem item : itemList(application.getId())) { - if (item.getSourceInventoryPositionId() == null || item.getTotalPackageCount() == null) continue; + if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null) continue; inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move( - item.getSourceInventoryPositionId(), item.getTotalPackageCount(), application.getRelabelNo(), null, + item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), application.getRelabelNo(), null, "RELABEL:" + application.getRelabelNo(), application.getIdempotencyKey() + ":RELEASE:" + item.getSourceInventoryPositionId(), "贴标结束释放库存至原入仓单")); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java index 62dc3f255..65b6fd645 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java @@ -30,6 +30,12 @@ public class PlatformReturnInventoryController { return ApiResponse.success(service.platformCartonPage(request)); } + @GetMapping("/movements") + @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')") + public ApiResponse> movements(@Valid ReturnInventoryRequests.PlatformQuery request) { + return ApiResponse.success(service.platformMovementPage(request)); + } + @GetMapping("/cartons/{cartonMark}") @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')") public ApiResponse carton(@PathVariable String cartonMark) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java index d8f31679e..1f9ce4e21 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java @@ -11,8 +11,8 @@ public final class ReturnInventoryResponses { String productNameZh, long quantity, int cartonSkuTypes, long cartonTotalQuantity, LocalDateTime receivedAt) { } public record SummaryItem(String overseasWarehouseId, String skuCode, String productNameZh, - long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { } - public record CartonDetail(String cartonMark, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status, + long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { } + public record CartonDetail(String cartonMark, String customerId, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status, int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight, LocalDateTime receivedAt, List items) { } public record CartonItem(String skuCode, String productNameZh, long quantity) { } @@ -24,9 +24,12 @@ public final class ReturnInventoryResponses { public record PackingSku(String skuCode, String productNameZh, long availableQuantity) { } public record PackingPreview(List orders, String customerId, List skus) { } public record PlatformStock(String customerId, String overseasWarehouseId, String skuCode, String productNameZh, - long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { } + long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { } public record PlatformCarton(String customerId, String overseasWarehouseId, String cartonMark, String sourceReturnNo, String sourceBatchNo, String status, int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight, LocalDateTime createdAt) { } + public record PlatformMovement(String customerId, String overseasWarehouseId, String skuCode, String productNameZh, + String movementType, String referenceNo, long totalDelta, long availableDelta, + LocalDateTime occurredAt) { } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java index 0fc67c398..e11a5fb8f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java @@ -16,6 +16,8 @@ public class ReturnInventoryBalance { private String overseasWarehouseId; private String skuCode; private String productNameZh; + /** 实物总库存;只有实际出库才会减少。 */ + private Long totalQuantity; private Long availableQuantity; private Long cartonQuantity; private LocalDateTime createdAt; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java index 4f5c34f26..18ef9bfa0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java @@ -11,13 +11,14 @@ import java.util.List; @Mapper public interface ReturnInventoryBalanceMapper extends BaseMapper { - @Insert("INSERT INTO ll_return_inventory_balance (id,customer_id,overseas_warehouse_id,sku_code,product_name_zh,available_quantity,carton_quantity,created_at,updated_at) " + - "VALUES (#{id},#{customerId},#{warehouseId},#{skuCode},#{productNameZh},#{quantity},#{cartons},NOW(3),NOW(3)) " + - "ON DUPLICATE KEY UPDATE available_quantity=available_quantity+VALUES(available_quantity),carton_quantity=carton_quantity+VALUES(carton_quantity), " + + @Insert("INSERT INTO ll_return_inventory_balance (id,customer_id,overseas_warehouse_id,sku_code,product_name_zh,total_quantity,available_quantity,carton_quantity,created_at,updated_at) " + + "VALUES (#{id},#{customerId},#{warehouseId},#{skuCode},#{productNameZh},#{totalQuantity},#{availableQuantity},#{cartons},NOW(3),NOW(3)) " + + "ON DUPLICATE KEY UPDATE total_quantity=total_quantity+VALUES(total_quantity),available_quantity=available_quantity+VALUES(available_quantity),carton_quantity=carton_quantity+VALUES(carton_quantity), " + "product_name_zh=COALESCE(NULLIF(VALUES(product_name_zh),''),product_name_zh),updated_at=NOW(3)") int adjust(@Param("id") long id, @Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode, @Param("productNameZh") String productNameZh, - @Param("quantity") long quantity, @Param("cartons") long cartons); + @Param("totalQuantity") long totalQuantity, @Param("availableQuantity") long availableQuantity, + @Param("cartons") long cartons); @Select("") + long platformCount(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode); + + @Select("") + List platformPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, + @Param("skuCode") String skuCode, @Param("offset") long offset, @Param("size") long size); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java index aedd3e0fd..d8ac8c380 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java @@ -15,6 +15,7 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper; import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryOperationMapper; import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryQueryMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryMovementMapper; import com.baiyue.lastleg.returnreceipt.model.ReturnInventoryRow; import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import jakarta.annotation.Resource; @@ -47,6 +48,7 @@ public class ReturnInventoryService { @Resource private ReturnInventoryBalanceMapper balances; @Resource private ReturnInventoryOperationMapper operations; @Resource private ReturnInventoryQueryMapper queries; + @Resource private ReturnInventoryMovementMapper movements; @Resource private LastlegOrderNoGenerator numbers; @Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders; @Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper receiptMapper; @@ -72,7 +74,7 @@ public class ReturnInventoryService { item.setSkuCode(line.skuCode()); item.setProductNameZh(text(line.productNameZh())); item.setQuantity(line.quantity()); cartonItems.insert(item); balances.adjust(IdWorker.getId(), order.getCustomerId(), order.getOverseasWarehouseId(), line.skuCode(), - text(line.productNameZh()), line.quantity(), 1); + text(line.productNameZh()), line.quantity(), line.quantity(), 1); } return carton; } @@ -105,7 +107,16 @@ public class ReturnInventoryService { List records = total == 0 ? List.of() : balances.platformPage(customerId, warehouseId, sku, (request.page() - 1) * request.size(), request.size()).stream().map(balance -> new ReturnInventoryResponses.PlatformStock( balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(), - value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt())).toList(); + value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt())).toList(); + return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size()); + } + + @Transactional(readOnly = true) + public PageResult platformMovementPage(ReturnInventoryRequests.PlatformQuery request) { + String customerId = text(request.customerId()); String warehouseId = text(request.overseasWarehouseId()); String sku = text(request.skuCode()); + long total = movements.platformCount(customerId, warehouseId, sku); + List records = total == 0 ? List.of() : movements.platformPage(customerId, warehouseId, sku, + (request.page() - 1) * request.size(), request.size()); return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size()); } @@ -251,7 +262,7 @@ public class ReturnInventoryService { targetItem.setId(IdWorker.getId()); targetItem.setCartonId(targetId); targetItem.setCustomerId(customerId); targetItem.setSkuCode(entry.getKey()); targetItem.setProductNameZh(productNames.get(entry.getKey())); targetItem.setQuantity(entry.getValue()); cartonItems.insert(targetItem); balances.adjust(IdWorker.getId(), customerId, target.getOverseasWarehouseId(), entry.getKey(), productNames.get(entry.getKey()), 0, - 1L - emptiedBySku.getOrDefault(entry.getKey(), 0)); + -entry.getValue(), 1L - emptiedBySku.getOrDefault(entry.getKey(), 0)); } operation.setResultCartonId(targetId); operation.setUpdatedAt(now); operations.updateById(operation); return new ReturnInventoryResponses.MergeResult(target.getCartonMark(), requested.keySet().iterator().next(), total, label(target)); @@ -281,13 +292,13 @@ public class ReturnInventoryService { private ReturnInventoryResponses.SummaryItem summary(com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance balance) { return new ReturnInventoryResponses.SummaryItem(balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(), - value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt()); + value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt()); } private ReturnInventoryResponses.CartonDetail cartonDetail(ReturnInventoryCarton carton) { List items = cartonItems.listCarton(carton.getId()).stream() .map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList(); - return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(), + return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(), carton.getStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items); } diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 95ffd5bf7..e442dde22 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -2050,8 +2050,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab 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 ( 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(sku_types>0 AND total_quantity>0), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED')) + 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='AVAILABLE' AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED')) ) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛'; +-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 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; +ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status='AVAILABLE' AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no')); PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt; CREATE TABLE IF NOT EXISTS ll_return_inventory_carton_item ( @@ -2059,9 +2063,14 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_carton_item ( PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_item(carton_id,sku_code), KEY idx_ll_return_inventory_carton_item_customer(customer_id,sku_code), CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK(quantity>0) ) ENGINE=InnoDB COMMENT='退货库存箱内SKU和数量'; CREATE TABLE IF NOT EXISTS ll_return_inventory_balance ( - id BIGINT NOT NULL COMMENT '退货库存余额ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', available_quantity BIGINT NOT NULL COMMENT '可用上架数量', carton_quantity BIGINT NOT NULL COMMENT '包含该SKU的可用箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', - PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code), CONSTRAINT chk_ll_return_inventory_balance_quantity CHECK(available_quantity>=0 AND carton_quantity>=0) + id BIGINT NOT NULL COMMENT '退货库存余额ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减', available_quantity BIGINT NOT NULL COMMENT '未合箱可用上架数量', carton_quantity BIGINT NOT NULL COMMENT '包含该SKU的可用箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', + PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code), CONSTRAINT chk_ll_return_inventory_balance_quantity CHECK(total_quantity>=available_quantity AND available_quantity>=0 AND carton_quantity>=0) ) ENGINE=InnoDB COMMENT='退货库存按客户海外仓SKU汇总余额'; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_balance' AND column_name='total_quantity'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_balance ADD COLUMN total_quantity BIGINT NULL COMMENT ''实物总库存,仅实际出库扣减'' AFTER product_name_zh')); +PREPARE return_inventory_balance_total_stmt FROM @ddl; EXECUTE return_inventory_balance_total_stmt; DEALLOCATE PREPARE return_inventory_balance_total_stmt; +-- 旧数据的可用量原先包含已合箱库存;升级时一次性拆分为总库存和未合箱可用库存。 +UPDATE ll_return_inventory_balance b LEFT JOIN (SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,SUM(i.quantity) merged_quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED' GROUP BY c.customer_id,c.overseas_warehouse_id,i.sku_code) m ON m.customer_id=b.customer_id AND m.overseas_warehouse_id=b.overseas_warehouse_id AND m.sku_code=b.sku_code SET b.total_quantity=b.available_quantity,b.available_quantity=GREATEST(0,b.available_quantity-COALESCE(m.merged_quantity,0)) WHERE b.total_quantity IS NULL; +ALTER TABLE ll_return_inventory_balance MODIFY COLUMN total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减'; CREATE TABLE IF NOT EXISTS ll_return_inventory_operation ( id BIGINT NOT NULL COMMENT '退货库存PDA操作ID', operator_id VARCHAR(128) NOT NULL COMMENT 'PDA操作人ID', idempotency_key VARCHAR(255) NOT NULL COMMENT '操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256', operation_type VARCHAR(32) NOT NULL COMMENT '操作类型,当前MERGE', result_carton_id BIGINT NULL COMMENT '合箱结果退货库存箱ID', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_operation_operator_key(operator_id,idempotency_key), KEY idx_ll_return_inventory_operation_result(result_carton_id) diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index a4c02b084..76fc1e42b 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -17,6 +17,7 @@ import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper; import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import com.baiyue.lastleg.wallet.WalletCommandOutboxService; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; +import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.entity.InventoryBalance; import com.baiyue.lastleg.inventory.entity.InventoryPosition; import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; @@ -129,6 +130,25 @@ class RelabelApplicationServiceTest { assertEquals("FBR-1", result.application().relabelNo()); } + @Test + void partialRelabelFreezesOnlyNotifiedPackages() { + RelabelRequests.Create request = request("key-partial", 10, 2); + RelabelApplication persisted = application(11L, "FBR-PARTIAL", "10001", "key-partial", null, + RelabelApplicationService.PENDING_PROCESSING); + persisted.setLastlegOrderId(21L); + when(applications.findNo(eq("10001"), any())).thenReturn(persisted); + when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 10, 2))); + when(histories.selectList(any())).thenReturn(List.of()); + when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-PARTIAL", + RelabelApplicationService.PENDING_PROCESSING)); + + service.create("10001", "operator-1", request); + + ArgumentCaptor move = ArgumentCaptor.forClass(InventoryCommandRequests.Move.class); + verify(inventoryCommands).hold(eq("10001"), eq("operator-1"), move.capture()); + assertEquals(2L, move.getValue().packages()); + } + @Test void createRejectsNotifiedBoxesAboveTotalBeforeAnyWrite() { ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> @@ -315,6 +335,26 @@ class RelabelApplicationServiceTest { assertEquals("COMPLETED", order.getRelabelStatus()); } + @Test + void completingPartialRelabelReleasesOnlyNotifiedPackages() { + RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", "PROCESSING"); + application.setLastlegOrderId(21L); + LastlegOrder order = order(21L, "10001", "LMO-RELABEL-PARTIAL", "PROCESSING"); + when(applications.lockNo("10001", "FBR-PARTIAL")).thenReturn(application); + stubDetail(application); + RelabelItem relabelItem = item(31L, application.getId(), "SKU-A", 10, 6); + relabelItem.setSourceInventoryPositionId(201L); + when(items.selectList(any())).thenReturn(List.of(relabelItem)); + when(orders.selectById(21L)).thenReturn(order); + + service.changeStatus("10001", "FBR-PARTIAL", "operator-2", + new RelabelRequests.ChangeStatus("COMPLETED", "finished")); + + ArgumentCaptor move = ArgumentCaptor.forClass(InventoryCommandRequests.Move.class); + verify(inventoryCommands).release(eq("10001"), eq("operator-2"), move.capture()); + assertEquals(6L, move.getValue().packages()); + } + private void stubDetail(RelabelApplication application) { when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application); when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6))); diff --git a/scripts/db/incremental.sql b/scripts/db/incremental.sql index 893e5f2e4..2551dd5af 100644 --- a/scripts/db/incremental.sql +++ b/scripts/db/incremental.sql @@ -3977,8 +3977,11 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE return_receipt_carton_mark_incremental_stmt FROM @ddl; EXECUTE return_receipt_carton_mark_incremental_stmt; DEALLOCATE PREPARE return_receipt_carton_mark_incremental_stmt; CREATE TABLE IF NOT EXISTS ll_return_inventory_carton ( 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(sku_types>0 AND total_quantity>0), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED')) + 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='AVAILABLE' AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED')) ) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛'; +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_incremental_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_incremental_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_incremental_stmt; +ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status='AVAILABLE' AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no')); PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='length'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN length DECIMAL(12,3) NULL COMMENT ''箱长CM'', ADD COLUMN width DECIMAL(12,3) NULL COMMENT ''箱宽CM'', ADD COLUMN height DECIMAL(12,3) NULL COMMENT ''箱高CM'', ADD COLUMN weight DECIMAL(12,3) NULL COMMENT ''箱重KG''')); @@ -3988,9 +3991,13 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_carton_item ( PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_item(carton_id,sku_code), KEY idx_ll_return_inventory_carton_item_customer(customer_id,sku_code), CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK(quantity>0) ) ENGINE=InnoDB COMMENT='退货库存箱内SKU和数量'; CREATE TABLE IF NOT EXISTS ll_return_inventory_balance ( - id BIGINT NOT NULL COMMENT '退货库存余额ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', available_quantity BIGINT NOT NULL COMMENT '可用上架数量', carton_quantity BIGINT NOT NULL COMMENT '包含该SKU的可用箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', - PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code), CONSTRAINT chk_ll_return_inventory_balance_quantity CHECK(available_quantity>=0 AND carton_quantity>=0) + id BIGINT NOT NULL COMMENT '退货库存余额ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减', available_quantity BIGINT NOT NULL COMMENT '未合箱可用上架数量', carton_quantity BIGINT NOT NULL COMMENT '包含该SKU的可用箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', + PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code), CONSTRAINT chk_ll_return_inventory_balance_quantity CHECK(total_quantity>=available_quantity AND available_quantity>=0 AND carton_quantity>=0) ) ENGINE=InnoDB COMMENT='退货库存按客户海外仓SKU汇总余额'; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_balance' AND column_name='total_quantity'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_balance ADD COLUMN total_quantity BIGINT NULL COMMENT ''实物总库存,仅实际出库扣减'' AFTER product_name_zh')); +PREPARE return_inventory_balance_total_incremental_stmt FROM @ddl; EXECUTE return_inventory_balance_total_incremental_stmt; DEALLOCATE PREPARE return_inventory_balance_total_incremental_stmt; +UPDATE ll_return_inventory_balance b LEFT JOIN (SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,SUM(i.quantity) merged_quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED' GROUP BY c.customer_id,c.overseas_warehouse_id,i.sku_code) m ON m.customer_id=b.customer_id AND m.overseas_warehouse_id=b.overseas_warehouse_id AND m.sku_code=b.sku_code SET b.total_quantity=b.available_quantity,b.available_quantity=GREATEST(0,b.available_quantity-COALESCE(m.merged_quantity,0)) WHERE b.total_quantity IS NULL; +ALTER TABLE ll_return_inventory_balance MODIFY COLUMN total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减'; CREATE TABLE IF NOT EXISTS ll_return_inventory_operation ( id BIGINT NOT NULL COMMENT '退货库存PDA操作ID', operator_id VARCHAR(128) NOT NULL COMMENT 'PDA操作人ID', idempotency_key VARCHAR(255) NOT NULL COMMENT '操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256', operation_type VARCHAR(32) NOT NULL COMMENT '操作类型,当前MERGE', result_carton_id BIGINT NULL COMMENT '合箱结果退货库存箱ID', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_operation_operator_key(operator_id,idempotency_key), KEY idx_ll_return_inventory_operation_result(result_carton_id)