From a3890e7a5584d554e9b77505b62cb2d4c3c074e3 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 13:56:07 +0800 Subject: [PATCH] fix(events): allow manual nodes without tracking query --- ...FirstLegContainerShippingNodeRecordedV1.java | 5 ++++- .../openapi/cross-region-events-v1.yaml | 5 ++--- ...tLegContainerShippingNodeRecordedV1Test.java | 12 ++++++++++++ .../CarrierScheduleNodeOutboxServiceTest.java | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 4 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 9a3734ce8..69ae72cc2 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 @@ -28,7 +28,10 @@ public record FirstLegContainerShippingNodeRecordedV1( "EMPTY_RETURNED", 100); public FirstLegContainerShippingNodeRecordedV1 { - if (queryId == null || queryId.isBlank() || oceanBookingId == null || oceanBookingId.isBlank() + if (queryId != null && queryId.isBlank()) { + throw new IllegalArgumentException("船节点跟踪任务标识不能为空字符串"); + } + if (oceanBookingId == null || oceanBookingId.isBlank() || 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 ba458d494..9b9f656e5 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,9 @@ components: destinationType: {type: string, enum: [FBA, OVERSEAS]} FirstLegContainerShippingNodeRecordedV1: type: object - required: [queryId, customerId, oceanBookingId, sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash] + required: [oceanBookingId, sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash] properties: - queryId: {type: string} - customerId: {type: string} + queryId: {type: [string, 'null'], description: Automatic tracking task ID; null for a direct platform manual node.} oceanBookingId: {type: string} sourceContainerBookingId: {type: string, minLength: 1} containerNo: {type: string, minLength: 1, description: Uppercase normalized physical container number; never cabinetNo or a technical identity} 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 ceed06186..6614cb3d8 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 @@ -35,6 +35,18 @@ class FirstLegContainerShippingNodeRecordedV1Test { .hasMessageContaining("normalized"); } + @Test + void acceptsDirectManualNodeWithoutTrackingQuery() { + var payload = payload(); + var manual = new FirstLegContainerShippingNodeRecordedV1( + null, payload.oceanBookingId(), 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(); + } + @Test void rejectsEstimatedOnlyOrIncompleteProviderFacts() { assertThatThrownBy(() -> new FirstLegContainerShippingNodeRecordedV1( 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 0a0c66761..f2bcfdc58 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 @@ -93,6 +93,23 @@ class CarrierScheduleNodeOutboxServiceTest { verify(mapper, never()).insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)); } + @Test + void directManualActualFactDoesNotRequireTrackingQuery() throws Exception { + CarrierScheduleNodeFactEntity fact = fact(); + fact.setProviderCode("MANUAL"); fact.setProviderEventId("manual-event"); + fact.setSourceReference("MANUAL:7"); + + service.publishContainerNode(2088000000000000004L, 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("sourceContainerBookingId").asText()) + .isEqualTo("2088000000000000003"); + } + @Test void initialOutboundDepartureUsesVersionZeroWithoutEnvelopeFailure() throws Exception { CarrierScheduleQueryEntity query = query();