Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-09 15:41:50 +08:00
50 changed files with 1045 additions and 51 deletions
@@ -30,6 +30,16 @@ spring:
size: ${FIRSTLEG_SCHEDULING_POOL_SIZE:5}
thread-name-prefix: firstleg-scheduler-
baiyue:
terminal-drayage:
mail:
enabled: ${TERMINAL_DRAYAGE_MAIL_ENABLED:false}
host: ${TERMINAL_DRAYAGE_MAIL_HOST:}
port: ${TERMINAL_DRAYAGE_MAIL_PORT:587}
username: ${TERMINAL_DRAYAGE_MAIL_USERNAME:}
password: ${TERMINAL_DRAYAGE_MAIL_PASSWORD:}
from: ${TERMINAL_DRAYAGE_MAIL_FROM:}
ssl: ${TERMINAL_DRAYAGE_MAIL_SSL:false}
scan-delay: ${TERMINAL_DRAYAGE_MAIL_SCAN_DELAY:10000}
carrier-schedule:
providers:
matson-automated: ${MATSON_TRACKING_ENABLED:false}
@@ -23,10 +23,33 @@ CREATE TABLE IF NOT EXISTS fl_terminal_drayage (
CONSTRAINT fk_terminal_drayage_container FOREIGN KEY(container_booking_id) REFERENCES fl_container_booking(id),
CONSTRAINT chk_terminal_drayage_times CHECK(
(actual_pickup_at IS NULL OR pickup_appointment_at IS NOT NULL) AND
(actual_empty_return_at IS NULL OR actual_pickup_at IS NOT NULL) AND
(actual_empty_return_at IS NULL OR actual_empty_return_at >= actual_pickup_at))
(actual_empty_return_at IS NULL OR actual_pickup_at IS NULL OR actual_empty_return_at >= actual_pickup_at))
) ENGINE=InnoDB COMMENT='目的港货柜级码头拖车与还空事实';
CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail (
id BIGINT UNSIGNED NOT NULL COMMENT '邮件任务ID',
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '归属物理货柜ID',
fleet_supplier_id BIGINT UNSIGNED NOT NULL COMMENT '车队供应商ID快照',
fleet_name VARCHAR(128) NOT NULL COMMENT '车队名称快照',
recipient VARCHAR(254) NOT NULL COMMENT '收件人邮箱',
kind VARCHAR(32) NOT NULL COMMENT '邮件类型:REQUEST_PICKUP提柜请求、CANCEL_PICKUP取消提柜',
subject VARCHAR(255) NOT NULL COMMENT '邮件主题',
body TEXT NOT NULL COMMENT '邮件正文',
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '投递状态:PENDING待发送、SENDING发送中、SENT已发送、FAILED失败',
attempts INT NOT NULL DEFAULT 0 COMMENT '发送尝试次数',
failure_reason VARCHAR(500) NULL COMMENT '发送失败原因',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后更新人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
sent_at DATETIME(3) NULL COMMENT '发送成功时间',
PRIMARY KEY(id),
KEY idx_terminal_mail_dispatch(status,id),
KEY idx_terminal_mail_container(container_booking_id,id,status),
CONSTRAINT fk_terminal_mail_container FOREIGN KEY(container_booking_id) REFERENCES fl_container_booking(id),
CONSTRAINT chk_terminal_mail_kind CHECK(kind IN ('REQUEST_PICKUP','CANCEL_PICKUP')),
CONSTRAINT chk_terminal_mail_status CHECK(status IN ('PENDING','SENDING','SENT','FAILED'))
) ENGINE=InnoDB COMMENT='码头车队邮件任务与投递记录';
CREATE TABLE IF NOT EXISTS fl_terminal_drayage_history (
id BIGINT UNSIGNED NOT NULL COMMENT '历史ID',
drayage_id BIGINT UNSIGNED NOT NULL COMMENT '码头拖车聚合ID',
@@ -2033,7 +2033,7 @@ components:
allowedActions:
type: array
description: 已同时按真实业务状态与当前用户权限裁剪
items: {type: string, enum: [EDIT_CONTAINER, MANAGE_CUSTOMS, CONFIRM_OUTBOUND, MANAGE_DESTINATION_DOCUMENTS, SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN]}
items: {type: string, enum: [EDIT_CONTAINER, MANAGE_CUSTOMS, CONFIRM_OUTBOUND, MANAGE_DESTINATION_DOCUMENTS, SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN, SEND_PICKUP_EMAIL]}
actualFacts: {$ref: '#/components/schemas/ContainerActualFacts'}
terminalDrayage: {$ref: '#/components/schemas/ContainerTerminalDrayage'}
ContainerWorkbenchStatusSummary:
@@ -2080,7 +2080,7 @@ components:
actualEmptyReturnAt: {type: string, format: date-time, nullable: true}
unloadingCompletedAt: {type: string, format: date-time, nullable: true}
emptyReturnWarning: {type: object, nullable: true}
allowedActions: {type: array, items: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN]}}
allowedActions: {type: array, items: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN, SEND_PICKUP_EMAIL]}}
ContainerBooking:
type: object
required: [id, bookingNo, cabinetNo, sourceType, recordStatus, warehouseId, warehouseName, containerType, capacityCbm, maxWeightKg, planStatus, exceptionActive, destinationClearanceDocumentStatus, customsDeclarationDocumentStatus, deliveryOrderDocumentStatus, version]
@@ -88,16 +88,16 @@ paths:
'200': {description: Original file bytes, content: {application/octet-stream: {schema: {type: string, format: binary}}}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/prints/shipping-instruction:
get:
summary: Render the released booking as an export shipping instruction PDF
description: Firstleg maps the authoritative released aggregate to SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1 and synchronously proxies the generated PDF.
summary: Render the current saved booking as an export shipping instruction PDF
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1 and synchronously proxies the generated PDF.
x-permission: platform:firstleg:ocean-booking:list
parameters: [{$ref: '#/components/parameters/OceanBookingId'}]
responses:
'200': {description: PDF or a business-error JSON envelope, content: {application/pdf: {schema: {type: string, format: binary}}, application/json: {schema: {type: object}}}}
/api/platform/v1/ocean-bookings/{oceanBookingId}/prints/bill-of-lading-confirmation:
get:
summary: Render the released booking as a bill-of-lading confirmation PDF
description: Firstleg maps the authoritative released aggregate to BILL_OF_LADING_CONFIRMATION_A4_V1 and synchronously proxies the generated PDF.
summary: Render the current saved booking as a bill-of-lading confirmation PDF
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to BILL_OF_LADING_CONFIRMATION_A4_V1 and synchronously proxies the generated PDF.
x-permission: platform:firstleg:ocean-booking:list
parameters: [{$ref: '#/components/parameters/OceanBookingId'}]
responses:
@@ -52,11 +52,63 @@ paths:
parameters: [{$ref: '#/components/parameters/ContainerBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ActualTimeRequest'}}}}
responses: {'200': {$ref: '#/components/responses/ViewResponse'}}
/api/platform/v1/terminal-drayages/{containerBookingId}/pickup-email:
post:
summary: 将当前车队提柜邮件加入发送队列
description: 初次选择车队不发信;此命令携带版本和幂等键,发送成功是任务已入队,不表示已送达。首次更换车队自动原子入队取消和新提柜两封邮件。同一车队重复选择不发信。
x-permission: platform:firstleg:terminal-drayage:fleet:edit
parameters: [{$ref: '#/components/parameters/ContainerBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {type: object, required: [expectedVersion], properties: {expectedVersion: {type: integer, minimum: 0}}}}}}
responses: {'200': {$ref: '#/components/responses/ViewResponse'}}
/api/platform/v1/terminal-drayages/{containerBookingId}/emails:
get:
summary: 查询最近100封车队邮件及发信开关
x-permission: platform:firstleg:terminal-drayage:list
parameters: [{$ref: '#/components/parameters/ContainerBookingId'}]
responses:
'200':
description: 邮件记录;SENT表示SMTP已接受,不表示收件人已阅读
content:
application/json:
schema:
type: object
properties:
code: {type: string}
data:
type: object
properties:
deliveryEnabled: {type: boolean}
records: {type: array, items: {$ref: '#/components/schemas/EmailTask'}}
/api/platform/v1/terminal-drayages/{containerBookingId}/emails/{mailId}/retry:
post:
summary: 核对失败邮件未收到后手动重试
description: 仅FAILED状态可重试,服务端校验mailId归属本柜;失败可能已经送达,须人工核对。同柜邮件依次发送,失败阻断后续邮件。
x-permission: platform:firstleg:terminal-drayage:fleet:edit
parameters:
- {$ref: '#/components/parameters/ContainerBookingId'}
- {name: mailId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
responses: {'200': {description: 已重新入队}}
components:
parameters:
ContainerBookingId: {name: containerBookingId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 255}}
schemas:
EmailTask:
type: object
properties:
id: {type: string}
containerBookingId: {type: string}
fleetSupplierId: {type: string}
fleetName: {type: string}
recipient: {type: string, format: email}
kind: {type: string, enum: [REQUEST_PICKUP, CANCEL_PICKUP]}
subject: {type: string}
body: {type: string}
status: {type: string, enum: [PENDING, SENDING, SENT, FAILED]}
attempts: {type: integer}
failureReason: {type: string, nullable: true}
createdAt: {type: string, format: date-time}
sentAt: {type: string, format: date-time, nullable: true}
FleetSelectionRequest:
type: object
required: [expectedVersion, fleetSupplierId]
@@ -104,7 +156,7 @@ components:
actualEmptyReturnAt: {type: string, format: date-time, nullable: true}
unloadingCompletedAt: {type: string, format: date-time, nullable: true}
emptyReturnWarning: {$ref: '#/components/schemas/Warning'}
allowedActions: {type: array, items: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN]}}
allowedActions: {type: array, items: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN, SEND_PICKUP_EMAIL]}}
actualFacts: {$ref: '#/components/schemas/ContainerActualFacts'}
updatedAt: {type: string, format: date-time, nullable: true}
ContainerActualFacts:
@@ -127,7 +179,7 @@ components:
type: object
required: [action, fromStatus, toStatus, fromVersion, toVersion, operatorId, createdAt]
properties:
action: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN]}
action: {type: string, enum: [SELECT_FLEET, SCHEDULE_PICKUP, RECORD_PICKUP, RECORD_EMPTY_RETURN, SEND_PICKUP_EMAIL]}
fromStatus: {type: string}
toStatus: {type: string}
fromVersion: {type: integer, minimum: 0}