listPallet(@Param("customerId") String customerId,
@Param("palletId") Long palletId);
+ /** Physical carriers can contain stock owned by more than one customer. */
+ @Select("SELECT COUNT(*) FROM ll_inventory_position WHERE overseas_warehouse_id=#{warehouseId} " +
+ "AND pallet_id=#{palletId} AND (available_packages>0 OR hold_packages>0 OR allocated_packages>0 " +
+ "OR picked_packages>0 OR receiving_packages>0)")
+ long activePhysicalPallet(@Param("warehouseId") String warehouseId, @Param("palletId") Long palletId);
+
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId} " +
"AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE")
InventoryPosition lockPosition(@Param("customerId") String customerId,
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java
index 1e4cf9624..2330c0b53 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java
@@ -108,7 +108,8 @@ public class PdaPalletOperationService {
PalletPhysicalGateway.PalletReference pallet = physical.resolve(carton.getPalletCode());
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId())
throw conflict("退货换标来源托盘不属于当前仓库");
- long quantity = value(carton.getTotalQuantity());
+ long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType()))
+ ? value(carton.getTotalQuantity()) : 1;
result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L,
pallet.palletId(), pallet.palletCode(), null, carton.getSourceBatchNo(), quantity, quantity,
entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList()));
@@ -159,11 +160,12 @@ public class PdaPalletOperationService {
String fbaValue = destination == null ? pallet.fbaWarehouseCode() : destination;
String fba = HtmlUtils.htmlEscape(fbaValue == null ? "" : fbaValue);
if (fba == null || fba.isBlank()) fba = "待绑定";
- String html = "" + code
- + "托盘标签
"
- + code + "
换标单:" + order + "
FBA目的仓:" + fba + "
";
+ String html = "托盘标签 " + code
+ + ""
+ + code + "
"
+ + com.baiyue.lastleg.returnreceipt.service.ReturnCartonBarcode.svg(pallet.palletCode())
+ + "
" + order + "
" + fba + "
";
return new PalletOperationResponses.PrintablePallet(pallet.palletId(), pallet.palletCode(), pallet.version(),
relabelNo, html);
}
@@ -335,7 +337,8 @@ public class PdaPalletOperationService {
String requestHash = hash(request);
PdaPalletOperation operation = begin(identity, "RELABEL_PALLET_DISASSEMBLE", key, requestHash);
if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay();
- physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托");
+ var transition = physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托");
+ try {
return transaction.execute(status -> {
for (int index = 0; index < cartons.size(); index++) {
RelabelItem item = selected.get(index);
@@ -345,7 +348,8 @@ public class PdaPalletOperationService {
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|| !"RELABEL_PENDING".equals(carton.getStatus()))
throw conflict("退货换标来源库存已变化,请刷新后重试");
- long move = value(item.getNotifiedPackageCount());
+ long move = "RETURN_BULK_SKU_LABEL".equals(item.getOperationType())
+ ? value(item.getNotifiedPackageCount()) : value(carton.getTotalQuantity());
if (move <= 0 || move > value(carton.getTotalQuantity())) throw conflict("来源托盘可拆数量不足");
if (move < value(carton.getTotalQuantity())) {
splitReturnRelabelCarton(application, item, carton, target, move);
@@ -367,6 +371,11 @@ public class PdaPalletOperationService {
operation.setUpdatedAt(result.completedAt()); mapper.updateById(operation);
return result;
});
+ } catch (RuntimeException failure) {
+ try { physical.compensate(identity.overseasWarehouseId(), transition); }
+ catch (RuntimeException compensationFailure) { failure.addSuppressed(compensationFailure); }
+ throw failure;
+ }
}
/** Keep the unlabelled remainder on its original pallet; move only the confirmed SKU pieces. */
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 796f0daf1..845654c94 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
@@ -100,6 +100,7 @@ public class RelabelApplicationService implements WalletResultHandler {
@Resource private OutboundOrderMapper outboundOrders;
@Resource private OutboundPalletItemMapper outboundItems;
@Resource private OutboundPdaTaskMapper outboundTasks;
+ @Resource private com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper deliveryOrders;
@Resource private OverseasFeeSourceService feeSources;
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferRelabelCompletionService transferRelabelCompletion;
@@ -264,6 +265,7 @@ public class RelabelApplicationService implements WalletResultHandler {
"INBOUND", null, null, List.of(), 0);
}).filter(candidate -> candidate.availablePackages() > 0).toList());
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
+ if ("PARCEL".equals(carton.getSourceUnitType())) continue;
String keyword = trim(inboundCode);
if (StringUtils.hasText(keyword)
&& !containsIgnoreCase(carton.getCartonMark(), keyword)
@@ -390,6 +392,11 @@ public class RelabelApplicationService implements WalletResultHandler {
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
}
+ if (returnCartonItems.lockCarton(carton.getId()).stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
+ throw conflict("退货箱SKU已被合箱计划锁定");
+ if (Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(item.operationType())
+ && (item.totalPackageCount() != 1 || item.notifiedPackageCount() != 1))
+ throw conflict("退货箱唛换标按真实箱计数,每个来源箱只能填写一箱");
long previouslyRelabelled = returnCartons.relabelledQuantity(carton.getId(), item.skuCode());
long requestedPackages = selectedReturnPackages.merge(carton.getId(),
(long) item.notifiedPackageCount(), Math::addExact);
@@ -910,8 +917,13 @@ public class RelabelApplicationService implements WalletResultHandler {
carton.setRelabelStatus("COMPLETED"); carton.setRelabelCompletedAt(now);
// 换标已完成后释放“当前申请占用”关系,但保留 COMPLETED 作为历史状态。
// 否则退货箱虽显示 AVAILABLE,下一次贴标申请仍会被旧 relabel_application_id 拦截。
- carton.setRelabelApplicationId(null); carton.setRelabelNo(null);
- carton.setStatus("AVAILABLE"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
+ boolean awaitingTruck = requiresOutbound && "TRUCK".equals(application.getDeliveryMethod());
+ if (!StringUtils.hasText(application.getOutboundOrderNo()) && !awaitingTruck) {
+ carton.setRelabelApplicationId(null); carton.setRelabelNo(null);
+ }
+ carton.setStatus(StringUtils.hasText(application.getOutboundOrderNo()) ? "OUTBOUND_PENDING"
+ : awaitingTruck ? "RELABEL_PENDING" : "AVAILABLE");
+ carton.setUpdatedAt(now); returnCartons.updateById(carton);
}
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
request.remark(), now);
@@ -992,11 +1004,20 @@ public class RelabelApplicationService implements WalletResultHandler {
return;
}
List relabelItems = itemList(application.getId());
+ for (RelabelItem source : relabelItems) {
+ if (source.getReturnCartonId() == null) continue;
+ ReturnInventoryCarton carton = returnCartons.lockId(source.getReturnCartonId());
+ if (carton == null || !application.getCustomerId().equals(carton.getCustomerId())
+ || !application.getId().equals(carton.getRelabelApplicationId())
+ || !"RELABEL_PENDING".equals(carton.getStatus())
+ || intValue(source.getNotifiedPackageCount()) != 1)
+ throw conflict("退货整箱换标出库必须覆盖该箱全部 SKU 数量:" + source.getCartonMark());
+ }
OutboundOrder outbound = new OutboundOrder();
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType("RELABEL_OUTBOUND");
- outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> intValue(item.getNotifiedPackageCount())).sum());
+ outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> item.getReturnCartonId() == null ? intValue(item.getNotifiedPackageCount()) : 1).sum());
outbound.setPalletCount((int) relabelItems.stream().map(this::outboundSourceKey).distinct().count());
outbound.setRecipientInfo(application.getDeliveryMethod()); outbound.setIdempotencyKey(key);
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
@@ -1012,9 +1033,13 @@ public class RelabelApplicationService implements WalletResultHandler {
item.setCustomerId(application.getCustomerId()); item.setOutboundOrderId(outbound.getId());
item.setWarehousePalletId(position == null ? null : position.getPalletId());
item.setPalletNo(position == null ? null : position.getPalletNo());
- item.setPackageCount(group.stream().mapToInt(value -> intValue(value.getNotifiedPackageCount())).sum());
+ item.setPackageCount(group.stream().mapToInt(value -> value.getReturnCartonId() == null ? intValue(value.getNotifiedPackageCount()) : 1).sum());
item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
- item.setCartonMarks(group.stream().map(RelabelItem::getCartonMark).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
+ // Return-carton relabel picking must scan the original warehouse carton mark;
+ // the requested/new carton mark is retained in the relabel snapshot only.
+ item.setCartonMarks(group.stream()
+ .map(value -> value.getReturnCartonId() == null ? value.getCartonMark() : value.getSourceCartonMark())
+ .filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
item.setDestinationWarehouseCode(group.stream().map(RelabelItem::getDestinationFbaWarehouseCode)
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
item.setInboundCode(group.stream().map(RelabelItem::getSourceInboundCode).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
@@ -1024,6 +1049,27 @@ public class RelabelApplicationService implements WalletResultHandler {
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(application.getCustomerId());
task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId());
task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); outboundTasks.insert(task);
+ if (relabelItems.stream().allMatch(item -> item.getReturnCartonId() != null)) {
+ Map> destinations = relabelItems.stream().collect(Collectors.groupingBy(
+ item -> Objects.toString(item.getDestinationFbaWarehouseCode(), ""), LinkedHashMap::new, Collectors.toList()));
+ int index = 0;
+ for (var destination : destinations.entrySet()) {
+ var delivery = new com.baiyue.lastleg.delivery.entity.DeliveryOrder();
+ delivery.setId(IdWorker.getId()); delivery.setCustomerId(application.getCustomerId());
+ delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", application.getOverseasWarehouseId(), delivery.getId()));
+ delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
+ delivery.setOverseasWarehouseId(application.getOverseasWarehouseId());
+ delivery.setDeliveryGroupKey("RETURN_RELABEL:" + destination.getKey());
+ delivery.setDestinationWarehouseCode(destination.getKey());
+ delivery.setDestinationType(StringUtils.hasText(destination.getKey()) ? "FBA" : null);
+ delivery.setContactName(application.getContactName()); delivery.setContactPhone(application.getContactPhone());
+ delivery.setDeliveryMethod(application.getDeliveryMethod()); delivery.setStatus("WAIT_DELIVERY");
+ delivery.setPackageCount(destination.getValue().size()); delivery.setPalletCount(0);
+ delivery.setIdempotencyKey(key + ":DELIVERY:" + index++); delivery.setVersion(0L);
+ delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
+ deliveryOrders.insert(delivery);
+ }
+ }
application.setOutboundOrderNo(outbound.getOutboundNo());
}
@@ -1071,6 +1117,7 @@ public class RelabelApplicationService implements WalletResultHandler {
ReturnInventoryCarton carton = sourceInventory.returnCarton();
item.setReturnCartonId(carton.getId()); item.setSourceCartonMark(carton.getCartonMark());
item.setSourceInboundOrderNo(carton.getSourceReturnNo());
+ item.setSourceInboundCode(carton.getSourceReturnNo());
item.setSourceWarehouseId(carton.getOverseasWarehouseId());
}
item.setCreatedAt(now); items.insert(item);
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java
index be761f0a4..88cb5602f 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java
@@ -44,6 +44,14 @@ public class RelabelBindingGuard {
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
}
for (RelabelItem item : cartons) {
+ if (item.getReturnCartonId() != null && !"RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
+ var carton = returnCartons.selectById(item.getReturnCartonId());
+ if (carton == null || !app.getCustomerId().equals(carton.getCustomerId())) throw conflict("退货箱来源缺失");
+ if (!"TRUCK".equals(app.getDeliveryMethod())) continue;
+ if (!bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode()))
+ throw conflict("退货箱尚未完成排车托盘绑定");
+ continue;
+ }
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
if (carton == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnPackingPlanController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnPackingPlanController.java
new file mode 100644
index 000000000..c0475a619
--- /dev/null
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnPackingPlanController.java
@@ -0,0 +1,38 @@
+package com.baiyue.lastleg.returnreceipt.controller;
+
+import com.baiyue.common.core.api.ApiResponse;
+import com.baiyue.common.core.api.PageResult;
+import com.baiyue.common.security.AuthenticatedUser;
+import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
+import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
+import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+@RestController
+@RequestMapping("/api/merchant/v1/last-leg/return-packing-plans")
+public class MerchantReturnPackingPlanController {
+ @Resource private ReturnInventoryService service;
+
+ @GetMapping
+ @PreAuthorize("hasAuthority('merchant:overseas-warehouse:return-inventory:list')")
+ public ApiResponse> list(@AuthenticationPrincipal AuthenticatedUser user, @Valid ReturnInventoryRequests.PackingPlanQuery request) {
+ return ApiResponse.success(service.customerPackingPlans(customer(user), request));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAuthority('merchant:overseas-warehouse:return-inventory:list')")
+ public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnInventoryRequests.PackingPlanCreate request) {
+ return ApiResponse.success(service.createPackingPlan(user == null ? "merchant" : user.username(), customer(user), request));
+ }
+
+ private String customer(AuthenticatedUser user) {
+ if (user == null || user.tenantId() == null || "PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
+ return String.valueOf(user.tenantId());
+ }
+}
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java
index 98a8da122..9702c17b1 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java
@@ -35,9 +35,47 @@ public class PdaReturnInventoryController {
return ApiResponse.success(service.merge(PdaWarehouseIdentity.require(user), request));
}
+ @PostMapping("/cartons/pack")
+ public ApiResponse pack(@AuthenticationPrincipal AuthenticatedUser user,
+ @Valid @RequestBody ReturnInventoryRequests.PackingExecution request) {
+ return ApiResponse.success(service.pack(PdaWarehouseIdentity.require(user), request));
+ }
+
+ @GetMapping("/cartons/{cartonMark}")
+ public ApiResponse scanCarton(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String cartonMark) {
+ return ApiResponse.success(service.scanCarton(PdaWarehouseIdentity.require(user), cartonMark));
+ }
+
+ @PostMapping("/cartons/merge-boxes")
+ public ApiResponse mergeBoxes(@AuthenticationPrincipal AuthenticatedUser user,
+ @Valid @RequestBody ReturnInventoryRequests.BoxMerge request) {
+ return ApiResponse.success(service.mergeBoxes(PdaWarehouseIdentity.require(user), request));
+ }
+
@GetMapping("/cartons/{cartonMark}/label")
public ApiResponse label(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String cartonMark) {
return ApiResponse.success(service.label(PdaWarehouseIdentity.require(user), cartonMark));
}
+
+ @PostMapping("/cartons/{cartonMark}/putaway")
+ public ApiResponse putaway(@AuthenticationPrincipal AuthenticatedUser user,
+ @PathVariable String cartonMark, @Valid @RequestBody ReturnInventoryRequests.Putaway request) {
+ return ApiResponse.success(service.putaway(PdaWarehouseIdentity.require(user), cartonMark, request));
+ }
+
+ @GetMapping("/packing/plans")
+ public ApiResponse> plans(@AuthenticationPrincipal AuthenticatedUser user) {
+ return ApiResponse.success(service.listPackingPlans(PdaWarehouseIdentity.require(user)));
+ }
+
+ @GetMapping("/packing/plans/{planNo}")
+ public ApiResponse plan(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo) {
+ return ApiResponse.success(service.packingPlan(PdaWarehouseIdentity.require(user), planNo));
+ }
+
+ @PostMapping("/packing/plans/{planNo}/complete")
+ public ApiResponse completePlan(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @Valid @RequestBody ReturnInventoryRequests.PackingPlanComplete request) {
+ return ApiResponse.success(service.completePackingPlan(PdaWarehouseIdentity.require(user), planNo, request));
+ }
}
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java
index c3547af94..a12f8cebd 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java
@@ -77,7 +77,7 @@ public class PdaReturnReceiptController {
String code = org.springframework.web.util.HtmlUtils.htmlEscape(pallet.palletCode());
String returnNo = org.springframework.web.util.HtmlUtils.htmlEscape(request.returnNo());
String sku = org.springframework.web.util.HtmlUtils.htmlEscape(request.skuCode());
- String html = "" + code + "退货托盘标签
" + code + "
退货单:" + returnNo + "
SKU:" + sku + "
";
+ String html = "" + code + "" + code + "
" + returnNo + "
" + sku + "
";
return ApiResponse.success(new Pallet(pallet.palletCode(), pallet.palletId(), pallet.status(), html));
}
@PostMapping("/pallets/check") public ApiResponse checkPallet(@AuthenticationPrincipal AuthenticatedUser user,
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 58e99a99e..22e8be5e5 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
@@ -36,6 +36,12 @@ public class PlatformReturnInventoryController {
return ApiResponse.success(service.platformMovementPage(request));
}
+ @GetMapping("/packing-sources")
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('platform:overseas-warehouse:return-cartons:list')")
+ public ApiResponse> packingSources(@Valid ReturnInventoryRequests.PlatformQuery request) {
+ return ApiResponse.success(service.platformPackingSourcePage(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/controller/PlatformReturnOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnOrderController.java
index ef7009596..4d2578fcc 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnOrderController.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnOrderController.java
@@ -14,6 +14,20 @@ import org.springframework.web.bind.annotation.*;
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
public class PlatformReturnOrderController {
@Resource private ReturnReceiptService service;
+ @Resource private com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService inventory;
+ @PostMapping("/{returnNo}/receipts/{receiptNo}/repair-cartons")
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
+ public ApiResponse> repairCartons(
+ @org.springframework.security.core.annotation.AuthenticationPrincipal com.baiyue.common.security.AuthenticatedUser user,
+ @RequestHeader("X-Customer-Id") String customerId, @PathVariable String returnNo, @PathVariable String receiptNo,
+ @Valid @RequestBody com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.ReceiptCartonRepair request) {
+ return ApiResponse.success(inventory.repairReceiptCartons("platform:" + user.username(), customerId, returnNo, receiptNo, request));
+ }
+ @GetMapping("/{returnNo}")
+ public ApiResponse detail(@RequestHeader("X-Customer-Id") String customerId,
+ @PathVariable String returnNo) {
+ return ApiResponse.success(service.detail(customerId, returnNo));
+ }
@PostMapping("/{returnNo}/review")
public ApiResponse review(@RequestHeader("X-Customer-Id") String customerId,
@PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Review request) {
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnPackingPlanController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnPackingPlanController.java
new file mode 100644
index 000000000..14c3ff1c4
--- /dev/null
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnPackingPlanController.java
@@ -0,0 +1,42 @@
+package com.baiyue.lastleg.returnreceipt.controller;
+
+import com.baiyue.common.core.api.ApiResponse;
+import com.baiyue.common.security.AuthenticatedUser;
+import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
+import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
+import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/platform/v1/last-leg/return-packing-plans")
+public class PlatformReturnPackingPlanController {
+ @Resource private ReturnInventoryService service;
+
+ @PostMapping
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
+ public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnInventoryRequests.PackingPlanCreate request) {
+ return ApiResponse.success(service.createPackingPlan(user == null ? "platform" : user.username(), request));
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
+ public ApiResponse> list(@Valid ReturnInventoryRequests.PackingPlanQuery request) {
+ return ApiResponse.success(service.platformPackingPlans(request));
+ }
+
+ @PostMapping("/{planNo}/review")
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
+ public ApiResponse review(@PathVariable String planNo) {
+ return ApiResponse.success(service.reviewPackingPlanPlatform(planNo));
+ }
+
+ @PostMapping("/{planNo}/cancel")
+ @PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
+ public ApiResponse cancel(@PathVariable String planNo) {
+ return ApiResponse.success(service.cancelPackingPlan(planNo));
+ }
+}
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java
index d5d6a6d62..2d16026cd 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.util.List;
+import java.math.BigDecimal;
public final class PdaReturnReceiptRequests {
private PdaReturnReceiptRequests() { }
@@ -18,10 +19,25 @@ public final class PdaReturnReceiptRequests {
public record Confirm(@NotBlank @Size(max = 128) String returnNo,
@Size(max = 128) String trackingNo,
@NotBlank @Size(max = 16) String startedBy,
- @NotBlank @Size(max = 128) String palletCode,
+ @Size(max = 128) String palletCode,
@NotEmpty @Size(max = 500) List<@Valid Item> items,
- @NotBlank @Size(max = 255) String idempotencyKey) { }
+ @NotBlank @Size(max = 255) String idempotencyKey,
+ @Size(max = 16) String unitType,
+ @Size(max = 128) String unitCode,
+ BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
+ Long totalBoxCount) {
+ public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
+ List<@Valid Item> items, String idempotencyKey) {
+ this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey,
+ null, null, null, null, null, null, null);
+ }
+ }
public record Item(@NotBlank @Size(max = 128) String skuCode,
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = ReturnQuantityDeserializer.class)
- @Positive long receivedQuantity) { }
+ @Positive long receivedQuantity,
+ Long boxCount) {
+ public Item(String skuCode, long receivedQuantity) {
+ this(skuCode, receivedQuantity, null);
+ }
+ }
}
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java
index d60e7bb1b..e06373664 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java
@@ -16,5 +16,7 @@ public final class PdaReturnReceiptResponses {
public record Item(String skuCode, String productNameZh, long expectedQuantity, long receivedQuantity) { }
public record Confirmed(String returnNo, String receiptNo, String receiptBatchNo, String palletCode, String orderStatus,
boolean fullyMatched, long expectedQuantity, long receivedQuantity,
- LocalDateTime signedAt) { }
+ LocalDateTime signedAt, String unitType, String unitCode,
+ String cartonMark, String printableHtml, long totalBoxCount,
+ List labels) { }
}
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java
index 693bf18f7..e3f4d37d5 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java
@@ -14,6 +14,8 @@ import java.util.List;
public final class ReturnInventoryRequests {
private ReturnInventoryRequests() { }
+ public record ReceiptCartonRepair(@Positive long confirmedBoxCount,
+ @NotBlank @Size(max = 255) String idempotencyKey) { }
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String overseasWarehouseId,
String skuCode, String cartonMark, String status, String returnNo) {
@@ -31,7 +33,46 @@ public final class ReturnInventoryRequests {
public PlatformQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
}
- public record PackingQuery(@Size(max = 128) String returnNo) { }
+ public record PackingQuery(@Size(max = 128) String returnNo, @Size(max = 128) String skuCode) { }
+
+ public record PackingPlanQuery(@Min(1) long page, @Min(1) @Max(200) long size, String status, @Size(max = 128) String planNo,
+ @Size(max = 64) String customerId, @Size(max = 64) String overseasWarehouseId, @Size(max = 128) String skuCode) {
+ public PackingPlanQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
+ }
+
+ public record PackingPlanCreate(@NotEmpty @Size(max = 100) List<@NotBlank @Size(max = 128) String> returnNos,
+ @NotBlank @Size(max = 40) String mode,
+ @NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
+ @Size(max = 1000) String remark,
+ @NotBlank @Size(max = 255) String idempotencyKey,
+ @Size(max = 500) List<@NotBlank String> sourceCartonMarks) {
+ public PackingPlanCreate(List returnNos, String mode, List boxes, String remark, String idempotencyKey) {
+ this(returnNos, mode, boxes, remark, idempotencyKey, List.of());
+ }
+ }
+
+ public record PlanBox(@Size(max = 100) List<@Valid PlanItem> items,
+ @Positive BigDecimal length, @Positive BigDecimal width,
+ @Positive BigDecimal height, @Positive BigDecimal weight,
+ @Size(max = 128) String targetCartonMark) {
+ public PlanBox(List items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) {
+ this(items, length, width, height, weight, null);
+ }
+ }
+
+ public record PlanItem(@NotBlank @Size(max = 128) String skuCode,
+ @Positive long quantity) { }
+
+ public record PackingPlanComplete(@NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
+ @NotBlank @Size(max = 255) String idempotencyKey,
+ @Size(max = 1000) List<@Valid SourceDeduction> sourceDeductions) {
+ public PackingPlanComplete(List boxes, String idempotencyKey) { this(boxes, idempotencyKey, null); }
+ }
+ public record SourceDeduction(@NotBlank String cartonMark, @NotBlank String skuCode, @Positive long quantity) { }
+
+ public record PackingExecution(@NotEmpty @Size(max = 100) List<@NotBlank String> returnNos,
+ @NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
+ @NotBlank @Size(max = 180) String idempotencyKey) { }
public record Merge(@NotEmpty @Size(min = 1, max = 100) List<@NotBlank @Size(max = 128) String> returnNos,
@NotEmpty @Size(max = 500) List<@Valid ValidItem> items,
@@ -44,4 +85,19 @@ public final class ReturnInventoryRequests {
public record ValidItem(@NotBlank @Size(max = 128) String skuCode,
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = ReturnQuantityDeserializer.class)
@Positive long quantity) { }
+
+ public record Putaway(@NotBlank @Size(max = 128) String locationCode,
+ @Size(max = 128) String palletCode,
+ @NotBlank @Size(max = 255) String idempotencyKey) { }
+
+ public record BoxMerge(@NotEmpty @Size(max = 100) List<@NotBlank String> sourceCartonMarks,
+ String targetCartonMark,
+ @NotBlank @Size(max = 255) String idempotencyKey,
+ @Size(max = 1000) List<@Valid SourceDeduction> sourceDeductions,
+ @Positive BigDecimal length, @Positive BigDecimal width,
+ @Positive BigDecimal height, @Positive BigDecimal weight) {
+ public BoxMerge(List sourceCartonMarks, String targetCartonMark, String idempotencyKey) {
+ this(sourceCartonMarks, targetCartonMark, idempotencyKey, null, null, null, null, null);
+ }
+ }
}
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 5130349d8..9da92ed40 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
@@ -6,6 +6,8 @@ import java.util.List;
public final class ReturnInventoryResponses {
private ReturnInventoryResponses() { }
+ public record PackingSource(String cartonMark, String sourceReturnNo, String sourceUnitType,
+ List items) { }
public record CartonSummary(String cartonMark, String overseasWarehouseId, String sourceBatchNo,
String sourceReturnNo, String status, int skuTypes, long totalQuantity,
@@ -20,22 +22,42 @@ public final class ReturnInventoryResponses {
public record CartonDetail(String cartonMark, String customerId, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status,
String fbaCartonMark, String destinationWarehouseCode, String destinationWarehouseName, String relabelNo, String relabelStatus,
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) { }
+ BigDecimal weight, LocalDateTime receivedAt, List items,
+ String locationCode, String palletCode, LocalDateTime putawayAt, List mergeHistory) { }
+ public record MergeHistory(String planNo, String sourceCartonMark, String targetCartonMark, String returnNo,
+ String skuCode, long quantity, LocalDateTime createdAt) { }
+ public record CartonItem(String skuCode, String productNameZh, long quantity, long availableQuantity) {
+ public CartonItem(String skuCode, String productNameZh, long quantity) { this(skuCode, productNameZh, quantity, quantity); }
+ }
public record Label(String cartonMark, String overseasWarehouseId, String customerId, String sourceBatchNo, String sourceReturnNo,
long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
List items, LocalDateTime generatedAt, String printableHtml) { }
- public record MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label) { }
+ public record MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label, String planNo) {
+ public MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label) { this(cartonMark, skuCode, totalQuantity, label, null); }
+ }
+ public record PutawayResult(String cartonMark, String locationCode, String palletCode, LocalDateTime putawayAt) { }
public record PackingOrder(String returnNo, String customerId, String customerReferenceNo, String status) { }
public record PackingSku(String skuCode, String productNameZh, long availableQuantity) { }
- public record PackingPreview(List orders, String customerId, List skus) { }
+ public record PackingCustomer(String customerId, List orders, List skus) { }
+ public record PackingPreview(List orders, String customerId, List skus, List customers) {
+ public PackingPreview(List orders, String customerId, List skus) { this(orders, customerId, skus, List.of()); }
+ }
+ public record PackingPlan(String planNo, String customerId, String overseasWarehouseId, List returnNos,
+ String mode, String status, List boxes, String remark, LocalDateTime pushedAt,
+ List sourceCartonMarks, List sources,
+ LocalDateTime createdAt, List resultCartons, LocalDateTime completedAt) { }
+ public record PlanSource(String cartonMark, List items) { }
+ public record PlanBox(int boxNo, List items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) { }
+ public record PlanItem(String skuCode, long quantity) { }
+ public record PackingPlanCompletion(String planNo, List