fix(dropship): project sealed carton identities into overseas receiving

This commit is contained in:
2026-09-09 13:20:11 +08:00
parent 90973532f0
commit 748855793d
19 changed files with 558 additions and 33 deletions
@@ -20,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Isolation;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import java.math.BigDecimal;
import java.util.Map;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.ArrayList;
@@ -62,7 +63,7 @@ public class LoadingCompletedConsumer {
String sourceContainerBookingId = sourceContainerBookingId(envelope, payload);
if(payload.orders()==null||payload.orders().isEmpty()) throw new IllegalArgumentException("事件没有入仓单");
OwnerSnapshot owner = requireOwnerSnapshot(payload.orders(), envelope.tenantId());
if (envelope.eventVersion() == 2) validateCartonSkuMappings(payload);
if (envelope.eventVersion() == 2) validateCartonSkuMappings(payload, envelope.payload());
freezeContainerIdentity(envelope, sourceContainerBookingId, containerNo);
LastlegEventInbox existing=inboxMapper.selectById(envelope.eventId());
if(existing!=null) {
@@ -102,8 +103,8 @@ public class LoadingCompletedConsumer {
try {
ObjectMapper compatibleMapper = objectMapper.copy()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// V2 only adds immutable item remarks; the current tail-leg snapshot model
// intentionally ignores those fields until its schema exposes them.
// The internal order model keeps the shared fields. V2 pallet source-detail
// identity is validated separately from the original immutable payload.
return compatibleMapper.treeToValue(envelope.payload(), FirstLegLoadingPlanCompletedV1.class);
}
catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); }
@@ -168,8 +169,9 @@ public class LoadingCompletedConsumer {
return normalized;
}
private void validateCartonSkuMappings(FirstLegLoadingPlanCompletedV1 payload) {
for (FirstLegLoadingPlanCompletedV1.Order order : payload.orders()) {
private void validateCartonSkuMappings(FirstLegLoadingPlanCompletedV1 payload, JsonNode rawPayload) {
for (int index = 0; index < payload.orders().size(); index++) {
FirstLegLoadingPlanCompletedV1.Order order = payload.orders().get(index);
Set<String> orderCartonMarks = new HashSet<>();
if ("ONE_CLICK_DROPSHIP".equals(order.orderType())) {
for (FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet : order.pallets()) {
@@ -177,6 +179,7 @@ public class LoadingCompletedConsumer {
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), orderCartonMarks);
}
}
dropshipReceivingLabels(order, rawPayload.path("orders").get(index));
continue;
}
String destinationType = destinationType(payload, order);
@@ -273,6 +276,8 @@ public class LoadingCompletedConsumer {
}
createInboundOrderSnapshot(event, payload, source, order, orderHash);
Map<String, List<String>> dropshipLabels = "ONE_CLICK_DROPSHIP".equals(orderType)
&& "OVERSEAS".equals(destinationType) ? dropshipReceivingLabels(source, rawSource) : Map.of();
for(var item:source.items()) {
LastlegOrderDetail detail=new LastlegOrderDetail();
detail.setCustomerId(customerId);
@@ -283,7 +288,9 @@ public class LoadingCompletedConsumer {
detail.setDestinationWarehouseId(item.destinationWarehouseId()); detail.setDestinationWarehouseCode(item.destinationWarehouseCode());
detail.setDestinationWarehouseName(item.destinationWarehouseName()); detail.setFbaTrackingNo(item.fbaTrackingNo());
detail.setShipmentNo(item.shipmentNo()); detail.setShippingMark(item.shippingMark()); detailMapper.insert(detail);
createCartonLabels(customerId, order.getId(), destinationType, item);
createCartonLabels(customerId, order.getId(), destinationType, item,
"ONE_CLICK_DROPSHIP".equals(orderType) ? dropshipLabels.getOrDefault(item.sourceInboundDetailId(), List.of())
: item.cartonMarks());
}
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory(); history.setCustomerId(customerId);
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
@@ -353,10 +360,38 @@ public class LoadingCompletedConsumer {
}
}
private Map<String, List<String>> dropshipReceivingLabels(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) {
Map<String, FirstLegLoadingPlanCompletedV1.Item> itemsByDetail = new java.util.LinkedHashMap<>();
for (var item : source.items()) {
if (item.sourceInboundDetailId() == null || item.sourceInboundDetailId().isBlank()
|| itemsByDetail.putIfAbsent(item.sourceInboundDetailId(), item) != null) {
throw new IllegalArgumentException("一件代发来源明细标识缺失或重复");
}
}
Map<String, List<String>> marksByDetail = new java.util.LinkedHashMap<>();
for (JsonNode pallet : rawSource.path("pallets")) {
for (JsonNode sku : pallet.path("skuPackages")) {
String detailId = sku.path("sourceInboundDetailId").asText(null);
var item = itemsByDetail.get(detailId);
if (item == null || !Objects.equals(item.skuCode(), sku.path("skuCode").asText(null))) {
throw new IllegalArgumentException("一件代发托盘来源明细缺失或与 SKU 不一致");
}
List<String> marks = marksByDetail.computeIfAbsent(detailId, ignored -> new java.util.ArrayList<>());
sku.path("cartonMarks").forEach(mark -> marks.add(mark.asText()));
}
}
for (var item : source.items()) {
if (marksByDetail.getOrDefault(item.sourceInboundDetailId(), List.of()).size() != item.packageCount()) {
throw new IllegalArgumentException("一件代发托盘箱唛与来源明细箱数不一致");
}
}
return marksByDetail;
}
private void createCartonLabels(String customerId, Long orderId, String destinationType,
FirstLegLoadingPlanCompletedV1.Item item) {
FirstLegLoadingPlanCompletedV1.Item item, List<String> sourceMarks) {
if (!"OVERSEAS".equals(destinationType)) return;
List<String> marks = item.cartonMarks() == null ? List.of() : item.cartonMarks();
List<String> marks = sourceMarks == null ? List.of() : sourceMarks;
if (marks.isEmpty()) return;
if (marks.stream().anyMatch(mark -> mark == null || mark.isBlank()) || marks.stream().distinct().count() != marks.size()) {
throw new IllegalArgumentException("装柜事件的箱唛池不可包含空值或重复箱唛");
@@ -409,10 +444,19 @@ public class LoadingCompletedConsumer {
private List<DropshipRequests.Pallet> pallets(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) {
if (source.pallets() != null && !source.pallets().isEmpty()) {
return source.pallets().stream().map(pallet -> new DropshipRequests.Pallet(pallet.palletNo(),
pallet.skuPackages().stream().map(sku -> new DropshipRequests.SkuPackages(
sku.skuCode(), sku.productName(), sku.packageCount(), sku.productQuantity(),
sku.cartonMarks())).toList())).toList();
return source.pallets().stream().map(pallet -> {
Map<String, DropshipRequests.SkuPackages> inventorySkus = new java.util.LinkedHashMap<>();
for (var sku : pallet.skuPackages()) {
inventorySkus.merge(sku.skuCode(), new DropshipRequests.SkuPackages(sku.skuCode(),
sku.productName(), sku.packageCount(), sku.productQuantity(), sku.cartonMarks()), (left, right) -> {
List<String> marks = new ArrayList<>(left.cartonMarks()); marks.addAll(right.cartonMarks());
return new DropshipRequests.SkuPackages(left.skuCode(), left.productName(),
Math.addExact(left.packageCount(), right.packageCount()),
Math.addExact(left.productQuantity(), right.productQuantity()), List.copyOf(marks));
});
}
return new DropshipRequests.Pallet(pallet.palletNo(), List.copyOf(inventorySkus.values()));
}).toList();
}
return legacyPallets(rawSource);
}