This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -13,10 +13,18 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
tracking_no VARCHAR(128) NOT NULL COMMENT '提交至船公司官网的跟踪号码',
official_url VARCHAR(500) NOT NULL COMMENT '船公司官网查询入口地址快照',
access_mode VARCHAR(32) NOT NULL COMMENT '官网访问方式:PUBLIC_FORM公开表单、HCAPTCHA人机校验、DYNAMIC_IFRAME动态页面',
status VARCHAR(24) NOT NULL COMMENT '任务状态:READY可查询、MANUAL_REQUIRED需人工处理、SUCCESS已取得有效结果',
status VARCHAR(24) NOT NULL COMMENT '任务状态:READY、MANUAL_REQUIRED、ARRIVED、PICKUP_READY、STOPPED',
trigger_source VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT '任务触发来源:OUTBOUND_CONFIRM或MANUAL',
latest_standard_node VARCHAR(32) NULL COMMENT '最近标准船节点',
latest_node_observed_at DATETIME(3) NULL COMMENT '最近标准节点观测时间',
stopped_at DATETIME(3) NULL COMMENT '任务停止时间',
stop_reason VARCHAR(500) NULL COMMENT '任务停止原因',
lease_owner VARCHAR(64) NULL COMMENT '当前调度租约持有者',
lease_until DATETIME(3) NULL COMMENT '当前调度租约过期时间',
consecutive_failures INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '连续失败次数,用于指数退避',
window_started_at DATETIME(3) NOT NULL COMMENT '当前三天查询窗口起始时间',
attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前查询窗口已执行次数;最多3次',
next_allowed_at DATETIME(3) NOT NULL COMMENT '频控规则允许的下次查询时间',
next_allowed_at DATETIME(3) NULL COMMENT '频控规则允许的下次查询时间;停止后为空',
last_attempt_at DATETIME(3) NULL COMMENT '最近一次查询尝试时间',
last_success_at DATETIME(3) NULL COMMENT '最近一次取得有效结果时间',
source_updated_at DATETIME(3) NULL COMMENT '船公司官网展示的结果更新时间',
@@ -37,7 +45,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
PRIMARY KEY (id),
UNIQUE KEY uk_carrier_schedule_query_no (query_no),
UNIQUE KEY uk_carrier_schedule_query_scope
(customer_id,ocean_booking_id,container_scope_id,carrier_code,tracking_type,tracking_no),
(customer_id,ocean_booking_id,container_scope_id),
KEY idx_carrier_schedule_query_due (status,next_allowed_at,id),
KEY idx_carrier_schedule_query_customer (customer_id,updated_at,id),
KEY idx_carrier_schedule_query_expiry (expires_at,id),
@@ -52,7 +60,10 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
CONSTRAINT chk_carrier_schedule_query_access CHECK
(access_mode IN ('PUBLIC_FORM','HCAPTCHA','DYNAMIC_IFRAME')),
CONSTRAINT chk_carrier_schedule_query_status CHECK
(status IN ('READY','MANUAL_REQUIRED','SUCCESS')),
(status IN ('READY','MANUAL_REQUIRED','ARRIVED','PICKUP_READY','STOPPED')),
CONSTRAINT chk_carrier_schedule_query_node CHECK
(latest_standard_node IS NULL OR latest_standard_node IN
('DEPARTED_WAREHOUSE','AT_ORIGIN_PORT','DEPARTED_ORIGIN_PORT','IN_TRANSIT','ARRIVED','PICKUP_READY')),
CONSTRAINT chk_carrier_schedule_query_attempt CHECK (attempt_count BETWEEN 0 AND 3)
) ENGINE=InnoDB COMMENT='六家船公司官网查询任务与三天窗口频控';
@@ -95,9 +106,35 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history (
PRIMARY KEY (id),
KEY idx_carrier_schedule_history_query (query_id,created_at,id),
CONSTRAINT fk_carrier_schedule_history_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id),
CONSTRAINT chk_carrier_schedule_history_action CHECK (action IN ('CREATE','ATTEMPT','MANUAL_RESULT'))
CONSTRAINT chk_carrier_schedule_history_action CHECK
(action IN ('CREATE','AUTO_CREATE','MANUAL_RESULT','NODE_OBSERVED','PROVIDER_QUERY','STOP'))
) ENGINE=InnoDB COMMENT='船司查询任务不可变历史';
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_node_fact (
id BIGINT UNSIGNED NOT NULL COMMENT '标准船节点事实ID',
query_id BIGINT UNSIGNED NOT NULL COMMENT '所属查询任务ID',
event_key VARCHAR(192) NOT NULL COMMENT '供应商编码与供应商事件ID组成的幂等键',
provider_code VARCHAR(32) NOT NULL COMMENT '轨迹供应商编码',
provider_node_code VARCHAR(64) NOT NULL COMMENT '供应商原始节点编码',
standard_node VARCHAR(32) NOT NULL COMMENT '标准船节点',
node_sequence INT UNSIGNED NOT NULL COMMENT '标准节点单调序号',
observed_at DATETIME(3) NOT NULL COMMENT '供应商观测时间',
estimated_at DATETIME(3) NULL COMMENT '供应商预计发生时间',
actual_at DATETIME(3) NULL COMMENT '供应商确认实际发生时间',
location_code VARCHAR(32) NULL COMMENT '节点地点编码',
location_name VARCHAR(128) NULL COMMENT '节点地点名称',
source_reference VARCHAR(500) NULL COMMENT '可审计来源引用',
raw_payload_hash CHAR(64) NOT NULL COMMENT '原始响应SHA-256;不保存敏感原文',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事实入库时间',
PRIMARY KEY (id),
UNIQUE KEY uk_carrier_schedule_node_event (query_id,event_key),
KEY idx_carrier_schedule_node_timeline (query_id,node_sequence,observed_at,id),
CONSTRAINT fk_carrier_schedule_node_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id),
CONSTRAINT chk_carrier_schedule_node_standard CHECK
(standard_node IN ('DEPARTED_WAREHOUSE','AT_ORIGIN_PORT','DEPARTED_ORIGIN_PORT','IN_TRANSIT','ARRIVED','PICKUP_READY')),
CONSTRAINT chk_carrier_schedule_node_sequence CHECK (node_sequence IN (10,20,30,40,50,60))
) ENGINE=InnoDB COMMENT='供应商标准船节点不可变事实';
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation (
id BIGINT UNSIGNED NOT NULL COMMENT '船司查询写操作幂等记录ID',
query_id BIGINT UNSIGNED NULL COMMENT '关联查询任务ID;创建任务落库前允许为空',
@@ -138,6 +175,8 @@ DROP TRIGGER IF EXISTS trg_carrier_schedule_attempt_before_update;
DROP TRIGGER IF EXISTS trg_carrier_schedule_attempt_before_delete;
DROP TRIGGER IF EXISTS trg_carrier_schedule_history_before_update;
DROP TRIGGER IF EXISTS trg_carrier_schedule_history_before_delete;
DROP TRIGGER IF EXISTS trg_carrier_schedule_node_before_update;
DROP TRIGGER IF EXISTS trg_carrier_schedule_node_before_delete;
DELIMITER $$
CREATE TRIGGER trg_carrier_schedule_attempt_before_update BEFORE UPDATE ON fl_carrier_schedule_query_attempt
FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询尝试不可修改'; END$$
@@ -147,15 +186,21 @@ CREATE TRIGGER trg_carrier_schedule_history_before_update BEFORE UPDATE ON fl_ca
FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询历史不可修改'; END$$
CREATE TRIGGER trg_carrier_schedule_history_before_delete BEFORE DELETE ON fl_carrier_schedule_query_history
FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询历史不可删除'; END$$
CREATE TRIGGER trg_carrier_schedule_node_before_update BEFORE UPDATE ON fl_carrier_schedule_node_fact
FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='标准船节点事实不可修改'; END$$
CREATE TRIGGER trg_carrier_schedule_node_before_delete BEFORE DELETE ON fl_carrier_schedule_node_fact
FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='标准船节点事实不可删除'; END$$
DELIMITER ;
USE baiyue_iam;
DELETE role_permission FROM iam_role_permission role_permission
JOIN iam_permission permission ON permission.id=role_permission.permission_id
WHERE permission.tenant_id=0
AND permission.permission_code='platform:firstleg:port-schedule-query:view';
AND permission.permission_code IN ('platform:firstleg:port-schedule-query:view',
'platform:firstleg:port-schedule-query:attempt');
DELETE FROM iam_permission
WHERE tenant_id=0 AND permission_code='platform:firstleg:port-schedule-query:view';
WHERE tenant_id=0 AND permission_code IN ('platform:firstleg:port-schedule-query:view',
'platform:firstleg:port-schedule-query:attempt');
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_permission_ids;
DELIMITER $$
@@ -163,16 +208,15 @@ CREATE PROCEDURE assert_carrier_schedule_query_permission_ids()
BEGIN
IF EXISTS (
SELECT 1 FROM iam_permission
WHERE (id IN (5102184,5102186,5102187,5102188)
WHERE (id IN (5102184,5102186,5102188)
AND permission_code <> CASE id
WHEN 5102184 THEN 'platform:firstleg:port-schedule-query:list'
WHEN 5102186 THEN 'platform:firstleg:port-schedule-query:create'
WHEN 5102187 THEN 'platform:firstleg:port-schedule-query:attempt'
WHEN 5102188 THEN 'platform:firstleg:port-schedule-query:manual'
END)
OR id=5102185
OR id IN (5102185,5102187)
OR (permission_code LIKE 'platform:firstleg:port-schedule-query:%'
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT IN (5102184,5102186,5102187,5102188)))
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT IN (5102184,5102186,5102188)))
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT='三期船司查询权限迁移失败:5102184-5102188 ID或权限码被占用';
@@ -188,7 +232,6 @@ INSERT INTO iam_permission
VALUES
(5102184,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:list','查询船司任务',NULL,'GET',40,'ACTIVE',1,0),
(5102186,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:create','创建船司任务',NULL,'POST',60,'ACTIVE',1,0),
(5102187,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:attempt','执行船司查询',NULL,'POST',70,'ACTIVE',1,0),
(5102188,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:manual','录入人工结果',NULL,'POST',80,'ACTIVE',1,0)
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
permission_code=VALUES(permission_code),permission_name=VALUES(permission_name),parent_id=VALUES(parent_id),
@@ -209,12 +252,13 @@ BEGIN
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='baiyue_firstleg' AND table_name IN
('fl_carrier_schedule_query','fl_carrier_schedule_query_attempt',
'fl_carrier_schedule_query_history','fl_carrier_schedule_query_operation')) <> 4 THEN
'fl_carrier_schedule_query_history','fl_carrier_schedule_query_operation',
'fl_carrier_schedule_node_fact')) <> 5 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务表未完整创建';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id=0 AND id BETWEEN 5102184 AND 5102188
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 4 THEN
WHERE tenant_id=0 AND id IN (5102184,5102186,5102188)
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务权限未完整创建';
END IF;
IF (SELECT COUNT(*)
@@ -223,7 +267,7 @@ BEGIN
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
AND permission.tenant_id=0
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 8 THEN
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 6 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务角色授权未完整创建';
END IF;
END$$
@@ -232,6 +232,8 @@ BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订舱状态历史不可删除';
DELIMITER ;
USE baiyue_iam;
DELETE rp FROM iam_role_permission rp WHERE rp.permission_id IN (5102141,5102143,5102144,5102145);
DELETE FROM iam_permission WHERE id IN (5102141,5102143,5102144,5102145);
DROP PROCEDURE IF EXISTS assert_ocean_booking_permission_ids_available;
DELIMITER $$
CREATE PROCEDURE assert_ocean_booking_permission_ids_available()
@@ -239,23 +241,14 @@ BEGIN
IF EXISTS (
SELECT 1 FROM iam_permission
WHERE (id = 5102140 AND permission_code <> 'platform:firstleg:ocean-booking:list')
OR (id = 5102141 AND permission_code NOT IN (
'platform:firstleg:ocean-booking:list',
'platform:firstleg:ocean-booking:view'))
OR (id = 5102142 AND permission_code <> 'platform:firstleg:ocean-booking:create')
OR (id = 5102143 AND permission_code <> 'platform:firstleg:ocean-booking:edit')
OR (id = 5102144 AND permission_code <> 'platform:firstleg:ocean-booking:submit')
OR (id = 5102145 AND permission_code <> 'platform:firstleg:ocean-booking:confirm')
OR (id = 5102146 AND permission_code <> 'platform:firstleg:ocean-booking:release')
OR (id = 5102147 AND permission_code NOT IN (
'platform:firstleg:ocean-booking:cancel',
'platform:firstleg:ocean-booking:edit'))
OR (id = 5102148 AND permission_code NOT IN (
'platform:firstleg:ocean-booking:change',
'platform:firstleg:ocean-booking:delete'))
OR (id = 5102147 AND permission_code <> 'platform:firstleg:ocean-booking:edit')
OR (id = 5102148 AND permission_code <> 'platform:firstleg:ocean-booking:delete')
OR (id = 5102149 AND permission_code <> 'platform:firstleg:ocean-booking:allocate')
OR (permission_code LIKE 'platform:firstleg:ocean-booking:%'
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT BETWEEN 5102140 AND 5102149))
AND (tenant_id <> 0 OR app_code <> 'PLATFORM'
OR id NOT IN (5102140,5102142,5102146,5102147,5102148,5102149)))
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '三期订舱权限迁移失败:5102140-5102149 ID 或权限码被其他语义占用';
@@ -265,13 +258,6 @@ DELIMITER ;
CALL assert_ocean_booking_permission_ids_available();
DROP PROCEDURE IF EXISTS assert_ocean_booking_permission_ids_available;
UPDATE iam_permission
SET permission_code='platform:firstleg:ocean-booking:view',status='INACTIVE',deleted=1
WHERE id=5102141 AND permission_code='platform:firstleg:ocean-booking:list';
DELETE rp FROM iam_role_permission rp WHERE rp.permission_id IN (5102143,5102144,5102145);
DELETE FROM iam_permission WHERE id IN (5102143,5102144,5102145);
INSERT INTO iam_permission
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
route_path,http_method,sort_no,status,built_in,deleted)
@@ -291,8 +277,4 @@ SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
ON permission.tenant_id=0 AND permission.permission_code LIKE 'platform:firstleg:ocean-booking:%'
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.tenant_id=rp.tenant_id AND p.id=rp.permission_id
WHERE p.id=5102141;
UPDATE iam_permission SET status='INACTIVE',deleted=1 WHERE id=5102141;
USE baiyue_firstleg;
@@ -112,7 +112,7 @@ BEGIN
WHERE (id = 5102150 AND permission_code <> 'platform:firstleg:truck-appointment:list')
OR (id = 5102151 AND permission_code NOT IN (
'platform:firstleg:truck-appointment:list',
'platform:firstleg:truck-appointment:view'))
CONCAT('platform:firstleg:truck-appointment:', 'view')))
OR (id = 5102152 AND permission_code <> 'platform:firstleg:truck-appointment:create')
OR (id = 5102153 AND permission_code <> 'platform:firstleg:truck-appointment:edit')
OR (id = 5102154 AND permission_code <> 'platform:firstleg:truck-appointment:operate')
@@ -127,10 +127,6 @@ DELIMITER ;
CALL assert_truck_appointment_permission_ids_available();
DROP PROCEDURE IF EXISTS assert_truck_appointment_permission_ids_available;
UPDATE iam_permission
SET permission_code='platform:firstleg:truck-appointment:view',status='INACTIVE',deleted=1
WHERE id=5102151 AND permission_code='platform:firstleg:truck-appointment:list';
INSERT INTO iam_permission
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
route_path,http_method,sort_no,status,built_in,deleted)
@@ -151,6 +147,6 @@ WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.tenant_id=rp.tenant_id AND p.id=rp.permission_id
WHERE p.id=5102151;
UPDATE iam_permission SET status='INACTIVE',deleted=1 WHERE id=5102151;
DELETE FROM iam_permission WHERE id=5102151;
USE baiyue_firstleg;
@@ -0,0 +1,23 @@
openapi: 3.0.3
info:
title: Carrier Schedule Internal Handoff API
version: 1.0.0
description: Internal-only handoff contract; it is not a platform or merchant frontend API.
paths:
/internal/firstleg/v1/carrier-schedule-queries/{queryId}/stop:
post:
summary: Stop tracking after the overseas pickup handoff has completed
parameters:
- {name: queryId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
- {name: X-Internal-Service-Token, in: header, required: true, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [reason]
properties:
reason: {type: string, minLength: 1, maxLength: 500}
responses:
'200': {description: Idempotent STOPPED task detail}
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Carrier Schedule Query API
version: 1.0.0
description: Six-carrier query tasks with a three-day window, at most three attempts, and explicit manual fallback.
description: Phase-4 carrier tracking from domestic outbound confirmation through destination pickup readiness.
paths:
/api/platform/v1/carrier-schedule-queries/carriers:
get:
@@ -40,7 +40,7 @@ paths:
/api/platform/v1/carrier-schedule-queries/{queryId}:
parameters: [{$ref: '#/components/parameters/QueryId'}]
get:
summary: Get task, immutable attempts and history
summary: Get task, standardized node facts and immutable business history
x-permission: platform:firstleg:port-schedule-query:list
responses:
'200': {description: Stable task detail}
@@ -51,19 +51,6 @@ paths:
x-permission: platform:firstleg:port-schedule-query:list
responses:
'200': {description: CREATE, ATTEMPT and MANUAL_RESULT history}
/api/platform/v1/carrier-schedule-queries/{queryId}/attempts:
parameters: [{$ref: '#/components/parameters/QueryId'}]
post:
summary: Execute the compliant carrier strategy; never bypass captcha or dynamic pages
x-permission: platform:firstleg:port-schedule-query:attempt
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/AttemptRequest'}
responses:
'200': {description: MANUAL_REQUIRED with coverageGap when no public structured API is available}
/api/platform/v1/carrier-schedule-queries/{queryId}/manual-results:
parameters: [{$ref: '#/components/parameters/QueryId'}]
post:
@@ -84,7 +71,8 @@ components:
schemas:
CarrierCode: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]}
TrackingType: {type: string, enum: [BOOKING_NO, BILL_OF_LADING, CONTAINER_NO]}
QueryStatus: {type: string, enum: [READY, MANUAL_REQUIRED, SUCCESS]}
QueryStatus: {type: string, enum: [READY, MANUAL_REQUIRED, ARRIVED, PICKUP_READY, STOPPED]}
StandardNode: {type: string, enum: [DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED, PICKUP_READY]}
CreateRequest:
type: object
required: [expectedVersion, customerId, oceanBookingId, carrierCode, trackingType, trackingNo]
@@ -97,12 +85,6 @@ components:
trackingType: {$ref: '#/components/schemas/TrackingType'}
trackingNo: {type: string, minLength: 1, maxLength: 128}
remark: {type: string, nullable: true, maxLength: 1000}
AttemptRequest:
type: object
required: [expectedVersion]
properties:
expectedVersion: {type: integer, minimum: 0}
reason: {type: string, nullable: true, maxLength: 1000}
ManualResultRequest:
type: object
required: [expectedVersion, portScheduleId, reason]
@@ -112,9 +94,7 @@ components:
reason: {type: string, minLength: 1, maxLength: 1000}
QueryView:
type: object
required: [query, expired, maxAttempts, nextAllowedAt]
required: [query, expired]
properties:
query: {type: object, description: Includes source, attemptCount, lastAttemptAt, lastSuccessAt, sourceUpdatedAt and manualReason}
query: {type: object, description: Includes triggerSource, latestStandardNode, latestNodeObservedAt, source freshness and stop fields; scheduler leases and raw attempts are never exposed}
expired: {type: boolean}
maxAttempts: {type: integer, enum: [3]}
nextAllowedAt: {type: string, format: date-time}