package com.baiyue.lastleg.service; import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.dropship.dto.DropshipRequests; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.baiyue.lastleg.shipping.entity.ContainerShippingGate; import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.DeserializationFeature; import jakarta.annotation.Resource; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; 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; import java.util.HashSet; import java.util.List; import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; @Service public class LoadingCompletedConsumer { static final String DEFAULT_RECEIVING_WAREHOUSE_ID = "193000000000200003"; static final String DEFAULT_RECEIVING_WAREHOUSE_CODE = "US-LAX-01"; static final String DEFAULT_RECEIVING_WAREHOUSE_NAME = "洛杉矶仓"; @Resource private LastlegEventInboxMapper inboxMapper; @Resource private LastlegOrderMapper orderMapper; @Resource private LastlegInboundOrderMapper inboundOrderMapper; @Resource private LastlegOrderDetailMapper detailMapper; @Resource private LastlegOrderStatusHistoryMapper historyMapper; @Resource private InboundPlanCartonMapper cartonMapper; @Resource private ObjectMapper objectMapper; @Resource private LastlegOrderNoGenerator orderNoGenerator; @Resource private DropshipOrderService dropshipOrders; @Resource private InventoryCommandService inventory; @Resource private ContainerShippingGateMapper shippingGates; @Resource private LastlegSourcePalletMapper sourcePallets; @Transactional(isolation = Isolation.READ_COMMITTED) public int consume(EventEnvelope envelope) { envelope.requireValid(); if (!EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(envelope.eventType()) || (envelope.eventVersion() != 1 && envelope.eventVersion() != 2)) throw new IllegalArgumentException("不支持的尾程交接事件"); if (envelope.sourceRegion() != RegionCode.CN || envelope.targetRegion() != RegionCode.OVERSEAS) throw new IllegalArgumentException("尾程交接事件区域无效"); if (!"LOADING_PLAN".equals(envelope.aggregateType())) throw new IllegalArgumentException("尾程交接事件聚合类型无效"); if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()), envelope.payloadHash())) throw new IllegalArgumentException("payloadHash 与尾程事件负载不一致"); FirstLegLoadingPlanCompletedV1 payload = parsePayload(envelope); String containerNo = ContainerNumbers.requireCanonical(payload.containerNo()); 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, envelope.payload()); freezeContainerIdentity(envelope, sourceContainerBookingId, containerNo); LastlegEventInbox existing=inboxMapper.selectById(envelope.eventId()); if(existing!=null) { if(!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) throw new IllegalArgumentException("事件摘要冲突"); return countOrders(envelope,containerNo); } LastlegEventInbox inbox=new LastlegEventInbox(); inbox.setEventId(envelope.eventId()); inbox.setCustomerId(envelope.tenantId()); inbox.setEventType(envelope.eventType()); inbox.setEventVersion(envelope.eventVersion()); inbox.setPayloadHash(envelope.payloadHash()); inbox.setPayloadJson(envelope.payload().toString()); inbox.setStatus("RECEIVED"); inbox.setReceivedAt(LocalDateTime.now()); try { inboxMapper.insert(inbox); } catch(DuplicateKeyException exception) { LastlegEventInbox raced = inboxMapper.selectById(envelope.eventId()); if (raced == null || raced.getPayloadHash() == null || !raced.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) { throw new IllegalArgumentException("事件摘要冲突", exception); } return countOrders(envelope,containerNo); } // The loading-plan and container-shipping events are independent aggregate streams. // Seeding the shipping version from the loading-plan version makes a valid first node // (aggregateVersion=0) look stale whenever the loading plan has a higher version. JsonNode rawOrders = envelope.payload().path("orders"); String orderNo = orderNoGenerator.forContainerNo(payload.containerNo()); for (int index = 0; index < payload.orders().size(); index++) { var source = payload.orders().get(index); JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode(); createOrder(envelope, payload, source, orderNo, rawSource, containerNo, sourceContainerBookingId, owner); } inbox.setStatus("PROCESSED"); inbox.setProcessedAt(LocalDateTime.now()); inboxMapper.updateById(inbox); return payload.orders().size(); } private FirstLegLoadingPlanCompletedV1 parsePayload(EventEnvelope envelope) { try { ObjectMapper compatibleMapper = objectMapper.copy() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 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); } } private int countOrders(EventEnvelope envelope,String containerNo) { return Math.toIntExact(orderMapper.countContainer(envelope.tenantId(),containerNo)); } private void freezeContainerIdentity(EventEnvelope envelope, String sourceContainerBookingId, String containerNo) { shippingGates.initialize(sourceContainerBookingId, "", containerNo, 0, envelope.handoverVersion()); ContainerShippingGate frozen = shippingGates.lock(sourceContainerBookingId); if (frozen == null) { throw new IllegalStateException("来源柜真实集装箱号冻结失败"); } if (!containerNo.equals(ContainerNumbers.requireCanonical(frozen.getContainerNo()))) { throw new IllegalStateException("同一 sourceContainerBookingId 的真实集装箱号冲突"); } for (String existing : orderMapper.lockContainerNumbers(sourceContainerBookingId)) { if (!containerNo.equals(ContainerNumbers.requireCanonical(existing))) { throw new IllegalStateException("同一 sourceContainerBookingId 的尾程订单柜号冲突"); } } } private String sourceContainerBookingId(EventEnvelope envelope, FirstLegLoadingPlanCompletedV1 payload) { String value = envelope.eventVersion() == 2 ? envelope.payload().path("sourceContainerBookingId").asText(null) : payload.containerBookingId(); if (value == null || value.isBlank() || value.trim().length() > 128) { throw new IllegalArgumentException("事件缺少稳定来源柜预订 ID sourceContainerBookingId"); } return value.trim(); } private OwnerSnapshot requireOwnerSnapshot(List orders, String trustedCustomerId) { String customerId = requiredOwnerValue(orders.get(0).customerId(), "customerId"); String customerName = requiredOwnerValue(orders.get(0).customerName(), "customerName"); if (!trustedCustomerId.equals(customerId)) { throw new IllegalArgumentException("Order customerId does not match the trusted event customer"); } for (FirstLegLoadingPlanCompletedV1.Order order : orders) { if (!customerId.equals(requiredOwnerValue(order.customerId(), "customerId")) || !customerName.equals(requiredOwnerValue(order.customerName(), "customerName"))) { throw new IllegalArgumentException("同一装柜完成事件的货主快照不一致"); } } return new OwnerSnapshot(customerId, customerName); } private String requiredOwnerValue(String value, String field) { if (value == null || value.isBlank()) { throw new IllegalArgumentException("尾程事件缺少不可变货主快照 " + field); } String normalized = value.trim(); if (normalized.length() > 128) { throw new IllegalArgumentException("尾程事件货主快照过长 " + field); } return normalized; } private void validateCartonSkuMappings(FirstLegLoadingPlanCompletedV1 payload, JsonNode rawPayload) { for (int index = 0; index < payload.orders().size(); index++) { FirstLegLoadingPlanCompletedV1.Order order = payload.orders().get(index); Set orderCartonMarks = new HashSet<>(); if ("ONE_CLICK_DROPSHIP".equals(order.orderType())) { for (FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet : order.pallets()) { for (FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot sku : pallet.skuPackages()) { requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), orderCartonMarks); } } dropshipReceivingLabels(order, rawPayload.path("orders").get(index)); continue; } String destinationType = destinationType(payload, order); for (FirstLegLoadingPlanCompletedV1.Item item : order.items()) { if ("OVERSEAS".equals(destinationType)) { requireSkuCartonMapping(item.skuCode(), item.packageCount(), item.cartonMarks(), orderCartonMarks); } else { validateOptionalFbaCartonMapping(item.skuCode(), item.packageCount(), item.cartonMarks(), orderCartonMarks); } } } } private void validateOptionalFbaCartonMapping(String skuCode, int loadedPackages, List cartonMarks, Set orderCartonMarks) { List marks = cartonMarks == null ? List.of() : cartonMarks; if (skuCode == null || skuCode.isBlank()) { throw new IllegalArgumentException("V2 尾程交接事件缺少 SKU"); } if (marks.isEmpty()) return; requireSkuCartonMapping(skuCode, loadedPackages, marks, orderCartonMarks); } private void requireSkuCartonMapping(String skuCode, int loadedPackages, List cartonMarks, Set orderCartonMarks) { if (skuCode == null || skuCode.isBlank()) { throw new IllegalArgumentException("V2 尾程交接事件的箱唛映射缺少 SKU"); } List marks = cartonMarks == null ? List.of() : cartonMarks; if (marks.size() != loadedPackages) { throw new IllegalArgumentException("V2 尾程交接事件的 SKU 箱唛数量必须等于实装箱数: " + skuCode); } for (String mark : marks) { if (mark == null || mark.isBlank() || !orderCartonMarks.add(mark)) { throw new IllegalArgumentException("V2 尾程交接事件包含空值或重复箱唛: " + skuCode); } } } private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload, FirstLegLoadingPlanCompletedV1.Order source,String orderNo, JsonNode rawSource, String containerNo, String sourceContainerBookingId, OwnerSnapshot owner) { String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(rawSource)); LastlegOrder existing=orderMapper.findSource(event.tenantId(),event.sourceRegion().name(),source.sourceInboundId()); if(existing!=null) { if(!Objects.equals(existing.getSourcePayloadHash(),orderHash)) throw new IllegalStateException("来源入仓单快照冲突: "+source.sourceInboundId()); return; } LastlegOrder order=new LastlegOrder(); String orderType = source.orderType(); String destinationType = destinationType(payload, source); String customerId = owner.customerId(); String customerName = owner.customerName(); List items = "ONE_CLICK_DROPSHIP".equals(orderType) ? canonicalItems(source.items()) : source.items(); List dropshipPallets = "ONE_CLICK_DROPSHIP".equals(orderType) ? pallets(source, rawSource) : List.of(); if ("ONE_CLICK_DROPSHIP".equals(orderType) && dropshipPallets.isEmpty()) { throw new IllegalArgumentException("One-click dropship event has no pallet snapshot"); } order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setCustomerId(customerId); order.setOrderType(orderType); order.setDestinationType(destinationType); order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name()); order.setLxCode("ONE_CLICK_DROPSHIP".equals(orderType) ? source.lxCode() : null); order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId()); order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId()); order.setCustomerName(customerName); order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName()); order.setOverseasWarehouseId(DEFAULT_RECEIVING_WAREHOUSE_ID); order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE); order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME); order.setReceivingWarehouseVersion(0); order.setFbaWarehouseCode("FBA".equals(destinationType) ? source.destinationWarehouseCode() : null); order.setContainerNo(containerNo); order.setAbnormal(false); order.setDetained(false); order.setPackageCount(source.packageCount()); order.setPalletCount("ONE_CLICK_DROPSHIP".equals(orderType) ? dropshipPallets.size() : Objects.requireNonNullElse(source.palletCount(), 0)); order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm()); order.setEstimatedWarehouseAt(parsePlannedArrival(payload.estimatedWarehouseArrivalAt())); order.setVolumeWeightKg(items.stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg) .filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add)); order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSourceEventId(event.eventId()); order.setSourceLoadingPlanId(payload.loadingPlanId()); order.setSourceContainerBookingId(sourceContainerBookingId); order.setSourceLoadingPlanVersion(event.aggregateVersion()); order.setSourceHandoverVersion(event.handoverVersion()); order.setSourcePayloadHash(orderHash); order.setCreatedAt(LocalDateTime.now()); order.setUpdatedAt(LocalDateTime.now()); try { orderMapper.insert(order); } catch (DuplicateKeyException exception) { LastlegOrder raced = orderMapper.findSource(event.tenantId(), event.sourceRegion().name(), source.sourceInboundId()); if (raced != null && Objects.equals(raced.getSourcePayloadHash(), orderHash)) return; throw exception; } createInboundOrderSnapshot(event, payload, source, order, orderHash); Map> dropshipLabels = "ONE_CLICK_DROPSHIP".equals(orderType) && "OVERSEAS".equals(destinationType) ? dropshipReceivingLabels(source, rawSource) : Map.of(); if ("FIRST_LEG".equals(orderType) && event.eventVersion() >= 2) { List pallets = source.pallets() == null ? List.of() : source.pallets(); Set palletNos = new HashSet<>(); for (int palletIndex = 0; palletIndex < pallets.size(); palletIndex++) { String palletNo = pallets.get(palletIndex).palletNo(); if (palletNo == null || palletNo.isBlank() || palletNo.length() > 128 || !palletNos.add(palletNo)) { throw new IllegalArgumentException("散货来源托盘码为空、重复或过长"); } sourcePallets.insert(IdWorker.getId(), customerId, order.getId(), palletNo.trim(), palletIndex + 1, event.eventId()); } if (!pallets.isEmpty() && order.getPalletCount() > 0 && pallets.size() != order.getPalletCount()) { throw new IllegalArgumentException("散货来源托盘码数量与托盘数不一致"); } } for(var item:items) { LastlegOrderDetail detail=new LastlegOrderDetail(); detail.setCustomerId(customerId); detail.setOrderId(order.getId()); detail.setSourceInboundDetailId(item.sourceInboundDetailId()); detail.setSkuCode(item.skuCode()); detail.setProductNameZh(item.productNameZh()); detail.setProductNameEn(item.productNameEn()); detail.setPackageCount(item.packageCount()); detail.setProductQuantity(item.productQuantity()); detail.setPalletCount(item.palletCount()); detail.setGrossWeightKg(item.grossWeightKg()); detail.setNetWeightKg(item.netWeightKg()); detail.setDimensionalWeightKg(item.dimensionalWeightKg()); detail.setVolumeCbm(item.volumeCbm()); 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, "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()); history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history); if ("FIRST_LEG".equals(orderType)) { // The cross-region handover freezes the source inbound code only. An inbound plan is // created later by an explicit platform conversion, which replaces this display snapshot. registerInTransit(event, payload, source, order, source.inboundCode()); } if ("ONE_CLICK_DROPSHIP".equals(orderType)) { long warehouseId; try { warehouseId = Long.parseLong(payload.warehouseId()); } catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); } dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId, event.eventId() + ":DROPSHIP:" + source.sourceInboundId(), dropshipPallets)); } } private LocalDateTime parsePlannedArrival(String value) { if (value == null || value.isBlank()) return null; try { return LocalDateTime.parse(value.trim()); } catch (java.time.format.DateTimeParseException exception) { throw new IllegalArgumentException("事件计划到仓时间 estimatedWarehouseArrivalAt 无效", exception); } } private void createInboundOrderSnapshot(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload, FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order, String orderHash) { LastlegInboundOrder snapshot = new LastlegInboundOrder(); snapshot.setId(order.getId()); snapshot.setCustomerId(order.getCustomerId()); snapshot.setSourceRegion(event.sourceRegion().name()); snapshot.setSourceInboundId(source.sourceInboundId()); snapshot.setRootInboundId(source.rootInboundId()); snapshot.setSourceOrderId(source.sourceOrderId()); snapshot.setSourceOrderNo(source.sourceOrderNo()); snapshot.setRootOrderId(source.rootOrderId()); snapshot.setInboundCode(source.inboundCode()); snapshot.setLxCode("ONE_CLICK_DROPSHIP".equals(order.getOrderType()) ? source.lxCode() : null); snapshot.setDestinationWarehouseId(source.destinationWarehouseId()); snapshot.setDestinationWarehouseCode(source.destinationWarehouseCode()); snapshot.setDestinationWarehouseName(source.destinationWarehouseName()); snapshot.setDestinationType(order.getDestinationType()); snapshot.setPackageCount(source.packageCount()); snapshot.setProductQuantity(source.productQuantity()); snapshot.setPalletCount(source.palletCount()); snapshot.setGrossWeightKg(source.grossWeightKg()); snapshot.setNetWeightKg(source.netWeightKg()); snapshot.setVolumeCbm(source.volumeCbm()); snapshot.setSourceEventId(event.eventId()); snapshot.setSourceLoadingPlanId(payload.loadingPlanId()); snapshot.setSourceLoadingPlanVersion(event.aggregateVersion()); snapshot.setHandoverVersion(event.handoverVersion()); snapshot.setPayloadHash(orderHash); snapshot.setStatus("READY"); snapshot.setExceptionFlag("NONE"); snapshot.setCreatedAt(order.getCreatedAt()); snapshot.setUpdatedAt(order.getUpdatedAt()); inboundOrderMapper.insert(snapshot); } private void registerInTransit(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload, FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order, String inboundOrderNo) { for (int index = 0; index < source.items().size(); index++) { FirstLegLoadingPlanCompletedV1.Item item = source.items().get(index); if (item.packageCount() <= 0) continue; inventory.registerInTransit(order.getCustomerId(), "SYSTEM", new InventoryCommandRequests.InTransit(order.getCustomerName(), order.getOverseasWarehouseId(), order.getOverseasWarehouseCode(), order.getOverseasWarehouseName(), item.destinationWarehouseId(), item.destinationWarehouseCode(), item.destinationWarehouseName(), order.getId(), inboundOrderNo, source.inboundCode(), item.skuCode(), item.productNameZh(), item.productNameEn(), item.packageCount(), order.getOrderNo(), event.eventId(), "TAIL_IN_TRANSIT:" + event.eventId() + ":" + order.getId() + ":" + index, "Tail-leg order created from completed loading plan")); } } private Map> dropshipReceivingLabels(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) { Map itemsByDetail = new java.util.LinkedHashMap<>(); for (var item : canonicalItems(source.items())) { itemsByDetail.put(item.sourceInboundDetailId(), item); } Map> 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 marks = marksByDetail.computeIfAbsent(detailId, ignored -> new java.util.ArrayList<>()); sku.path("cartonMarks").forEach(mark -> marks.add(mark.asText())); } } for (var item : itemsByDetail.values()) { if (marksByDetail.getOrDefault(item.sourceInboundDetailId(), List.of()).size() != item.packageCount()) { throw new IllegalArgumentException("一件代发托盘箱唛与来源明细箱数不一致"); } } return marksByDetail; } /** 接受同一来源明细因多托装柜形成的多个分片,并在尾程落库前合并为唯一明细。 */ private List canonicalItems(List source) { Map> byDetail = new java.util.LinkedHashMap<>(); for (var item : source == null ? List.of() : source) { if (item.sourceInboundDetailId() == null || item.sourceInboundDetailId().isBlank()) { throw new IllegalArgumentException("一件代发来源明细标识缺失"); } byDetail.computeIfAbsent(item.sourceInboundDetailId(), ignored -> new ArrayList<>()).add(item); } List result = new ArrayList<>(); for (List fragments : byDetail.values()) { var first = fragments.get(0); if (fragments.stream().anyMatch(item -> !Objects.equals(first.skuCode(), item.skuCode()) || !Objects.equals(first.destinationWarehouseId(), item.destinationWarehouseId()) || !Objects.equals(first.destinationWarehouseCode(), item.destinationWarehouseCode()))) { throw new IllegalArgumentException("同一来源明细的尾程事件分片快照不一致"); } List marks = fragments.stream().flatMap(item -> item.cartonMarks().stream()).toList(); result.add(new FirstLegLoadingPlanCompletedV1.Item(first.sourceInboundDetailId(), first.skuCode(), first.productNameZh(), first.productNameEn(), fragments.stream().mapToInt(FirstLegLoadingPlanCompletedV1.Item::packageCount).sum(), sumNullableInt(fragments, FirstLegLoadingPlanCompletedV1.Item::productQuantity), sumNullableInt(fragments, FirstLegLoadingPlanCompletedV1.Item::palletCount), sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::grossWeightKg), sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::netWeightKg), sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg), sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::volumeCbm), first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(), first.fbaTrackingNo(), first.shipmentNo(), first.shippingMark(), first.palletNo(), marks)); } return List.copyOf(result); } private static Integer sumNullableInt(List values, java.util.function.Function getter) { List present = values.stream().map(getter).filter(Objects::nonNull).toList(); return present.isEmpty() ? null : present.stream().mapToInt(Integer::intValue).sum(); } private static BigDecimal sumDecimal(List values, java.util.function.Function getter) { List present = values.stream().map(getter).filter(Objects::nonNull).toList(); return present.isEmpty() ? null : present.stream().reduce(BigDecimal.ZERO, BigDecimal::add); } private void createCartonLabels(String customerId, Long orderId, String destinationType, FirstLegLoadingPlanCompletedV1.Item item, List sourceMarks) { if (!"OVERSEAS".equals(destinationType)) return; List 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("装柜事件的箱唛池不可包含空值或重复箱唛"); } for (String mark : marks) { InboundPlanCarton carton = new InboundPlanCarton(); carton.setId(IdWorker.getId()); carton.setCustomerId(customerId); carton.setOrderId(orderId); carton.setSourceInboundDetailId(item.sourceInboundDetailId()); carton.setSkuCode(item.skuCode()); carton.setCartonMark(mark); carton.setDestinationWarehouseId(item.destinationWarehouseId()); carton.setDestinationWarehouseCode(item.destinationWarehouseCode()); carton.setDestinationWarehouseName(item.destinationWarehouseName()); carton.setStatus("UNASSIGNED"); carton.setVersion(0); carton.setUpdatedAt(LocalDateTime.now()); try { cartonMapper.insert(carton); } catch (DuplicateKeyException ignored) { } } } private String destinationType(FirstLegLoadingPlanCompletedV1 payload, FirstLegLoadingPlanCompletedV1.Order order) { String explicit = normalizeDestinationType(order.destinationType()); if (explicit != null) return explicit; boolean fbaEvidence = order.items().stream().anyMatch(item -> hasText(item.fbaTrackingNo()) || hasText(item.shipmentNo()) || startsWithFba(item.destinationWarehouseName())); boolean overseasWarehouseIdentity = hasText(payload.warehouseId()) && Objects.equals(payload.warehouseId(), order.destinationWarehouseId()); if (fbaEvidence && overseasWarehouseIdentity) { throw new IllegalArgumentException("历史尾程事件目的类型证据冲突: " + order.sourceInboundId()); } if (fbaEvidence) return "FBA"; if (overseasWarehouseIdentity) return "OVERSEAS"; throw new IllegalArgumentException("历史尾程事件缺少 destinationType 且无法安全推断: " + order.sourceInboundId()); } private String normalizeDestinationType(String value) { if (!hasText(value)) return null; String normalized = value.trim().toUpperCase(java.util.Locale.ROOT); if (!"FBA".equals(normalized) && !"OVERSEAS".equals(normalized)) { throw new IllegalArgumentException("尾程事件 destinationType 必须为 FBA 或 OVERSEAS"); } return normalized; } private boolean startsWithFba(String value) { return hasText(value) && value.trim().toUpperCase(java.util.Locale.ROOT).startsWith("FBA"); } private boolean hasText(String value) { return value != null && !value.isBlank(); } private List pallets(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) { if (source.pallets() != null && !source.pallets().isEmpty()) { List result = new ArrayList<>(); JsonNode rawPallets = rawSource.path("pallets"); for (int palletIndex = 0; palletIndex < source.pallets().size(); palletIndex++) { FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet = source.pallets().get(palletIndex); JsonNode rawPallet = rawPallets.isArray() && palletIndex < rawPallets.size() ? rawPallets.get(palletIndex) : objectMapper.createObjectNode(); Map inventorySkus = new java.util.LinkedHashMap<>(); for (int skuIndex = 0; skuIndex < pallet.skuPackages().size(); skuIndex++) { var sku = pallet.skuPackages().get(skuIndex); inventorySkus.merge(sku.skuCode(), new DropshipRequests.SkuPackages(sku.skuCode(), sku.productName(), sku.packageCount(), sku.productQuantity(), sku.cartonMarks(), cartonQuantities(rawPallet.path("skuPackages").path(skuIndex))), (left, right) -> { List marks = new ArrayList<>(left.cartonMarks()); marks.addAll(right.cartonMarks()); Map quantities = new java.util.LinkedHashMap<>(left.cartonQuantities()); right.cartonQuantities().forEach((mark, quantity) -> { if (quantities.putIfAbsent(mark, quantity) != null) throw new IllegalArgumentException("Duplicate carton quantity: " + mark); }); return new DropshipRequests.SkuPackages(left.skuCode(), left.productName(), Math.addExact(left.packageCount(), right.packageCount()), Math.addExact(left.productQuantity(), right.productQuantity()), List.copyOf(marks), Map.copyOf(quantities)); }); } result.add(new DropshipRequests.Pallet(pallet.palletNo(), List.copyOf(inventorySkus.values()), positiveManifest(rawPallet, "manifestVersion"), positiveManifest(rawPallet, "manifestPackageCount"), positiveManifest(rawPallet, "manifestFragmentCount"), requiredManifestHash(rawPallet))); } return List.copyOf(result); } return legacyPallets(rawSource); } private List legacyPallets(JsonNode source) { List result = new ArrayList<>(); JsonNode nodes = source.path("pallets"); if (nodes.isArray()) { for (JsonNode pallet : nodes) { String palletNo = pallet.path("palletNo").asText(pallet.path("sourcePalletNo").asText(null)); List skuRows = skuPackages(pallet); if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) { result.add(new DropshipRequests.Pallet(palletNo, skuRows, positiveManifest(pallet, "manifestVersion"), positiveManifest(pallet, "manifestPackageCount"), positiveManifest(pallet, "manifestFragmentCount"), requiredManifestHash(pallet))); } } return result; } String palletNo = source.path("palletNo").asText(source.path("sourcePalletNo").asText(null)); List skuRows = skuPackages(source); if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) { result.add(new DropshipRequests.Pallet(palletNo, skuRows, positiveManifest(source, "manifestVersion"), positiveManifest(source, "manifestPackageCount"), positiveManifest(source, "manifestFragmentCount"), requiredManifestHash(source))); } return result; } private int positiveManifest(JsonNode source, String field) { int value = source.path(field).asInt(0); if (value <= 0) throw new IllegalArgumentException("One-click dropship pallet is missing " + field); return value; } private String requiredManifestHash(JsonNode source) { String value = source.path("manifestHash").asText(null); if (value == null || !value.matches("(?i)[0-9a-f]{64}")) { throw new IllegalArgumentException("One-click dropship pallet manifestHash is invalid"); } return value.toLowerCase(java.util.Locale.ROOT); } private Map cartonQuantities(JsonNode sku) { JsonNode node = sku.path("cartonQuantities"); if (!node.isObject()) throw new IllegalArgumentException("One-click dropship cartonQuantities is required"); Map result = new java.util.LinkedHashMap<>(); node.fields().forEachRemaining(entry -> { if (!entry.getValue().isIntegralNumber() || !entry.getValue().canConvertToLong() || entry.getValue().longValue() <= 0) { throw new IllegalArgumentException("Invalid carton quantity: " + entry.getKey()); } result.put(entry.getKey(), entry.getValue().longValue()); }); return Map.copyOf(result); } private List skuPackages(JsonNode source) { List result = new ArrayList<>(); JsonNode nodes = source.path("skuPackages"); if (!nodes.isArray()) nodes = source.path("items"); if (!nodes.isArray()) return result; for (JsonNode item : nodes) { String sku = item.path("skuCode").asText(null); long packageCount = item.path("packageCount").asLong(0); long productQuantity = item.path("productQuantity").asLong(0); JsonNode markNodes = item.path("cartonMarks"); if (!markNodes.isArray()) markNodes = item.path("boxMarks"); if (!markNodes.isArray()) markNodes = item.path("cartonNos"); List marks = new ArrayList<>(); if (markNodes.isArray()) { for (JsonNode mark : markNodes) if (!mark.asText().isBlank()) marks.add(mark.asText()); } if (sku != null && !sku.isBlank() && packageCount > 0 && productQuantity > 0 && !marks.isEmpty()) { String productName = item.path("productName").asText(item.path("productNameZh").asText(null)); result.add(new DropshipRequests.SkuPackages(sku, productName, packageCount, productQuantity, marks, cartonQuantities(item))); } } return result; } private record OwnerSnapshot(String customerId, String customerName) { } }