修改提交

This commit is contained in:
2026-09-06 15:02:33 +08:00
parent 42ddc19826
commit 3ae8964fe5
15 changed files with 242 additions and 137 deletions
@@ -34,7 +34,7 @@ public record OrderApprovedInboundCommand(
Integer totalPackageCount, Integer totalPackageCount,
String pickupMethod, String pickupMethod,
LocalDateTime requestedPickupAt, LocalDateTime requestedPickupAt,
String fulfillmentType String fulfillmentType, String lxCodes
) { ) {
public OrderApprovedInboundCommand( public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName, long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
@@ -49,7 +49,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName, deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType, carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName, destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, totalPackageCount, null, null, "STANDARD"); expressTrackingNo, totalPackageCount, null, null, "STANDARD", null);
} }
/** 兼容尚未携带订单总箱数的历史事件。 */ /** 兼容尚未携带订单总箱数的历史事件。 */
public OrderApprovedInboundCommand( public OrderApprovedInboundCommand(
@@ -64,7 +64,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName, deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType, carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName, destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null, null, null, "STANDARD"); expressTrackingNo, null, null, null, "STANDARD", null);
} }
/** 兼容未携带报价包税维度的历史调用方。 */ /** 兼容未携带报价包税维度的历史调用方。 */
@@ -80,6 +80,6 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName, deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType, carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName, destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null, null, null, "STANDARD"); expressTrackingNo, null, null, null, "STANDARD", null);
} }
} }
@@ -39,6 +39,7 @@ public class InboundOrderEntity extends BaseEntity {
private String deliveryWarehouseName; private String deliveryWarehouseName;
private String businessType; private String businessType;
private String fulfillmentType; private String fulfillmentType;
private String lxCodes;
private String channelCode; private String channelCode;
private String channelName; private String channelName;
private Long carrierId; private Long carrierId;
@@ -69,6 +69,7 @@ public class InboundOrderCreationService {
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName()); inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
inbound.setBusinessType(command.businessType()); inbound.setBusinessType(command.businessType());
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType())); inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
inbound.setLxCodes("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType()) ? command.lxCodes() : null);
inbound.setChannelCode(command.channelCode()); inbound.setChannelCode(command.channelCode());
inbound.setChannelName(command.channelName()); inbound.setChannelName(command.channelName());
inbound.setCarrierId(command.carrierId()); inbound.setCarrierId(command.carrierId());
@@ -221,6 +222,7 @@ public class InboundOrderCreationService {
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName()); inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
inbound.setBusinessType(command.businessType()); inbound.setBusinessType(command.businessType());
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType())); inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
inbound.setLxCodes("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType()) ? command.lxCodes() : null);
inbound.setChannelCode(command.channelCode()); inbound.setChannelCode(command.channelCode());
inbound.setChannelName(command.channelName()); inbound.setChannelName(command.channelName());
inbound.setCarrierId(command.carrierId()); inbound.setCarrierId(command.carrierId());
@@ -19,7 +19,7 @@ public final class RelabelRequests {
public record Create( public record Create(
@NotBlank @Size(max = 128) String customerOrderNo, @NotBlank @Size(max = 128) String customerOrderNo,
@NotBlank @Size(max = 128) String overseasWarehouseId, @Size(max = 128) String overseasWarehouseId,
@NotNull LocalDateTime labelDeadline, @NotNull LocalDateTime labelDeadline,
@NotBlank @Size(max = 128) String contactName, @NotBlank @Size(max = 128) String contactName,
@NotBlank @Size(max = 64) String contactPhone, @NotBlank @Size(max = 64) String contactPhone,
@@ -41,10 +41,23 @@ public final class RelabelRequests {
@Size(max = 255) String trackingCode, @Size(max = 255) String trackingCode,
@NotBlank @Size(max = 128) String newSkuCode, @NotBlank @Size(max = 128) String newSkuCode,
@Size(max = 1000) String labelRequirement, @Size(max = 1000) String labelRequirement,
@Size(max = 1000) String remark,
@Size(max = 128) String relabelFileId, @Size(max = 128) String relabelFileId,
@Size(max = 255) String relabelFileName, @Size(max = 255) String relabelFileName,
@Size(max = 1000) String relabelFileUrl @Size(max = 1000) String relabelFileUrl,
) {} @NotBlank @Size(max = 255) String cartonMark,
@NotBlank @Size(max = 255) String fbaCartonMark,
@Size(max = 128) String destinationWarehouseId
) {
public Item(String skuCode, String productImageFileId, String productImageFileName,
String productImageUrl, String productNameZh, int totalPackageCount,
int notifiedPackageCount, String shipmentNo, String trackingCode, String newSkuCode,
String labelRequirement, String relabelFileId, String relabelFileName, String relabelFileUrl) {
this(skuCode, productImageFileId, productImageFileName, productImageUrl, productNameZh,
totalPackageCount, notifiedPackageCount, shipmentNo, trackingCode, newSkuCode,
labelRequirement, null, relabelFileId, relabelFileName, relabelFileUrl, skuCode, shipmentNo, null);
}
}
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo, public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo,
String customerOrderNo, String skuCode, String status, String customerOrderNo, String skuCode, String status,
@@ -17,9 +17,13 @@ public final class RelabelResponses {
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName, public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
String productImageUrl, String productNameZh, int totalPackageCount, String productImageUrl, String productNameZh, int totalPackageCount,
int notifiedPackageCount, String shipmentNo, String trackingCode, int notifiedPackageCount, String shipmentNo, String trackingCode,
String newSkuCode, String labelRequirement, String relabelFileId, String newSkuCode, String labelRequirement, String remark, String relabelFileId,
String relabelFileName, String relabelFileUrl) {} String relabelFileName, String relabelFileUrl,
String cartonMark, String fbaCartonMark, String destinationWarehouseId) {}
public record TailOrder(Long id, String orderNo, String overseasWarehouseId, int packageCount,
String status) {}
public record History(String fromStatus, String toStatus, String operatorId, public record History(String fromStatus, String toStatus, String operatorId,
String remark, LocalDateTime createdAt) {} String remark, LocalDateTime createdAt) {}
public record Detail(Summary application, String remark, List<Item> items, List<History> history) {} public record Detail(Summary application, String remark, List<Item> items, List<History> history,
List<TailOrder> tailOrders) {}
} }
@@ -24,8 +24,12 @@ public class RelabelItem {
private String trackingCode; private String trackingCode;
private String newSkuCode; private String newSkuCode;
private String labelRequirement; private String labelRequirement;
private String remark;
private String relabelFileId; private String relabelFileId;
private String relabelFileName; private String relabelFileName;
private String relabelFileUrl; private String relabelFileUrl;
private String cartonMark;
private String fbaCartonMark;
private String destinationWarehouseId;
private LocalDateTime createdAt; private LocalDateTime createdAt;
} }
@@ -33,7 +33,9 @@ import java.security.MessageDigest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -56,12 +58,15 @@ public class RelabelApplicationService implements WalletResultHandler {
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) { public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
int total = 0; int total = 0;
int notified = 0; int notified = 0;
Map<String, Integer> packagesByWarehouse = new LinkedHashMap<>();
for (RelabelRequests.Item item : request.items()) { for (RelabelRequests.Item item : request.items()) {
if (item.notifiedPackageCount() > item.totalPackageCount()) { if (item.notifiedPackageCount() > item.totalPackageCount()) {
throw badRequest("Notified relocation packages cannot exceed total packages for SKU " + item.skuCode()); throw badRequest("Notified relocation packages cannot exceed total packages for SKU " + item.skuCode());
} }
total = Math.addExact(total, item.totalPackageCount()); total = Math.addExact(total, item.totalPackageCount());
notified = Math.addExact(notified, item.notifiedPackageCount()); notified = Math.addExact(notified, item.notifiedPackageCount());
String warehouseId = destination(item, request.overseasWarehouseId());
packagesByWarehouse.merge(warehouseId, item.totalPackageCount(), Math::addExact);
} }
String requestHash = hash(request); String requestHash = hash(request);
RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim()); RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim());
@@ -71,40 +76,20 @@ public class RelabelApplicationService implements WalletResultHandler {
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long applicationId = IdWorker.getId(); long applicationId = IdWorker.getId();
String relabelNo = orderNoGenerator.next("RELABEL", request.overseasWarehouseId(), applicationId); String primaryWarehouseId = packagesByWarehouse.keySet().iterator().next();
LastlegOrder order = new LastlegOrder(); String relabelNo = orderNoGenerator.next("RELABEL", primaryWarehouseId, applicationId);
order.setId(IdWorker.getId()); List<LastlegOrder> tailOrders = packagesByWarehouse.entrySet().stream()
order.setOrderNo(orderNoGenerator.next("RELABEL", request.overseasWarehouseId(), order.getId())); .map(entry -> insertTailOrder(customerId, request, requestHash, relabelNo,
order.setCustomerId(customerId); entry.getKey(), entry.getValue(), now))
order.setOrderType("RELABEL_APPLICATION"); .toList();
order.setInboundCode(relabelNo);
order.setSourceRegion("CUSTOMER");
order.setSourceInboundId(relabelNo);
order.setSourceOrderNo(request.customerOrderNo().trim());
order.setOverseasWarehouseId(request.overseasWarehouseId().trim());
order.setPalletCount(0);
order.setPackageCount(total);
order.setPickedPackageCount(0);
order.setShippedPackageCount(0);
order.setCurrentStatus("PENDING_ASSIGNMENT");
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING);
order.setRelabelOrderNo(relabelNo);
order.setRelabelStatus(PENDING_PROCESSING);
order.setCustomerRemark(trim(request.remark()));
order.setSourceEventId("CUSTOMER_RELABEL:" + relabelNo);
order.setSourceLoadingPlanId("RELABEL_APPLICATION");
order.setSourcePayloadHash("sha256:" + requestHash);
order.setCreatedAt(now);
order.setUpdatedAt(now);
orders.insert(order);
RelabelApplication application = new RelabelApplication(); RelabelApplication application = new RelabelApplication();
application.setId(applicationId); application.setId(applicationId);
application.setCustomerId(customerId); application.setCustomerId(customerId);
application.setRelabelNo(relabelNo); application.setRelabelNo(relabelNo);
application.setLastlegOrderId(order.getId()); application.setLastlegOrderId(tailOrders.get(0).getId());
application.setCustomerOrderNo(request.customerOrderNo().trim()); application.setCustomerOrderNo(request.customerOrderNo().trim());
application.setOverseasWarehouseId(request.overseasWarehouseId().trim()); application.setOverseasWarehouseId(primaryWarehouseId);
application.setLabelDeadline(request.labelDeadline()); application.setLabelDeadline(request.labelDeadline());
application.setContactName(request.contactName().trim()); application.setContactName(request.contactName().trim());
application.setContactPhone(request.contactPhone().trim()); application.setContactPhone(request.contactPhone().trim());
@@ -128,10 +113,12 @@ public class RelabelApplicationService implements WalletResultHandler {
application.setWalletCommandEventId(walletEventId); application.setWalletCommandEventId(walletEventId);
applications.updateById(application); applications.updateById(application);
for (RelabelRequests.Item source : request.items()) insertItem(customerId, applicationId, source, now); for (RelabelRequests.Item source : request.items()) {
insertItem(customerId, applicationId, source, request.overseasWarehouseId(), now);
}
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now); insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
insertOrderHistory(customerId, order.getId(), null, order.getCurrentStatus(), tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
"Relabel application submitted", now); order.getCurrentStatus(), "Relabel application submitted", now));
return detail(customerId, relabelNo); return detail(customerId, relabelNo);
} }
@@ -145,7 +132,9 @@ public class RelabelApplicationService implements WalletResultHandler {
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo()) .like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo()) .like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
.eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status())
.eq(StringUtils.hasText(query.overseasWarehouseId()), RelabelApplication::getOverseasWarehouseId, query.overseasWarehouseId()) .apply(StringUtils.hasText(query.overseasWarehouseId()),
"EXISTS (SELECT 1 FROM ll_relabel_item rw WHERE rw.application_id=ll_relabel_application.id AND rw.customer_id=ll_relabel_application.customer_id AND rw.destination_warehouse_id={0})",
query.overseasWarehouseId())
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom()) .ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo()) .le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
.apply(StringUtils.hasText(query.skuCode()), .apply(StringUtils.hasText(query.skuCode()),
@@ -166,10 +155,13 @@ public class RelabelApplicationService implements WalletResultHandler {
.eq(RelabelStatusHistory::getCustomerId, recordCustomerId) .eq(RelabelStatusHistory::getCustomerId, recordCustomerId)
.eq(RelabelStatusHistory::getApplicationId, application.getId()) .eq(RelabelStatusHistory::getApplicationId, application.getId())
.orderByAsc(RelabelStatusHistory::getCreatedAt)); .orderByAsc(RelabelStatusHistory::getCreatedAt));
List<LastlegOrder> tailOrders = tailOrders(application);
return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(), return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(),
detailItems.stream().map(this::item).toList(), statusHistory.stream().map(value -> detailItems.stream().map(this::item).toList(), statusHistory.stream().map(value ->
new RelabelResponses.History(value.getFromStatus(), value.getToStatus(), value.getOperatorId(), new RelabelResponses.History(value.getFromStatus(), value.getToStatus(), value.getOperatorId(),
value.getRemark(), value.getCreatedAt())).toList()); value.getRemark(), value.getCreatedAt())).toList(), tailOrders.stream().map(value ->
new RelabelResponses.TailOrder(value.getId(), value.getOrderNo(), value.getOverseasWarehouseId(),
Objects.requireNonNullElse(value.getPackageCount(), 0), value.getCurrentStatus())).toList());
} }
@Transactional @Transactional
@@ -201,30 +193,32 @@ public class RelabelApplicationService implements WalletResultHandler {
"换标取消,释放冻结", 2L); "换标取消,释放冻结", 2L);
application.setWalletStatus("RELEASE_PENDING"); applications.updateById(application); application.setWalletStatus("RELEASE_PENDING"); applications.updateById(application);
} }
LastlegOrder order = orders.selectById(application.getLastlegOrderId()); List<LastlegOrder> linkedOrders = tailOrders(application);
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing"); if (linkedOrders.isEmpty()) throw conflict("Linked tail-leg order is missing");
String orderBefore = order.getCurrentStatus(); for (LastlegOrder order : linkedOrders) {
String mainStatus = STATUSES.contains(orderBefore) ? "PENDING_ASSIGNMENT" : orderBefore; String orderBefore = order.getCurrentStatus();
// A return-relabel tail order joins the normal inbound queue after labels are finished. String mainStatus = STATUSES.contains(orderBefore) ? "PENDING_ASSIGNMENT" : orderBefore;
String orderStatus = switch (next) { String orderStatus = switch (next) {
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType()) case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
? "PENDING_ASSIGNMENT" : "COMPLETED"; ? "PENDING_ASSIGNMENT" : "COMPLETED";
case "REJECTED", "CANCELLED" -> "CANCELLED"; case "REJECTED", "CANCELLED" -> "CANCELLED";
default -> mainStatus; default -> mainStatus;
}; };
order.setCurrentStatus(orderStatus); order.setCurrentStatus(orderStatus);
order.setSubOrderStatus("RELABEL:" + next); order.setSubOrderStatus("RELABEL:" + next);
order.setRelabelStatus(next); order.setRelabelStatus(next);
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now); if (!Objects.equals(orderBefore, orderStatus)) {
if (!Objects.equals(orderBefore, orderStatus)) { insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now);
insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now); }
} }
insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now);
return detail(customerId, relabelNo); return detail(customerId, relabelNo);
} }
private void insertItem(String customerId, long applicationId, RelabelRequests.Item source, LocalDateTime now) { private void insertItem(String customerId, long applicationId, RelabelRequests.Item source,
String defaultWarehouseId, LocalDateTime now) {
RelabelItem item = new RelabelItem(); RelabelItem item = new RelabelItem();
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId); item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
item.setSkuCode(source.skuCode().trim()); item.setProductImageFileId(trim(source.productImageFileId())); item.setSkuCode(source.skuCode().trim()); item.setProductImageFileId(trim(source.productImageFileId()));
@@ -232,16 +226,21 @@ public class RelabelApplicationService implements WalletResultHandler {
item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount()); item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount());
item.setNotifiedPackageCount(source.notifiedPackageCount()); item.setShipmentNo(source.shipmentNo().trim()); item.setNotifiedPackageCount(source.notifiedPackageCount()); item.setShipmentNo(source.shipmentNo().trim());
item.setTrackingCode(trim(source.trackingCode())); item.setNewSkuCode(source.newSkuCode().trim()); item.setTrackingCode(trim(source.trackingCode())); item.setNewSkuCode(source.newSkuCode().trim());
item.setLabelRequirement(trim(source.labelRequirement())); item.setRelabelFileId(trim(source.relabelFileId())); item.setLabelRequirement(trim(source.labelRequirement())); item.setRemark(trim(source.remark())); item.setRelabelFileId(trim(source.relabelFileId()));
item.setRelabelFileName(trim(source.relabelFileName())); item.setRelabelFileUrl(trim(source.relabelFileUrl())); item.setRelabelFileName(trim(source.relabelFileName())); item.setRelabelFileUrl(trim(source.relabelFileUrl()));
item.setCartonMark(source.cartonMark().trim()); item.setFbaCartonMark(source.fbaCartonMark().trim());
item.setDestinationWarehouseId(destination(source, defaultWarehouseId));
item.setCreatedAt(now); items.insert(item); item.setCreatedAt(now); items.insert(item);
} }
private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) { private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) {
LastlegOrder order = orders.selectById(value.getLastlegOrderId()); LastlegOrder order = orders.selectById(value.getLastlegOrderId());
String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(",")); String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(","));
String warehouses = detailItems.stream().map(RelabelItem::getDestinationWarehouseId)
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(","));
return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(), return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(),
order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(), value.getOverseasWarehouseId(), order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(),
StringUtils.hasText(warehouses) ? warehouses : value.getOverseasWarehouseId(),
value.getLabelDeadline(), value.getContactName(), value.getContactPhone(), value.getStatus(), value.getLabelDeadline(), value.getContactName(), value.getContactPhone(), value.getStatus(),
Objects.requireNonNullElse(value.getItemCount(), detailItems.size()), Objects.requireNonNullElse(value.getItemCount(), detailItems.size()),
Objects.requireNonNullElse(value.getTotalPackageCount(), 0), Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
@@ -276,8 +275,47 @@ public class RelabelApplicationService implements WalletResultHandler {
return new RelabelResponses.Item(value.getId(), value.getSkuCode(), value.getProductImageFileId(), return new RelabelResponses.Item(value.getId(), value.getSkuCode(), value.getProductImageFileId(),
value.getProductImageFileName(), value.getProductImageUrl(), value.getProductNameZh(), value.getProductImageFileName(), value.getProductImageUrl(), value.getProductNameZh(),
value.getTotalPackageCount(), value.getNotifiedPackageCount(), value.getShipmentNo(), value.getTotalPackageCount(), value.getNotifiedPackageCount(), value.getShipmentNo(),
value.getTrackingCode(), value.getNewSkuCode(), value.getLabelRequirement(), value.getRelabelFileId(), value.getTrackingCode(), value.getNewSkuCode(), value.getLabelRequirement(), value.getRemark(), value.getRelabelFileId(),
value.getRelabelFileName(), value.getRelabelFileUrl()); value.getRelabelFileName(), value.getRelabelFileUrl(), value.getCartonMark(), value.getFbaCartonMark(),
value.getDestinationWarehouseId());
}
private LastlegOrder insertTailOrder(String customerId, RelabelRequests.Create request, String requestHash,
String relabelNo, String warehouseId, int packageCount,
LocalDateTime now) {
LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId());
order.setOrderNo(orderNoGenerator.next("RELABEL", warehouseId, order.getId()));
order.setCustomerId(customerId); order.setOrderType("RELABEL_APPLICATION");
order.setInboundCode(relabelNo); order.setSourceRegion("CUSTOMER");
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("PENDING_ASSIGNMENT");
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo);
order.setRelabelStatus(PENDING_PROCESSING); 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);
return order;
}
private List<LastlegOrder> tailOrders(RelabelApplication application) {
List<LastlegOrder> linked = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, application.getCustomerId())
.eq(LastlegOrder::getRelabelOrderNo, application.getRelabelNo())
.orderByAsc(LastlegOrder::getId));
if (!linked.isEmpty()) return linked;
LastlegOrder legacy = orders.selectById(application.getLastlegOrderId());
return legacy == null ? List.of() : List.of(legacy);
}
private static String destination(RelabelRequests.Item item, String fallback) {
String value = StringUtils.hasText(item.destinationWarehouseId())
? item.destinationWarehouseId().trim()
: trim(fallback);
if (!StringUtils.hasText(value)) throw badRequest("Destination warehouse is required for every carton");
return value;
} }
private List<RelabelItem> itemList(Long applicationId) { private List<RelabelItem> itemList(Long applicationId) {
@@ -1456,9 +1456,13 @@ CREATE TABLE IF NOT EXISTS ll_relabel_item (
tracking_code VARCHAR(255) NULL COMMENT '物流跟踪编码', tracking_code VARCHAR(255) NULL COMMENT '物流跟踪编码',
new_sku_code VARCHAR(128) NOT NULL COMMENT '换标后SKU编码', new_sku_code VARCHAR(128) NOT NULL COMMENT '换标后SKU编码',
label_requirement VARCHAR(1000) NULL COMMENT '换标要求', label_requirement VARCHAR(1000) NULL COMMENT '换标要求',
remark VARCHAR(1000) NULL COMMENT '货物明细备注',
relabel_file_id VARCHAR(128) NULL COMMENT '换标文件ID', relabel_file_id VARCHAR(128) NULL COMMENT '换标文件ID',
relabel_file_name VARCHAR(255) NULL COMMENT '换标文件名', relabel_file_name VARCHAR(255) NULL COMMENT '换标文件名',
relabel_file_url VARCHAR(1000) NULL COMMENT '换标文件地址', relabel_file_url VARCHAR(1000) NULL COMMENT '换标文件地址',
carton_mark VARCHAR(255) NULL COMMENT '客户自有箱唛',
fba_carton_mark VARCHAR(255) NULL COMMENT '关联FBA箱唛',
destination_warehouse_id VARCHAR(128) NOT NULL COMMENT '该箱目的海外仓ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
PRIMARY KEY (id), PRIMARY KEY (id),
KEY idx_ll_relabel_item_application (customer_id,application_id), KEY idx_ll_relabel_item_application (customer_id,application_id),
@@ -678,27 +678,6 @@ paths:
required: true required: true
content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementReview' } } } content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementReview' } } }
responses: { '200': { description: 统一 ApiResponse 包装的采购订单详情 } } responses: { '200': { description: 统一 ApiResponse 包装的采购订单详情 } }
/api/platform/v1/last-leg/overseas-procurements/{orderNo}/files/{category}:
post:
summary: 上传采购签收文件或费用凭证
description: 权限 platform:overseas-business:purchase:edit;category 仅允许 RECEIPT 或 VOUCHER。
security: [{ bearerAuth: [] }]
parameters:
- { name: orderNo, in: path, required: true, schema: { type: string } }
- { name: category, in: path, required: true, schema: { type: string, enum: [RECEIPT, VOUCHER] } }
- { name: customerId, in: query, required: true, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } } }
responses: { '200': { description: 已保存文件元数据 } }
/api/platform/v1/last-leg/overseas-procurements/{orderNo}/fees:
post:
summary: 登记采购费用并发起资金冻结
description: 权限 platform:overseas-business:purchase:edit;订单必须已审核通过,费用凭证必须属于本订单。
security: [{ bearerAuth: [] }]
parameters:
- { name: orderNo, in: path, required: true, schema: { type: string } }
- { name: customerId, in: query, required: true, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementFeeCreate' } } } }
responses: { '200': { description: 已创建费用并进入冻结流程 }, '409': { description: 订单状态或凭证不允许 } }
/api/merchant/v1/last-leg/delivery-orders: /api/merchant/v1/last-leg/delivery-orders:
get: get:
summary: 客户分页查询自己的派送单 summary: 客户分页查询自己的派送单
@@ -1662,20 +1641,20 @@ components:
RelabelStatus: { type: string, enum: [PENDING_PROCESSING, PROCESSING, COMPLETED, REJECTED, CANCELLED] } RelabelStatus: { type: string, enum: [PENDING_PROCESSING, PROCESSING, COMPLETED, REJECTED, CANCELLED] }
RelabelCreate: RelabelCreate:
type: object type: object
required: [customerOrderNo, overseasWarehouseId, labelDeadline, contactName, contactPhone, chargeAmount, idempotencyKey, items] required: [customerOrderNo, labelDeadline, contactName, contactPhone, chargeAmount, idempotencyKey, items]
properties: properties:
customerOrderNo: { type: string, maxLength: 128 } customerOrderNo: { type: string, maxLength: 128 }
overseasWarehouseId: { type: string, maxLength: 128 } overseasWarehouseId: { type: string, nullable: true, maxLength: 128, description: 统一目的海外仓;明细未填写目的仓时使用 }
labelDeadline: { type: string, format: date-time } labelDeadline: { type: string, format: date-time }
contactName: { type: string, maxLength: 128 } contactName: { type: string, maxLength: 128 }
contactPhone: { type: string, maxLength: 64 } contactPhone: { type: string, maxLength: 64 }
chargeAmount: { type: number, minimum: 0.01, description: 换标费用,币种固定 CNY } chargeAmount: { type: number, minimum: 0.01, description: 本次贴标费用(CNY) }
remark: { type: string, nullable: true, maxLength: 1000 } remark: { type: string, nullable: true, maxLength: 1000 }
idempotencyKey: { type: string, maxLength: 255 } idempotencyKey: { type: string, maxLength: 255 }
items: { type: array, minItems: 1, maxItems: 500, items: { $ref: '#/components/schemas/RelabelItem' } } items: { type: array, minItems: 1, maxItems: 500, items: { $ref: '#/components/schemas/RelabelItem' } }
RelabelItem: RelabelItem:
type: object type: object
required: [skuCode, productNameZh, totalPackageCount, notifiedPackageCount, shipmentNo, newSkuCode] required: [skuCode, productNameZh, totalPackageCount, notifiedPackageCount, shipmentNo, newSkuCode, cartonMark, fbaCartonMark]
properties: properties:
skuCode: { type: string, maxLength: 128 } skuCode: { type: string, maxLength: 128 }
productImageFileId: { type: string, nullable: true, maxLength: 128 } productImageFileId: { type: string, nullable: true, maxLength: 128 }
@@ -1691,6 +1670,9 @@ components:
relabelFileId: { type: string, nullable: true, maxLength: 128 } relabelFileId: { type: string, nullable: true, maxLength: 128 }
relabelFileName: { type: string, nullable: true, maxLength: 255 } relabelFileName: { type: string, nullable: true, maxLength: 255 }
relabelFileUrl: { type: string, nullable: true, maxLength: 1000 } relabelFileUrl: { type: string, nullable: true, maxLength: 1000 }
cartonMark: { type: string, maxLength: 255, description: 客户自有箱唛(不在编辑列表展示) }
fbaCartonMark: { type: string, maxLength: 255, description: 关联 FBA 箱唛 }
destinationWarehouseId: { type: string, nullable: true, maxLength: 128, description: 该箱目的海外仓;为空时使用申请统一目的仓 }
RelabelStatusChange: RelabelStatusChange:
type: object type: object
required: [status] required: [status]
@@ -1786,7 +1768,7 @@ components:
updatedAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time }
ProcurementDetail: ProcurementDetail:
type: object type: object
required: [summary, histories, outboundDetails, files, fees] required: [summary, histories, outboundDetails]
properties: properties:
summary: { $ref: '#/components/schemas/ProcurementSummary' } summary: { $ref: '#/components/schemas/ProcurementSummary' }
productUrl: { type: string, nullable: true } productUrl: { type: string, nullable: true }
@@ -1805,42 +1787,6 @@ components:
reviewedAt: { type: string, format: date-time, nullable: true } reviewedAt: { type: string, format: date-time, nullable: true }
histories: { type: array, items: { $ref: '#/components/schemas/BusinessHistory' } } histories: { type: array, items: { $ref: '#/components/schemas/BusinessHistory' } }
outboundDetails: { type: array, items: { $ref: '#/components/schemas/ProcurementOutboundDetail' } } outboundDetails: { type: array, items: { $ref: '#/components/schemas/ProcurementOutboundDetail' } }
files: { type: array, items: { $ref: '#/components/schemas/ProcurementFile' } }
fees: { type: array, items: { $ref: '#/components/schemas/ProcurementFee' } }
ProcurementFeeCreate:
type: object
required: [feeItemCode, feeItemName, amount, voucherFileId]
properties:
feeItemCode: { type: string, maxLength: 64 }
feeItemName: { type: string, maxLength: 128 }
amount: { type: number, minimum: 0.01 }
voucherFileId: { type: string, pattern: '^[0-9]+$' }
remark: { type: string, nullable: true, maxLength: 500 }
ProcurementFile:
type: object
properties:
id: { type: string }
category: { type: string, enum: [RECEIPT, VOUCHER] }
fileId: { type: string }
fileName: { type: string }
contentType: { type: string, nullable: true }
sizeBytes: { type: integer, format: int64 }
createdAt: { type: string, format: date-time }
ProcurementFee:
type: object
properties:
id: { type: string }
feeNo: { type: string }
feeItemCode: { type: string }
feeItemName: { type: string }
amount: { type: number }
currency: { type: string, enum: [CNY] }
voucherFileId: { type: string }
voucherFileName: { type: string }
remark: { type: string, nullable: true }
walletStatus: { type: string }
walletErrorMessage: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
ProcurementOutboundDetail: ProcurementOutboundDetail:
type: object type: object
properties: properties:
@@ -40,6 +40,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class RelabelApplicationServiceTest { class RelabelApplicationServiceTest {
@@ -98,7 +99,7 @@ class RelabelApplicationServiceTest {
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus()); assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion()); assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}")); assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId()); assertEquals(tailOrder.getValue().getInboundCode() + ":WH-1", tailOrder.getValue().getSourceInboundId());
assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo()); assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo());
assertTrue(tailOrder.getValue().getInboundCode().startsWith("RL-")); assertTrue(tailOrder.getValue().getInboundCode().startsWith("RL-"));
assertEquals(10, tailOrder.getValue().getPackageCount()); assertEquals(10, tailOrder.getValue().getPackageCount());
@@ -120,6 +121,71 @@ class RelabelApplicationServiceTest {
verify(items, never()).insert(any(RelabelItem.class)); verify(items, never()).insert(any(RelabelItem.class));
} }
@Test
void createPersistsCartonMarkBinding() {
RelabelRequests.Item attachedItem = new RelabelRequests.Item("SKU-A", null, null, null,
"Chinese name", 2, 2, "SHP-1", null, "SKU-NEW", null, null, null, null, null,
"BOX-1", "FBA-BOX-1", "WH-1");
RelabelRequests.Create request = new RelabelRequests.Create("CUSTOMER-1", "WH-1",
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", BigDecimal.ONE, null, "key-marks",
List.of(attachedItem));
RelabelApplication persisted = application(11L, "FBR-1", "100", "key-files", null,
RelabelApplicationService.PENDING_PROCESSING);
persisted.setLastlegOrderId(21L); persisted.setItemCount(1); persisted.setTotalPackageCount(2);
persisted.setNotifiedPackageCount(2);
when(applications.findNo(eq("100"), any())).thenReturn(persisted);
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 2, 2)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "100", "LMO-RELABEL-1",
RelabelApplicationService.PENDING_PROCESSING));
service.create("100", "operator-1", request);
ArgumentCaptor<RelabelItem> saved = ArgumentCaptor.forClass(RelabelItem.class);
verify(items).insert(saved.capture());
assertEquals("BOX-1", saved.getValue().getCartonMark());
assertEquals("FBA-BOX-1", saved.getValue().getFbaCartonMark());
assertEquals("WH-1", saved.getValue().getDestinationWarehouseId());
}
@Test
void createGroupsCartonsIntoOneTailOrderPerDestinationWarehouse() {
RelabelRequests.Item west = carton("SKU-A", "BOX-1", "FBA-1", "ONT8");
RelabelRequests.Item east = carton("SKU-B", "BOX-2", "FBA-2", "ABE8");
RelabelRequests.Create request = new RelabelRequests.Create("CUSTOMER-1", null,
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", BigDecimal.ONE, null, "key-multi",
List.of(west, east));
RelabelApplication persisted = application(11L, "FBR-1", "10001", "key-multi", null,
RelabelApplicationService.PENDING_PROCESSING);
persisted.setLastlegOrderId(21L);
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 1, 1)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1",
RelabelApplicationService.PENDING_PROCESSING));
service.create("10001", "operator-1", request);
ArgumentCaptor<LastlegOrder> created = ArgumentCaptor.forClass(LastlegOrder.class);
verify(orders, times(2)).insert(created.capture());
assertEquals(List.of("ONT8", "ABE8"), created.getAllValues().stream()
.map(LastlegOrder::getOverseasWarehouseId).toList());
assertTrue(created.getAllValues().stream().allMatch(value -> value.getPackageCount() == 1));
}
@Test
void createRejectsCartonWithoutDetailOrDefaultDestinationWarehouse() {
RelabelRequests.Create request = new RelabelRequests.Create("CUSTOMER-1", null,
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", BigDecimal.ONE, null, "key-no-destination",
List.of(carton("SKU-A", "BOX-1", "FBA-1", null)));
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.create("10001", "operator-1", request));
assertEquals(400, exception.getStatusCode().value());
verify(orders, never()).insert(any(LastlegOrder.class));
}
@Test @Test
void sameIdempotencyKeyAndPayloadReturnsExistingApplicationWithoutWrites() { void sameIdempotencyKeyAndPayloadReturnsExistingApplicationWithoutWrites() {
RelabelRequests.Create request = request("key-1", 10, 6); RelabelRequests.Create request = request("key-1", 10, 6);
@@ -224,6 +290,13 @@ class RelabelApplicationServiceTest {
"SKU-NEW", "Label on carton", "file-1", "relabel.pdf", "https://files/relabel.pdf"))); "SKU-NEW", "Label on carton", "file-1", "relabel.pdf", "https://files/relabel.pdf")));
} }
private static RelabelRequests.Item carton(String sku, String cartonMark, String fbaCartonMark,
String destinationWarehouseId) {
return new RelabelRequests.Item(sku, null, null, null, "Chinese name", 1, 1,
"SHP-1", null, sku + "-NEW", null, null, null, null, null,
cartonMark, fbaCartonMark, destinationWarehouseId);
}
private static RelabelApplication application(long id, String no, String customerId, String key, String requestHash, private static RelabelApplication application(long id, String no, String customerId, String key, String requestHash,
String status) { String status) {
RelabelApplication value = new RelabelApplication(); RelabelApplication value = new RelabelApplication();
@@ -60,7 +60,7 @@ public interface FirstlegInboundClient {
Boolean customsInspection, String destinationType, Long destinationWarehouseId, Boolean customsInspection, String destinationType, Long destinationWarehouseId,
String destinationCode, String destinationName, String publicRemark, String destinationCode, String destinationName, String publicRemark,
String expressCompanyName, String expressTrackingNo, Integer totalPackageCount, String expressCompanyName, String expressTrackingNo, Integer totalPackageCount,
String pickupMethod, LocalDateTime requestedPickupAt, String fulfillmentType String pickupMethod, LocalDateTime requestedPickupAt, String fulfillmentType, String lxCodes
) { ) {
} }
} }
@@ -76,6 +76,8 @@ public final class OrderRequests {
@Size(max = 40) @Pattern(regexp = "[A-Za-z0-9._:-]+") String inboundCode, @Size(max = 40) @Pattern(regexp = "[A-Za-z0-9._:-]+") String inboundCode,
@Schema(description = "Fulfillment type; omitted means STANDARD / 履约类型;省略时为普通头程", allowableValues = {"STANDARD", "ONE_CLICK_DROPSHIP"}, defaultValue = "STANDARD") @Schema(description = "Fulfillment type; omitted means STANDARD / 履约类型;省略时为普通头程", allowableValues = {"STANDARD", "ONE_CLICK_DROPSHIP"}, defaultValue = "STANDARD")
@Pattern(regexp = "STANDARD|ONE_CLICK_DROPSHIP") String fulfillmentType, @Pattern(regexp = "STANDARD|ONE_CLICK_DROPSHIP") String fulfillmentType,
@Schema(description = "Lingxing inbound codes for one-click dropship; multiple values separated by comma, space or newline", maxLength = 1000)
@Size(max = 1000) String lxCodes,
@Schema(description = "Selected customer pickup address ID; required for PICKUP / 已选客户取货地址 ID;PICKUP 时必填") @Schema(description = "Selected customer pickup address ID; required for PICKUP / 已选客户取货地址 ID;PICKUP 时必填")
@Min(1) Long senderAddressId, @Min(1) Long senderAddressId,
@Schema(description = "Selected private destination address ID; required for PRIVATE / 已选私人目的仓地址 ID;PRIVATE 时必填") @Schema(description = "Selected private destination address ID; required for PRIVATE / 已选私人目的仓地址 ID;PRIVATE 时必填")
@@ -96,7 +98,7 @@ public final class OrderRequests {
sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId, sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId,
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode, receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode, pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
shipmentId, referenceId, inboundCode, fulfillmentType, null, null); shipmentId, referenceId, inboundCode, fulfillmentType, null, null, null);
} }
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm, public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
@@ -114,7 +116,7 @@ public final class OrderRequests {
sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId, sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId,
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode, receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode, pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
shipmentId, referenceId, inboundCode, null, null, null); shipmentId, referenceId, inboundCode, null, null, null, null);
} }
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm, public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
@@ -131,7 +133,7 @@ public final class OrderRequests {
sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId, sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId,
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode, receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode, pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
shipmentId, referenceId, null, null, null, null); shipmentId, referenceId, null, null, null, null, null);
} }
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm, public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
@@ -148,7 +150,7 @@ public final class OrderRequests {
sender, receiver, cargoes, pickupMethod, null, receivingWarehouseId, sender, receiver, cargoes, pickupMethod, null, receivingWarehouseId,
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode, receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode, pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
shipmentId, referenceId, null, null, null, null); shipmentId, referenceId, null, null, null, null, null);
} }
@AssertTrue(message = "承运商 ID 和名称必须同时填写或同时留空") @AssertTrue(message = "承运商 ID 和名称必须同时填写或同时留空")
@@ -52,6 +52,7 @@ public class OrderEntity extends BaseEntity {
private Boolean customsInspection; private Boolean customsInspection;
private String destinationType; private String destinationType;
private String fulfillmentType; private String fulfillmentType;
private String lxCodes;
private String amazonWarehouseCode; private String amazonWarehouseCode;
private String shipmentId; private String shipmentId;
private String referenceId; private String referenceId;
@@ -488,6 +488,7 @@ public class OrderApplicationService {
order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection()); order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection());
order.setDestinationType(request.destinationType()); order.setDestinationType(request.destinationType());
order.setFulfillmentType(fulfillmentType(request.fulfillmentType())); order.setFulfillmentType(fulfillmentType(request.fulfillmentType()));
order.setLxCodes("ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType()) ? normalizeLxCodes(request.lxCodes()) : null);
order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null
? snapshots.destination().warehouse().warehouseCode() : request.amazonWarehouseCode()); ? snapshots.destination().warehouse().warehouseCode() : request.amazonWarehouseCode());
order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId()); order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId());
@@ -519,6 +520,11 @@ public class OrderApplicationService {
throw new BizException(ResultCode.VALIDATION_FAILED, throw new BizException(ResultCode.VALIDATION_FAILED,
"履约类型必须为 STANDARD 或 ONE_CLICK_DROPSHIP"); "履约类型必须为 STANDARD 或 ONE_CLICK_DROPSHIP");
} }
private static String normalizeLxCodes(String value) {
if (value == null || value.isBlank()) return null;
return java.util.Arrays.stream(value.trim().split("[,,\\s]+"))
.map(String::trim).filter(item -> !item.isEmpty()).distinct().collect(java.util.stream.Collectors.joining(","));
}
private boolean legacyManualMode() { private boolean legacyManualMode() {
return inboundCodeModeProvider.legacyManual(); return inboundCodeModeProvider.legacyManual();
} }
@@ -683,7 +689,7 @@ public class OrderApplicationService {
order.getDestinationWarehouseId(), order.getAmazonWarehouseCode(), order.getDestinationWarehouseName(), order.getDestinationWarehouseId(), order.getAmazonWarehouseCode(), order.getDestinationWarehouseName(),
order.getCustomerRemark(), order.getExpressCompany(), order.getExpressTrackingNo(), totalPackageCount, order.getCustomerRemark(), order.getExpressCompany(), order.getExpressTrackingNo(), totalPackageCount,
order.getPickupMethod(), order.getRequestedPickupAt(), order.getPickupMethod(), order.getRequestedPickupAt(),
order.getFulfillmentType() == null ? "STANDARD" : order.getFulfillmentType()); order.getFulfillmentType() == null ? "STANDARD" : order.getFulfillmentType(), order.getLxCodes());
} }
private void prepareDraftInbound(OrderEntity order, OrderRequests.SaveRequest request) { private void prepareDraftInbound(OrderEntity order, OrderRequests.SaveRequest request) {
FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable(); FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable();
+12 -1
View File
@@ -1049,8 +1049,9 @@ CREATE TABLE IF NOT EXISTS ll_relabel_item (
product_image_file_name VARCHAR(255) NULL COMMENT '产品图片文件名', product_image_url VARCHAR(1000) NULL COMMENT '产品图片地址', product_image_file_name VARCHAR(255) NULL COMMENT '产品图片文件名', product_image_url VARCHAR(1000) NULL COMMENT '产品图片地址',
product_name_zh VARCHAR(255) NOT NULL COMMENT '产品中文名称', total_package_count INT NOT NULL COMMENT '总箱数', notified_package_count INT NOT NULL COMMENT '已通知箱数', product_name_zh VARCHAR(255) NOT NULL COMMENT '产品中文名称', total_package_count INT NOT NULL COMMENT '总箱数', notified_package_count INT NOT NULL COMMENT '已通知箱数',
shipment_no VARCHAR(128) NOT NULL COMMENT '货件编号', tracking_code VARCHAR(255) NULL COMMENT '物流跟踪编码', new_sku_code VARCHAR(128) NOT NULL COMMENT '换标后SKU编码', shipment_no VARCHAR(128) NOT NULL COMMENT '货件编号', tracking_code VARCHAR(255) NULL COMMENT '物流跟踪编码', new_sku_code VARCHAR(128) NOT NULL COMMENT '换标后SKU编码',
label_requirement VARCHAR(1000) NULL COMMENT '换标要求', relabel_file_id VARCHAR(128) NULL COMMENT '换标文件ID', label_requirement VARCHAR(1000) NULL COMMENT '换标要求', remark VARCHAR(1000) NULL COMMENT '货物明细备注', relabel_file_id VARCHAR(128) NULL COMMENT '换标文件ID',
relabel_file_name VARCHAR(255) NULL COMMENT '换标文件名', relabel_file_url VARCHAR(1000) NULL COMMENT '换标文件地址', relabel_file_name VARCHAR(255) NULL COMMENT '换标文件名', relabel_file_url VARCHAR(1000) NULL COMMENT '换标文件地址',
carton_mark VARCHAR(255) NULL COMMENT '客户自有箱唛', fba_carton_mark VARCHAR(255) NULL COMMENT '关联FBA箱唛', destination_warehouse_id VARCHAR(128) NULL COMMENT '该箱目的海外仓ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id) created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id)
) ENGINE=InnoDB COMMENT='换标货物冻结明细'; ) ENGINE=InnoDB COMMENT='换标货物冻结明细';
@@ -2911,6 +2912,16 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025; PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='label_requirement' AND COALESCE(column_comment,'') <> '换标要求'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN label_requirement VARCHAR(1000) NULL COMMENT ''换标要求''', 'SELECT 1')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='label_requirement' AND COALESCE(column_comment,'') <> '换标要求'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN label_requirement VARCHAR(1000) NULL COMMENT ''换标要求''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025; PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='remark'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN remark VARCHAR(1000) NULL COMMENT ''货物明细备注'' AFTER label_requirement'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='carton_mark'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN carton_mark VARCHAR(255) NULL COMMENT ''客户自有箱唛'' AFTER relabel_file_url'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='fba_carton_mark'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN fba_carton_mark VARCHAR(255) NULL COMMENT ''关联FBA箱唛'' AFTER carton_mark'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='destination_warehouse_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN destination_warehouse_id VARCHAR(128) NULL COMMENT ''该箱目的海外仓ID'' AFTER fba_carton_mark'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND index_name='idx_ll_relabel_item_destination'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD KEY idx_ll_relabel_item_destination (customer_id,destination_warehouse_id,application_id)'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='relabel_file_id' AND COALESCE(column_comment,'') <> '换标文件ID'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN relabel_file_id VARCHAR(128) NULL COMMENT ''换标文件ID''', 'SELECT 1')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='relabel_file_id' AND COALESCE(column_comment,'') <> '换标文件ID'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN relabel_file_id VARCHAR(128) NULL COMMENT ''换标文件ID''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025; PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='relabel_file_name' AND COALESCE(column_comment,'') <> '换标文件名'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN relabel_file_name VARCHAR(255) NULL COMMENT ''换标文件名''', 'SELECT 1')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='relabel_file_name' AND COALESCE(column_comment,'') <> '换标文件名'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN relabel_file_name VARCHAR(255) NULL COMMENT ''换标文件名''', 'SELECT 1'));