修改提交

This commit is contained in:
2026-09-08 20:25:05 +08:00
parent 2a27e28f26
commit 249bf0ab4b
10 changed files with 134 additions and 22 deletions
@@ -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(), "贴标结束释放库存至原入仓单"));
}
@@ -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<PageResult<ReturnInventoryResponses.PlatformMovement>> movements(@Valid ReturnInventoryRequests.PlatformQuery request) {
return ApiResponse.success(service.platformMovementPage(request));
}
@GetMapping("/cartons/{cartonMark}")
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')")
public ApiResponse<ReturnInventoryResponses.CartonDetail> carton(@PathVariable String cartonMark) {
@@ -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<CartonItem> 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<PackingOrder> orders, String customerId, List<PackingSku> 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) { }
}
@@ -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;
@@ -11,13 +11,14 @@ import java.util.List;
@Mapper
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
@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("<script>SELECT COUNT(*) FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
@@ -0,0 +1,33 @@
package com.baiyue.lastleg.returnreceipt.mapper;
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/** Read-only return inventory movement ledger reconstructed from signed receipts and merged cartons. */
@Mapper
public interface ReturnInventoryMovementMapper {
String MOVEMENTS = "SELECT o.customer_id customerId,o.overseas_warehouse_id overseasWarehouseId,ri.sku_code skuCode,oi.product_name_zh productNameZh," +
"'RECEIPT' movementType,r.receipt_no referenceNo,ri.received_quantity totalDelta,ri.received_quantity availableDelta,r.signed_at occurredAt " +
"FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id JOIN ll_return_receipt_item ri ON ri.receipt_id=r.id " +
"JOIN ll_return_order_item oi ON oi.id=ri.return_order_item_id " +
"UNION ALL SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,i.product_name_zh,'MERGE',c.carton_mark,0,-i.quantity,c.created_at " +
"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'";
@Select("<script>SELECT COUNT(*) FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
"<if test='warehouseId != null and warehouseId != \"\"'>AND m.overseasWarehouseId=#{warehouseId} </if>" +
"<if test='skuCode != null and skuCode != \"\"'>AND m.skuCode LIKE CONCAT('%',#{skuCode},'%') </if></script>")
long platformCount(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
@Select("<script>SELECT * FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
"<if test='warehouseId != null and warehouseId != \"\"'>AND m.overseasWarehouseId=#{warehouseId} </if>" +
"<if test='skuCode != null and skuCode != \"\"'>AND m.skuCode LIKE CONCAT('%',#{skuCode},'%') </if>" +
"ORDER BY occurredAt DESC LIMIT #{offset},#{size}</script>")
List<ReturnInventoryResponses.PlatformMovement> platformPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
@Param("skuCode") String skuCode, @Param("offset") long offset, @Param("size") long size);
}
@@ -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<ReturnInventoryResponses.PlatformStock> 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<ReturnInventoryResponses.PlatformMovement> 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<ReturnInventoryResponses.PlatformMovement> 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<ReturnInventoryResponses.CartonItem> 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);
}
@@ -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)
@@ -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<InventoryCommandRequests.Move> 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<InventoryCommandRequests.Move> 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)));
+10 -3
View File
@@ -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)