This commit is contained in:
2026-08-18 15:43:58 +08:00
parent 4e4d047862
commit 4b0b3e9172
71 changed files with 523 additions and 724 deletions
@@ -45,14 +45,10 @@ CALL phase2_measure_add_column('fl_inbound_measurement_item', 'box_specification
'VARCHAR(64) NOT NULL DEFAULT '''' COMMENT ''箱规标识'' AFTER item_no');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'girth_cm',
'DECIMAL(12,3) NOT NULL DEFAULT 0 COMMENT ''周长cm,规则为长+2*(宽+高)'' AFTER height_cm');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'unit_weight_kg',
'DECIMAL(18,3) NULL COMMENT ''单箱重量kg'' AFTER girth_cm');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'unit_volume_cbm',
'DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''单箱业务体积m3'' AFTER unit_weight_kg');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'total_weight_kg',
'DECIMAL(18,3) NULL COMMENT ''本箱规总重量kg'' AFTER volume_cbm');
'DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''单箱业务体积m3'' AFTER volume_cbm');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'total_volume_cbm',
'DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''本箱规总体积m3'' AFTER total_weight_kg');
'DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''本箱规总体积m3'' AFTER volume_cbm');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'volume_weight_kg',
'DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''本箱规材积重kg'' AFTER total_volume_cbm');
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'over_limit_warning',
@@ -242,11 +242,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
width_cm DECIMAL(12,3) NOT NULL COMMENT '原始宽度cm',
height_cm DECIMAL(12,3) NOT NULL COMMENT '原始高度cm',
girth_cm DECIMAL(12,3) NOT NULL COMMENT '周长cm,规则为长+2*(宽+高)',
unit_weight_kg DECIMAL(18,3) NULL COMMENT '单箱重量kg,第三方仅回传尺寸时可为空',
unit_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '单箱业务体积m3',
volume_raw_cbm DECIMAL(18,6) NOT NULL COMMENT '原始体积m3',
volume_cbm DECIMAL(18,4) NOT NULL COMMENT '业务体积m3',
total_weight_kg DECIMAL(18,3) NULL COMMENT '本箱规总重量kg',
total_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '本箱规总体积m3',
volume_weight_kg DECIMAL(18,3) NOT NULL COMMENT '本箱规材积重kg',
calculation_rule_version VARCHAR(64) NOT NULL COMMENT '体积计算和舍入规则版本',
@@ -276,7 +274,6 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
length_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测长度cm',
width_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测宽度cm',
height_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测高度cm',
weight_kg DECIMAL(18,3) NULL COMMENT '设备实测重量kg',
machine_no VARCHAR(64) NOT NULL COMMENT '设备编号',
measured_at DATETIME(3) NOT NULL COMMENT '设备测量时间或服务接收时间',
image_file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务中的墨家联调图片ID',
@@ -4,13 +4,13 @@ USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_ocean_booking (
id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID',
booking_no VARCHAR(40) NOT NULL COMMENT '平台海运订舱编号',
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT仅限创建事务内部、CONFIRMED已确认、RELEASED已放舱、CHANGE_PENDING船期待变更、CANCELLED已取消',
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、RELEASED已发布且永久只读',
customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照',
customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照',
channel_code VARCHAR(64) NOT NULL COMMENT '订舱渠道编码快照',
channel_name VARCHAR(128) NOT NULL COMMENT '订舱渠道名称快照',
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内中转仓ID快照',
warehouse_name VARCHAR(128) NOT NULL COMMENT '国内中转仓名称快照',
channel_code VARCHAR(64) NULL COMMENT '订舱渠道编码快照',
channel_name VARCHAR(128) NULL COMMENT '订舱渠道名称快照',
warehouse_id BIGINT UNSIGNED NULL COMMENT '国内中转仓ID快照',
warehouse_name VARCHAR(128) NULL COMMENT '国内中转仓名称快照',
bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号',
shipping_company_id BIGINT UNSIGNED NULL COMMENT '船公司逻辑ID快照',
shipping_company_name VARCHAR(128) NULL COMMENT '船公司名称快照',
@@ -23,8 +23,8 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照',
destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照',
place_of_delivery VARCHAR(128) NULL COMMENT '最终交货地快照',
service_term VARCHAR(32) NOT NULL COMMENT '运输服务条款快照',
freight_term VARCHAR(32) NOT NULL COMMENT '海运费支付条款快照',
service_term VARCHAR(32) NULL COMMENT '运输服务条款快照;发布前必填',
freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照;发布前必填',
estimated_sailing_at DATETIME(3) NULL COMMENT '当前确认预计开船时间',
estimated_arrival_at DATETIME(3) NULL COMMENT '当前确认预计到港时间',
pending_vessel_name VARCHAR(128) NULL COMMENT '待确认变更船名',
@@ -47,7 +47,7 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
KEY idx_ocean_booking_voyage (vessel_name,voyage_no,updated_at),
CONSTRAINT chk_ocean_booking_customer CHECK (customer_id > 0),
CONSTRAINT chk_ocean_booking_status CHECK (status IN
('DRAFT','CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED')),
('DRAFT','RELEASED')),
CONSTRAINT chk_ocean_booking_schedule CHECK
(estimated_sailing_at IS NULL OR estimated_arrival_at IS NULL OR estimated_arrival_at >= estimated_sailing_at),
CONSTRAINT chk_ocean_booking_pending_schedule CHECK
@@ -72,67 +72,6 @@ DROP TRIGGER IF EXISTS trg_ocean_requirement_before_insert;
DROP TRIGGER IF EXISTS trg_ocean_requirement_before_update;
DROP TRIGGER IF EXISTS trg_ocean_requirement_before_delete;
DROP TRIGGER IF EXISTS trg_ocean_history_before_delete;
DELIMITER $$
DROP PROCEDURE IF EXISTS reconcile_ocean_booking_legacy$$
CREATE PROCEDURE reconcile_ocean_booking_legacy()
BEGIN
DECLARE v_blank_terms INT DEFAULT 0;
DECLARE v_legacy INT DEFAULT 0;
DECLARE v_external INT DEFAULT 0;
DECLARE v_sql TEXT;
SELECT COUNT(*) INTO v_blank_terms
FROM fl_ocean_booking
WHERE NULLIF(TRIM(service_term),'') IS NULL
OR NULLIF(TRIM(freight_term),'') IS NULL;
IF v_blank_terms > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '订舱结构收敛暂停:存在 service_term/freight_term 为空的真实行,请按原始业务资料补齐后重跑,禁止猜值';
END IF;
SELECT COUNT(*) INTO v_legacy
FROM fl_ocean_booking
WHERE status IN ('DRAFT','SUBMITTED','FAILED');
IF v_legacy > 0 THEN
-- Tables from later phase blocks may not exist on a module-only run. Use
-- dynamic SQL so a clean initialization remains repeatable.
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema=DATABASE() AND table_name='fl_port_schedule') THEN
SET @ocean_legacy_external := 0;
SET v_sql = 'SELECT COUNT(*) INTO @ocean_legacy_external FROM fl_port_schedule p JOIN fl_ocean_booking b ON b.id=p.ocean_booking_id WHERE b.status IN (''DRAFT'',''SUBMITTED'',''FAILED'')';
PREPARE stmt FROM v_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET v_external = v_external + COALESCE(@ocean_legacy_external,0);
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema=DATABASE() AND table_name='fl_carrier_schedule_query') THEN
SET @ocean_legacy_external := 0;
SET v_sql = 'SELECT COUNT(*) INTO @ocean_legacy_external FROM fl_carrier_schedule_query q JOIN fl_ocean_booking b ON b.id=q.ocean_booking_id WHERE b.status IN (''DRAFT'',''SUBMITTED'',''FAILED'')';
PREPARE stmt FROM v_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET v_external = v_external + COALESCE(@ocean_legacy_external,0);
END IF;
IF v_external > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '订舱结构收敛暂停:旧订舱仍被港务船期/船司查询引用,请先按子表历史->主表归档顺序清理引用后重跑;不会伪造 CONFIRMED';
END IF;
DELETE FROM fl_ocean_booking_party
WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED'));
DELETE FROM fl_ocean_booking_container_allocation
WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED'));
DELETE FROM fl_ocean_booking_container_requirement
WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED'));
DELETE FROM fl_ocean_booking_operation
WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED'));
DELETE FROM fl_ocean_booking_status_history
WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED'));
DELETE FROM fl_ocean_booking
WHERE status IN ('DRAFT','SUBMITTED','FAILED');
END IF;
END$$
CALL reconcile_ocean_booking_legacy()$$
DROP PROCEDURE reconcile_ocean_booking_legacy$$
DELIMITER ;
-- Existing CHECK constraints are not reconciled by CREATE TABLE IF NOT EXISTS.
SET @ocean_drop_status_check = (
SELECT IF(COUNT(*) > 0,
@@ -146,10 +85,10 @@ PREPARE ocean_drop_status_stmt FROM @ocean_drop_status_check;
EXECUTE ocean_drop_status_stmt;
DEALLOCATE PREPARE ocean_drop_status_stmt;
ALTER TABLE fl_ocean_booking
MODIFY COLUMN service_term VARCHAR(32) NOT NULL COMMENT '运输服务条款快照',
MODIFY COLUMN freight_term VARCHAR(32) NOT NULL COMMENT '海运费支付条款快照',
MODIFY COLUMN service_term VARCHAR(32) NULL COMMENT '运输服务条款快照;发布前必填',
MODIFY COLUMN freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照;发布前必填',
ADD CONSTRAINT chk_ocean_booking_status CHECK
(status IN ('DRAFT','CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED'));
(status IN ('DRAFT','RELEASED'));
SET @ocean_drop_failure_reason = (
SELECT IF(COUNT(*) > 0,
@@ -248,13 +187,11 @@ DROP TRIGGER IF EXISTS trg_ocean_booking_before_update;
DELIMITER $$
CREATE TRIGGER trg_ocean_booking_before_update BEFORE UPDATE ON fl_ocean_booking FOR EACH ROW
BEGIN
IF OLD.status IN ('RELEASED','CANCELLED') THEN
IF OLD.status = 'RELEASED' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已放舱或已取消订舱不可修改';
END IF;
IF OLD.status <> NEW.status AND NOT (
(OLD.status='DRAFT' AND NEW.status='CONFIRMED')
OR (OLD.status='CONFIRMED' AND NEW.status IN ('RELEASED','CHANGE_PENDING','CANCELLED'))
OR (OLD.status='CHANGE_PENDING' AND NEW.status IN ('CONFIRMED','CANCELLED'))) THEN
(OLD.status='DRAFT' AND NEW.status='RELEASED')) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海运订舱状态迁移非法';
END IF;
END$$
@@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_truck_appointment (
customer_name VARCHAR(128) NULL COMMENT '客户名称快照',
carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID',
carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照',
bill_of_lading_no VARCHAR(128) NULL COMMENT '派车提单号快照;关联RELEASED订舱时由订舱事实源冻结',
fleet_contact VARCHAR(64) NULL COMMENT '车队联系人',
fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话',
driver_name VARCHAR(64) NULL COMMENT '司机姓名',
@@ -52,9 +53,9 @@ CREATE TABLE IF NOT EXISTS fl_truck_appointment_history (
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', request_id VARCHAR(128) NULL COMMENT '请求链路中的请求ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision)
) ENGINE=InnoDB COMMENT='拖车状态与改约历史';
CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment (
id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型编码;由业务动作约定',
id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型:BILL_OF_LADING提单、DISPATCH_ORDER派车单、DRIVER_DOCUMENT司机证件、VEHICLE_DOCUMENT车辆证件、OTHER其他',
file_name VARCHAR(255) NULL COMMENT '上传时文件名快照', file_hash CHAR(64) NULL COMMENT '文件内容SHA-256摘要', visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL' COMMENT '可见范围:INTERNAL仅平台内部、CLIENT客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID',
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id)
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id), CONSTRAINT chk_truck_attachment_type CHECK(attachment_type IN ('BILL_OF_LADING','DISPATCH_ORDER','DRIVER_DOCUMENT','VEHICLE_DOCUMENT','OTHER'))
) ENGINE=InnoDB COMMENT='拖车附件文件引用';
CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation (
id BIGINT UNSIGNED NOT NULL COMMENT '拖车写操作幂等记录ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求',
@@ -462,7 +462,6 @@ paths:
length: {type: number, format: decimal, minimum: 0.001}
width: {type: number, format: decimal, minimum: 0.001}
height: {type: number, format: decimal, minimum: 0.001}
weight: {type: number, format: decimal, minimum: 0.001, nullable: true}
machineNo: {type: string, maxLength: 64}
measuredAt: {type: string, format: date-time, nullable: true}
picture: {type: string, description: 'data:image/jpeg;base64,...'}
@@ -621,7 +620,6 @@ paths:
length: {type: number, format: double, minimum: 0.001}
width: {type: number, format: double, minimum: 0.001}
height: {type: number, format: double, minimum: 0.001}
weight: {type: number, format: double, minimum: 0.001, nullable: true}
machineNo: {type: string, maxLength: 64}
measuredAt: {type: string, format: date-time}
image: {type: string, format: binary}
@@ -653,7 +651,6 @@ paths:
length: {type: number, format: double, minimum: 0.001}
width: {type: number, format: double, minimum: 0.001}
height: {type: number, format: double, minimum: 0.001}
weight: {type: number, format: double, minimum: 0.001, nullable: true}
machineNo: {type: string, maxLength: 64}
measuredAt: {type: string, format: date-time}
image: {type: string, format: binary}
@@ -1245,9 +1242,7 @@ components:
widthCm: {type: number, format: double, nullable: true}
heightCm: {type: number, format: double, nullable: true}
girthCm: {type: number, format: double, nullable: true}
unitWeightKg: {type: number, format: double, nullable: true}
unitVolumeCbm: {type: number, format: double, nullable: true}
totalWeightKg: {type: number, format: double, nullable: true}
totalVolumeCbm: {type: number, format: double, nullable: true}
volumeWeightKg: {type: number, format: double, nullable: true}
intelligentImageFileId:
@@ -1331,7 +1326,7 @@ components:
items: {$ref: '#/components/schemas/InboundMeasurementItemRequest'}
InboundMeasurementItemRequest:
type: object
required: [inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm, unitWeightKg]
required: [inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm]
additionalProperties: false
properties:
inboundDetailId: {type: string, pattern: '^[1-9][0-9]*$'}
@@ -1341,7 +1336,6 @@ components:
lengthCm: {type: number, minimum: 0.001}
widthCm: {type: number, minimum: 0.001}
heightCm: {type: number, minimum: 0.001}
unitWeightKg: {type: number, minimum: 0.001}
intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
overLimitWarning: {type: string, maxLength: 500, nullable: true}
outerLengthCm: {type: number, minimum: 0, nullable: true}
@@ -1729,7 +1723,6 @@ components:
widthCm: {type: number, exclusiveMinimum: true, minimum: 0, example: 40.10}
heightCm: {type: number, exclusiveMinimum: true, minimum: 0, example: 30.00}
packageCount: {type: integer, minimum: 1, maximum: 1000000, example: 50}
unitWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, nullable: true, example: 20}
remark: {type: string, maxLength: 500, nullable: true}
InboundMeasurementEventResponse:
type: object
@@ -1761,9 +1754,11 @@ components:
maxWeightKg: {type: number, minimum: 0, exclusiveMinimum: true, description: 必须等于柜型固定限重}
ContainerOceanBookingLinkRequest:
type: object
required: [realContainerBookingId, expectedVersion]
required: [oceanBookingId, requirementId, containerNo, expectedVersion]
properties:
realContainerBookingId: {type: integer, format: int64, minimum: 1, description: 订舱创建的 OCEAN_BOOKING/ACTIVE 真实柜ID;成功合并后该来源记录转为 MERGED}
oceanBookingId: {type: integer, format: int64, minimum: 1, description: RELEASED订舱ID}
requirementId: {type: integer, format: int64, minimum: 1, description: 未满配的订舱柜型需求ID}
containerNo: {type: string, maxLength: 64, description: 港口提柜后取得的全局唯一真实集装箱号}
expectedVersion: {type: integer, minimum: 0}
ContainerOceanBookingUnlinkRequest:
type: object
@@ -1,218 +1,71 @@
openapi: 3.0.3
info: {title: Phase 3 Ocean Booking API, version: 1.1.0}
info: {title: Ocean Booking API, version: 2.0.0}
paths:
/api/platform/v1/ocean-bookings/channels:
get:
summary: List logistics channels from the current ACTIVE price book for an origin warehouse
description: 'Requires platform:firstleg:ocean-booking:list. Returns only channels with an ACTIVE price row; the selected channel name is frozen server-side when the offline-confirmed booking is registered.'
parameters:
- {name: originWarehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
responses: {'200': {description: Available channel options, content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/ChannelOption'}}}}}}
/api/platform/v1/ocean-bookings:
get:
summary: List only real confirmed booking records
description: 'Requires platform:firstleg:ocean-booking:list. The operational list exposes only CONFIRMED, CHANGE_PENDING, RELEASED and CANCELLED bookings; DRAFT is internal to the create transaction and legacy SUBMITTED/FAILED values are invalid.'
parameters:
- {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 1}}
- {name: status, in: query, schema: {type: string, enum: [CONFIRMED, CHANGE_PENDING, RELEASED, CANCELLED]}}
- {name: keyword, in: query, schema: {type: string, maxLength: 128}}
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
responses:
'200':
description: Operational real booking page; each record follows OceanBookingSummary
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
records: {type: array, items: {$ref: '#/components/schemas/OceanBookingSummary'}}
get: {summary: List DRAFT and RELEASED bookings, responses: {'200': {description: Booking page}}}
post:
summary: Register an offline-confirmed ocean booking with complete transport snapshots
description: 'Requires platform:firstleg:ocean-booking:create. Registers every real container unit atomically, creates one stable fl_container_booking plus one requirement/allocation per container, and persists the booking directly as CONFIRMED. RELEASED remains a separate later action.'
summary: Create an editable DRAFT
description: Creates only the booking aggregate and container requirements; no containerNo, container row, or allocation is created.
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateRequest'}}}}
responses: {'200': {description: CONFIRMED real booking detail, content: {application/json: {schema: {$ref: '#/components/schemas/Detail'}}}}}
responses: {'200': {description: DRAFT detail}}
/api/platform/v1/ocean-bookings/{oceanBookingId}:
get: {summary: Get booking detail, parameters: [{$ref: '#/components/parameters/OceanBookingId'}], responses: {'200': {description: Detail}}}
put:
summary: Replace a DRAFT
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/UpdateRequest'}}}}
responses: {'200': {description: Updated DRAFT}}
delete:
summary: Delete a DRAFT; RELEASED is immutable
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}}}}
responses: {'200': {description: Deleted}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/release:
post:
summary: Release an already registered booking and synchronize associated cabinet transport snapshots
description: 'The transport fields were registered at create time. The request must repeat the same bill of lading, vessel, voyage, ETD and ETA values; mismatches are rejected. This action does not create cabinets, change channels, replace cabinetNo, or recreate loading plans.'
summary: Validate and permanently release a DRAFT
description: Release validates the persisted aggregate and never requires or creates a real container number.
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ReleaseRequest'}}}}
responses: {'200': {description: RELEASED detail}}
/api/platform/v1/ocean-bookings/{oceanBookingId}:
get:
summary: Get an ocean booking with its real container units
description: Requires platform:firstleg:ocean-booking:list.
parameters: [{$ref: '#/components/parameters/OceanBookingId'}]
responses: {'200': {description: Booking detail, content: {application/json: {schema: {$ref: '#/components/schemas/Detail'}}}}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/cancel:
post:
summary: Cancel a confirmed or change-pending real booking
description: Requires platform:firstleg:ocean-booking:cancel. A non-blank reason is required; RELEASED bookings are irreversible.
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}}}}
responses: {'200': {description: CANCELLED detail}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/change-request:
post:
summary: Request a voyage change for a confirmed booking
description: Requires platform:firstleg:ocean-booking:change. Current transport snapshots remain active until change-confirm succeeds.
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ChangeRequest'}}}}
responses: {'200': {description: CHANGE_PENDING detail}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/change-confirm:
post:
summary: Confirm a pending voyage change
description: Requires platform:firstleg:ocean-booking:change.
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}}}}
responses: {'200': {description: CONFIRMED detail with the pending voyage snapshots applied}}
/api/platform/v1/ocean-bookings/channels:
get: {summary: List ACTIVE channels for an origin warehouse, responses: {'200': {description: Channel options}}}
components:
parameters:
OceanBookingId: {name: oceanBookingId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
schemas:
OceanBookingSummary:
CreateRequest: {type: object, required: [expectedVersion, data], properties: {expectedVersion: {type: integer, enum: [0]}, data: {$ref: '#/components/schemas/DraftData'}}}
UpdateRequest: {type: object, required: [expectedVersion, data], properties: {expectedVersion: {type: integer, minimum: 0}, data: {$ref: '#/components/schemas/DraftData'}}}
DraftData:
type: object
required: [id, bookingNo, status, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt, containerCount, version]
description: Fields may be incomplete in DRAFT. RELEASE validates the complete persisted aggregate.
properties:
id: {type: integer, format: int64}
bookingNo: {type: string}
status: {type: string, enum: [CONFIRMED, CHANGE_PENDING, RELEASED, CANCELLED]}
customerId: {type: integer, format: int64, nullable: true}
customerName: {type: string, nullable: true}
billOfLadingNo: {type: string}
shipperName: {type: string, nullable: true, description: SHIPPER party snapshot}
consigneeName: {type: string, nullable: true, description: CONSIGNEE party snapshot}
channelCode: {type: string, nullable: true}
warehouseId: {type: integer, format: int64, nullable: true}
billOfLadingNo: {type: string, nullable: true}
shippingCompanyId: {type: integer, format: int64, nullable: true}
shippingCompanyName: {type: string, nullable: true}
bookingAgentId: {type: integer, format: int64, nullable: true}
bookingAgentName: {type: string, nullable: true}
vesselName: {type: string}
voyageNo: {type: string}
vesselName: {type: string, nullable: true}
voyageNo: {type: string, nullable: true}
departurePortCode: {type: string, nullable: true}
departurePortName: {type: string, nullable: true}
destinationPortCode: {type: string, nullable: true}
destinationPortName: {type: string, nullable: true}
estimatedSailingAt: {type: string, format: date-time}
estimatedArrivalAt: {type: string, format: date-time}
containerCount: {type: integer, format: int64, minimum: 0, description: ACTIVE real container units allocated to the booking}
createdByName: {type: string, nullable: true}
updatedAt: {type: string, format: date-time}
version: {type: integer, minimum: 0}
CreateRequest:
type: object
required: [expectedVersion, data]
properties:
expectedVersion: {type: integer, enum: [0]}
data: {$ref: '#/components/schemas/DraftData'}
DraftData:
type: object
required: [channelCode, warehouseId, billOfLadingNo, vesselName, voyageNo, serviceTerm, freightTerm, estimatedSailingAt, estimatedArrivalAt, parties, containers]
properties:
customerId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional customer snapshot; must be supplied together with customerName.}
customerName: {type: string, maxLength: 128, nullable: true, description: Optional customer snapshot; must be supplied together with customerId.}
channelCode: {type: string, maxLength: 64, description: Selected from GET /api/platform/v1/ocean-bookings/channels; server validates it against the current ACTIVE price book and freezes channelName.}
warehouseId: {type: integer, format: int64, minimum: 1, description: ACTIVE domestic transit warehouse ID; warehouseName is resolved and frozen server-side.}
shippingCompanyId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional shipping-company master-data ID; the carrier is independent from the logistics channel and booking-agent Supplier.}
shippingCompanyName: {type: string, maxLength: 128, nullable: true, description: Optional shipping-company name snapshot.}
bookingAgentId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE Warehouse supplier ID validated only as category BOOKING; booking suppliers do not use domestic/international scope.}
bookingAgentName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot always comes from Warehouse and cannot be supplied without bookingAgentId.}
serviceTerm: {type: string, maxLength: 32, description: Required free-text Service Term snapshot, for example CY-CY.}
freightTerm: {type: string, maxLength: 32, description: Required free-text Freight Term snapshot, for example NINGBO.}
billOfLadingNo: {type: string, maxLength: 128, description: Required offline-confirmed bill of lading snapshot.}
vesselName: {type: string, maxLength: 128, description: Required offline-confirmed vessel snapshot.}
voyageNo: {type: string, maxLength: 64, description: Required offline-confirmed voyage snapshot.}
departurePortCode: {type: string, maxLength: 32, nullable: true}
departurePortName: {type: string, maxLength: 128, nullable: true}
destinationPortCode: {type: string, maxLength: 32, nullable: true}
destinationPortName: {type: string, maxLength: 128, nullable: true}
placeOfDelivery: {type: string, maxLength: 128, nullable: true}
estimatedSailingAt: {type: string, format: date-time, description: Required offline-confirmed ETD snapshot.}
estimatedArrivalAt: {type: string, format: date-time, description: Required offline-confirmed ETA snapshot; cannot be earlier than ETD.}
remark: {type: string, maxLength: 1000, nullable: true}
parties: {type: array, minItems: 3, maxItems: 3, items: {$ref: '#/components/schemas/PartyRequest'}}
containers: {type: array, minItems: 1, maxItems: 20, items: {$ref: '#/components/schemas/ContainerRequest'}}
PartyRequest:
type: object
required: [partyType, partyName]
properties:
partyType: {type: string, enum: [SHIPPER, CONSIGNEE, NOTIFY_PARTY]}
partyName: {type: string, maxLength: 255}
address: {type: string, maxLength: 1000, nullable: true}
contactName: {type: string, maxLength: 128, nullable: true}
contactPhone: {type: string, maxLength: 64, nullable: true}
ContainerRequest:
type: object
required: [containerNo, containerType, containerKind, capacityCbm, maxWeightKg]
additionalProperties: false
properties:
containerNo: {type: string, minLength: 1, maxLength: 64, description: Required real container number; unique across all ACTIVE and unmerged container records.}
containerType: {type: string, enum: [20GP, 40GP, 40HC, 40NR, 45HC], description: Platform-controlled container type}
containerKind: {type: string, maxLength: 32}
capacityCbm: {type: number, exclusiveMinimum: true, minimum: 0, description: Must match the fixed platform specification for containerType}
maxWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, description: Must match the fixed platform specification for containerType}
Detail:
type: object
required: [oceanBooking, parties, containers, histories]
properties:
oceanBooking: {type: object, description: Ocean booking aggregate and frozen transport snapshots.}
parties: {type: array, items: {type: object}}
containers: {type: array, items: {$ref: '#/components/schemas/RealContainer'}}
histories: {type: array, items: {type: object}}
RealContainer:
type: object
required: [containerBookingId, cabinetNo, containerNo, requirementId, allocationId, containerType, containerKind, capacityCbm, maxWeightKg, warehouseId, warehouseName, channelCode, channelName]
properties:
containerBookingId: {type: integer, format: int64, minimum: 1, description: Current ACTIVE operational container identity.}
cabinetNo: {type: string, maxLength: 64, description: Stable server-generated internal cabinet number.}
containerNo: {type: string, maxLength: 64, description: Unique real container number.}
requirementId: {type: integer, format: int64, minimum: 1}
allocationId: {type: integer, format: int64, minimum: 1}
containerType: {type: string, enum: [20GP, 40GP, 40HC, 40NR, 45HC]}
containerKind: {type: string, maxLength: 32}
capacityCbm: {type: number}
maxWeightKg: {type: number}
warehouseId: {type: integer, format: int64, minimum: 1}
warehouseName: {type: string, maxLength: 128}
channelCode: {type: string, maxLength: 64}
channelName: {type: string, maxLength: 128}
customerId: {type: integer, format: int64, minimum: 1, nullable: true}
customerName: {type: string, maxLength: 128, nullable: true}
ChannelOption:
type: object
required: [priceBookVersionId, channelCode, channelName, priceModel, pricingTaxModes]
properties:
priceBookVersionId: {type: integer, format: int64}
channelCode: {type: string, maxLength: 64}
channelName: {type: string, maxLength: 128}
priceModel: {type: string}
pricingTaxModes: {type: array, items: {type: string}}
ActionRequest:
type: object
required: [expectedVersion]
properties: {expectedVersion: {type: integer, minimum: 0}, reason: {type: string, maxLength: 1000, nullable: true}}
ChangeRequest:
type: object
required: [expectedVersion, vesselName, voyageNo, reason]
properties:
expectedVersion: {type: integer, minimum: 0}
vesselName: {type: string, maxLength: 128}
voyageNo: {type: string, maxLength: 64}
placeOfDelivery: {type: string, nullable: true}
serviceTerm: {type: string, nullable: true}
freightTerm: {type: string, nullable: true}
estimatedSailingAt: {type: string, format: date-time, nullable: true}
estimatedArrivalAt: {type: string, format: date-time, nullable: true}
reason: {type: string, maxLength: 1000}
ReleaseRequest:
type: object
required: [expectedVersion, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt]
properties:
expectedVersion: {type: integer, minimum: 0}
billOfLadingNo: {type: string, maxLength: 128}
vesselName: {type: string, maxLength: 128}
voyageNo: {type: string, maxLength: 64}
estimatedSailingAt: {type: string, format: date-time}
estimatedArrivalAt: {type: string, format: date-time}
reason: {type: string, maxLength: 1000}
remark: {type: string, nullable: true}
parties: {type: array, nullable: true, items: {$ref: '#/components/schemas/PartyRequest'}}
containerRequirements: {type: array, nullable: true, items: {$ref: '#/components/schemas/ContainerRequirementRequest'}}
PartyRequest: {type: object, required: [partyType, partyName], properties: {partyType: {type: string, enum: [SHIPPER, CONSIGNEE, NOTIFY_PARTY]}, partyName: {type: string}, address: {type: string, nullable: true}, contactName: {type: string, nullable: true}, contactPhone: {type: string, nullable: true}}}
ContainerRequirementRequest: {type: object, required: [containerType, containerKind, quantity, capacityCbm, maxWeightKg], properties: {containerType: {type: string}, containerKind: {type: string}, quantity: {type: integer, minimum: 1}, capacityCbm: {type: number, exclusiveMinimum: true, minimum: 0}, maxWeightKg: {type: number, exclusiveMinimum: true, minimum: 0}}}
ReleaseRequest: {type: object, required: [expectedVersion], properties: {expectedVersion: {type: integer, minimum: 0}, reason: {type: string, nullable: true}}}
ActionRequest: {type: object, required: [expectedVersion], properties: {expectedVersion: {type: integer, minimum: 0}, reason: {type: string, nullable: true}}}
Detail: {type: object, required: [oceanBooking, parties, containerRequirements, containers, histories], properties: {oceanBooking: {type: object}, parties: {type: array}, containerRequirements: {type: array}, containers: {type: array, description: Existing containers allocated after port pickup linking}, histories: {type: array}}}
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Truck Appointment API
version: 1.2.0
description: Truck appointments are independent operational records. Ocean booking, container booking and loading plan are optional links. After a PDA loading transaction commits, the server best-effort completes the unique active appointment explicitly linked to that container in an isolated transaction; absence, duplicates or concurrent truck edits never roll back loading completion.
description: Truck appointments are independent operational records. Ocean booking, container booking and loading plan are optional links. Draft creation/editing does not require a bill of lading or attachment; DISPATCH requires a nonblank billOfLadingNo and at least one active BILL_OF_LADING attachment owned by the appointment customer. When linked to a RELEASED booking, the server freezes and validates the booking bill of lading snapshot. After a PDA loading transaction commits, the server best-effort completes the unique active appointment explicitly linked to that container in an isolated transaction; absence, duplicates or concurrent truck edits never roll back loading completion.
paths:
/api/platform/v1/truck-appointments:
get:
@@ -109,9 +109,9 @@ paths:
multipart/form-data:
schema:
type: object
required: [file]
required: [attachmentType, file]
properties:
attachmentType: {type: string, maxLength: 64}
attachmentType: {type: string, enum: [BILL_OF_LADING, DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER], description: Attachment classification; BILL_OF_LADING is required before DISPATCH}
file: {type: string, format: binary}
responses:
'200': {description: Uploaded attachment reference}
@@ -140,10 +140,10 @@ components:
schemas:
AttachmentRequest:
type: object
required: [fileId]
required: [fileId, attachmentType]
properties:
fileId: {type: integer, format: int64, minimum: 1}
attachmentType: {type: string, maxLength: 64}
attachmentType: {type: string, enum: [BILL_OF_LADING, DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER], description: Attachment classification; BILL_OF_LADING is required before DISPATCH}
fileName: {type: string, maxLength: 255, description: Ignored; authoritative metadata comes from File service}
fileHash: {type: string, maxLength: 128, description: Ignored; authoritative metadata comes from File service}
Attachment:
@@ -152,7 +152,7 @@ components:
id: {type: integer, format: int64}
appointmentId: {type: integer, format: int64}
fileId: {type: integer, format: int64}
attachmentType: {type: string}
attachmentType: {type: string, enum: [BILL_OF_LADING, DISPATCH_ORDER, DRIVER_DOCUMENT, VEHICLE_DOCUMENT, OTHER]}
fileName: {type: string}
fileHash: {type: string}
visibility: {type: string, enum: [PLATFORM]}
@@ -177,6 +177,7 @@ components:
customerName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; persisted value is resolved from IAM customer master data.}
carrierId: {type: integer, format: int64, minimum: 1, description: Required ACTIVE Warehouse supplier ID validated as TRUCKING.}
carrierName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot comes from Warehouse supplier master data.}
billOfLadingNo: {type: string, maxLength: 128, nullable: true, description: Optional on draft; required before DISPATCH and server-frozen from a linked RELEASED booking when present.}
plannedLoadingAt: {type: string, format: date-time}
expectedVersion: {type: integer, minimum: 0, description: Must be 0 on create; current version on edit.}
DetailStableReferences:
@@ -200,4 +201,5 @@ components:
bookingAgentName: {type: string, nullable: true}
carrierId: {type: integer, format: int64}
carrierName: {type: string}
billOfLadingNo: {type: string, maxLength: 128, nullable: true, description: Frozen bill of lading snapshot used by dispatch readiness.}
attachmentCount: {type: integer, format: int64, minimum: 0, description: Number of attachment references owned by this appointment}