Merge test fixes into dev dropship flow

This commit is contained in:
2026-09-10 18:18:41 +08:00
131 changed files with 3062 additions and 651 deletions
@@ -24,6 +24,7 @@ CALL phase2_fba_add_column('fl_customer_charge_rule', 'length_factor', "DECIMAL(
CALL phase2_fba_add_column('fl_customer_charge_rule', 'width_factor', "DECIMAL(18,6) NOT NULL DEFAULT 1 COMMENT '宽度计算系数' AFTER length_factor");
CALL phase2_fba_add_column('fl_customer_charge_rule', 'height_factor', "DECIMAL(18,6) NOT NULL DEFAULT 1 COMMENT '高度计算系数' AFTER width_factor");
CALL phase2_fba_add_column('fl_customer_charge_rule', 'default_rule', "TINYINT(1) NOT NULL DEFAULT 0 COMMENT '客户默认生效规则' AFTER status");
CALL phase2_fba_add_column('fl_customer_charge_rule', 'initialization_key', "VARCHAR(128) NULL COMMENT '客户创建执行器幂等键及补偿归属' AFTER remark");
CALL phase2_fba_add_column('fl_carrier_fba_rate', 'version', "INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER deleted");
CALL phase2_fba_add_column('fl_inbound_fee', 'rule_code', "VARCHAR(50) NOT NULL DEFAULT '' COMMENT '客户规则编码快照' AFTER carrier_rate_version");
CALL phase2_fba_add_column('fl_inbound_fee', 'measurement_id', "BIGINT UNSIGNED NULL COMMENT '费用绑定的测量批次ID快照' AFTER inbound_id");
@@ -67,6 +68,14 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
DROP PROCEDURE IF EXISTS phase2_fba_add_column;
SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_customer_charge_rule'
AND INDEX_NAME = 'idx_charge_rule_initialization');
SET @sql = IF(@idx_exists = 0,
'ALTER TABLE fl_customer_charge_rule ADD INDEX idx_charge_rule_initialization (tenant_id, initialization_key)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 旧索引可能缺少业务类型、船司和结束时间;用新索引名并存,避免破坏线上依赖。
SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_carrier_fba_rate' AND INDEX_NAME = 'idx_fba_rate_unique_match');
@@ -431,6 +431,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule (
effective_to DATETIME(3) NULL COMMENT '生效结束时间',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态:ACTIVE启用、DISABLED停用',
remark VARCHAR(500) NULL COMMENT '规则备注',
initialization_key VARCHAR(128) NULL COMMENT '客户创建执行器幂等键及补偿归属',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
@@ -439,6 +440,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule (
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
UNIQUE KEY uk_charge_rule_version (tenant_id, rule_code, rule_version),
KEY idx_charge_rule_initialization (tenant_id, initialization_key),
KEY idx_charge_rule_match (tenant_id, customer_id, business_type, status, effective_from, effective_to)
) ENGINE=InnoDB COMMENT='客户头程计费规则版本';
@@ -52,7 +52,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
CONSTRAINT fk_carrier_schedule_query_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
CONSTRAINT fk_carrier_schedule_query_container FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking(id),
CONSTRAINT fk_carrier_schedule_query_result FOREIGN KEY (result_port_schedule_id) REFERENCES fl_port_schedule(id),
CONSTRAINT chk_carrier_schedule_query_customer CHECK (customer_id > 0),
CONSTRAINT chk_carrier_schedule_query_customer CHECK (customer_id >= 0),
CONSTRAINT chk_carrier_schedule_query_carrier CHECK
(carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER')),
CONSTRAINT chk_carrier_schedule_query_tracking CHECK
@@ -114,7 +114,8 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history (
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',
query_id BIGINT UNSIGNED NULL COMMENT '自动跟踪任务ID;人工补录时为空',
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属真实货柜ID',
event_key VARCHAR(192) NOT NULL COMMENT '供应商编码与供应商事件ID组成的幂等键',
provider_code VARCHAR(32) NOT NULL COMMENT '轨迹供应商编码',
provider_event_id VARCHAR(128) NOT NULL COMMENT '供应商稳定事件ID',
@@ -130,9 +131,11 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_node_fact (
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),
UNIQUE KEY uk_carrier_schedule_node_event (container_booking_id,event_key),
KEY idx_carrier_schedule_node_timeline (query_id,node_sequence,observed_at,id),
KEY idx_container_shipping_node_timeline (container_booking_id,observed_at,id),
CONSTRAINT fk_carrier_schedule_node_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id),
CONSTRAINT fk_carrier_schedule_node_container FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking(id),
CONSTRAINT chk_carrier_schedule_node_standard CHECK
(standard_node IN ('DEPARTED_WAREHOUSE','AT_ORIGIN_PORT','LOADED_ON_VESSEL','DEPARTED_ORIGIN_PORT',
'IN_TRANSIT','ARRIVED_DESTINATION_PORT','DISCHARGED_FROM_VESSEL','PICKUP_READY','PICKED_UP','EMPTY_RETURNED')),
@@ -34,7 +34,7 @@ paths:
summary: Query carrier schedule tasks
x-permission: platform:firstleg:port-schedule-query:list
parameters:
- {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 1}}
- {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 0}}
- {name: oceanBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}}
- {name: containerBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}}
- {name: carrierCode, in: query, schema: {$ref: '#/components/schemas/TaskCarrierCode'}}
@@ -126,7 +126,7 @@ components:
required: [expectedVersion, customerId, oceanBookingId, containerBookingId, carrierCode, trackingType, trackingNo]
properties:
expectedVersion: {type: integer, minimum: 0, maximum: 0}
customerId: {type: integer, format: int64, minimum: 1}
customerId: {type: integer, format: int64, minimum: 0}
oceanBookingId: {type: integer, format: int64, minimum: 1}
containerBookingId: {type: integer, format: int64, minimum: 1}
carrierCode: {$ref: '#/components/schemas/CarrierCode'}
@@ -123,13 +123,13 @@ components:
schemas:
LongId: {type: string, pattern: '^[1-9][0-9]*$', description: Long ID序列化为字符串}
ApiEnvelope: {type: object, required: [code, message, timestamp], properties: {code: {type: string}, message: {type: string}, requestId: {type: string, nullable: true}, traceId: {type: string, nullable: true}, timestamp: {type: string, format: date-time}}}
ContainerEnvelope: {allOf: [{$ref: '#/components/schemas/ApiEnvelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/ContainerView'}}}]}
ContainerEnvelope: {allOf: [{$ref: '#/components/schemas/ApiEnvelope'}, {type: object, properties: {data: {allOf: [{$ref: '#/components/schemas/ContainerView'}], nullable: true, description: 装柜计划尚未完成时返回null,表示报关流程尚未生成}}}]}
DocumentEnvelope: {allOf: [{$ref: '#/components/schemas/ApiEnvelope'}, {type: object, properties: {data: {$ref: '#/components/schemas/DocumentView'}}}]}
ActionRequest: {type: object, required: [expectedVersion, confirm], additionalProperties: false, properties: {expectedVersion: {type: integer, minimum: 0}, confirm: {type: boolean}, remark: {type: string, maxLength: 500, nullable: true}}}
ScopeRequest: {type: object, required: [inboundIds, expectedVersion], additionalProperties: false, properties: {inboundIds: {type: array, minItems: 1, maxItems: 200, items: {$ref: '#/components/schemas/LongId'}}, expectedVersion: {type: integer, minimum: 1}}}
FileView: {type: object, required: [fileId, fileName, fileHash, sizeBytes, version, uploadedAt], properties: {fileId: {$ref: '#/components/schemas/LongId'}, fileName: {type: string}, fileHash: {type: string}, contentType: {type: string, nullable: true}, sizeBytes: {type: integer, format: int64}, version: {type: integer}, uploadedAt: {type: string, format: date-time}}}
InboundView: {type: object, required: [inboundId, orderId, orderNo, inboundCode, tenantId, customerName, taxMode, covered], properties: {inboundId: {$ref: '#/components/schemas/LongId'}, orderId: {$ref: '#/components/schemas/LongId'}, orderNo: {type: string}, inboundCode: {type: string}, tenantId: {$ref: '#/components/schemas/LongId'}, customerName: {type: string}, taxMode: {type: string, enum: [TAX_REFUND, BUY_DECLARATION]}, covered: {type: boolean}}}
DocumentView: {type: object, required: [documentId, tenantId, source, inboundIds, inboundCodes, frozen], properties: {documentId: {$ref: '#/components/schemas/LongId'}, tenantId: {$ref: '#/components/schemas/LongId'}, source: {$ref: '#/components/schemas/FileView'}, inboundIds: {type: array, items: {$ref: '#/components/schemas/LongId'}}, inboundCodes: {type: array, items: {type: string}}, frozen: {type: boolean}, completion: {allOf: [{$ref: '#/components/schemas/FileView'}], nullable: true}}}
ContainerView: {type: object, required: [aggregateId, loadingPlanId, containerBookingId, status, version, taxRefundCount, coveredTaxRefundCount, inbounds, documents], properties: {aggregateId: {$ref: '#/components/schemas/LongId'}, loadingPlanId: {$ref: '#/components/schemas/LongId'}, containerBookingId: {$ref: '#/components/schemas/LongId'}, planNo: {type: string}, cabinetNo: {type: string}, containerNo: {type: string, nullable: true}, status: {type: string, enum: [WAITING_CUSTOMER_DOCUMENTS, READY_TO_START, DECLARING, COMPLETED]}, merchantStatus: {type: string, enum: [WAITING_UPLOAD, DOCUMENT_UPLOADED, DECLARING, CUSTOMS_COMPLETED], nullable: true}, version: {type: integer}, taxRefundCount: {type: integer}, coveredTaxRefundCount: {type: integer}, inbounds: {type: array, items: {$ref: '#/components/schemas/InboundView'}}, documents: {type: array, items: {$ref: '#/components/schemas/DocumentView'}}, startedAt: {type: string, format: date-time, nullable: true}, completedAt: {type: string, format: date-time, nullable: true}}}
ContainerView: {type: object, required: [aggregateId, loadingPlanId, containerBookingId, status, hasActualCargo, version, taxRefundCount, coveredTaxRefundCount, inbounds, documents], properties: {aggregateId: {$ref: '#/components/schemas/LongId'}, loadingPlanId: {$ref: '#/components/schemas/LongId'}, containerBookingId: {$ref: '#/components/schemas/LongId'}, planNo: {type: string}, cabinetNo: {type: string}, containerNo: {type: string, nullable: true}, status: {type: string, enum: [WAITING_CUSTOMER_DOCUMENTS, READY_TO_START, DECLARING, COMPLETED]}, hasActualCargo: {type: boolean, description: 当前完成装柜计划是否仍有实际装柜货物;false时查询成功返回空明细且无需发起报关}, merchantStatus: {type: string, enum: [WAITING_UPLOAD, DOCUMENT_UPLOADED, DECLARING, CUSTOMS_COMPLETED], nullable: true}, version: {type: integer}, taxRefundCount: {type: integer}, coveredTaxRefundCount: {type: integer}, inbounds: {type: array, items: {$ref: '#/components/schemas/InboundView'}}, documents: {type: array, items: {$ref: '#/components/schemas/DocumentView'}}, startedAt: {type: string, format: date-time, nullable: true}, completedAt: {type: string, format: date-time, nullable: true}}}
ExportBatchRequest: {type: object, required: [taskId, tenantId, appCode, requestedBy, dispatchVersion, parameters, limit], properties: {taskId: {type: string}, tenantId: {type: string, enum: ['0']}, appCode: {type: string, enum: [PLATFORM]}, requestedBy: {type: string}, dispatchVersion: {type: integer}, parameters: {type: object, required: [loadingPlanId], additionalProperties: false, properties: {loadingPlanId: {type: string}}}, cursor: {type: string, nullable: true}, snapshotToken: {type: string, nullable: true}, limit: {type: integer, minimum: 1, maximum: 500}}}
ExportBatchEnvelope: {allOf: [{$ref: '#/components/schemas/ApiEnvelope'}, {type: object, properties: {data: {type: object, properties: {schemaVersion: {type: integer}, schemaHash: {type: string}, snapshotToken: {type: string}, columns: {type: array, items: {type: object}}, rows: {type: array, items: {type: object}}, nextCursor: {type: string}, hasMore: {type: boolean}, totalRows: {type: integer, format: int64, nullable: true}}}}}]}
@@ -923,6 +923,39 @@ paths:
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
'403': {description: 无查看权限}
/api/platform/inbounds/carton-label-pdf:
post:
summary: 批量生成入仓箱唛 PDF
description: 需要 platform:firstleg:inbound:view 权限;可跨客户选择1至200个入库单,聚合生成一个 PDF,首次调用冻结各入库单整单箱号。
requestBody:
required: true
content:
application/json:
schema:
type: array
minItems: 1
maxItems: 200
items:
type: object
required: [tenantId, inboundId]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
responses:
'200':
description: 成功返回聚合 PDF;业务失败返回统一 JSON 响应,调用方必须按 Content-Type 区分。
content:
application/pdf: {schema: {type: string, format: binary}}
application/json:
schema:
type: object
required: [code, message]
properties:
code: {type: string}
message: {type: string}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
'403': {description: 无查看权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement:
get:
summary: 查询入库测量详情
@@ -964,6 +997,43 @@ paths:
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 无测量编辑权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/calculation-strategy:
put:
summary: 更新箱规计算规则
description: >-
独立更新当前测量批次全部明细的箱规计算规则并重算体积,不覆盖整批测量数据,
不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改;
仅当客户已确认费用且该票已装柜完成时冻结。
parameters:
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/InboundMeasurementCalculationStrategyRequest'}
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 无测量编辑权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/items/{itemId}/outer-dimensions:
put:
summary: 更新仓库外箱长宽高
description: >-
独立更新当前测量批次指定明细的仓库外箱尺寸并重算体积,不覆盖整批测量数据,
不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改;
仅当客户已确认费用且该票已装柜完成时冻结。
parameters:
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: itemId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/InboundMeasurementOuterDimensionsRequest'}
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 无测量编辑权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:
post:
summary: 平台模拟单箱设备扫描
@@ -1681,16 +1751,18 @@ components:
updatedAt: {type: string, format: date-time, nullable: true}
CustomerDimensionConfigInitializeRequest:
type: object
required: [tenantId, operatorId, initializationKey]
required: [tenantId, operatorId, initializationKey, customerName]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
operatorId: {type: string, pattern: '^[0-9]+$'}
initializationKey: {type: string, minLength: 1, maxLength: 128}
customerName: {type: string, minLength: 1, maxLength: 128}
CustomerDimensionConfigInitializeResponse:
type: object
required: [configId, createdByThisInitialization]
required: [configId, chargeRuleId, createdByThisInitialization]
properties:
configId: {type: string, pattern: '^[1-9][0-9]*$'}
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$'}
createdByThisInitialization: {type: boolean}
WeightPalletSnapshot:
type: object
@@ -1933,6 +2005,25 @@ components:
minItems: 1
maxItems: 500
items: {$ref: '#/components/schemas/InboundMeasurementItemRequest'}
InboundMeasurementCalculationStrategyRequest:
type: object
required: [measurementId, version, coefficientMode]
additionalProperties: false
properties:
measurementId: {type: string, pattern: '^[1-9][0-9]*$'}
version: {type: integer, minimum: 0}
coefficientMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY]}
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: MULTIPLY 模式必填}
InboundMeasurementOuterDimensionsRequest:
type: object
required: [measurementId, version]
additionalProperties: false
properties:
measurementId: {type: string, pattern: '^[1-9][0-9]*$'}
version: {type: integer, minimum: 0}
outerLengthCm: {type: number, minimum: 0.001, nullable: true}
outerWidthCm: {type: number, minimum: 0.001, nullable: true}
outerHeightCm: {type: number, minimum: 0.001, nullable: true}
InboundMeasurementItemRequest:
type: object
required: [inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm]
@@ -27,7 +27,7 @@ paths:
/api/platform/v1/containers/{id}/pda/photos/{fileId}/download:
get:
operationId: downloadContainerPdaPhoto
description: '权限:platform:firstleg:container:list;Firstleg按柜次、装柜计划、入库单和文件归属代理下载,不暴露内部服务令牌'
description: '权限:platform:firstleg:container:list;PDA完成及国内离仓后仍可读取ACTIVE历史照片;Firstleg按柜次、装柜计划、入库单和文件归属代理下载,不暴露内部服务令牌'
parameters: [{$ref: '#/components/parameters/ContainerId'}, {name: fileId, in: path, required: true, schema: {type: integer, format: int64}}]
responses: {'200': {description: photo bytes, content: {application/octet-stream: {schema: {type: string, format: binary}}}}, '404': {description: photo missing or not owned by the container}}
/api/platform/v1/containers/{id}/documents/{documentType}:
@@ -14,6 +14,7 @@ paths:
/api/platform/v1/terminal-drayages/{containerBookingId}:
get:
summary: 查询单柜码头拖车详情
description: 已确认国内离仓的货柜即使船节点跟踪任务尚未建立也返回WAITING_FOR_ARRIVAL空详情;实际操作仍按到港、清关和卸柜节点校验
x-permission: platform:firstleg:terminal-drayage:list
parameters: [{$ref: '#/components/parameters/ContainerBookingId'}]
responses: {'200': {$ref: '#/components/responses/ViewResponse'}}
@@ -63,6 +64,7 @@ paths:
/api/platform/v1/terminal-drayages/{containerBookingId}/emails:
get:
summary: 查询最近100封车队邮件及发信开关
description: 已确认国内离仓的货柜允许读取空邮件列表,不以船节点跟踪任务是否已经建立作为货柜存在性条件
x-permission: platform:firstleg:terminal-drayage:list
parameters: [{$ref: '#/components/parameters/ContainerBookingId'}]
responses: