This commit is contained in:
2026-10-16 11:41:24 +08:00
parent 4e2ea89403
commit 5855529e97
59 changed files with 1077 additions and 110 deletions
@@ -28,6 +28,14 @@ import java.util.List;
public class MerchantWarehouseTransferController {
@Resource private WarehouseTransferService service;
@GetMapping("/inventory-source-cartons")
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
public ApiResponse<List<WarehouseTransferResponses.SourceCarton>> sourceCartons(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String operatingWarehouseId,
@RequestParam String inboundCode) {
return ApiResponse.success(service.sourceCartons(customer(user), operatingWarehouseId, inboundCode));
}
@PostMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
@@ -29,13 +29,13 @@ public final class WarehouseTransferRequests {
@Size(max = 32) String privateDestinationPostalCode,
@Size(max = 128) String privateDestinationContactName,
@Size(max = 64) String privateDestinationContactPhone,
@NotBlank @Size(max = 1000) String deliveryAddress,
@Size(max = 1000) String deliveryAddress,
@Size(max = 128) String pickupContactName,
@Size(max = 64) String pickupContactPhone,
@Positive int packageCount,
@NotNull @DecimalMin(value = "0.0") BigDecimal volumeCbm,
@NotBlank @Size(max = 64) String deliveryMethod,
@NotNull LocalDateTime estimatedArrivalAt,
@Size(max = 64) String deliveryMethod,
LocalDateTime estimatedArrivalAt,
LocalDateTime estimatedShipAt,
@Size(max = 128) String fbaShipmentNo,
@Size(max = 255) String trackingNo,
@@ -51,6 +51,14 @@ public final class WarehouseTransferRequests {
@NotEmpty @Size(max = 500) List<@Valid Item> items,
@Size(max = 1000) List<@Valid RelabelItem> relabelItems
) {
@jakarta.validation.constraints.AssertTrue(message="外来货物须填写入仓方式、地址和预计入仓时间;库存转运须选择FBA或私人仓")
public boolean isInboundModeValid() {
if (inboundCode != null && !inboundCode.isBlank())
return "FBA".equals(destinationType) || "PRIVATE".equals(destinationType)
|| destinationType == null && fbaWarehouseCode != null && !fbaWarehouseCode.isBlank();
return deliveryMethod != null && !deliveryMethod.isBlank() && deliveryAddress != null
&& !deliveryAddress.isBlank() && estimatedArrivalAt != null;
}
public Create(String inboundCode, List<Long> sourceLastlegOrderIds, String operatingWarehouseId,
String fbaWarehouseCode, String deliveryAddress, int packageCount, BigDecimal volumeCbm,
String deliveryMethod, LocalDateTime estimatedArrivalAt, String fbaShipmentNo,
@@ -6,6 +6,9 @@ import java.util.List;
public final class WarehouseTransferResponses {
private WarehouseTransferResponses() {}
public record SourceCarton(String cartonMark, String productNameZh, Long quantity,
String destinationWarehouseCode, String destinationWarehouseName, List<SourceCartonSku> items) {}
public record SourceCartonSku(String skuCode, Long quantity) {}
public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark, Long quantity) {}
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status,
Long transferItemId, Long transferPalletId, BigDecimal length, BigDecimal width, BigDecimal height,
@@ -3,6 +3,25 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper public interface InboundSnapshotMapper {
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
@Select("""
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
UNION ALL
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
FROM ll_warehouse_transfer_carton c
JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
ORDER BY cartonMark
""")
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
@Param("warehouseId") String warehouseId, @Param("inboundCode") String inboundCode);
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND (inbound_code=#{code} OR source_inbound_id=#{code}) LIMIT 1")
Long findId(@Param("customerId") String customerId, @Param("code") String code);
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND source_inbound_id=#{sourceInboundId} LIMIT 1")
@@ -88,6 +88,7 @@ import java.util.stream.Collectors;
@Service
public class WarehouseTransferService {
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge fleetPool;
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderQueries fleetPoolQueries;
@@ -140,14 +141,35 @@ public class WarehouseTransferService {
@Resource private InventoryCommandMapper inventoryCommands;
@Resource private ObjectMapper objectMapper;
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
if (!StringUtils.hasText(warehouseId) || !StringUtils.hasText(inboundCode)) throw badRequest("请选择海外仓和库存来源单据");
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), inboundCode.trim()).stream().map(row -> {
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
}).toList();
}
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
}
private static String inboundMethod(WarehouseTransferRequests.Create request) {
return StringUtils.hasText(request.inboundCode()) ? "INVENTORY_TRANSFER" : request.deliveryMethod().trim();
}
private static String inboundAddress(WarehouseTransferRequests.Create request) {
return StringUtils.hasText(request.inboundCode()) ? "" : request.deliveryAddress().trim();
}
@Transactional
public WarehouseTransferResponses.Detail create(String customerId, String operatorId,
WarehouseTransferRequests.Create request) {
if (!request.isInboundModeValid()) throw badRequest("外来货物须填写入仓信息,库存转运须选择FBA仓或私人仓");
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
if (itemPackages != request.packageCount()) {
throw badRequest("Cargo item package total must equal transfer package count");
}
validateRelabelRequest(request);
validateRelabelRequest(customerId, request);
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
request.privateDestinationContactName(), request.privateDestinationContactPhone());
@@ -167,7 +189,7 @@ public class WarehouseTransferService {
}
LocalDateTime now = LocalDateTime.now();
long transferId = IdWorker.getId();
String transferNo = orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), transferId);
String transferNo = compactNumbers.next("ZY", customerId);
String requestedInboundCode = trim(request.inboundCode());
List<Long> selectedIds = request.sourceLastlegOrderIds() == null
? List.of() : request.sourceLastlegOrderIds().stream().distinct().toList();
@@ -210,13 +232,13 @@ public class WarehouseTransferService {
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
transfer.setFbaWarehouseCode(destinationCode(request));
applyDestination(transfer, request);
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
transfer.setDeliveryAddress(inboundAddress(request)); transfer.setPackageCount(request.packageCount());
transfer.setPickupContactName(trim(request.pickupContactName()));
transfer.setPickupContactPhone(trim(request.pickupContactPhone()));
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
transfer.setDeliveryMethod(inboundMethod(request)); transfer.setEstimatedArrivalAt(arrivalTime(request));
transfer.setEstimatedShipAt(request.estimatedShipAt());
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim()) ? trim(request.trackingNo()) : null);
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request)) ? trim(request.trackingNo()) : null);
transfer.setFbaBoxNos(trim(request.fbaBoxNos())); transfer.setRemark(trim(request.remark()));
transfer.setNeedRelabel(request.needRelabel()); transfer.setStatus(DRAFT);
transfer.setLabelDeadline(request.labelDeadline());
@@ -278,11 +300,12 @@ public class WarehouseTransferService {
if (!DRAFT.equals(transfer.getStatus())) {
throw conflict("Only a draft warehouse transfer order can be edited");
}
if (!request.isInboundModeValid()) throw badRequest("外来货物须填写入仓信息,库存转运须选择FBA仓或私人仓");
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
if (itemPackages != request.packageCount()) {
throw badRequest("Cargo item package total must equal transfer package count");
}
validateRelabelRequest(request);
validateRelabelRequest(customerId, request);
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
request.privateDestinationContactName(), request.privateDestinationContactPhone());
@@ -293,15 +316,15 @@ public class WarehouseTransferService {
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
transfer.setFbaWarehouseCode(destinationCode(request));
applyDestination(transfer, request);
transfer.setDeliveryAddress(request.deliveryAddress().trim());
transfer.setDeliveryAddress(inboundAddress(request));
transfer.setPackageCount(request.packageCount());
transfer.setPickupContactName(trim(request.pickupContactName()));
transfer.setPickupContactPhone(trim(request.pickupContactPhone()));
transfer.setDeliveryMethod(request.deliveryMethod().trim());
transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
transfer.setDeliveryMethod(inboundMethod(request));
transfer.setEstimatedArrivalAt(arrivalTime(request));
transfer.setEstimatedShipAt(request.estimatedShipAt());
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim())
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request))
? trim(request.trackingNo()) : null);
transfer.setFbaBoxNos(trim(request.fbaBoxNos()));
transfer.setRemark(trim(request.remark()));
@@ -319,7 +342,7 @@ public class WarehouseTransferService {
order.setFbaWarehouseCode(destinationCode(request));
order.setPackageCount(request.packageCount());
order.setVolumeCbm(request.volumeCbm());
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
order.setEstimatedWarehouseAt(arrivalTime(request));
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus());
order.setCustomerRemark(trim(request.remark()));
@@ -376,6 +399,19 @@ public class WarehouseTransferService {
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
? inboundSnapshots.sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).collect(Collectors.toSet())
: transferCartons.listOrder(customerId, transfer.getId()).stream()
.flatMap(carton -> java.util.stream.Stream.of(carton.getCartonMark(), carton.getCustomerCartonMark()))
.filter(StringUtils::hasText).collect(Collectors.toSet());
for (var binding : relabelBindings.listOrder(customerId, transfer.getId())) {
if (!allowedMarks.contains(binding.getCustomerCartonMark()))
throw badRequest("当前箱子不存在或不属于当前订单");
}
}
LastlegOrder order = requireTailOrder(customerId, transfer);
LocalDateTime now = LocalDateTime.now();
transfer.setStatus(PENDING_REVIEW);
@@ -859,7 +895,7 @@ public class WarehouseTransferService {
carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId());
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(input.transferItemId()); carton.setCustomerId(customerId);
carton.setCartonIndex(++nextIndex); carton.setTotalCartons(transfer.getPackageCount());
carton.setCartonMark(transferNo + "#EX-" + nextIndex); carton.setCreatedAt(now);
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId())); carton.setCreatedAt(now);
}
carton.setTransferPalletId(input.transferPalletId());
carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height());
@@ -1712,7 +1748,7 @@ public class WarehouseTransferService {
}
}
private void validateRelabelRequest(WarehouseTransferRequests.Create request) {
private void validateRelabelRequest(String customerId, WarehouseTransferRequests.Create request) {
if (!request.needRelabel()) {
if (request.relabelItems() != null && !request.relabelItems().isEmpty()) {
throw badRequest("Relabel items require needRelabel=true");
@@ -1720,6 +1756,14 @@ public class WarehouseTransferService {
return;
}
if (request.relabelItems() == null || request.relabelItems().isEmpty()) return;
Set<String> orderCartonMarks = StringUtils.hasText(request.inboundCode())
? inboundSnapshots.sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim())
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark)
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet())
: request.items().stream().flatMap(item -> splitMarks(item.shippingMark()).stream()).collect(Collectors.toSet());
if (!StringUtils.hasText(request.inboundCode()) && orderCartonMarks.isEmpty()) {
orderCartonMarks.addAll(splitMarks(request.fbaBoxNos()));
}
if (request.labelDeadline() == null || !StringUtils.hasText(request.relabelContactName())
|| !StringUtils.hasText(request.relabelContactPhone())) {
throw badRequest("Relabel deadline and contact are required");
@@ -1754,6 +1798,7 @@ public class WarehouseTransferService {
throw badRequest("SKU relabel requires a new SKU");
}
for (String mark : row.customerCartonMarks()) {
if (!orderCartonMarks.contains(mark.trim())) throw badRequest("当前箱子不存在或不属于当前订单");
if (!marks.add(mark.trim().toUpperCase())) throw badRequest("Customer carton marks must be unique");
}
}
@@ -1969,7 +2014,7 @@ public class WarehouseTransferService {
private LastlegOrder createTailOrder(String customerId, String customerName, Long inboundOrderId, String inboundCode, String transferNo,
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), order.getId())); order.setCustomerId(customerId);
order.setId(IdWorker.getId()); order.setOrderNo(transferNo); order.setCustomerId(customerId);
order.setCustomerName(customerName);
order.setOrderType(ORDER_TYPE);
order.setInboundCode(inboundCode); order.setSourceRegion("CUSTOMER");
@@ -1979,7 +2024,7 @@ public class WarehouseTransferService {
order.setVolumeCbm(request.volumeCbm());
order.setContainerNo(transferNo);
order.setPalletCount(0); order.setPickedPackageCount(0); order.setShippedPackageCount(0);
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
order.setEstimatedWarehouseAt(arrivalTime(request));
order.setCurrentStatus(DRAFT);
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus());
@@ -2007,7 +2052,7 @@ public class WarehouseTransferService {
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.setCustomerId(customerId); carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId()));
carton.setCustomerCartonMark(trim(customerCartonMark));
carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED");
carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton);
@@ -2020,7 +2065,7 @@ public class WarehouseTransferService {
detail.setOrderId(orderId); detail.setSourceInboundDetailId(transferNo + ":" + index);
detail.setSkuCode(trim(source.skuCode())); detail.setProductNameZh(trim(source.productNameZh()));
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(destinationCode(request));
detail.setFbaTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim())
detail.setFbaTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request))
? trim(request.trackingNo()) : null);
detail.setShipmentNo(trim(request.fbaShipmentNo()));
detail.setShippingMark(trim(source.shippingMark())); orderDetails.insert(detail);