Merge branch 'dev' into dev-lyhfive
# Conflicts: # firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java # firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java # lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java # lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaResponses.java # lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java # lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java # lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java # lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
This commit is contained in:
+78
-7
@@ -11,12 +11,14 @@ import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
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.time.LocalDateTime;
|
||||
@@ -40,8 +42,9 @@ public class LoadingCompletedConsumer {
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private ContainerShippingGateMapper shippingGates;
|
||||
|
||||
@Transactional
|
||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||
public int consume(EventEnvelope envelope) {
|
||||
envelope.requireValid();
|
||||
if (!EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(envelope.eventType())
|
||||
@@ -51,6 +54,8 @@ public class LoadingCompletedConsumer {
|
||||
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);
|
||||
if(payload.containerNo()==null||payload.containerNo().isBlank())
|
||||
throw new IllegalArgumentException("事件没有实际集装箱号 containerNo");
|
||||
@@ -67,9 +72,20 @@ public class LoadingCompletedConsumer {
|
||||
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,payload.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.
|
||||
shippingGates.initialize(payload.containerBookingId(), envelope.tenantId(), "",
|
||||
payload.containerNo(), 0, envelope.handoverVersion());
|
||||
|
||||
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
|
||||
JsonNode rawOrders = envelope.payload().path("orders");
|
||||
for (int index = 0; index < payload.orders().size(); index++) {
|
||||
@@ -107,12 +123,27 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String destinationType = destinationType(payload, order);
|
||||
for (FirstLegLoadingPlanCompletedV1.Item item : order.items()) {
|
||||
requireSkuCartonMapping(item.skuCode(), item.packageCount(), item.cartonMarks(), orderCartonMarks);
|
||||
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<String> cartonMarks,
|
||||
Set<String> orderCartonMarks) {
|
||||
List<String> 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<String> cartonMarks,
|
||||
Set<String> orderCartonMarks) {
|
||||
if (skuCode == null || skuCode.isBlank()) {
|
||||
@@ -139,6 +170,7 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
LastlegOrder order=new LastlegOrder();
|
||||
String orderType = source.orderType();
|
||||
String destinationType = destinationType(payload, source);
|
||||
String customerId = source.customerId() == null || source.customerId().isBlank()
|
||||
? event.tenantId() : source.customerId();
|
||||
if (!event.tenantId().equals(customerId)) {
|
||||
@@ -150,10 +182,12 @@ public class LoadingCompletedConsumer {
|
||||
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.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId());
|
||||
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
|
||||
order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName());
|
||||
order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName());
|
||||
order.setOverseasWarehouseId(null); order.setOverseasWarehouseName(null);
|
||||
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
|
||||
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
|
||||
order.setPackageCount(source.packageCount());
|
||||
@@ -188,7 +222,7 @@ 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(), item);
|
||||
createCartonLabels(customerId, order.getId(), destinationType, item);
|
||||
}
|
||||
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory(); history.setCustomerId(customerId);
|
||||
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
|
||||
@@ -219,7 +253,7 @@ public class LoadingCompletedConsumer {
|
||||
FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order,
|
||||
String orderHash) {
|
||||
LastlegInboundOrder snapshot = new LastlegInboundOrder();
|
||||
snapshot.setId(order.getId()); snapshot.setTenantId("BAIYUE");
|
||||
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());
|
||||
@@ -227,6 +261,7 @@ public class LoadingCompletedConsumer {
|
||||
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());
|
||||
@@ -254,9 +289,11 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private void createCartonLabels(String customerId, Long orderId, FirstLegLoadingPlanCompletedV1.Item item) {
|
||||
private void createCartonLabels(String customerId, Long orderId, String destinationType,
|
||||
FirstLegLoadingPlanCompletedV1.Item item) {
|
||||
if (!"OVERSEAS".equals(destinationType)) return;
|
||||
List<String> marks = item.cartonMarks() == null ? List.of() : item.cartonMarks();
|
||||
if (marks.isEmpty()) return; // 历史 V1 事件保持原有按箱数入库路径。
|
||||
if (marks.isEmpty()) return;
|
||||
if (marks.stream().anyMatch(mark -> mark == null || mark.isBlank()) || marks.stream().distinct().count() != marks.size()) {
|
||||
throw new IllegalArgumentException("装柜事件的箱唛池不可包含空值或重复箱唛");
|
||||
}
|
||||
@@ -272,6 +309,40 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
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<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(),
|
||||
|
||||
Reference in New Issue
Block a user