fix(events): allow container nodes before ocean booking

This commit is contained in:
2026-09-10 14:10:02 +08:00
parent c026edbb2f
commit 3e4d6b76aa
6 changed files with 17 additions and 9 deletions
@@ -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("船节点交接业务标识不能为空");
}
@@ -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}
@@ -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
@@ -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;货柜未关联订舱时为空';
@@ -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<InboundOutboxEntity> 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");
}
@@ -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 '节点实际发生时间',