贴标申请,仓储转运

This commit is contained in:
2026-09-24 09:01:14 +08:00
parent 10a2ab3e42
commit 186b6be7d8
27 changed files with 1203 additions and 118 deletions
@@ -17,6 +17,9 @@ import com.baiyue.lastleg.security.OverseasPermissions;
import com.baiyue.lastleg.service.LastlegOrderService;
import com.baiyue.lastleg.service.LastlegOrderCommandService;
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
import com.baiyue.common.event.EventTypes;
import com.baiyue.lastleg.wallet.WalletCommandCompletedConsumer;
@@ -52,6 +55,7 @@ public class LastlegOrderController {
@Resource private LastlegOrderCommandService commandService;
@Resource private InternalServiceTokenGuard tokenGuard;
@Resource private WalletCommandCompletedConsumer walletCommandCompletedConsumer;
@Resource private RelabelApplicationService relabelApplicationService;
@PostMapping("/internal/lastleg/events")
@Operation(summary = "Receive a first-leg loading or actual shipping-node event",
@@ -163,6 +167,18 @@ public class LastlegOrderController {
return ApiResponse.success(orderService.detail(platformScope(user, customerId), id));
}
@PutMapping("/api/platform/v1/last-leg/orders/{id}/relabel-review")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "Review a directly-created labeling order in the tail-leg order workbench")
public ApiResponse<RelabelResponses.Detail> reviewDirectRelabel(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable long id,
@Valid @RequestBody RelabelRequests.ChangeStatus request) {
return ApiResponse.success(relabelApplicationService.reviewDirectTailOrder(
platformScope(user, customerId), id, operatorId(user), request));
}
@PostMapping("/api/platform/v1/last-leg/orders/{id}/fees")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "Add a fee to one tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
@@ -430,33 +430,38 @@ public class DeliveryTransferService {
new LambdaQueryWrapper<WarehouseTransferOrder>()
.eq(StringUtils.hasText(customerId), WarehouseTransferOrder::getCustomerId, trim(customerId))
.eq(StringUtils.hasText(warehouseId), WarehouseTransferOrder::getOperatingWarehouseId, trim(warehouseId))
.eq(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION")
// 已确定目的仓的转运单在完成入库/换标后进入 READY_FOR_DISPATCH;
// 未确定目的仓的历史单据仍保留 WAIT_FBA_DESTINATION,均应进入出库候选。
.in(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION", "READY_FOR_DISPATCH")
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
List<DeliveryResponses.WarehouseTransferOutboundCandidate> result = new ArrayList<>();
for (WarehouseTransferOrder transfer : transfers) {
boolean destinationBound = "FBA".equals(transfer.getDestinationType())
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
: "PRIVATE".equals(transfer.getDestinationType())
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
// This screen is the destination-completion queue. Once a real
// destination is bound, the remaining inventory belongs in smart
// dispatch and must disappear from this list.
if (destinationBound) continue;
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
List<WarehouseTransferPallet> completedPallets = warehouseTransferPallets.listOrder(
transfer.getCustomerId(), transfer.getId()).stream()
.filter(pallet -> "COMPLETED".equals(pallet.getStatus()) && pallet.getWarehousePalletId() != null)
.toList();
long available = 0;
for (WarehouseTransferPallet pallet : completedPallets) {
long palletAvailable = inventoryPositions.listPallet(transfer.getCustomerId(), pallet.getWarehousePalletId())
.stream().filter(position -> transfer.getOperatingWarehouseId().equals(position.getOverseasWarehouseId()))
.mapToLong(position -> value(position.getAvailablePackages())).sum();
available += palletAvailable;
if (palletAvailable <= 0) continue;
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
pallet.getId(), pallet.getWarehousePalletId(), pallet.getPalletNo(), Math.toIntExact(palletAvailable),
pallet.getGrossWeightKg(), pallet.getVolumeCbm()));
}
if (available <= 0 || completedPallets.isEmpty()) continue;
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
null, null, null, Math.toIntExact(available),
completedPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add),
completedPallets.stream().map(WarehouseTransferPallet::getVolumeCbm).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add)));
}
return result;
}
@@ -60,7 +60,7 @@ public interface DispatchCandidateMapper {
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id),0) " +
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " +
@@ -79,15 +79,20 @@ public interface DispatchCandidateMapper {
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_id " +
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
"LEFT JOIN ll_order o ON o.id=l.order_id " +
"WHERE ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
"WHERE ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
"<if test='customerId != null and customerId != \"\"'> AND ip.customer_id=#{customerId}</if> " +
"<if test='planIds != null and planIds.size() > 0'> AND p.plan_id IN " +
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></if> " +
@@ -107,12 +112,18 @@ public interface DispatchCandidateMapper {
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " +
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
// Warehouse-transfer inventory is intentionally reserved before dispatch. It must still be
// eligible for the normal smart-dispatch flow once its destination is bound; other order
// types keep the reservation guard to prevent duplicate dispatching.
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
// A warehouse-transfer pallet may contain both relabel and non-relabel inventory.
// Do not hide the whole pallet because one carton/SKU has an active relabel item;
// the remaining available inventory must still be dispatchable.
"AND (ip.creation_mode!='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
"WHERE rli.customer_id=ip.customer_id AND rli.source_inbound_order_id IS NOT NULL " +
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
"ORDER BY p.plan_id,p.sequence_no" +
@@ -160,7 +171,7 @@ public interface DispatchCandidateMapper {
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " +
@@ -179,31 +190,36 @@ public interface DispatchCandidateMapper {
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_id " +
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
"LEFT JOIN ll_order o ON o.id=l.order_id " +
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
"AND ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
"AND NOT EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation ha " +
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages &gt; 0)) " +
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id) " +
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
"ORDER BY p.id FOR UPDATE" +
@@ -241,7 +241,12 @@ public class DispatchPdaWorkflowService {
}
}
for (DispatchItem item : dispatchItems) {
if (item.getOrderId() != null) warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
// A relabel dispatch may consume only part of a warehouse-transfer
// pallet. Do not close the whole transfer by its parent order; the
// remaining cartons still need destination binding/outbound.
if (item.getOrderId() != null && !"RELABEL_ORDER".equals(item.getSourceBusinessType())) {
warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
}
if ("RELABEL_ORDER".equals(item.getSourceBusinessType()) && item.getSourceBusinessNo() != null) {
warehouseTransfers.markOutboundByRelabel(customerId, item.getSourceBusinessNo(), now);
}
@@ -53,6 +53,12 @@ public class MerchantRelabelApplicationController {
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String relabelNo) { return ApiResponse.success(service.detail(customer(user), relabelNo)); }
@PutMapping("/{relabelNo}/resubmit")
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_CREATE + "')")
public ApiResponse<RelabelResponses.Detail> resubmit(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String relabelNo, @Valid @RequestBody RelabelRequests.Resubmit request) {
return ApiResponse.success(service.resubmit(customer(user), relabelNo, operator(user), request));
}
@PostMapping(value = "/{relabelNo}/shipping-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_FILES + "')")
public ApiResponse<RelabelResponses.Detail> uploadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
@@ -129,6 +129,12 @@ public final class RelabelRequests {
public record ChangeStatus(@NotBlank @Size(max = 32) String status,
@Size(max = 1000) String remark) {}
public record Resubmit(@NotNull LocalDateTime labelDeadline,
@NotBlank @Size(max = 128) String contactName,
@NotBlank @Size(max = 64) String contactPhone,
@Size(max = 1000) String remark,
@NotBlank @Size(max = 255) String idempotencyKey) {}
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
@@ -18,7 +18,7 @@ public final class RelabelResponses {
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
LocalDateTime createdAt) {}
String reviewChannel, String reviewRemark, LocalDateTime createdAt) {}
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
String productImageUrl, String productNameZh, int totalPackageCount,
int notifiedPackageCount, String shipmentNo, String trackingCode,
@@ -9,7 +9,8 @@ public final class PdaRelabelResponses {
public record Candidate(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
String applicationStatus, String taskStatus, String operationType, int itemCount,
int expectedPackages, int relabelledPackages, int skippedPackages,
int exceptionPackages, int version, boolean claimedByCurrentOperator) { }
int exceptionPackages, int version, boolean claimedByCurrentOperator,
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt) { }
public record Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
String contactName, String contactPhone, String applicationStatus, String operationType,
@@ -35,7 +35,8 @@ public interface PdaRelabelTaskMapper extends BaseMapper<PdaRelabelTask> {
relabelled_packages=#{task.relabelledPackages}, skipped_packages=#{task.skippedPackages},
exception_packages=#{task.exceptionPackages}, completion_photo_file_ids=#{task.completionPhotoFileIds},
completion_remark=#{task.completionRemark}, review_decision=#{task.reviewDecision},
review_remark=#{task.reviewRemark}, submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
review_remark=#{task.reviewRemark}, claimed_at=#{task.claimedAt},
submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
version=version+1, updated_at=#{task.updatedAt}
WHERE id=#{task.id} AND customer_id=#{task.customerId} AND version=#{expectedVersion}
""")
@@ -53,6 +53,7 @@ import java.util.stream.Collectors;
@Service
public class PdaRelabelService {
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard bindingGuard;
public static final String AVAILABLE = "AVAILABLE";
public static final String PROCESSING = "PROCESSING";
public static final String WAIT_REVIEW = "WAIT_REVIEW";
public static final String COMPLETED = "COMPLETED";
@@ -116,7 +117,16 @@ public class PdaRelabelService {
throw conflict("当前账号正在执行换标单 " + activeTask.getRelabelNo() + ",请先完成后再操作其他换标单");
}
if (task != null) {
if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
if (AVAILABLE.equals(task.getStatus()) && "REWORK".equals(task.getReviewDecision())) {
int expectedVersion = task.getVersion();
task.setStatus(PROCESSING);
task.setOperatorKey(operatorKey);
task.setOperatorName(operatorName);
task.setClaimedAt(LocalDateTime.now());
updateTask(task, expectedVersion);
insertHistory(task, "REWORK_CLAIM", AVAILABLE, PROCESSING, operatorKey, operatorName,
Map.of("relabelNo", relabelNo));
} else if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
throw conflict("Relabel task is already claimed or awaiting review");
}
} else {
@@ -488,7 +498,7 @@ public class PdaRelabelService {
requireVersion(task, request.expectedVersion());
// Preserve the previous round before freeing the active-label uniqueness constraint.
List<PdaRelabelUnitFact> previousFacts = factList(task);
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, PROCESSING, operatorKey, operatorName,
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, AVAILABLE, operatorKey, operatorName,
Map.of("facts", previousFacts, "photoFileIds", Objects.toString(task.getCompletionPhotoFileIds(), "[]"),
"completionRemark", Objects.toString(task.getCompletionRemark(), ""), "remark", request.remark()));
for (PdaRelabelUnitFact fact : previousFacts) {
@@ -501,7 +511,7 @@ public class PdaRelabelService {
applicationService.resetForRework(application.getCustomerId(), relabelNo, operatorKey, request.remark());
application.setStatus(PROCESSING);
String before = task.getStatus();
String after = PROCESSING;
String after = AVAILABLE;
task.setStatus(after); task.setReviewerKey(operatorKey); task.setReviewerName(operatorName);
task.setReviewDecision(decision); task.setReviewRemark(trim(request.remark()));
task.setReviewedAt(LocalDateTime.now());
@@ -694,7 +704,9 @@ public class PdaRelabelService {
task == null ? value(application.getNotifiedPackageCount()) : value(task.getExpectedPackages()),
task == null ? 0 : value(task.getRelabelledPackages()), task == null ? 0 : value(task.getSkippedPackages()),
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
task != null && operatorKey.equals(task.getOperatorKey()));
task != null && PROCESSING.equals(task.getStatus()) && operatorKey.equals(task.getOperatorKey()),
task == null ? null : task.getReviewDecision(), task == null ? null : task.getReviewRemark(),
task == null ? null : task.getReviewedAt());
}
private PdaRelabelResponses.Detail detail(RelabelApplication application, PdaRelabelTask task) {
@@ -332,7 +332,11 @@ public class RelabelApplicationService implements WalletResultHandler {
private RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request,
Long existingTailOrderId) {
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
boolean requiresOutbound = request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
LastlegOrder linkedSourceOrder = existingTailOrderId == null ? null : orders.selectById(existingTailOrderId);
boolean warehouseTransferRelabel = linkedSourceOrder != null
&& "WAREHOUSE_TRANSFER".equals(linkedSourceOrder.getOrderType());
boolean requiresOutbound = warehouseTransferRelabel
|| request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
String deliveryMethod = trim(request.deliveryMethod());
if (requiresOutbound && (!StringUtils.hasText(deliveryMethod) || !DELIVERY_METHODS.contains(deliveryMethod))) {
throw badRequest("Delivery method must be TRUCK, PICKUP or EXPRESS when carton labels are changed");
@@ -446,7 +450,7 @@ public class RelabelApplicationService implements WalletResultHandler {
application.setPrintShippingLabel(Boolean.TRUE.equals(request.printShippingLabel()));
application.setFulfillmentStatus(requiresOutbound ? "WAIT_RELABEL" : "NOT_REQUIRED");
application.setRemark(trim(request.remark()));
application.setStatus(PENDING_PROCESSING);
application.setStatus(PENDING_REVIEW);
application.setItemCount(request.items().size());
application.setTotalPackageCount(total);
application.setNotifiedPackageCount(notified);
@@ -476,7 +480,7 @@ public class RelabelApplicationService implements WalletResultHandler {
for (SourceInventory source : sources) {
insertItem(customerId, applicationId, source, now);
}
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
insertHistory(customerId, applicationId, null, PENDING_REVIEW, operatorId, "Relabel application submitted for review", now);
if (existingTailOrderId == null) {
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
order.getCurrentStatus(), "Relabel application submitted", now));
@@ -541,6 +545,24 @@ public class RelabelApplicationService implements WalletResultHandler {
@Transactional
public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
RelabelRequests.ChangeStatus request) {
return changeStatus(customerId, relabelNo, operatorId, request, false);
}
@Transactional
public RelabelResponses.Detail reviewDirectTailOrder(String customerId, long orderId, String operatorId,
RelabelRequests.ChangeStatus request) {
LastlegOrder order = orders.selectById(orderId);
if (order == null || !customerId.equals(order.getCustomerId())
|| !"RELABEL_APPLICATION".equals(order.getOrderType())
|| !StringUtils.hasText(order.getRelabelOrderNo())) {
throw notFound("Direct relabel tail order not found");
}
return changeStatus(customerId, order.getRelabelOrderNo(), operatorId, request, true);
}
private RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
RelabelRequests.ChangeStatus request,
boolean directTailOrderReview) {
RelabelApplication application = StringUtils.hasText(customerId)
? applications.lockNo(customerId, relabelNo)
: applications.lockAnyByRelabelNo(relabelNo);
@@ -548,6 +570,16 @@ public class RelabelApplicationService implements WalletResultHandler {
customerId = application.getCustomerId();
String next = request.status().trim().toUpperCase();
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
if ("REJECTED".equals(next) && !StringUtils.hasText(request.remark())) {
throw badRequest("审核驳回必须填写原因");
}
LastlegOrder primaryOrder = orders.selectById(application.getLastlegOrderId());
boolean directApplication = primaryOrder != null && "RELABEL_APPLICATION".equals(primaryOrder.getOrderType());
if (PENDING_REVIEW.equals(application.getStatus()) && directApplication != directTailOrderReview) {
throw conflict(directApplication
? "直接创建的贴标单请在尾程订单审核"
: "关联业务产生的换标单请在换标单审核");
}
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
validateTransition(application.getStatus(), next);
if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next)
@@ -614,7 +646,8 @@ public class RelabelApplicationService implements WalletResultHandler {
case CUSTOMER_CONFIRM_PENDING -> mainStatus;
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
? "PENDING_ASSIGNMENT" : "COMPLETED";
case "REJECTED", "CANCELLED" -> "CANCELLED";
case "REJECTED" -> directApplication ? "REJECTED" : "CANCELLED";
case "CANCELLED" -> "CANCELLED";
default -> mainStatus;
};
// Reviewing/processing a return relabel must not move the already
@@ -700,6 +733,51 @@ public class RelabelApplicationService implements WalletResultHandler {
return detail(customerId, relabelNo);
}
@Transactional
public RelabelResponses.Detail resubmit(String customerId, String relabelNo, String operatorId,
RelabelRequests.Resubmit request) {
RelabelApplication application = applications.lockNo(customerId, relabelNo);
if (application == null) throw notFound("Relabel application not found");
if (!"REJECTED".equals(application.getStatus())) {
throw conflict("Only a rejected relabel application can be resubmitted");
}
LocalDateTime now = LocalDateTime.now();
int holdIndex = 0;
for (RelabelItem item : itemList(application.getId())) {
if (item.getReturnCartonId() != null) {
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
if (carton == null || !customerId.equals(carton.getCustomerId())
|| !"AVAILABLE".equals(carton.getStatus())
|| returnCartons.markRelabelPending(carton.getId(), application.getId(), relabelNo, now) != 1) {
throw conflict("退货箱已被其他作业占用,无法重新提交");
}
} else if (item.getSourceInventoryPositionId() != null
&& Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) > 0) {
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo, null,
"RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++,
"换标驳回修改后重新冻结库存"));
}
}
application.setLabelDeadline(request.labelDeadline());
application.setContactName(request.contactName().trim());
application.setContactPhone(request.contactPhone().trim());
application.setRemark(trim(request.remark()));
application.setStatus(PENDING_REVIEW);
application.setFulfillmentStatus("WAIT_REVIEW");
application.setUpdatedAt(now);
applications.updateById(application);
for (LastlegOrder order : tailOrders(application)) {
order.setRelabelStatus(PENDING_REVIEW);
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW);
order.setUpdatedAt(now);
orders.updateById(order);
}
insertHistory(customerId, application.getId(), "REJECTED", PENDING_REVIEW, operatorId,
request.remark(), now);
return detail(customerId, relabelNo);
}
@Transactional
public void resetForRework(String customerId, String relabelNo, String operatorId, String remark) {
RelabelApplication application = applications.lockNo(customerId, relabelNo);
@@ -801,19 +879,22 @@ public class RelabelApplicationService implements WalletResultHandler {
if (linkedTransfer != null) {
WarehouseTransferOrder transfer = linkedTransfer;
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
Set<String> destinations = relabelItems.stream()
.map(RelabelItem::getDestinationFbaWarehouseCode)
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet());
if (!StringUtils.hasText(transfer.getFbaWarehouseCode()) && destinations.size() == 1) {
transfer.setFbaWarehouseCode(destinations.iterator().next());
}
String transferStatus = StringUtils.hasText(transfer.getFbaWarehouseCode())
// Item-level relabel destinations apply only to those cartons.
// They must never bind the whole transfer, because its remaining
// cartons may still require a separate destination selection.
boolean transferHasDestination = "FBA".equals(transfer.getDestinationType())
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
: "PRIVATE".equals(transfer.getDestinationType())
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
String transferStatus = transferHasDestination
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
transfer.setStatus(transferStatus);
transfer.setRelabelCompletedAt(now);
transfer.setUpdatedAt(now);
warehouseTransfers.updateById(transfer);
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus)
boolean relabelHasDestination = !relabelItems.isEmpty() && relabelItems.stream()
.allMatch(item -> StringUtils.hasText(item.getDestinationFbaWarehouseCode()));
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
? "WAIT_PRE_DISPATCH" : transferStatus);
applications.updateById(application);
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
@@ -945,6 +1026,8 @@ public class RelabelApplicationService implements WalletResultHandler {
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER",
null,
value.getCreatedAt());
}
@@ -996,8 +1079,8 @@ public class RelabelApplicationService implements WalletResultHandler {
.and(w -> w.eq(LastlegOrder::getSourceOrderNo, customerOrderNo).or().eq(LastlegOrder::getOrderNo, customerOrderNo))
.last("LIMIT 1"));
if (existingReturn != null) {
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_PROCESSING);
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_PROCESSING); existingReturn.setPackageCount(packageCount);
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_REVIEW);
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_REVIEW); existingReturn.setPackageCount(packageCount);
existingReturn.setUpdatedAt(now); orders.updateById(existingReturn); return existingReturn;
}
LastlegOrder order = new LastlegOrder();
@@ -1008,9 +1091,9 @@ public class RelabelApplicationService implements WalletResultHandler {
order.setSourceInboundId(relabelNo + ":" + warehouseId);
order.setSourceOrderNo(request.customerOrderNo().trim()); order.setOverseasWarehouseId(warehouseId);
order.setPalletCount(0); order.setPackageCount(packageCount); order.setPickedPackageCount(0);
order.setShippedPackageCount(0); order.setCurrentStatus("WAIT_RELABEL");
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo);
order.setRelabelStatus(PENDING_PROCESSING); order.setCustomerRemark(trim(request.remark()));
order.setShippedPackageCount(0); order.setCurrentStatus(PENDING_REVIEW);
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW); order.setRelabelOrderNo(relabelNo);
order.setRelabelStatus(PENDING_REVIEW); order.setCustomerRemark(trim(request.remark()));
order.setSourceEventId("CUSTOMER_RELABEL:" + relabelNo + ":" + warehouseId);
order.setSourceLoadingPlanId("RELABEL_APPLICATION"); order.setSourcePayloadHash("sha256:" + requestHash);
order.setCreatedAt(now); order.setUpdatedAt(now); orders.insert(order);
@@ -1023,8 +1106,12 @@ public class RelabelApplicationService implements WalletResultHandler {
if (order == null || !customerId.equals(order.getCustomerId())) {
throw conflict("Existing tail order does not belong to this customer");
}
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_PROCESSING);
order.setPackageCount(packageCount); order.setUpdatedAt(now); orders.updateById(order);
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_REVIEW);
// A linked warehouse-transfer relabel can cover only part of the
// transfer. Keep the authoritative whole-order package count so later
// dispatches can attribute the remaining cartons correctly.
if (!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) order.setPackageCount(packageCount);
order.setUpdatedAt(now); orders.updateById(order);
return order;
}
@@ -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,10 @@ 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.
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
@@ -279,8 +281,6 @@ 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());
LastlegOrder order = requireTailOrder(customerId, transfer);
LocalDateTime now = LocalDateTime.now();
@@ -422,12 +422,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 +915,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 +1218,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 +1411,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 +1612,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);
}