From 3e4d6b76aabc4849ed1b6c94a3b82a44746c6959 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 14:10:02 +0800 Subject: [PATCH] fix(events): allow container nodes before ocean booking --- .../event/FirstLegContainerShippingNodeRecordedV1.java | 6 ++++-- .../main/resources/openapi/cross-region-events-v1.yaml | 4 ++-- .../FirstLegContainerShippingNodeRecordedV1Test.java | 3 ++- deploy/sql/海外区域统一增量-MySQL8.sql | 8 ++++++-- .../service/CarrierScheduleNodeOutboxServiceTest.java | 3 ++- lastleg-service/src/main/resources/db/lastleg.sql | 2 +- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1.java b/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1.java index 69ae72cc2..d7389536f 100644 --- a/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1.java +++ b/common-event-contract/src/main/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1.java @@ -31,8 +31,10 @@ public record FirstLegContainerShippingNodeRecordedV1( if (queryId != null && queryId.isBlank()) { throw new IllegalArgumentException("船节点跟踪任务标识不能为空字符串"); } - if (oceanBookingId == null || oceanBookingId.isBlank() - || containerBookingId == null || containerBookingId.isBlank() + if (oceanBookingId != null && oceanBookingId.isBlank()) { + throw new IllegalArgumentException("船节点订舱标识不能为空字符串"); + } + if (containerBookingId == null || containerBookingId.isBlank() || containerNo == null || containerNo.isBlank()) { throw new IllegalArgumentException("船节点交接业务标识不能为空"); } diff --git a/common-event-contract/src/main/resources/openapi/cross-region-events-v1.yaml b/common-event-contract/src/main/resources/openapi/cross-region-events-v1.yaml index 9b9f656e5..acf11ff61 100644 --- a/common-event-contract/src/main/resources/openapi/cross-region-events-v1.yaml +++ b/common-event-contract/src/main/resources/openapi/cross-region-events-v1.yaml @@ -42,10 +42,10 @@ components: destinationType: {type: string, enum: [FBA, OVERSEAS]} FirstLegContainerShippingNodeRecordedV1: type: object - required: [oceanBookingId, sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash] + required: [sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash] properties: queryId: {type: [string, 'null'], description: Automatic tracking task ID; null for a direct platform manual node.} - oceanBookingId: {type: string} + oceanBookingId: {type: [string, 'null'], description: Ocean booking ID when the physical container has already been linked; null is valid for a direct manual node.} sourceContainerBookingId: {type: string, minLength: 1} containerNo: {type: string, minLength: 1, description: Uppercase normalized physical container number; never cabinetNo or a technical identity} standardNode: {type: string} diff --git a/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1Test.java b/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1Test.java index 6614cb3d8..c69cea8be 100644 --- a/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1Test.java +++ b/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegContainerShippingNodeRecordedV1Test.java @@ -39,12 +39,13 @@ class FirstLegContainerShippingNodeRecordedV1Test { void acceptsDirectManualNodeWithoutTrackingQuery() { var payload = payload(); var manual = new FirstLegContainerShippingNodeRecordedV1( - null, payload.oceanBookingId(), payload.containerBookingId(), payload.containerNo(), + null, null, payload.containerBookingId(), payload.containerNo(), payload.standardNode(), payload.nodeSequence(), payload.observedAt(), payload.actualAt(), payload.locationCode(), payload.locationName(), "MANUAL", "manual-event", payload.providerNodeCode(), "MANUAL:7", payload.rawPayloadHash()); assertThat(manual.queryId()).isNull(); + assertThat(manual.oceanBookingId()).isNull(); } @Test diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 573d1ab37..826c7efb6 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS ll_loading_event_inbox ( CREATE TABLE IF NOT EXISTS ll_container_shipping_gate ( container_booking_id VARCHAR(128) NOT NULL COMMENT '来源头程柜预订ID', - ocean_booking_id VARCHAR(128) NOT NULL COMMENT '来源海运订舱ID', container_no VARCHAR(64) NOT NULL COMMENT '实际集装箱号', + ocean_booking_id VARCHAR(128) NULL COMMENT '来源海运订舱ID;货柜未关联订舱时为空', container_no VARCHAR(64) NOT NULL COMMENT '实际集装箱号', status VARCHAR(32) NOT NULL DEFAULT 'AWAITING_PICKUP_READY' COMMENT '海外收货门禁状态:AWAITING_PICKUP_READY、RECEIVING_AVAILABLE', standard_node VARCHAR(64) NULL COMMENT '最新有效标准船节点', node_sequence INT NOT NULL DEFAULT 0 COMMENT '最新有效节点单调序号', observed_at DATETIME(3) NULL COMMENT '供应商节点观测时间', actual_at DATETIME(3) NULL COMMENT '节点实际发生时间', @@ -3307,7 +3307,7 @@ PREPARE phase4_inventory_stmt FROM @ddl; EXECUTE phase4_inventory_stmt; DEALLOCA -- 海运节点达到可提柜后才允许海外 PDA 认领;装柜完成只建立等待门禁快照。 CREATE TABLE IF NOT EXISTS ll_container_shipping_gate ( container_booking_id VARCHAR(128) NOT NULL COMMENT '来源头程柜预订ID', - ocean_booking_id VARCHAR(128) NOT NULL COMMENT '来源海运订舱ID', + ocean_booking_id VARCHAR(128) NULL COMMENT '来源海运订舱ID;货柜未关联订舱时为空', container_no VARCHAR(64) NOT NULL COMMENT '实际集装箱号', status VARCHAR(32) NOT NULL DEFAULT 'AWAITING_PICKUP_READY' COMMENT '海外收货门禁状态:AWAITING_PICKUP_READY、RECEIVING_AVAILABLE', standard_node VARCHAR(64) NULL COMMENT '最新有效标准船节点', @@ -4604,3 +4604,7 @@ CALL assert_overseas_finance_p0_ready(); DROP PROCEDURE IF EXISTS assert_overseas_finance_p0_ready; SELECT 'PASS' AS migration_status, '海外财务P0费用来源字段已就绪' AS message; + +-- 人工货柜节点独立于订舱:货柜尚未关联海运订舱时仍可同步当前节点。 +ALTER TABLE ll_container_shipping_gate + MODIFY ocean_booking_id VARCHAR(128) NULL COMMENT '来源海运订舱ID;货柜未关联订舱时为空'; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxServiceTest.java index f2bcfdc58..b648aa0d5 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxServiceTest.java @@ -99,13 +99,14 @@ class CarrierScheduleNodeOutboxServiceTest { fact.setProviderCode("MANUAL"); fact.setProviderEventId("manual-event"); fact.setSourceReference("MANUAL:7"); - service.publishContainerNode(2088000000000000004L, 2088000000000000003L, + service.publishContainerNode(null, 2088000000000000003L, fact, "ZCSU7881970"); ArgumentCaptor captor = ArgumentCaptor.forClass(InboundOutboxEntity.class); verify(mapper).insert(captor.capture()); EventEnvelope envelope = objectMapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class); assertThat(envelope.payload().path("queryId").isNull()).isTrue(); + assertThat(envelope.payload().path("oceanBookingId").isNull()).isTrue(); assertThat(envelope.payload().path("sourceContainerBookingId").asText()) .isEqualTo("2088000000000000003"); } diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 4d39e0b4f..542940f90 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS ll_loading_event_inbox ( CREATE TABLE IF NOT EXISTS ll_container_shipping_gate ( container_booking_id VARCHAR(128) NOT NULL COMMENT '来源头程柜预订ID', - ocean_booking_id VARCHAR(128) NOT NULL COMMENT '来源海运订舱ID', container_no VARCHAR(64) NOT NULL COMMENT '实际集装箱号', + ocean_booking_id VARCHAR(128) NULL COMMENT '来源海运订舱ID;货柜未关联订舱时为空', container_no VARCHAR(64) NOT NULL COMMENT '实际集装箱号', status VARCHAR(32) NOT NULL DEFAULT 'AWAITING_PICKUP_READY' COMMENT '海外收货门禁状态:AWAITING_PICKUP_READY、RECEIVING_AVAILABLE', standard_node VARCHAR(64) NULL COMMENT '最新有效标准船节点', node_sequence INT NOT NULL DEFAULT 0 COMMENT '最新有效节点单调序号', observed_at DATETIME(3) NULL COMMENT '供应商节点观测时间', actual_at DATETIME(3) NULL COMMENT '节点实际发生时间',