贴标申请,仓储转运

This commit is contained in:
2026-10-12 15:57:47 +08:00
parent a1cd47dabd
commit 6c4ee2bfde
7 changed files with 189 additions and 18 deletions
@@ -39,7 +39,7 @@ public final class RelabelResponses {
long availablePackages, long version, List<InventoryPosition> positions, long availablePackages, long version, List<InventoryPosition> positions,
String sourceType, Long returnCartonId, String cartonMark, String sourceType, Long returnCartonId, String cartonMark,
List<SkuItem> skuItems, long relabelledPackages, String relabelStatus, List<SkuItem> skuItems, long relabelledPackages, String relabelStatus,
long relabelledQuantity, long relabelReservedQuantity) { long relabelledQuantity, long relabelReservedQuantity, Long stockQuantity, String inventoryStatus, String occupiedRelabelNo, List<String> mergedIntoCartonMarks, boolean hasOutbound) {
public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode, public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode,
String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName, String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName,
String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh, String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh,
@@ -48,7 +48,7 @@ public final class RelabelResponses {
this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId, this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId,
overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName, overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName,
skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark, skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark,
skuItems, relabelledPackages, null, relabelledPackages, 0); skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false);
} }
} }
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo, public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
@@ -94,6 +94,7 @@ public class RelabelApplicationService implements WalletResultHandler {
@Resource private ReturnInventoryCartonMapper returnCartons; @Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems; @Resource private ReturnInventoryCartonItemMapper returnCartonItems;
@Resource private ReturnOrderMapper returnOrders; @Resource private ReturnOrderMapper returnOrders;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper returnPackingTraces;
@Resource private RelabelDocumentFileGateway documentFiles; @Resource private RelabelDocumentFileGateway documentFiles;
@Resource private PdaRelabelTaskMapper pdaTasks; @Resource private PdaRelabelTaskMapper pdaTasks;
@Resource private RelabelPhotoFileGateway photoFiles; @Resource private RelabelPhotoFileGateway photoFiles;
@@ -269,7 +270,11 @@ public class RelabelApplicationService implements WalletResultHandler {
if (StringUtils.hasText(keyword) if (StringUtils.hasText(keyword)
&& !containsIgnoreCase(carton.getCartonMark(), keyword) && !containsIgnoreCase(carton.getCartonMark(), keyword)
&& !containsIgnoreCase(carton.getSourceReturnNo(), keyword)) continue; && !containsIgnoreCase(carton.getSourceReturnNo(), keyword)) continue;
List<RelabelResponses.SkuItem> cartonSkus = returnCartonItems.listCarton(carton.getId()).stream() var cartonLines = returnCartonItems.listCarton(carton.getId());
String productNames = cartonLines.stream().filter(item -> value(item.getQuantity()) > 0)
.map(item -> trim(item.getProductNameZh())).filter(StringUtils::hasText).distinct()
.collect(Collectors.joining("、"));
List<RelabelResponses.SkuItem> cartonSkus = cartonLines.stream()
.filter(item -> value(item.getQuantity()) > 0) .filter(item -> value(item.getQuantity()) > 0)
.map(item -> { .map(item -> {
long remaining = "AVAILABLE".equals(carton.getStatus()) long remaining = "AVAILABLE".equals(carton.getStatus())
@@ -277,7 +282,7 @@ public class RelabelApplicationService implements WalletResultHandler {
return new RelabelResponses.SkuItem(item.getSkuCode(), null, return new RelabelResponses.SkuItem(item.getSkuCode(), null,
Math.toIntExact(Math.max(remaining, 0)), null); Math.toIntExact(Math.max(remaining, 0)), null);
}).toList(); }).toList();
if (cartonSkus.isEmpty()) continue; if (cartonSkus.isEmpty() && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue;
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode) String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum(); long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
@@ -285,9 +290,12 @@ public class RelabelApplicationService implements WalletResultHandler {
long reserved = returnCartons.relabelReservedQuantity(carton.getId()); long reserved = returnCartons.relabelReservedQuantity(carton.getId());
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(), result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(), null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
carton.getDestinationWarehouseName(), skuCodes, "退货入库库存", availablePackages, 0, List.of(), carton.getDestinationWarehouseName(), skuCodes, productNames, availablePackages, 0, List.of(),
"PARCEL".equals(carton.getSourceUnitType()) ? "RETURN_PARCEL" : "RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages, "PARCEL".equals(carton.getSourceUnitType()) ? "RETURN_PARCEL" : "RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages,
com.baiyue.lastleg.returnreceipt.service.ReturnRelabelProgress.status(value(carton.getTotalQuantity()), relabelledPackages, reserved), relabelledPackages, reserved)); com.baiyue.lastleg.returnreceipt.service.ReturnRelabelProgress.status(value(carton.getTotalQuantity()), relabelledPackages, reserved), relabelledPackages, reserved, java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus()) ? 0L : value(carton.getTotalQuantity()), carton.getStatus(), carton.getRelabelNo(),
returnPackingTraces.listCartonHistory(customerId, carton.getCartonMark()).stream()
.filter(history -> carton.getCartonMark().equals(history.sourceCartonMark()))
.map(history -> history.targetCartonMark()).distinct().toList(), returnCartons.hasOutbound(carton.getId())));
} }
return result; return result;
} }
@@ -679,10 +687,10 @@ public class RelabelApplicationService implements WalletResultHandler {
// Reviewing/processing a return relabel must not move the already // Reviewing/processing a return relabel must not move the already
// completed inbound order back to assignment or relabel waiting. // completed inbound order back to assignment or relabel waiting.
if ("RETURN_RELABEL".equals(order.getOrderType())) { if ("RETURN_RELABEL".equals(order.getOrderType())) {
orderStatus = "INBOUND_COMPLETED"; orderStatus = orderBefore;
} }
order.setCurrentStatus(orderStatus); order.setCurrentStatus(orderStatus);
order.setSubOrderStatus("RELABEL:" + next); if (!"RETURN_RELABEL".equals(order.getOrderType())) order.setSubOrderStatus("RELABEL:" + next);
order.setRelabelStatus(businessRelabelStatus(next)); order.setRelabelStatus(businessRelabelStatus(next));
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
@@ -794,7 +802,7 @@ public class RelabelApplicationService implements WalletResultHandler {
applications.updateById(application); applications.updateById(application);
for (LastlegOrder order : tailOrders(application)) { for (LastlegOrder order : tailOrders(application)) {
order.setRelabelStatus(PENDING_REVIEW); order.setRelabelStatus(PENDING_REVIEW);
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW); if (!"RETURN_RELABEL".equals(order.getOrderType())) order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW);
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
} }
@@ -813,7 +821,8 @@ public class RelabelApplicationService implements WalletResultHandler {
application.setUpdatedAt(LocalDateTime.now()); applications.updateById(application); application.setUpdatedAt(LocalDateTime.now()); applications.updateById(application);
for (LastlegOrder order : tailOrders(application)) { for (LastlegOrder order : tailOrders(application)) {
if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue; if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue;
order.setRelabelStatus("PROCESSING"); order.setSubOrderStatus("RELABEL:PROCESSING"); order.setRelabelStatus("PROCESSING");
if (!"RETURN_RELABEL".equals(order.getOrderType())) order.setSubOrderStatus("RELABEL:PROCESSING");
order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order); order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
} }
insertHistory(customerId, application.getId(), before, "PROCESSING", operatorId, remark, LocalDateTime.now()); insertHistory(customerId, application.getId(), before, "PROCESSING", operatorId, remark, LocalDateTime.now());
@@ -930,9 +939,9 @@ public class RelabelApplicationService implements WalletResultHandler {
String orderBefore = order.getCurrentStatus(); String orderBefore = order.getCurrentStatus();
// 换标确认时货物已经完成入库,不得退回最初建单的“待分配”。 // 换标确认时货物已经完成入库,不得退回最初建单的“待分配”。
// 独立换标先进入待出库;仓储转运会在下方按目的仓进一步收敛为待排车或待确认目的仓。 // 独立换标先进入待出库;仓储转运会在下方按目的仓进一步收敛为待排车或待确认目的仓。
order.setCurrentStatus("RETURN_RELABEL".equals(order.getOrderType()) ? "INBOUND_COMPLETED" order.setCurrentStatus("RETURN_RELABEL".equals(order.getOrderType()) ? orderBefore
: requiresOutbound ? "WAIT_OUTBOUND" : "COMPLETED"); : requiresOutbound ? "WAIT_OUTBOUND" : "COMPLETED");
order.setSubOrderStatus("RELABEL:" + application.getFulfillmentStatus()); if (!"RETURN_RELABEL".equals(order.getOrderType())) order.setSubOrderStatus("RELABEL:" + application.getFulfillmentStatus());
order.setRelabelStatus("COMPLETED"); order.setRelabelStatus("COMPLETED");
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
@@ -1235,7 +1244,6 @@ public class RelabelApplicationService implements WalletResultHandler {
.last("LIMIT 1")); .last("LIMIT 1"));
if (existingReturn != null) { if (existingReturn != null) {
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_REVIEW); existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_REVIEW);
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_REVIEW); existingReturn.setPackageCount(packageCount);
existingReturn.setUpdatedAt(now); orders.updateById(existingReturn); return existingReturn; existingReturn.setUpdatedAt(now); orders.updateById(existingReturn); return existingReturn;
} }
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
@@ -1265,7 +1273,7 @@ public class RelabelApplicationService implements WalletResultHandler {
// A linked warehouse-transfer relabel can cover only part of the // A linked warehouse-transfer relabel can cover only part of the
// transfer. Keep the authoritative whole-order package count so later // transfer. Keep the authoritative whole-order package count so later
// dispatches can attribute the remaining cartons correctly. // dispatches can attribute the remaining cartons correctly.
if (!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) order.setPackageCount(packageCount); if (!"WAREHOUSE_TRANSFER".equals(order.getOrderType()) && !"RETURN_RELABEL".equals(order.getOrderType())) order.setPackageCount(packageCount);
order.setUpdatedAt(now); orders.updateById(order); order.setUpdatedAt(now); orders.updateById(order);
return order; return order;
} }
@@ -10,6 +10,8 @@ import java.util.List;
@Mapper @Mapper
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> { public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
@Select("SELECT EXISTS(SELECT 1 FROM ll_return_inventory_outbound_movement WHERE carton_id=#{cartonId} AND quantity>0)")
boolean hasOutbound(@Param("cartonId") long cartonId);
@Update("UPDATE ll_return_inventory_carton SET relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL WHERE id=#{id}") @Update("UPDATE ll_return_inventory_carton SET relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL WHERE id=#{id}")
int clearRelabel(@Param("id") long id); int clearRelabel(@Param("id") long id);
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} ORDER BY id FOR UPDATE") @Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} ORDER BY id FOR UPDATE")
@@ -40,7 +42,7 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
"ORDER BY created_at DESC,id DESC LIMIT 500</script>") "ORDER BY created_at DESC,id DESC LIMIT 500</script>")
List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId); List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " + @Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
"AND count_status='CONFIRMED' AND status IN ('AVAILABLE','RELABEL_PENDING','PACKING_RESERVED') ORDER BY created_at DESC,id DESC LIMIT 500") "AND count_status='CONFIRMED' AND status IN ('AVAILABLE','RELABEL_PENDING','PACKING_RESERVED','MERGED','OUTBOUNDED','OUTBOUND_PENDING','DISCARD_PENDING') ORDER BY created_at DESC,id DESC LIMIT 500")
List<ReturnInventoryCarton> findRelabelCandidates(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId); List<ReturnInventoryCarton> findRelabelCandidates(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1") @Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark); ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark);
@@ -444,9 +444,8 @@ public class ReturnReceiptService {
// Do not create a second independent tail order or overwrite the inbound status. // Do not create a second independent tail order or overwrite the inbound status.
tail.setRelabelOrderNo(relabelNo); tail.setRelabelOrderNo(relabelNo);
tail.setRelabelStatus("PENDING_REVIEW"); tail.setRelabelStatus("PENDING_REVIEW");
tail.setSubOrderStatus("RETURN_RELABEL:PENDING_REVIEW");
tail.setCurrentStatus("RETURN_RELABEL".equals(tail.getOrderType()) ? "INBOUND_COMPLETED" : tail.getCurrentStatus());
if (!"RETURN_RELABEL".equals(tail.getOrderType())) { if (!"RETURN_RELABEL".equals(tail.getOrderType())) {
tail.setSubOrderStatus("RETURN_RELABEL:PENDING_REVIEW");
tail.setPackageCount(Math.toIntExact(totalQuantity)); tail.setPackageCount(Math.toIntExact(totalQuantity));
tail.setProductQuantity(Math.toIntExact(totalQuantity)); tail.setProductQuantity(Math.toIntExact(totalQuantity));
} }
@@ -66,6 +66,26 @@ import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class RelabelApplicationServiceTest { class RelabelApplicationServiceTest {
@Test void returnInventoryCandidateUsesRealProductNames() {
var cartonMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
ReflectionTestUtils.setField(service, "returnCartons", cartonMapper);
ReflectionTestUtils.setField(service, "returnCartonItems", lineMapper);
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
carton.setId(20L); carton.setStatus("AVAILABLE"); carton.setSourceUnitType("BOX"); carton.setTotalQuantity(4L);
var first = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
first.setSkuCode("A"); first.setProductNameZh(" 中文商品甲 "); first.setQuantity(2L);
var second = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
second.setSkuCode("B"); second.setProductNameZh("中文商品乙"); second.setQuantity(2L);
when(cartonMapper.findRelabelCandidates("C", "W")).thenReturn(List.of(carton));
when(lineMapper.listCarton(20L)).thenReturn(List.of(first, second));
var candidate = service.candidates("C", "W", null).get(0);
assertEquals("中文商品甲、中文商品乙", candidate.productNameZh());
assertEquals(4L, candidate.stockQuantity());
assertEquals("AVAILABLE", candidate.inventoryStatus());
assertEquals(List.of(), candidate.mergedIntoCartonMarks());
}
@Mock RelabelApplicationMapper applications; @Mock RelabelApplicationMapper applications;
@Mock RelabelItemMapper items; @Mock RelabelItemMapper items;
@Mock RelabelStatusHistoryMapper histories; @Mock RelabelStatusHistoryMapper histories;
@@ -554,9 +574,11 @@ class RelabelApplicationServiceTest {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND); RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L); application.setLastlegOrderId(21L);
application.setFulfillmentStatus("PRE_DISPATCH_CREATED"); application.setFulfillmentStatus("PRE_DISPATCH_CREATED");
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "PROCESSING"); LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "INBOUND_COMPLETED");
order.setOrderType("RETURN_RELABEL"); order.setOrderType("RETURN_RELABEL");
order.setRelabelOrderNo("FBR-1"); order.setRelabelOrderNo("FBR-1");
order.setPackageCount(7); order.setProductQuantity(110);
order.setSubOrderStatus("RETURN_RELABEL:COMPLETED");
when(applications.lockNo("10001", "FBR-1")).thenReturn(application); when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
stubDetail(application); stubDetail(application);
when(orders.selectById(21L)).thenReturn(order); when(orders.selectById(21L)).thenReturn(order);
@@ -568,6 +590,31 @@ class RelabelApplicationServiceTest {
assertEquals("OUTBOUNDED", application.getFulfillmentStatus()); assertEquals("OUTBOUNDED", application.getFulfillmentStatus());
assertEquals("INBOUND_COMPLETED", order.getCurrentStatus()); assertEquals("INBOUND_COMPLETED", order.getCurrentStatus());
assertEquals("COMPLETED", order.getRelabelStatus()); assertEquals("COMPLETED", order.getRelabelStatus());
assertEquals(7, order.getPackageCount());
assertEquals(110, order.getProductQuantity());
assertEquals("RETURN_RELABEL:COMPLETED", order.getSubOrderStatus());
}
@Test void partialReturnRelabelCreationPreservesParentReceiptQuantitiesAndStatus() {
LastlegOrder parent = order(21L, "10001", "RT-ORIGINAL", "INBOUND_COMPLETED");
parent.setOrderType("RETURN_RELABEL"); parent.setSourceOrderNo("RT-ORIGINAL");
parent.setPackageCount(7); parent.setReceivedPackageCount(7); parent.setProductQuantity(110);
parent.setSubOrderStatus("RETURN_RELABEL:COMPLETED");
when(orders.selectById(21L)).thenReturn(parent);
ReflectionTestUtils.invokeMethod(service, "linkExistingTailOrder", "10001", 21L, "RL-PARTIAL", 1, LocalDateTime.now());
assertEquals(7, parent.getPackageCount()); assertEquals(7, parent.getReceivedPackageCount());
assertEquals(110, parent.getProductQuantity());
assertEquals("RT-ORIGINAL", parent.getSourceOrderNo());
assertEquals("INBOUND_COMPLETED", parent.getCurrentStatus());
assertEquals("RETURN_RELABEL:COMPLETED", parent.getSubOrderStatus());
assertEquals("RL-PARTIAL", parent.getRelabelOrderNo());
when(orders.selectOne(any())).thenReturn(parent);
var request = org.mockito.Mockito.mock(RelabelRequests.Create.class);
when(request.customerOrderNo()).thenReturn("RT-ORIGINAL");
ReflectionTestUtils.invokeMethod(service, "insertTailOrder", "10001", request, "hash", "RL-SECOND", "W", 1, LocalDateTime.now());
assertEquals(7, parent.getPackageCount()); assertEquals(110, parent.getProductQuantity());
assertEquals("RETURN_RELABEL:COMPLETED", parent.getSubOrderStatus());
assertEquals("RL-SECOND", parent.getRelabelOrderNo());
} }
@Test @Test
@@ -1013,6 +1060,7 @@ class RelabelApplicationServiceTest {
var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class); var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
ReflectionTestUtils.setField(service, "returnCartons", cartonMapper); ReflectionTestUtils.setField(service, "returnCartons", cartonMapper);
ReflectionTestUtils.setField(service, "returnCartonItems", lineMapper); ReflectionTestUtils.setField(service, "returnCartonItems", lineMapper);
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
carton.setId(901L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1"); carton.setId(901L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1");
carton.setCartonMark("RETURN-BOX-1"); carton.setStatus("AVAILABLE"); carton.setSourceUnitType("BOX"); carton.setCartonMark("RETURN-BOX-1"); carton.setStatus("AVAILABLE"); carton.setSourceUnitType("BOX");
@@ -28,6 +28,7 @@ class RelabelReturnCartonCandidateTest {
ReflectionTestUtils.setField(service, "inventoryPositions", positions); ReflectionTestUtils.setField(service, "inventoryPositions", positions);
ReflectionTestUtils.setField(service, "returnCartons", cartons); ReflectionTestUtils.setField(service, "returnCartons", cartons);
ReflectionTestUtils.setField(service, "returnCartonItems", cartonItems); ReflectionTestUtils.setField(service, "returnCartonItems", cartonItems);
ReflectionTestUtils.setField(service, "returnPackingTraces", mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
ReturnInventoryCarton carton = new ReturnInventoryCarton(); ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setId(9001L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1"); carton.setId(9001L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1");
@@ -0,0 +1,113 @@
-- 退货近期增量汇总(2026-10-12),MySQL 8.0.16+,可重复执行。
-- 连接到需要升级的 MySQL 实例后,整份执行;目标仅 baiyue_lastleg。
-- USE 指定数据库,不指定服务器;本脚本不创建数据库,不写测试单据,不修改库存/清点事实。
-- 前提:已有尾程和退货合箱/出库基线表(不是新库初始化脚本)。
-- 汇总10月10日/12日直接退货增量,并补齐清点、事实数量及多次换标申请结构。
-- 排车车队/IAM权限为独立业务基线,不纳入本退货脚本。
-- PARCEL三类型换标、RETURN_PACK/RETURN_COMPLETION_TRANSFER历史以及清点续作均复用现有结构。
-- DDL隐式提交;执行客户端遇错须停止,不使用 --force。可重复执行不等于整个脚本事务回滚。
USE `baiyue_lastleg`;
SELECT DATABASE() AS target_database, @@hostname AS mysql_host, @@port AS mysql_port, VERSION() AS mysql_version;
-- 一、件数出库快照、部分扣件守恒、同箱分批出库索引。
-- 退货单 SKU 按件数出库快照。目标库:baiyue_lastleg。可重复执行。
USE baiyue_lastleg;
SET @return_sku_quantity_ddl = IF(
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity'),
'SELECT 1',
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN return_sku_quantity BIGINT NULL COMMENT ''退货单SKU本来源计划出库件数快照,NULL为历史整箱出库'' AFTER sku_code'
);
PREPARE return_sku_quantity_stmt FROM @return_sku_quantity_ddl;
EXECUTE return_sku_quantity_stmt;
DEALLOCATE PREPARE return_sku_quantity_stmt;
-- 部分出库仅扣件数;整箱扣一箱,保留守恒和非负约束。
SET @return_sku_check_ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND constraint_name='chk_ll_return_outbound_movement_delta'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)', 'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
PREPARE return_sku_check_stmt FROM @return_sku_check_ddl;
EXECUTE return_sku_check_stmt;
DEALLOCATE PREPARE return_sku_check_stmt;
-- 二、标签状态(旧记录保留NULL,应用按既有兼容规则处理)。
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='label_status'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN label_status VARCHAR(32) NULL COMMENT ''WAIT_LABEL待贴标 CONFIRMED已扫码确认标签''');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 三、清点结构;不将历史已签收箱自动回退为待清点。
-- 退货托盘签收箱内清点;MySQL 8;仅在目标尾程库执行,可重复运行。
USE baiyue_lastleg;
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='count_status'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN count_status VARCHAR(16) NOT NULL DEFAULT ''CONFIRMED'' COMMENT ''箱内清点状态 PENDING待清点 CONFIRMED已确认''');
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='counted_at'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN counted_at DATETIME(3) NULL COMMENT ''箱内清点确认时间''');
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='counted_by'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN counted_by VARCHAR(128) NULL COMMENT ''箱内清点确认操作人''');
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND index_name='idx_ll_return_count'),'SELECT 1','CREATE INDEX idx_ll_return_count ON ll_return_inventory_carton(overseas_warehouse_id,count_status,status,source_receipt_id)');
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
-- MySQL 8,本地尾程库,可重复执行;不修改现有清点数量或自动释放未完成任务。
USE baiyue_lastleg;
-- 退货箱内清点作业会话:每位操作员只能认领一个批次,每个批次只能被一人认领。
CREATE TABLE IF NOT EXISTS ll_return_carton_count_session (
operator_id VARCHAR(128) NOT NULL COMMENT '平台PDA操作员标识',
warehouse_id VARCHAR(128) NOT NULL COMMENT '认领时绑定海外仓标识',
receipt_id BIGINT NULL COMMENT '正在清点的签收批次,完成后释放为NULL',
started_at DATETIME(3) NULL COMMENT '本次开始清点时间',
created_at DATETIME(3) NOT NULL COMMENT '会话创建时间',
updated_at DATETIME(3) NOT NULL COMMENT '会话更新时间',
PRIMARY KEY(operator_id),
UNIQUE KEY uk_ll_return_count_session_receipt(receipt_id)
) ENGINE=InnoDB COMMENT='退货箱内清点单人单任务持久会话';
-- 四、换标事实数量与一单多申请;保留幂等唯一键。
SET @ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'll_relabel_pda_unit_fact'
AND column_name = 'quantity'
),
'SELECT 1',
'ALTER TABLE ll_relabel_pda_unit_fact ADD COLUMN quantity INT NOT NULL DEFAULT 1 COMMENT ''本次事实处理件数,箱级作业固定为1'' AFTER reason'
)
);
PREPARE ll_return_bulk_relabel_quantity_stmt FROM @ddl;
EXECUTE ll_return_bulk_relabel_quantity_stmt;
DEALLOCATE PREPARE ll_return_bulk_relabel_quantity_stmt;
SET @relabel_has_unique = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'll_relabel_application'
AND index_name = 'uk_ll_relabel_application_order');
SET @relabel_has_index = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'll_relabel_application'
AND index_name = 'idx_ll_relabel_application_order');
SET @relabel_index_sql = CASE
WHEN @relabel_has_unique > 0 AND @relabel_has_index = 0 THEN
'ALTER TABLE ll_relabel_application DROP INDEX uk_ll_relabel_application_order, ADD INDEX idx_ll_relabel_application_order (lastleg_order_id)'
WHEN @relabel_has_unique > 0 THEN
'ALTER TABLE ll_relabel_application DROP INDEX uk_ll_relabel_application_order'
WHEN @relabel_has_index = 0 THEN
'ALTER TABLE ll_relabel_application ADD INDEX idx_ll_relabel_application_order (lastleg_order_id)'
ELSE 'SELECT 1' END;
PREPARE relabel_index_migration FROM @relabel_index_sql;
EXECUTE relabel_index_migration;
DEALLOCATE PREPARE relabel_index_migration;
-- 同箱分批索引:同时已有旧/新索引或旧索引已删除时也可执行。
SET @return_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='uk_ll_return_outbound_carton_sku'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku', 'SELECT 1');
PREPARE return_index_stmt FROM @return_index_ddl; EXECUTE return_index_stmt; DEALLOCATE PREPARE return_index_stmt;
SET @return_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='idx_ll_return_outbound_carton_sku'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_outbound_movement ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)');
PREPARE return_index_stmt FROM @return_index_ddl; EXECUTE return_index_stmt; DEALLOCATE PREPARE return_index_stmt;
-- 校验输出:核对新增字段、索引和守恒约束。
SELECT table_name,column_name,column_type,is_nullable,column_default FROM information_schema.columns
WHERE table_schema=DATABASE() AND (
(table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity') OR
(table_name='ll_return_inventory_carton' AND column_name IN('label_status','count_status','counted_at','counted_by')) OR
(table_name='ll_relabel_pda_unit_fact' AND column_name='quantity')) ORDER BY table_name,column_name;
SELECT table_name,index_name,non_unique,GROUP_CONCAT(column_name ORDER BY seq_in_index) AS indexed_columns
FROM information_schema.statistics WHERE table_schema=DATABASE() AND index_name IN
('idx_ll_return_outbound_carton_sku','uk_ll_return_outbound_pick_sku','idx_ll_relabel_application_order','uk_ll_return_count_session_receipt','idx_ll_return_count') GROUP BY table_name,index_name,non_unique;
SELECT constraint_name,check_clause FROM information_schema.check_constraints WHERE constraint_schema=DATABASE() AND constraint_name='chk_ll_return_outbound_movement_delta';