Merge branch 'dev' into test

# Conflicts:
#	lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java
This commit is contained in:
2026-09-24 09:26:50 +08:00
28 changed files with 1247 additions and 111 deletions
@@ -30,6 +30,7 @@ public final class WarehouseTransferResponses {
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
String relabelApplicationStatus, String relabelFulfillmentStatus,
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
@@ -23,7 +23,11 @@ import org.apache.ibatis.annotations.Update;
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId,
@Param("outboundAt") java.time.LocalDateTime outboundAt);
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND outbound_at IS NULL")
@Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " +
"WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " +
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " +
"JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " +
"WHERE wtp.customer_id=wto.customer_id AND wtp.transfer_order_id=wto.id AND ip.available_packages>0)")
int markOutboundByRelabel(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo,
@Param("outboundAt") java.time.LocalDateTime outboundAt);
}
@@ -147,8 +147,12 @@ public class WarehouseTransferService {
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
request.privateDestinationContactName(), request.privateDestinationContactPhone());
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
request.needRelabel());
// A split relabel transfer may intentionally defer the whole-order
// destination: relabeled cartons go through relabel dispatch while the
// remaining cartons are bound later from warehouse-transfer outbound.
// All other transfer types must still select FBA or PRIVATE up front.
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
@@ -279,8 +283,8 @@ public class WarehouseTransferService {
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
request.privateDestinationContactName(), request.privateDestinationContactPhone());
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
request.needRelabel());
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
LastlegOrder order = requireTailOrder(customerId, transfer);
LocalDateTime now = LocalDateTime.now();
@@ -422,12 +426,16 @@ public class WarehouseTransferService {
.le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo())
.orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId);
Page<WarehouseTransferOrder> result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper);
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(result.getRecords());
List<WarehouseTransferResponses.Summary> records = result.getRecords().stream().map(value ->
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
value.getStatus(),
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
}
@@ -911,7 +919,7 @@ public class WarehouseTransferService {
transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
order.setCurrentStatus(INBOUND_COMPLETED);
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_PROCESSING" : "NOT_REQUIRED");
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_REVIEW" : "NOT_REQUIRED");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
order.setUpdatedAt(now); orders.updateById(order);
if (!java.util.Objects.equals(before, order.getCurrentStatus())) {
@@ -1214,18 +1222,34 @@ public class WarehouseTransferService {
@Transactional(readOnly = true)
public List<WarehouseTransferResponses.Summary> pdaCandidates(long overseasWarehouseId) {
return transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
List<WarehouseTransferOrder> candidates = transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
.eq(WarehouseTransferOrder::getOperatingWarehouseId, String.valueOf(overseasWarehouseId))
.in(WarehouseTransferOrder::getStatus, List.of(WAIT_PDA_RECEIVING, APPROVED, ALLOCATED, WAIT_RELABEL, READY_FOR_DISPATCH))
.orderByDesc(WarehouseTransferOrder::getUpdatedAt)).stream().map(value ->
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(candidates);
return candidates.stream().map(value ->
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
value.getStatus(),
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
}
private Map<String, RelabelApplication> relabelApplications(List<WarehouseTransferOrder> transferOrders) {
Set<String> relabelNos = transferOrders.stream().map(WarehouseTransferOrder::getRelabelNo)
.filter(StringUtils::hasText).collect(Collectors.toSet());
if (relabelNos.isEmpty()) return Map.of();
List<RelabelApplication> applications = relabelApplications.selectList(
new LambdaQueryWrapper<RelabelApplication>().in(RelabelApplication::getRelabelNo, relabelNos));
if (applications == null || applications.isEmpty()) return Map.of();
return applications.stream().collect(Collectors.toMap(RelabelApplication::getRelabelNo,
Function.identity(), (left, right) -> right));
}
@Transactional(readOnly = true)
public WarehouseTransferResponses.Detail pdaDetail(long overseasWarehouseId, String transferNo) {
WarehouseTransferOrder transfer = transfers.findNoByWarehouse(transferNo, String.valueOf(overseasWarehouseId));
@@ -1391,19 +1415,13 @@ public class WarehouseTransferService {
}
}
private static void validateTransferDestinationRelabel(String destinationType, String fbaWarehouseCode,
String destinationWarehouseCode, boolean needRelabel) {
private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode,
boolean allowDeferredForRelabel) {
String type = trim(destinationType);
if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA";
if (type == null || "NONE".equalsIgnoreCase(type)) {
if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) {
throw badRequest("仓储转运必须选择 FBA 仓或私人仓");
}
if ("FBA".equalsIgnoreCase(type) && !needRelabel) {
throw badRequest("转运到 FBA 仓必须同时创建贴标单");
}
if ("PRIVATE".equalsIgnoreCase(type) && needRelabel) {
throw badRequest("不需要贴标时请选择私人仓作为目的地");
}
}
private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) {
@@ -1598,7 +1616,7 @@ public class WarehouseTransferService {
transfer.getOperatingWarehouseId(), transfer.getLabelDeadline(), transfer.getRelabelContactName(),
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
trim(transfer.getRelabelDeliveryMethod()), false);
firstText(transfer.getRelabelDeliveryMethod(), "TRUCK"), false);
return relabelService.createForExistingTailOrder(customerId, operatorId,
transfer.getLastlegOrderId(), request);
}