贴标申请,仓储转运

This commit is contained in:
2026-09-22 08:49:21 +08:00
parent 9147190d67
commit 9a8412279c
28 changed files with 549 additions and 48 deletions
@@ -72,6 +72,8 @@ public record LastlegOrderSummary(
String customerRemark,
String warehouseRemark,
LocalDateTime createdAt,
LocalDateTime updatedAt
LocalDateTime updatedAt,
@Schema(description = "同一主单关联的全部换标子单及各自状态,按创建时间排序")
List<RelabelSubOrder> relabelApplications
) {
}
@@ -0,0 +1,7 @@
package com.baiyue.lastleg.dto;
import java.time.LocalDateTime;
/** A relabel application keeps its own status within the received parent order. */
public record RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status,
Integer quantity, LocalDateTime createdAt) { }
@@ -16,6 +16,11 @@ import java.math.BigDecimal;
@Mapper
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity,r.created_at " +
"FROM ll_relabel_application r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id " +
"WHERE r.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"ORDER BY r.created_at,r.id</script>")
List<com.baiyue.lastleg.dto.RelabelSubOrder> findRelabelSubOrders(@Param("orderIds") List<Long> orderIds);
String WAREHOUSE_FILTER_SQL = "(LOWER(TRIM(overseas_warehouse_code)) LIKE CONCAT('%',LOWER({0}),'%') " +
"OR LOWER(TRIM(overseas_warehouse_name)) LIKE CONCAT('%',LOWER({0}),'%'))";
@@ -38,7 +38,7 @@ public final class RelabelResponses {
String destinationFbaWarehouseName, String skuCode, String productNameZh,
long availablePackages, long version, List<InventoryPosition> positions,
String sourceType, Long returnCartonId, String cartonMark,
List<SkuItem> skuItems) {}
List<SkuItem> skuItems, long relabelledPackages) {}
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
String relabelOrderNo, String orderType, String status, Integer packageCount,
LocalDateTime createdAt) {}
@@ -260,7 +260,7 @@ public class RelabelApplicationService implements WalletResultHandler {
balance.getOverseasWarehouseCode(), balance.getOverseasWarehouseName(),
balance.getDestinationWarehouseCode(), balance.getDestinationWarehouseName(), balance.getSkuCode(),
balance.getProductNameZh(), available, value(balance.getVersion()), positions,
"INBOUND", null, null, List.of());
"INBOUND", null, null, List.of(), 0);
}).filter(candidate -> candidate.availablePackages() > 0).toList());
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
String keyword = trim(inboundCode);
@@ -269,19 +269,23 @@ public class RelabelApplicationService implements WalletResultHandler {
&& !containsIgnoreCase(carton.getSourceReturnNo(), keyword)) continue;
List<RelabelResponses.SkuItem> cartonSkus = returnCartonItems.listCarton(carton.getId()).stream()
.filter(item -> value(item.getQuantity()) > 0)
.map(item -> new RelabelResponses.SkuItem(item.getSkuCode(), null,
Math.toIntExact(item.getQuantity()), null)).toList();
.map(item -> {
long used = returnCartons.relabelledQuantity(carton.getId(), item.getSkuCode());
long remaining = value(item.getQuantity()) - used;
return new RelabelResponses.SkuItem(item.getSkuCode(), null,
Math.toIntExact(Math.max(remaining, 0)), null);
}).filter(item -> item.quantity() > 0).toList();
if (cartonSkus.isEmpty()) continue;
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
.collect(Collectors.joining(","));
long availablePackages = value(carton.getTotalQuantity());
if (availablePackages <= 0) {
availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
}
long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
if (availablePackages <= 0) continue;
long relabelledPackages = carton.getTotalQuantity() == null ? 0
: Math.max(0, carton.getTotalQuantity() - availablePackages);
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
carton.getDestinationWarehouseName(), skuCodes, "退货入库库存", availablePackages, 0, List.of(),
"RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus));
"RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages));
}
return result;
}
@@ -380,9 +384,10 @@ public class RelabelApplicationService implements WalletResultHandler {
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
}
long previouslyRelabelled = returnCartons.relabelledQuantity(carton.getId(), item.skuCode());
long requestedPackages = selectedReturnPackages.merge(carton.getId(),
(long) item.notifiedPackageCount(), Math::addExact);
if (requestedPackages > value(carton.getTotalQuantity())) {
if (requestedPackages + previouslyRelabelled > value(carton.getTotalQuantity())) {
throw conflict("退货托盘 " + carton.getCartonMark() + " 可用箱数不足");
}
sources.add(new SourceInventory(item, null, carton));
@@ -455,9 +460,9 @@ public class RelabelApplicationService implements WalletResultHandler {
applications.insert(application);
for (ReturnInventoryCarton carton : selectedReturnCartons.values()) {
carton.setRelabelApplicationId(applicationId); carton.setRelabelNo(relabelNo);
carton.setRelabelStatus("PENDING"); carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now);
returnCartons.updateById(carton);
if (returnCartons.markRelabelPending(carton.getId(), applicationId, relabelNo, now) != 1) {
throw conflict("退货箱状态已变化,请刷新后重试");
}
}
if (requiresWallet) {
@@ -600,6 +605,7 @@ public class RelabelApplicationService implements WalletResultHandler {
}
}
for (LastlegOrder order : linkedOrders) {
if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue;
String orderBefore = order.getCurrentStatus();
String mainStatus = "RELABEL_APPLICATION".equals(order.getOrderType())
? "WAIT_RELABEL" : orderBefore;
@@ -612,8 +618,7 @@ public class RelabelApplicationService implements WalletResultHandler {
};
// Reviewing/processing a return relabel must not move the already
// completed inbound order back to assignment or relabel waiting.
if ("RETURN_RELABEL".equals(order.getOrderType())
&& !Set.of("COMPLETED", "REJECTED", "CANCELLED").contains(next)) {
if ("RETURN_RELABEL".equals(order.getOrderType())) {
orderStatus = "INBOUND_COMPLETED";
}
order.setCurrentStatus(orderStatus);
@@ -703,6 +708,7 @@ public class RelabelApplicationService implements WalletResultHandler {
application.setStatus("PROCESSING"); application.setFulfillmentStatus("WAIT_RELABEL");
application.setUpdatedAt(LocalDateTime.now()); applications.updateById(application);
for (LastlegOrder order : tailOrders(application)) {
if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue;
order.setRelabelStatus("PROCESSING"); order.setSubOrderStatus("RELABEL:PROCESSING");
order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
}
@@ -748,9 +754,18 @@ public class RelabelApplicationService implements WalletResultHandler {
}
application.setUpdatedAt(now);
applications.updateById(application);
WarehouseTransferOrder linkedTransfer = warehouseTransfers == null ? null
: warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
for (LastlegOrder order : tailOrders(application)) {
if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue;
// 仓储转运尾程单由下方按转运目的仓一次性写入精确状态,避免先落入通用状态。
if (linkedTransfer != null && "WAIT_RELABEL".equals(linkedTransfer.getStatus())
&& Objects.equals(order.getId(), linkedTransfer.getLastlegOrderId())) continue;
String orderBefore = order.getCurrentStatus();
order.setCurrentStatus(requiresOutbound ? "PENDING_ASSIGNMENT" : "COMPLETED");
// 换标确认时货物已经完成入库,不得退回最初建单的“待分配”。
// 独立换标先进入待出库;仓储转运会在下方按目的仓进一步收敛为待排车或待确认目的仓。
order.setCurrentStatus("RETURN_RELABEL".equals(order.getOrderType()) ? "INBOUND_COMPLETED"
: requiresOutbound ? "WAIT_OUTBOUND" : "COMPLETED");
order.setSubOrderStatus("RELABEL:" + application.getFulfillmentStatus());
order.setRelabelStatus("COMPLETED");
order.setUpdatedAt(now);
@@ -774,13 +789,16 @@ public class RelabelApplicationService implements WalletResultHandler {
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
if (carton == null || !Objects.equals(carton.getRelabelApplicationId(), application.getId())) continue;
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);
}
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
request.remark(), now);
// 仓储转运换标在客户确认后立即恢复为可排车,发车时再转为 OUTBOUNDED。
if (warehouseTransfers != null) {
WarehouseTransferOrder transfer = warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
if (linkedTransfer != null) {
WarehouseTransferOrder transfer = linkedTransfer;
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
Set<String> destinations = relabelItems.stream()
.map(RelabelItem::getDestinationFbaWarehouseCode)
@@ -8,7 +8,8 @@ public final class ReturnOrderResponses {
public record Summary(String returnNo, String lastlegOrderNo, String overseasWarehouseId,
String warehouseContactName, String warehouseContactPhone, String customerReferenceNo, String status,
int skuTypes, long expectedQuantity, long receivedQuantity, boolean needRelabel,
String relabelNo, String relabelStatus, String discrepancyReason, LocalDateTime createdAt) { }
String relabelNo, String relabelStatus, String discrepancyReason, LocalDateTime createdAt,
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications) { }
public record Item(String skuCode, String productNameZh, long expectedQuantity,
long receivedQuantity) { }
public record Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts, List<PalletDetail> pallets, List<Photo> photos) { }
@@ -36,6 +36,12 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
ReturnInventoryCarton lockId(@Param("id") long id);
@Select("SELECT COALESCE(SUM(ri.notified_package_count),0) FROM ll_relabel_item ri " +
"JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.return_carton_id=#{cartonId} AND ri.sku_code=#{skuCode} " +
"AND ra.status NOT IN ('CANCELLED','REJECTED')")
long relabelledQuantity(@Param("cartonId") long cartonId, @Param("skuCode") String skuCode);
@Update("UPDATE ll_return_inventory_carton SET total_quantity=#{remaining},sku_types=#{skuTypes},status='AVAILABLE'," +
"relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL,updated_at=NOW(3) " +
"WHERE id=#{id} AND customer_id=#{customerId} AND relabel_application_id=#{applicationId} " +
@@ -44,6 +50,12 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Param("applicationId") long applicationId, @Param("remaining") long remaining,
@Param("skuTypes") int skuTypes);
@Update("UPDATE ll_return_inventory_carton SET relabel_application_id=#{applicationId},relabel_no=#{relabelNo}," +
"relabel_status='PENDING',status='RELABEL_PENDING',updated_at=#{updatedAt} " +
"WHERE id=#{id} AND status='AVAILABLE' AND sku_types>0 AND total_quantity>0 AND relabel_application_id IS NULL")
int markRelabelPending(@Param("id") long id, @Param("applicationId") long applicationId,
@Param("relabelNo") String relabelNo, @Param("updatedAt") java.time.LocalDateTime updatedAt);
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
@@ -13,6 +13,8 @@ public interface ReturnOrderMapper extends BaseMapper<ReturnOrder> {
ReturnOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND return_no=#{returnNo} LIMIT 1")
ReturnOrder findNo(@Param("customerId") String customerId, @Param("returnNo") String returnNo);
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND return_no=#{returnNo} LIMIT 1 FOR UPDATE")
ReturnOrder lockCustomerNo(@Param("customerId") String customerId, @Param("returnNo") String returnNo);
@Select("SELECT * FROM ll_return_order WHERE return_no=#{returnNo} LIMIT 1 FOR UPDATE")
ReturnOrder lockNo(@Param("returnNo") String returnNo);
@Select("SELECT * FROM ll_return_order WHERE return_no=#{returnNo} AND overseas_warehouse_id=#{warehouseId} " +
@@ -186,7 +186,9 @@ public class ReturnReceiptService {
w.in(ReturnOrder::getId, ids);
}
Page<ReturnOrder> result = orders.selectPage(Page.of(query.page(), query.size()), w);
return new PageResult<>(result.getRecords().stream().map(this::summary).toList(), result.getCurrent(),
var relabels = relabelSubOrders(result.getRecords());
return new PageResult<>(result.getRecords().stream().map(order -> summary(order,
relabels.getOrDefault(order.getLastlegOrderId(), List.of()))).toList(), result.getCurrent(),
result.getSize(), result.getTotal(), result.getPages());
}
@@ -216,7 +218,7 @@ public class ReturnReceiptService {
}).toList();
List<ReturnOrderResponses.Photo> photos = photoList(order).stream()
.map(file -> new ReturnOrderResponses.Photo(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes())).toList();
return new ReturnOrderResponses.Detail(summary(order), order.getRemark(), items, receiptViews, palletViews, photos);
return new ReturnOrderResponses.Detail(summary(order, relabelSubOrders(List.of(order)).getOrDefault(order.getLastlegOrderId(), List.of())), order.getRemark(), items, receiptViews, palletViews, photos);
}
public com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway.DownloadedFile downloadPhoto(String customerId, String returnNo, long fileId) {
@@ -265,7 +267,7 @@ public class ReturnReceiptService {
@Transactional
public ReturnOrderResponses.RelabelCreated createRelabel(String customerId, String operatorId, String returnNo,
ReturnOrderRequests.RelabelCreate request) {
ReturnOrder order = orders.findNo(customerId, text(returnNo));
ReturnOrder order = orders.lockCustomerNo(customerId, text(returnNo));
if (order == null) throw notFound("退货单不存在");
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
String fileScope = norm(request.fileScope());
@@ -273,9 +275,14 @@ public class ReturnReceiptService {
String key = text(request.idempotencyKey());
String requestHash = hash(request);
RelabelApplication replay = relabelApplications.findKey(customerId, key);
if (replay != null) return new ReturnOrderResponses.RelabelCreated(replay.getRelabelNo(), replay.getId(),
order.getReturnNo(), request.items().stream().map(ReturnOrderRequests.RelabelItem::skuCode).toList(), replay.getStatus());
if (text(order.getRelabelNo()) != null) throw conflict("当前退货单已经生成贴标申请");
if (replay != null) {
if (!order.getReturnNo().equals(replay.getCustomerOrderNo()) || !requestHash.equals(replay.getRequestHash()))
throw conflict("相同幂等键的贴标申请内容不一致");
return new ReturnOrderResponses.RelabelCreated(replay.getRelabelNo(), replay.getId(),
order.getReturnNo(), request.items().stream().map(ReturnOrderRequests.RelabelItem::skuCode).toList(), replay.getStatus());
}
// The order keeps the latest application reference, not an order-wide inventory lock.
// Each new application must still allocate only unlabelled, available carton quantities below.
var destination = fbaWarehouseValidator.required(request.destinationFbaWarehouseId(), request.destinationFbaWarehouseCode(), null);
Map<String, ReturnOrderItem> receivedBySku = orderItems.lockOrder(order.getId()).stream()
.collect(java.util.stream.Collectors.toMap(item -> norm(item.getSkuCode()), item -> item));
@@ -311,8 +318,10 @@ public class ReturnReceiptService {
tail.setRelabelStatus("PENDING_REVIEW");
tail.setSubOrderStatus("RETURN_RELABEL:PENDING_REVIEW");
tail.setCurrentStatus("RETURN_RELABEL".equals(tail.getOrderType()) ? "INBOUND_COMPLETED" : tail.getCurrentStatus());
tail.setPackageCount(Math.toIntExact(totalQuantity));
tail.setProductQuantity(Math.toIntExact(totalQuantity));
if (!"RETURN_RELABEL".equals(tail.getOrderType())) {
tail.setPackageCount(Math.toIntExact(totalQuantity));
tail.setProductQuantity(Math.toIntExact(totalQuantity));
}
tail.setUpdatedAt(now);
lastlegOrders.updateById(tail);
RelabelApplication app = new RelabelApplication(); app.setId(appId); app.setCustomerId(customerId); app.setRelabelNo(relabelNo); app.setLastlegOrderId(tail.getId());
@@ -331,6 +340,7 @@ public class ReturnReceiptService {
long skuQuantity = cartonItems.listCarton(candidate.getId()).stream()
.filter(line -> norm(requested.skuCode()).equals(norm(line.getSkuCode())))
.mapToLong(line -> value(line.getQuantity())).sum();
skuQuantity -= cartons.relabelledQuantity(candidate.getId(), norm(requested.skuCode()));
if (skuQuantity <= 0) continue;
ReturnInventoryCarton carton = selectedCartons.computeIfAbsent(candidate.getId(), ignored -> cartons.lockId(candidate.getId()));
if (carton == null || !"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null)
@@ -551,10 +561,16 @@ public class ReturnReceiptService {
return new PdaReturnReceiptResponses.Confirmed(order.getReturnNo(), receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(),
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt());
}
private ReturnOrderResponses.Summary summary(ReturnOrder order) { return new ReturnOrderResponses.Summary(order.getReturnNo(), order.getLastlegOrderNo(),
private Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabelSubOrders(List<ReturnOrder> values) {
var ids = values.stream().map(ReturnOrder::getLastlegOrderId).filter(java.util.Objects::nonNull).distinct().toList();
if (ids.isEmpty()) return java.util.Collections.emptyMap();
var rows = lastlegOrders.findRelabelSubOrders(ids);
return rows == null ? java.util.Collections.emptyMap() : rows.stream().collect(java.util.stream.Collectors.groupingBy(com.baiyue.lastleg.dto.RelabelSubOrder::lastlegOrderId));
}
private ReturnOrderResponses.Summary summary(ReturnOrder order, List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications) { return new ReturnOrderResponses.Summary(order.getReturnNo(), order.getLastlegOrderNo(),
order.getOverseasWarehouseId(), order.getWarehouseContactName(), order.getWarehouseContactPhone(), order.getCustomerReferenceNo(), order.getStatus(),
order.getExpectedSkuTypes() == null ? 0 : order.getExpectedSkuTypes(), value(order.getExpectedQuantity()),
value(order.getReceivedQuantity()), Boolean.TRUE.equals(order.getNeedRelabel()), order.getRelabelNo(), order.getRelabelStatus(), order.getDiscrepancyReason(), order.getCreatedAt()); }
value(order.getReceivedQuantity()), Boolean.TRUE.equals(order.getNeedRelabel()), order.getRelabelNo(), order.getRelabelStatus(), order.getDiscrepancyReason(), order.getCreatedAt(), relabelApplications); }
private ReturnOrderResponses.Item item(ReturnOrderItem item) { return new ReturnOrderResponses.Item(item.getSkuCode(),
item.getProductNameZh(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity())); }
private PdaReturnReceiptResponses.Item pdaItem(ReturnOrderItem item) { return new PdaReturnReceiptResponses.Item(item.getSkuCode(),
@@ -96,10 +96,12 @@ public class LastlegOrderService {
Map<String, String> containerNodes = containerNodes(page.getRecords());
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
Map<Long, LastlegOrderMapper.OrderWeightRow> weights = orderWeights(page.getRecords());
Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabels = relabelSubOrders(page.getRecords());
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()),
StringUtils.hasText(order.getSourceContainerBookingId())
? containerNodes.get(order.getSourceContainerBookingId()) : null,
dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"), weights.get(order.getId()))).toList(),
dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"), weights.get(order.getId()),
relabels.getOrDefault(order.getId(), List.of()))).toList(),
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
}
@@ -353,7 +355,7 @@ public class LastlegOrderService {
.sorted(java.util.Comparator.comparingInt(value -> MANUAL_NODE_CODES.indexOf(value.nodeCode()))).toList();
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId()),
containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED"),
orderWeights(List.of(order)).get(order.getId())), manualNodes, cargo, history.stream()
orderWeights(List.of(order)).get(order.getId()), relabelSubOrders(List.of(order)).getOrDefault(order.getId(), List.of())), manualNodes, cargo, history.stream()
.map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(),
value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream()
.map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(),
@@ -577,7 +579,10 @@ public class LastlegOrderService {
.like(LastlegOrder::getOrderNo, query.orderNo())
.or()
.like(LastlegOrder::getSourceOrderNo, query.orderNo()))
.like(StringUtils.hasText(relabelNo), LastlegOrder::getRelabelOrderNo, relabelNo)
.and(StringUtils.hasText(relabelNo), nested -> nested
.like(LastlegOrder::getRelabelOrderNo, relabelNo)
.or().apply("EXISTS (SELECT 1 FROM ll_relabel_application r WHERE r.lastleg_order_id=ll_order.id " +
"AND r.customer_id=ll_order.customer_id AND r.relabel_no LIKE CONCAT('%',{0},'%'))", relabelNo))
.eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType())
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
.like(StringUtils.hasText(query.lxCode()), LastlegOrder::getLxCode, query.lxCode())
@@ -637,9 +642,16 @@ public class LastlegOrderService {
return rows.stream().collect(Collectors.toMap(LastlegOrderMapper.OrderWeightRow::orderId, row -> row));
}
private Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabelSubOrders(List<LastlegOrder> orders) {
if (orders.isEmpty()) return Map.of();
var rows = orderMapper.findRelabelSubOrders(orders.stream().map(LastlegOrder::getId).toList());
return rows == null ? Map.of() : rows.stream().collect(Collectors.groupingBy(com.baiyue.lastleg.dto.RelabelSubOrder::lastlegOrderId));
}
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
String containerNode, String dispatchStatus,
LastlegOrderMapper.OrderWeightRow weight) {
LastlegOrderMapper.OrderWeightRow weight,
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications) {
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(),
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
@@ -657,7 +669,7 @@ public class LastlegOrderService {
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt(), relabelApplications);
}
private static Integer packageDifference(LastlegOrder order) {
@@ -1826,7 +1826,7 @@ CREATE TABLE IF NOT EXISTS ll_relabel_application (
PRIMARY KEY (id),
UNIQUE KEY uk_ll_relabel_application_no (customer_id,relabel_no),
UNIQUE KEY uk_ll_relabel_application_idempotency (customer_id,idempotency_key),
UNIQUE KEY uk_ll_relabel_application_order (lastleg_order_id),
KEY idx_ll_relabel_application_order (lastleg_order_id),
KEY idx_ll_relabel_application_list (customer_id,status,created_at),
KEY idx_ll_relabel_application_customer (customer_id,customer_order_no),
KEY idx_ll_relabel_application_warehouse (customer_id,overseas_warehouse_id)
@@ -228,7 +228,7 @@ paths:
- { name: detained, in: query, schema: { type: boolean } }
- { name: createdFrom, in: query, schema: { type: string, format: date-time } }
- { name: createdTo, in: query, schema: { type: string, format: date-time } }
responses: { '200': { description: 统一 ApiResponse 包装的尾程订单分页;列表行 destinationType 为 FBA 或 OVERSEAS;inboundCode 仅返回真实来源入仓码,无入仓关系的换标申请和本土散板为空 } }
responses: { '200': { description: 统一 ApiResponse 包装的尾程订单分页;每行 relabelApplications 数组按创建时间返回所有换标子单的 relabelNo、status、quantity、createdAt,旧标量字段只代表最近一张;列表行 destinationType 为 FBA 或 OVERSEAS;inboundCode 仅返回真实来源入仓码,无入仓关系的换标申请和本土散板为空 } }
/api/platform/v1/last-leg/orders/{id}:
get:
summary: 查询尾程订单详情
@@ -238,7 +238,7 @@ paths:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
responses:
'200': { description: 统一 ApiResponse 包装的尾程订单详情 }
'200': { description: 统一 ApiResponse 包装的尾程订单详情;order.relabelApplications 返回所有换标子单的独立状态和本次数量 }
'404': { description: 尾程订单不存在或不属于查询客户 }
/api/platform/v1/last-leg/orders/{id}/fees:
post:
@@ -1524,7 +1524,7 @@ paths:
- { name: trackingNo, in: query, schema: { type: string } }
- { name: skuCode, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string, enum: [PENDING_RECEIPT, PARTIALLY_RECEIVED, COMPLETED] } }
responses: { '200': { description: 当前客户退货订单分页 } }
responses: { '200': { description: 当前客户退货订单分页;每行 relabelApplications 返回所有换标子单的 relabelNo、status、quantity、createdAt,主单 status 仍表示退货签收状态 } }
post:
summary: 客户创建退货预报订单
description: 权限 merchant:overseas-warehouse:return-order:create;客户只预报不重复的 SKU 和预计数量,不要求提前知道快递单号;仓库 PDA 实收数量才是实际入库事实。系统退货编号由业务前缀和25位大写Base36完整随机UUID编码组成,退货单RT、签收单RR、批次RB、箱唛RC及退货主单RETU均独立生成,不编码内部ID、仓库或日期。幂等重放返回已保存的编号,不重新生成。
@@ -2691,6 +2691,15 @@ components:
batchNo: { type: string, nullable: true }
warehouseLocationCode: { type: string, nullable: true }
availablePackages: { type: integer, format: int64 }
RelabelSubOrder:
type: object
description: 同一主单可分批生成多张换标申请;各自保留独立状态,取消或完成子单不会覆盖主单签收状态。
properties:
lastlegOrderId: { type: string }
relabelNo: { type: string }
status: { type: string }
quantity: { type: integer, description: 本次申请数量 }
createdAt: { type: string, format: date-time }
RelabelHistoricalOrder:
type: object
properties: