优化提交

This commit is contained in:
2026-09-14 09:58:30 +08:00
parent 5ffd6c0118
commit 8cf7c4b9c9
93 changed files with 2945 additions and 188 deletions
@@ -36,6 +36,13 @@ public class MerchantWarehouseTransferController {
return ApiResponse.success(service.detail(customer(user), transferNo));
}
@PostMapping("/{transferNo}/confirm-measurement")
public ApiResponse<WarehouseTransferResponses.Detail> confirmMeasurement(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.CustomerConfirm request) {
return ApiResponse.success(service.confirmMeasurement(customer(user), transferNo, operator(user), request));
}
private String customer(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
@@ -0,0 +1,43 @@
package com.baiyue.lastleg.transfer.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/pda/v1/last-leg/warehouse-transfers")
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
public class PdaWarehouseTransferController {
@Resource private WarehouseTransferService service;
@GetMapping("/candidates")
public ApiResponse<List<WarehouseTransferResponses.Summary>> candidates(
@AuthenticationPrincipal AuthenticatedUser user) {
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
return ApiResponse.success(service.pdaCandidates(identity.overseasWarehouseId()));
}
@PostMapping("/{transferNo}/palletize")
public ApiResponse<WarehouseTransferResponses.Detail> palletize(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Palletize request) {
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
return ApiResponse.success(service.palletize(identity.operatorKey(), identity.overseasWarehouseId(),
transferNo, request));
}
}
@@ -3,12 +3,14 @@ package com.baiyue.lastleg.transfer.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.security.OverseasPermissions;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
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;
@@ -26,22 +28,31 @@ public class WarehouseTransferController {
}
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @Valid WarehouseTransferRequests.Query query) {
return ApiResponse.success(service.page(scope(user, customerId), query));
}
@GetMapping("/{transferNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
return ApiResponse.success(service.detail(scope(user, customerId), transferNo));
}
@GetMapping("/{transferNo}/carton-label")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
public ApiResponse<WarehouseTransferResponses.Label> cartonLabel(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
return ApiResponse.success(service.cartonLabel(scope(user, customerId), transferNo));
}
@PostMapping("/{transferNo}/review")
@PreAuthorize("hasAuthority('" + OverseasPermissions.WAREHOUSE_TRANSFER_APPROVE + "')")
public ApiResponse<WarehouseTransferResponses.Detail> review(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Review request) {
requireApprovalPermission(user);
return ApiResponse.success(service.review(scope(user, customerId), transferNo, operator(user), request));
}
@@ -69,13 +80,6 @@ public class WarehouseTransferController {
return ApiResponse.success(service.receive(scope(user, customerId), transferNo, operator(user), request));
}
private void requireApprovalPermission(AuthenticatedUser user) {
if (user == null || user.permissions() == null
|| !user.permissions().contains("lastleg:warehouse-transfer:approve")) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Warehouse transfer approval permission is required");
}
}
private void requirePermission(AuthenticatedUser user, String permission) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (user.permissions() == null || !user.permissions().contains(permission)) {
@@ -23,17 +23,34 @@ public final class WarehouseTransferRequests {
@Size(max = 128) String fbaWarehouseCode,
@NotBlank @Size(max = 1000) String deliveryAddress,
@Positive int packageCount,
@NotNull @DecimalMin(value = "0.0001") BigDecimal volumeCbm,
@NotNull @DecimalMin(value = "0.0") BigDecimal volumeCbm,
@NotBlank @Size(max = 64) String deliveryMethod,
@NotNull LocalDateTime estimatedArrivalAt,
LocalDateTime estimatedShipAt,
@Size(max = 128) String fbaShipmentNo,
@Size(max = 255) String trackingNo,
@Size(max = 2000) String fbaBoxNos,
@Size(max = 1000) String remark,
boolean needRelabel,
LocalDateTime labelDeadline,
@Size(max = 128) String relabelContactName,
@Size(max = 64) String relabelContactPhone,
@DecimalMin(value = "0.01") BigDecimal relabelChargeAmount,
@Size(max = 32) String relabelDeliveryMethod,
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 500) List<@Valid Item> items
) {}
@NotEmpty @Size(max = 500) List<@Valid Item> items,
@Size(max = 1000) List<@Valid RelabelItem> relabelItems
) {
public Create(String inboundCode, List<Long> sourceLastlegOrderIds, String operatingWarehouseId,
String fbaWarehouseCode, String deliveryAddress, int packageCount, BigDecimal volumeCbm,
String deliveryMethod, LocalDateTime estimatedArrivalAt, String fbaShipmentNo,
String trackingNo, String fbaBoxNos, String remark, boolean needRelabel,
String idempotencyKey, List<Item> items) {
this(inboundCode, sourceLastlegOrderIds, operatingWarehouseId, fbaWarehouseCode, deliveryAddress,
packageCount, volumeCbm, deliveryMethod, estimatedArrivalAt, null, fbaShipmentNo, trackingNo,
fbaBoxNos, remark, needRelabel, null, null, null, null, null, idempotencyKey, items, null);
}
}
public record Review(@NotBlank @Size(max = 32) String decision,
@Size(max = 1000) String remark) {}
@@ -52,6 +69,24 @@ public final class WarehouseTransferRequests {
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
public record Palletize(
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 100) List<@Valid Pallet> pallets
) {}
public record Pallet(
@Size(max = 128) String palletCode,
@Positive int cartonCount,
@NotNull @DecimalMin("0.000001") BigDecimal volumeCbm,
@NotNull @DecimalMin("0.01") BigDecimal grossWeightKg,
@NotEmpty @Size(max = 500) List<@Valid PalletItem> items
) {}
public record PalletItem(@NotNull Long transferItemId, @Positive int packageCount) {}
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
public record Receipt(
@NotNull Long sourceInventoryPositionId,
@NotBlank @Size(max = 128) String destinationOverseasWarehouseId,
@@ -76,6 +111,17 @@ public final class WarehouseTransferRequests {
@Size(max = 255) String shippingMark
) {}
public record RelabelItem(
@NotBlank @Size(max = 32) String operationType,
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> customerCartonMarks,
@NotBlank @Size(max = 128) String originalSku,
@Size(max = 128) String newSku,
@Positive int quantity,
@Size(max = 255) String fbaCartonMark,
@Size(max = 128) String destinationWarehouseCode,
@Size(max = 1000) String remark
) {}
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
String transferNo, String inboundCode, String operatingWarehouseId,
String fbaWarehouseCode, String status, Boolean needRelabel,
@@ -1,12 +1,23 @@
package com.baiyue.lastleg.transfer.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
public final class WarehouseTransferResponses {
private WarehouseTransferResponses() {}
public record Item(String skuCode, String productNameZh, int packageCount, String shippingMark) {}
public record Carton(String cartonMark, int cartonIndex, int totalCartons, String status) {}
public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark) {}
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status) {}
public record Label(String transferNo, List<Carton> cartons, String printableHtml) {}
public record PalletItem(Long transferItemId, String skuCode, int packageCount) {}
public record Pallet(Long id, int sequenceNo, Long warehousePalletId, String palletNo,
Long warehouseLocationId, String warehouseLocationCode, int cartonCount,
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
BigDecimal grossWeightKg, BigDecimal volumeCbm, String status,
List<PalletItem> items, LocalDateTime completedAt) {}
public record RelabelBinding(String operationType, String customerCartonMark, String originalSku,
String newSku, int quantity, String fbaCartonMark,
String destinationWarehouseCode, String remark) {}
public record SourceOrder(Long lastlegOrderId, String lastlegOrderNo, Long inboundOrderId, String inboundCode) {}
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
Long palletId, String palletNo, int packageCount, String status,
@@ -19,9 +30,12 @@ public final class WarehouseTransferResponses {
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
String status,
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
int packageCount, String deliveryMethod, LocalDateTime estimatedArrivalAt,
LocalDateTime estimatedShipAt,
String fbaShipmentNo, String trackingNo, String fbaBoxNos, String remark,
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons, List<Allocation> allocations,
LocalDateTime outboundAt, LocalDateTime createdAt) {}
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons,
List<Pallet> pallets, List<RelabelBinding> relabelBindings,
List<Allocation> allocations, LocalDateTime customerConfirmedAt,
LocalDateTime actualShipAt, LocalDateTime createdAt) {}
}
@@ -15,6 +15,7 @@ public class WarehouseTransferCarton {
private Long transferItemId;
private String customerId;
private String cartonMark;
private String customerCartonMark;
private Integer cartonIndex;
private Integer totalCartons;
private String status;
@@ -22,19 +22,31 @@ public class WarehouseTransferOrder {
private Integer packageCount;
private String deliveryMethod;
private LocalDateTime estimatedArrivalAt;
private LocalDateTime estimatedShipAt;
private String fbaShipmentNo;
private String trackingNo;
private String fbaBoxNos;
private String remark;
private Boolean needRelabel;
private LocalDateTime labelDeadline;
private String relabelContactName;
private String relabelContactPhone;
private java.math.BigDecimal relabelChargeAmount;
private String relabelDeliveryMethod;
private Long relabelApplicationId;
private String relabelNo;
private LocalDateTime relabelCompletedAt;
private String status;
private String allocationIdempotencyKey;
private String allocationRequestHash;
private String outboundIdempotencyKey;
private String outboundRequestHash;
private LocalDateTime outboundAt;
private String palletizeIdempotencyKey;
private String palletizeRequestHash;
private String confirmationIdempotencyKey;
private String confirmationRequestHash;
private LocalDateTime customerConfirmedAt;
private String idempotencyKey;
private String requestHash;
private String createdBy;
@@ -0,0 +1,33 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_pallet")
public class WarehouseTransferPallet {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private String customerId;
private Integer sequenceNo;
private Long warehousePalletId;
private String palletNo;
private Long warehouseLocationId;
private String warehouseLocationCode;
private Long warehouseResourceLockId;
private Integer cartonCount;
private BigDecimal lengthCm;
private BigDecimal widthCm;
private BigDecimal heightCm;
private BigDecimal grossWeightKg;
private BigDecimal volumeCbm;
private String status;
private LocalDateTime completedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,20 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_pallet_item")
public class WarehouseTransferPalletItem {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private Long transferPalletId;
private Long transferItemId;
private String customerId;
private Integer packageCount;
private LocalDateTime createdAt;
}
@@ -0,0 +1,25 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_relabel_binding")
public class WarehouseTransferRelabelBinding {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private String customerId;
private String operationType;
private String customerCartonMark;
private String originalSku;
private String newSku;
private Integer quantity;
private String fbaCartonMark;
private String destinationWarehouseCode;
private String remark;
private LocalDateTime createdAt;
}
@@ -4,6 +4,7 @@ import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper public interface WarehouseTransferOrderMapper extends BaseMapper<WarehouseTransferOrder> {
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
WarehouseTransferOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
@@ -11,4 +12,16 @@ import org.apache.ibatis.annotations.Select;
WarehouseTransferOrder findNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1 FOR UPDATE")
WarehouseTransferOrder lockNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE transfer_no=#{transferNo} AND operating_warehouse_id=#{warehouseId} LIMIT 1 FOR UPDATE")
WarehouseTransferOrder lockForPda(@Param("transferNo") String transferNo, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id")
java.util.List<WarehouseTransferOrder> pdaCandidates(@Param("warehouseId") String warehouseId);
@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")
int markOutboundByRelabel(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo,
@Param("outboundAt") java.time.LocalDateTime outboundAt);
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPalletItem;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface WarehouseTransferPalletItemMapper extends BaseMapper<WarehouseTransferPalletItem> {
@Select("SELECT * FROM ll_warehouse_transfer_pallet_item WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY transfer_pallet_id,id")
List<WarehouseTransferPalletItem> listOrder(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId);
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface WarehouseTransferPalletMapper extends BaseMapper<WarehouseTransferPallet> {
@Select("SELECT * FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY sequence_no")
List<WarehouseTransferPallet> listOrder(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId);
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface WarehouseTransferRelabelBindingMapper extends BaseMapper<WarehouseTransferRelabelBinding> {
@Select("SELECT * FROM ll_warehouse_transfer_relabel_binding WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY id")
List<WarehouseTransferRelabelBinding> listOrder(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId);
}
@@ -20,6 +20,20 @@ import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
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.inboundplan.entity.InboundPlan;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
@@ -29,6 +43,9 @@ import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferReceipt;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPalletItem;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding;
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
@@ -36,6 +53,9 @@ import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMa
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferSourceOrderMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletItemMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
@@ -53,7 +73,12 @@ import java.util.HexFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.ArrayList;
import java.util.Comparator;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -68,11 +93,18 @@ public class WarehouseTransferService {
public static final String OUTBOUNDED = "OUTBOUNDED";
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
public static final String RECEIVED = "RECEIVED";
public static final String WAIT_PDA_RECEIVING = "WAIT_PDA_RECEIVING";
public static final String WAIT_CUSTOMER_CONFIRMATION = "WAIT_CUSTOMER_CONFIRMATION";
public static final String READY_FOR_DISPATCH = "READY_FOR_DISPATCH";
public static final String WAIT_RELABEL = "WAIT_RELABEL";
@Resource private WarehouseTransferOrderMapper transfers;
@Resource private WarehouseTransferItemMapper transferItems;
@Resource private WarehouseTransferCartonMapper transferCartons;
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
@Resource private WarehouseTransferPalletMapper transferPallets;
@Resource private WarehouseTransferPalletItemMapper transferPalletItems;
@Resource private WarehouseTransferRelabelBindingMapper relabelBindings;
@Resource private WarehouseTransferInventoryAllocationMapper inventoryAllocations;
@Resource private WarehouseTransferReceiptMapper transferReceipts;
@Resource private InboundSnapshotMapper inboundSnapshots;
@@ -81,6 +113,13 @@ public class WarehouseTransferService {
@Resource private LastlegOrderDetailMapper orderDetails;
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
@Resource private RelabelApplicationMapper relabelApplications;
@Resource private RelabelApplicationService relabelService;
@Resource private WarehouseResourceGateway warehouseResources;
@Resource private InboundPlanMapper inboundPlans;
@Resource private InboundPlanLineMapper inboundPlanLines;
@Resource private InboundPlanPalletMapper inboundPlanPallets;
@Resource private InboundPlanPalletAllocationMapper inboundPlanAllocations;
@Resource private InboundStockReceiptMapper stockReceipts;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Resource private InventoryCommandService inventory;
@Resource private InventoryCommandMapper inventoryCommands;
@@ -93,6 +132,7 @@ public class WarehouseTransferService {
if (itemPackages != request.packageCount()) {
throw badRequest("Cargo item package total must equal transfer package count");
}
validateRelabelRequest(request);
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
@@ -142,10 +182,16 @@ public class WarehouseTransferService {
transfer.setFbaWarehouseCode(trim(request.fbaWarehouseCode()));
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
transfer.setEstimatedShipAt(request.estimatedShipAt());
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim()) ? trim(request.trackingNo()) : null);
transfer.setFbaBoxNos(trim(request.fbaBoxNos())); transfer.setRemark(trim(request.remark()));
transfer.setNeedRelabel(request.needRelabel()); transfer.setStatus(PENDING_REVIEW);
transfer.setLabelDeadline(request.labelDeadline());
transfer.setRelabelContactName(trim(request.relabelContactName()));
transfer.setRelabelContactPhone(trim(request.relabelContactPhone()));
transfer.setRelabelChargeAmount(request.relabelChargeAmount());
transfer.setRelabelDeliveryMethod(trim(request.relabelDeliveryMethod()));
transfer.setIdempotencyKey(key); transfer.setRequestHash(requestHash); transfer.setCreatedBy(trim(operatorId));
transfer.setCreatedAt(now); transfer.setUpdatedAt(now);
transfers.insert(transfer);
@@ -164,14 +210,17 @@ public class WarehouseTransferService {
sourceOrders.insert(link);
}
if (request.needRelabel() && request.relabelItems() != null) insertRelabelBindings(customerId, transferId, request.relabelItems(), now);
int cartonIndex = 0;
for (int index = 0; index < request.items().size(); index++) {
WarehouseTransferRequests.Item source = request.items().get(index);
long itemId = insertTransferItem(customerId, transferId, source, now);
List<String> customerMarks = splitMarks(source.shippingMark());
for (int itemCarton = 0; itemCarton < source.packageCount(); itemCarton++) {
cartonIndex++;
insertTransferCarton(customerId, transferId, itemId, transferNo,
cartonIndex, request.packageCount(), now);
cartonIndex, request.packageCount(), itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null, now);
}
insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now);
}
@@ -213,16 +262,36 @@ public class WarehouseTransferService {
List<WarehouseTransferResponses.Item> items = transferItems.selectList(
new LambdaQueryWrapper<WarehouseTransferItem>().eq(WarehouseTransferItem::getCustomerId, customerId)
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId))
.stream().map(value -> new WarehouseTransferResponses.Item(value.getSkuCode(), value.getProductNameZh(),
.stream().map(value -> new WarehouseTransferResponses.Item(value.getId(), value.getSkuCode(), value.getProductNameZh(),
value.getPackageCount(), value.getShippingMark())).toList();
Map<Long, WarehouseTransferResponses.Item> itemById = items.stream()
.collect(Collectors.toMap(WarehouseTransferResponses.Item::id, Function.identity()));
List<WarehouseTransferResponses.SourceOrder> sources = sourceOrders.selectList(
new LambdaQueryWrapper<WarehouseTransferSourceOrder>().eq(WarehouseTransferSourceOrder::getCustomerId, customerId)
.eq(WarehouseTransferSourceOrder::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferSourceOrder::getId))
.stream().map(value -> new WarehouseTransferResponses.SourceOrder(value.getSourceLastlegOrderId(),
value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList();
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId())
.stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(),
.stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(),
value.getCartonIndex(), value.getTotalCartons(), value.getStatus())).toList();
List<WarehouseTransferPalletItem> palletItemRows = transferPalletItems == null
? List.of() : transferPalletItems.listOrder(customerId, transfer.getId());
Map<Long, List<WarehouseTransferPalletItem>> palletItemsByPallet = palletItemRows.stream()
.collect(Collectors.groupingBy(WarehouseTransferPalletItem::getTransferPalletId));
List<WarehouseTransferResponses.Pallet> pallets = transferPallets == null ? List.of() : transferPallets.listOrder(customerId, transfer.getId()).stream()
.map(value -> new WarehouseTransferResponses.Pallet(value.getId(), value.getSequenceNo(),
value.getWarehousePalletId(), value.getPalletNo(), value.getWarehouseLocationId(),
value.getWarehouseLocationCode(), value.getCartonCount(), value.getLengthCm(), value.getWidthCm(),
value.getHeightCm(), value.getGrossWeightKg(), value.getVolumeCbm(), value.getStatus(),
palletItemsByPallet.getOrDefault(value.getId(), List.of()).stream().map(row -> {
WarehouseTransferResponses.Item item = itemById.get(row.getTransferItemId());
return new WarehouseTransferResponses.PalletItem(row.getTransferItemId(),
item == null ? null : item.skuCode(), row.getPackageCount());
}).toList(), value.getCompletedAt())).toList();
List<WarehouseTransferResponses.RelabelBinding> bindings = relabelBindings == null ? List.of() : relabelBindings.listOrder(customerId, transfer.getId())
.stream().map(value -> new WarehouseTransferResponses.RelabelBinding(value.getOperationType(),
value.getCustomerCartonMark(), value.getOriginalSku(), value.getNewSku(), value.getQuantity(),
value.getFbaCartonMark(), value.getDestinationWarehouseCode(), value.getRemark())).toList();
Map<Long, WarehouseTransferReceipt> receiptByAllocation = transferReceipts.selectList(
new LambdaQueryWrapper<WarehouseTransferReceipt>().eq(WarehouseTransferReceipt::getCustomerId, customerId)
.eq(WarehouseTransferReceipt::getTransferOrderId, transfer.getId()))
@@ -252,9 +321,32 @@ public class WarehouseTransferService {
Boolean.TRUE.equals(transfer.getNeedRelabel()), transfer.getRelabelApplicationId(), transfer.getRelabelNo(),
relabelStatus,
transfer.getOperatingWarehouseId(), transfer.getFbaWarehouseCode(), transfer.getDeliveryAddress(),
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(), transfer.getFbaShipmentNo(),
transfer.getTrackingNo(), transfer.getFbaBoxNos(), transfer.getRemark(), sources, items, cartons, allocations,
transfer.getOutboundAt(), transfer.getCreatedAt());
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(),
transfer.getEstimatedShipAt(), transfer.getFbaShipmentNo(), transfer.getTrackingNo(), transfer.getFbaBoxNos(),
transfer.getRemark(), sources, items, cartons, pallets, bindings, allocations,
transfer.getCustomerConfirmedAt(), transfer.getOutboundAt(), transfer.getCreatedAt());
}
@Transactional(readOnly = true)
public WarehouseTransferResponses.Label cartonLabel(String customerId, String transferNo) {
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus())).toList();
StringBuilder labels = new StringBuilder();
for (WarehouseTransferResponses.Carton carton : cartons) {
labels.append("<section class=label><div class=title>仓储转运箱唛</div><div class=mark>")
.append(escape(carton.cartonMark())).append("</div><div class=meta>")
.append(carton.cartonIndex()).append(" / ").append(carton.totalCartons()).append(" 转运单号:")
.append(escape(transferNo)).append("</div>");
if (text(carton.customerCartonMark()) != null) labels.append("<div class=customer>客户箱唛:")
.append(escape(carton.customerCartonMark())).append("</div>");
labels.append("</section>");
}
String html = "<!doctype html><html><head><meta charset=utf-8><title>" + escape(transferNo) + "-箱唛</title><style>"
+ "@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:12mm 0 6mm;font-size:30px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.meta,.customer{font-size:12px;margin-top:3mm}</style></head><body>"
+ labels + "</body></html>";
return new WarehouseTransferResponses.Label(transferNo, cartons, html);
}
@Transactional
@@ -269,15 +361,18 @@ public class WarehouseTransferService {
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (!PENDING_REVIEW.equals(transfer.getStatus())) {
if (next.equals(transfer.getStatus())) return detail(customerId, transferNo);
if (next.equals(transfer.getStatus()) || APPROVED.equals(next)
&& Set.of(WAIT_PDA_RECEIVING, WAIT_CUSTOMER_CONFIRMATION, READY_FOR_DISPATCH, WAIT_RELABEL)
.contains(transfer.getStatus())) return detail(customerId, transferNo);
throw conflict("Warehouse transfer order was already reviewed");
}
LastlegOrder order = orders.selectById(transfer.getLastlegOrderId());
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing");
LocalDateTime now = LocalDateTime.now();
String before = order.getCurrentStatus();
transfer.setStatus(next); transfer.setUpdatedAt(now); transfers.updateById(transfer);
boolean incomingTransit = transfer.getTransferNo().equals(transfer.getInboundCode());
String transferStatus = APPROVED.equals(next) && incomingTransit ? WAIT_PDA_RECEIVING : next;
transfer.setStatus(transferStatus); transfer.setUpdatedAt(now); transfers.updateById(transfer);
String orderStatus = APPROVED.equals(next)
? (incomingTransit
? ("WAREHOUSE_PICKUP".equals(transfer.getDeliveryMethod()) ? "WAIT_PICKUP" : "WAIT_DELIVERY")
@@ -294,6 +389,191 @@ public class WarehouseTransferService {
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail palletize(String operatorId, long overseasWarehouseId,
String transferNo,
WarehouseTransferRequests.Palletize request) {
WarehouseTransferOrder transfer = transfers.lockForPda(transferNo, String.valueOf(overseasWarehouseId));
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (!String.valueOf(overseasWarehouseId).equals(transfer.getOperatingWarehouseId())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Warehouse transfer belongs to another warehouse");
}
String customerId = transfer.getCustomerId();
String requestHash = hash(request);
if (WAIT_CUSTOMER_CONFIRMATION.equals(transfer.getStatus())
&& request.idempotencyKey().equals(transfer.getPalletizeIdempotencyKey())) {
if (!requestHash.equals(transfer.getPalletizeRequestHash())) {
throw conflict("Palletization idempotency key was reused with different data");
}
return detail(customerId, transferNo);
}
if (!WAIT_PDA_RECEIVING.equals(transfer.getStatus())) {
throw conflict("Warehouse transfer is not awaiting PDA receiving");
}
List<WarehouseTransferItem> items = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
.eq(WarehouseTransferItem::getCustomerId, customerId)
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId));
Map<Long, WarehouseTransferItem> itemById = items.stream()
.collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
Map<Long, Integer> totals = new java.util.HashMap<>();
int palletCartons = 0;
for (WarehouseTransferRequests.Pallet pallet : request.pallets()) {
int itemTotal = pallet.items().stream().mapToInt(WarehouseTransferRequests.PalletItem::packageCount).sum();
if (itemTotal != pallet.cartonCount()) throw badRequest("Each pallet carton count must equal its SKU carton total");
palletCartons = Math.addExact(palletCartons, pallet.cartonCount());
Set<Long> uniqueItems = new HashSet<>();
for (WarehouseTransferRequests.PalletItem item : pallet.items()) {
if (!uniqueItems.add(item.transferItemId()) || !itemById.containsKey(item.transferItemId())) {
throw badRequest("Pallet SKU rows must be unique and belong to this transfer");
}
totals.merge(item.transferItemId(), item.packageCount(), Math::addExact);
}
}
if (palletCartons != transfer.getPackageCount()) throw badRequest("Pallet carton total must equal transfer package count");
for (WarehouseTransferItem item : items) {
if (totals.getOrDefault(item.getId(), 0) != item.getPackageCount()) {
throw badRequest("Palletized carton total does not match declared SKU " + item.getSkuCode());
}
}
String batchKey = "WAREHOUSE_TRANSFER:" + transferNo + ":PALLETIZE";
List<WarehouseResourceGateway.PalletBatchItemRequest> batchItems = new ArrayList<>();
for (int index = 0; index < request.pallets().size(); index++) {
WarehouseTransferRequests.Pallet pallet = request.pallets().get(index);
batchItems.add(new WarehouseResourceGateway.PalletBatchItemRequest(Long.parseLong(customerId),
transferNo + ":" + (index + 1), transferNo, index + 1, "STANDARD",
trim(transfer.getFbaWarehouseCode()), trim(pallet.palletCode())));
}
WarehouseResourceGateway.PalletBatchResponse ensured = warehouseResources.ensurePalletBatch(
new WarehouseResourceGateway.PalletBatchEnsure(overseasWarehouseId, batchKey, batchItems));
WarehouseResourceGateway.PalletBatchResponse confirmed = warehouseResources.confirmPalletBatch(
new WarehouseResourceGateway.PalletBatchCommand(overseasWarehouseId, batchKey,
request.idempotencyKey() + ":CONFIRM"));
if (confirmed == null || confirmed.items() == null || confirmed.items().size() != request.pallets().size()) {
throw conflict("Warehouse pallet batch did not return every pallet");
}
LocalDateTime now = LocalDateTime.now();
for (int index = 0; index < request.pallets().size(); index++) {
int sequenceNo = index + 1;
WarehouseTransferRequests.Pallet source = request.pallets().get(index);
WarehouseResourceGateway.PalletBatchItem resource = confirmed.items().stream()
.filter(value -> value.sequenceNo() == sequenceNo).findFirst()
.orElseThrow(() -> conflict("Warehouse pallet sequence is incomplete"));
WarehouseResourceGateway.WarehouseLocationReference location = warehouseResources.resolveLocation(
overseasWarehouseId, resource.locationId());
WarehouseTransferPallet pallet = new WarehouseTransferPallet();
pallet.setId(IdWorker.getId()); pallet.setTransferOrderId(transfer.getId()); pallet.setCustomerId(customerId);
pallet.setSequenceNo(sequenceNo); pallet.setWarehousePalletId(resource.palletId());
pallet.setPalletNo(resource.palletCode()); pallet.setWarehouseLocationId(resource.locationId());
pallet.setWarehouseLocationCode(location.locationCode()); pallet.setWarehouseResourceLockId(resource.resourceLockId());
// 转运测量以打托后的托盘为单位,体积由 PDA 直接录入托盘总体积,不再按单箱尺寸换算。
// 旧表字段仍为 NOT NULL;按托模式不采集长宽高,以 0 兼容存储且不得参与总体积计算。
pallet.setLengthCm(BigDecimal.ZERO); pallet.setWidthCm(BigDecimal.ZERO); pallet.setHeightCm(BigDecimal.ZERO);
pallet.setCartonCount(source.cartonCount()); pallet.setGrossWeightKg(source.grossWeightKg());
pallet.setVolumeCbm(source.volumeCbm().setScale(6, RoundingMode.HALF_UP));
pallet.setStatus("COMPLETED"); pallet.setCompletedAt(now); pallet.setCreatedAt(now); pallet.setUpdatedAt(now);
transferPallets.insert(pallet);
for (WarehouseTransferRequests.PalletItem sourceItem : source.items()) {
WarehouseTransferPalletItem row = new WarehouseTransferPalletItem();
row.setId(IdWorker.getId()); row.setTransferOrderId(transfer.getId()); row.setTransferPalletId(pallet.getId());
row.setTransferItemId(sourceItem.transferItemId()); row.setCustomerId(customerId);
row.setPackageCount(sourceItem.packageCount()); row.setCreatedAt(now); transferPalletItems.insert(row);
}
}
transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim());
transfer.setPalletizeRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
order.setCurrentStatus(WAIT_CUSTOMER_CONFIRMATION); order.setPalletCount(request.pallets().size());
order.setVolumeCbm(transferPallets.listOrder(customerId, transfer.getId()).stream()
.map(WarehouseTransferPallet::getVolumeCbm).reduce(BigDecimal.ZERO, BigDecimal::add));
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + WAIT_CUSTOMER_CONFIRMATION); order.setUpdatedAt(now); orders.updateById(order);
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
"PDA receiving measurement and palletization completed", now);
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail confirmMeasurement(String customerId, String transferNo,
String operatorId,
WarehouseTransferRequests.CustomerConfirm request) {
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
String requestHash = hash(request);
if (Set.of(READY_FOR_DISPATCH, WAIT_RELABEL, OUTBOUNDED).contains(transfer.getStatus())
&& request.idempotencyKey().equals(transfer.getConfirmationIdempotencyKey())) {
if (!requestHash.equals(transfer.getConfirmationRequestHash())) {
throw conflict("Confirmation idempotency key was reused with different data");
}
return detail(customerId, transferNo);
}
if (!WAIT_CUSTOMER_CONFIRMATION.equals(transfer.getStatus())) {
throw conflict("Warehouse transfer measurement is not awaiting customer confirmation");
}
List<WarehouseTransferItem> items = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
.eq(WarehouseTransferItem::getCustomerId, customerId)
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId));
Map<Long, WarehouseTransferItem> itemById = items.stream()
.collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
List<WarehouseTransferPallet> pallets = transferPallets.listOrder(customerId, transfer.getId());
List<WarehouseTransferPalletItem> palletItems = transferPalletItems.listOrder(customerId, transfer.getId());
WarehouseResourceGateway.WarehouseReference warehouse = warehouseResources.requireActiveSharedWarehouse(
Long.parseLong(transfer.getOperatingWarehouseId()));
Map<String, List<ConfirmedStockSource>> stockSourcesBySku = new java.util.LinkedHashMap<>();
LocalDateTime now = LocalDateTime.now();
for (WarehouseTransferPallet pallet : pallets) {
for (WarehouseTransferPalletItem row : palletItems.stream()
.filter(value -> value.getTransferPalletId().equals(pallet.getId())).toList()) {
WarehouseTransferItem item = itemById.get(row.getTransferItemId());
String operationKey = "WAREHOUSE_TRANSFER_STOCK_IN:" + transferNo + ":" + pallet.getSequenceNo() + ":" + item.getId();
InventoryCommandResponse result = inventory.stockIn(customerId, operatorId,
new InventoryCommandRequests.StockIn(customerId, "客户-" + customerId,
transfer.getOperatingWarehouseId(), warehouse.warehouseCode(), warehouse.warehouseName(),
null, trim(transfer.getFbaWarehouseCode()), trim(transfer.getFbaWarehouseCode()),
transfer.getInboundOrderId(), transferNo, transferNo, item.getSkuCode(), item.getProductNameZh(), null,
pallet.getWarehousePalletId(), pallet.getPalletNo(), transferNo, pallet.getWarehouseLocationId(),
pallet.getWarehouseLocationCode(), row.getPackageCount(), transferNo,
"WAREHOUSE_TRANSFER_CONFIRMED:" + transferNo, operationKey, trim(request.remark())));
stockSourcesBySku.computeIfAbsent(item.getSkuCode(), ignored -> new ArrayList<>())
.add(new ConfirmedStockSource(result.balanceId(), result.positionId(), row.getPackageCount()));
InboundStockReceipt receipt = new InboundStockReceipt();
receipt.setId(IdWorker.getId()); receipt.setCustomerId(customerId);
// 转运收货没有独立 PDA 入仓任务,使用本业务链的稳定主键填充通用收货快照维度。
receipt.setPlanId(transfer.getInboundOrderId()); receipt.setTaskId(transfer.getId());
receipt.setAllocationId(row.getId()); receipt.setLineId(item.getId());
receipt.setOrderId(transfer.getLastlegOrderId());
receipt.setInboundCode(transferNo); receipt.setSkuCode(item.getSkuCode()); receipt.setProductNameZh(item.getProductNameZh());
receipt.setWarehousePalletId(pallet.getWarehousePalletId()); receipt.setPalletNo(pallet.getPalletNo());
receipt.setWarehouseLocationId(pallet.getWarehouseLocationId()); receipt.setWarehouseLocationCode(pallet.getWarehouseLocationCode());
receipt.setBatchNo(transferNo); receipt.setReceivedPackages(Long.valueOf(row.getPackageCount()));
receipt.setInventoryBalanceId(result.balanceId()); receipt.setInventoryPositionId(result.positionId());
receipt.setInventoryOperationKey(operationKey); receipt.setReceivedAt(now); receipt.setCreatedAt(now); stockReceipts.insert(receipt);
}
}
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
RelabelResponses.Detail relabel = createLinkedRelabel(customerId, operatorId, transfer, itemById,
stockSourcesBySku);
transfer.setRelabelApplicationId(relabel.application().id());
transfer.setRelabelNo(relabel.application().relabelNo()); transfer.setStatus(WAIT_RELABEL);
} else {
if (!StringUtils.hasText(transfer.getFbaWarehouseCode())) {
throw conflict("A destination warehouse is required before dispatch");
}
createDispatchPlan(customerId, transfer, items, pallets, palletItems, now);
transfer.setStatus(READY_FOR_DISPATCH);
}
transfer.setConfirmationIdempotencyKey(request.idempotencyKey().trim());
transfer.setConfirmationRequestHash(requestHash); transfer.setCustomerConfirmedAt(now); transfer.setUpdatedAt(now);
transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
order.setCurrentStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? WAIT_RELABEL : READY_FOR_DISPATCH);
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_PROCESSING" : "NOT_REQUIRED");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
order.setUpdatedAt(now); orders.updateById(order);
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
"Customer confirmed PDA measurements; actual inventory stocked in", now);
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail allocate(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.Allocate request) {
@@ -490,6 +770,182 @@ public class WarehouseTransferService {
return detail(customerId, transferNo);
}
@Transactional(readOnly = true)
public List<WarehouseTransferResponses.Summary> pdaCandidates(long overseasWarehouseId) {
return transfers.pdaCandidates(String.valueOf(overseasWarehouseId)).stream().map(value ->
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
value.getFbaWarehouseCode(), value.getPackageCount(), value.getDeliveryMethod(),
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
}
private void validateRelabelRequest(WarehouseTransferRequests.Create request) {
if (!request.needRelabel()) {
if (request.relabelItems() != null && !request.relabelItems().isEmpty()) {
throw badRequest("Relabel items require needRelabel=true");
}
return;
}
if (request.relabelItems() == null || request.relabelItems().isEmpty()) return;
if (request.labelDeadline() == null || !StringUtils.hasText(request.relabelContactName())
|| !StringUtils.hasText(request.relabelContactPhone()) || request.relabelChargeAmount() == null) {
throw badRequest("Relabel deadline, contact and charge are required");
}
Set<String> declaredSkus = request.items().stream().map(item -> item.skuCode().trim()).collect(Collectors.toSet());
Set<String> marks = new HashSet<>();
for (WarehouseTransferRequests.RelabelItem row : request.relabelItems()) {
String type = row.operationType().trim().toUpperCase();
if (!Set.of("CARTON_LABEL", "SKU_LABEL", "CARTON_AND_SKU").contains(type)) {
throw badRequest("Unsupported relabel operation type");
}
if (!declaredSkus.contains(row.originalSku().trim())) {
throw badRequest("Relabel SKU must exist in transfer cargo: " + row.originalSku());
}
if (("CARTON_LABEL".equals(type) || "CARTON_AND_SKU".equals(type))
&& (!StringUtils.hasText(row.fbaCartonMark()) || !StringUtils.hasText(row.destinationWarehouseCode()))) {
throw badRequest("Carton relabel requires new FBA carton mark and destination warehouse");
}
if (("SKU_LABEL".equals(type) || "CARTON_AND_SKU".equals(type)) && !StringUtils.hasText(row.newSku())) {
throw badRequest("SKU relabel requires a new SKU");
}
for (String mark : row.customerCartonMarks()) {
if (!marks.add(mark.trim().toUpperCase())) throw badRequest("Customer carton marks must be unique");
}
}
if (marks.size() > request.packageCount()) throw badRequest("Relabel carton marks cannot exceed transfer cartons");
}
private void insertRelabelBindings(String customerId, long transferId,
List<WarehouseTransferRequests.RelabelItem> rows, LocalDateTime now) {
if (rows == null || rows.isEmpty()) return;
for (WarehouseTransferRequests.RelabelItem source : rows) {
for (String mark : source.customerCartonMarks()) {
WarehouseTransferRelabelBinding row = new WarehouseTransferRelabelBinding();
row.setId(IdWorker.getId()); row.setTransferOrderId(transferId); row.setCustomerId(customerId);
row.setOperationType(source.operationType().trim().toUpperCase());
row.setCustomerCartonMark(mark.trim()); row.setOriginalSku(source.originalSku().trim());
row.setNewSku(trim(source.newSku())); row.setQuantity(source.quantity());
row.setFbaCartonMark(trim(source.fbaCartonMark()));
row.setDestinationWarehouseCode(trim(source.destinationWarehouseCode()));
row.setRemark(trim(source.remark())); row.setCreatedAt(now); relabelBindings.insert(row);
}
}
}
private RelabelResponses.Detail createLinkedRelabel(String customerId, String operatorId,
WarehouseTransferOrder transfer,
Map<Long, WarehouseTransferItem> itemById,
Map<String, List<ConfirmedStockSource>> stockSourcesBySku) {
Map<String, WarehouseTransferItem> itemsBySku = itemById.values().stream()
.collect(Collectors.toMap(WarehouseTransferItem::getSkuCode, Function.identity(), (left, right) -> left));
List<RelabelRequests.Item> items = new ArrayList<>();
for (WarehouseTransferRelabelBinding binding : relabelBindings.listOrder(customerId, transfer.getId())) {
WarehouseTransferItem source = itemsBySku.get(binding.getOriginalSku());
ConfirmedStockSource stockSource = nextStockSource(stockSourcesBySku.get(binding.getOriginalSku()));
if (source == null || stockSource == null) {
throw conflict("Relabel binding cannot find confirmed inventory for SKU " + binding.getOriginalSku());
}
List<RelabelRequests.SkuItem> skuItems = Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(binding.getOperationType())
? List.of(new RelabelRequests.SkuItem(binding.getOriginalSku(), binding.getNewSku(),
binding.getQuantity(), binding.getRemark())) : List.of();
items.add(new RelabelRequests.Item(stockSource.balanceId, stockSource.positionId, transfer.getInboundOrderId(),
binding.getOriginalSku(), null, null, null, source.getProductNameZh(), 1, 1,
StringUtils.hasText(transfer.getFbaShipmentNo()) ? transfer.getFbaShipmentNo() : transfer.getTransferNo(),
transfer.getTrackingNo(), binding.getNewSku(), "仓储转运自动换标", binding.getRemark(),
null, null, null, binding.getCustomerCartonMark(), binding.getFbaCartonMark(),
binding.getDestinationWarehouseCode(), binding.getOperationType(), skuItems));
}
RelabelRequests.Create request = new RelabelRequests.Create(transfer.getTransferNo(),
transfer.getOperatingWarehouseId(), transfer.getLabelDeadline(), transfer.getRelabelContactName(),
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
trim(transfer.getRelabelDeliveryMethod()), false);
return relabelService.create(customerId, operatorId, request);
}
private static ConfirmedStockSource nextStockSource(List<ConfirmedStockSource> sources) {
if (sources == null) return null;
for (ConfirmedStockSource source : sources) {
if (source.remainingPackages > 0) {
source.remainingPackages--;
return source;
}
}
return null;
}
private static final class ConfirmedStockSource {
private final Long balanceId;
private final Long positionId;
private int remainingPackages;
private ConfirmedStockSource(Long balanceId, Long positionId, int remainingPackages) {
this.balanceId = balanceId;
this.positionId = positionId;
this.remainingPackages = remainingPackages;
}
}
private void createDispatchPlan(String customerId, WarehouseTransferOrder transfer,
List<WarehouseTransferItem> items, List<WarehouseTransferPallet> pallets,
List<WarehouseTransferPalletItem> palletItems, LocalDateTime now) {
long planId = IdWorker.getId();
InboundPlan plan = new InboundPlan(); plan.setId(planId); plan.setPlanNo("WT-" + transfer.getTransferNo());
plan.setCustomerId(customerId); plan.setSourceOrderId(transfer.getLastlegOrderId());
plan.setContainerNo(transfer.getTransferNo()); plan.setOverseasWarehouseId(Long.parseLong(transfer.getOperatingWarehouseId()));
plan.setPlannedArrivalDate(transfer.getEstimatedArrivalAt().toLocalDate()); plan.setPalletType("STANDARD");
plan.setAssignedFlag(true); plan.setMergeRequired(false); plan.setUrgentFlag(false);
plan.setCreationMode("WAREHOUSE_TRANSFER"); plan.setPlanRemark("Customer-confirmed transfer measurements");
plan.setStatus("COMPLETED"); plan.setLineCount(items.size()); plan.setPalletCount(pallets.size());
plan.setTotalVolumeCbm(pallets.stream().map(WarehouseTransferPallet::getVolumeCbm)
.reduce(BigDecimal.ZERO, BigDecimal::add)); plan.setCreatedAt(now); plan.setUpdatedAt(now); plan.setVersion(0);
inboundPlans.insert(plan);
Map<Long, Long> lineIds = new java.util.HashMap<>();
for (WarehouseTransferItem source : items) {
InboundPlanLine line = new InboundPlanLine(); line.setId(IdWorker.getId()); line.setCustomerId(customerId);
line.setPlanId(planId); line.setOrderId(transfer.getLastlegOrderId()); line.setInboundCode(transfer.getTransferNo());
line.setDestinationType("FBA"); line.setFbaWarehouseCode(transfer.getFbaWarehouseCode());
line.setDestinationWarehouseCode(transfer.getFbaWarehouseCode()); line.setDestinationWarehouseName(transfer.getFbaWarehouseCode());
line.setVolumeCbm(BigDecimal.ZERO); line.setRequiredPackageCount(source.getPackageCount());
line.setPlannedPalletCount((int) palletItems.stream().filter(row -> row.getTransferItemId().equals(source.getId())).count());
inboundPlanLines.insert(line); lineIds.put(source.getId(), line.getId());
}
for (WarehouseTransferPallet source : pallets) {
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(IdWorker.getId()); pallet.setCustomerId(customerId);
pallet.setPlanId(planId); pallet.setPalletNo(source.getPalletNo()); pallet.setPalletType("STANDARD");
pallet.setDestinationWarehouseCode(transfer.getFbaWarehouseCode()); pallet.setDestinationWarehouseName(transfer.getFbaWarehouseCode());
pallet.setAllocatedVolumeCbm(source.getVolumeCbm()); pallet.setSequenceNo(source.getSequenceNo());
pallet.setWarehousePalletId(source.getWarehousePalletId()); pallet.setWarehouseLocationId(source.getWarehouseLocationId());
pallet.setWarehouseResourceLockId(source.getWarehouseResourceLockId()); pallet.setAssignmentStatus("ASSIGNED");
pallet.setLockedAt(source.getCompletedAt()); pallet.setActualVolumeCbm(source.getVolumeCbm());
pallet.setActualPackageCount(source.getCartonCount()); inboundPlanPallets.insert(pallet);
for (WarehouseTransferPalletItem sourceItem : palletItems.stream()
.filter(row -> row.getTransferPalletId().equals(source.getId())).toList()) {
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId());
allocation.setCustomerId(customerId); allocation.setPlanId(planId); allocation.setLineId(lineIds.get(sourceItem.getTransferItemId()));
allocation.setPalletId(pallet.getId()); allocation.setAllocatedVolumeCbm(BigDecimal.ZERO);
allocation.setPlannedPackageCount(sourceItem.getPackageCount()); allocation.setCountedPackageCount(sourceItem.getPackageCount());
allocation.setPickedPackageCount(0); allocation.setActualVolumeCbm(source.getVolumeCbm());
allocation.setStatus("COMPLETED"); allocation.setVersion(0); inboundPlanAllocations.insert(allocation);
}
}
}
private static List<String> splitMarks(String value) {
if (!StringUtils.hasText(value)) return List.of();
return java.util.Arrays.stream(value.split("[,\\n\\r]+"))
.map(String::trim).filter(StringUtils::hasText).distinct().toList();
}
private static String escape(String value) {
return org.springframework.web.util.HtmlUtils.htmlEscape(Objects.toString(value, ""));
}
private static String text(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
private Long createInboundSnapshot(String customerId, long inboundOrderId, String transferNo,
WarehouseTransferRequests.Create request, String requestHash,
LocalDateTime now) {
@@ -544,10 +1000,11 @@ public class WarehouseTransferService {
private void insertTransferCarton(String customerId, long transferId, long transferItemId,
String transferNo, int cartonIndex, int totalCartons,
LocalDateTime now) {
String customerCartonMark, LocalDateTime now) {
WarehouseTransferCarton carton = new WarehouseTransferCarton();
carton.setId(IdWorker.getId()); carton.setTransferOrderId(transferId); carton.setTransferItemId(transferItemId);
carton.setCustomerId(customerId); carton.setCartonMark(transferNo + "#" + totalCartons + "-" + cartonIndex);
carton.setCustomerCartonMark(trim(customerCartonMark));
carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED");
carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton);
}