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

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	event-bridge-service/src/main/java/com/baiyue/eventbridge/metrics/EventBridgeMetrics.java
#	firstleg/src/main/resources/db/phase3-container-customs-schema.sql
#	order/src/main/resources/db/order.sql
#	scripts/db/incremental.sql
This commit is contained in:
2026-09-02 17:56:20 +08:00
45 changed files with 1005 additions and 1063 deletions
+14 -1
View File
@@ -318,7 +318,7 @@ export function parseArguments(argv) {
const modules = normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean))
const merchantUsername = values.get('merchant-username') || process.env.MERCHANT_TEST_USERNAME || ''
const merchantPassword = values.get('merchant-password') || process.env.MERCHANT_TEST_PASSWORD || ''
if (merchantUsername && merchantPassword) {
if (merchantUsername && merchantPassword && !modules.includes('phase4-overseas-business')) {
const fixtureIndex = modules.indexOf('merchant-qualification-fixture')
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1)
}
@@ -1620,6 +1620,19 @@ export class AcceptanceRun {
if (this.options.waitSplitCount > 1 || this.options.waitSplitPackageCount > 0
|| this.options.orderProfileMode === 'randomized' || this.options.routeMode === 'random') {
this.applyWaitSplitVariant(this.options.waitSplitSequence || 1)
} else if (this.options.originWarehouseName || this.options.channelName || this.options.channelCode) {
this.context.waitSplitRoute = {
originWarehouseName: this.options.originWarehouseName || '',
channelName: this.options.channelName || '',
channelCode: this.options.channelCode || '',
randomize: false,
randomIndex: 0,
destinationIndex: 0,
taxModeIndex: 0,
}
// A fixed non-default origin may not carry the Yiwu-only TP-001/YW-TP-002 fixtures.
// Reuse an existing pallet from the selected warehouse while keeping order data deterministic.
this.context.allowAnyExistingPallet = true
}
const loginResponse = await this.request('POST', '/api/auth/login', { loginId: this.options.username, password: this.options.password }, '')
const login = this.apiSuccess('authentication', '平台管理员登录', loginResponse)
@@ -210,6 +210,8 @@ test('module is context injected, blocks missing prerequisites and excludes forb
assert.doesNotMatch(source, /https?:\/\/|process\.env|fetch\(/)
assert.doesNotMatch(source, /addCoverageGap|coverageGap\(/)
assert.match(source, /deploymentBlocker: true/)
assert.match(source, /contactPhone: '\d+'/)
assert.doesNotMatch(source, /contactPhone: '\+[^']+'/)
for (const forbidden of ['/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) {
assert.equal(source.includes(forbidden), false, forbidden)
}
@@ -56,7 +56,7 @@ function partnerWarehousePayload(context) {
return {
tenantId: 0, warehouseCode: `P4B-${suffix}`.slice(0, 50), warehouseName: `${context.marker} 采购合作仓`,
warehouseType: 'OVERSEAS_PARTNER', countryCode: 'US', timezone: 'UTC-8', contactName: 'Phase4 QA',
contactPhone: '+1-213-555-0177', contactEmail: null, status: 'ACTIVE', remark: `${context.marker}-procurement-fixture`,
contactPhone: '12135550177', contactEmail: null, status: 'ACTIVE', remark: `${context.marker}-procurement-fixture`,
address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California', city: 'Los Angeles',
district: 'Commerce', postalCode: '90040', addressLine1: '601 Procurement Partner Avenue', addressLine2: null,
latitude: null, longitude: null },
@@ -6,6 +6,16 @@ const TRUCK = '/api/platform/v1/truck-appointments'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
async function requestWithTransientRetry(context, method, path, body) {
let response = null
for (let attempt = 1; attempt <= 3; attempt += 1) {
response = await context.request(method, path, body)
if (response?.http !== 0) return response
if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, 500 * attempt))
}
return response
}
async function actionRequest(context, path, endpoint, version) {
try {
const response = await fetch(`${context.options.gateway}${path}/${endpoint}`, {
@@ -64,7 +74,7 @@ export async function run(context) {
context.assert(MODULE, '已放舱订舱可直接创建订舱级拖车预约', oceanResponse,
id(booking?.id) === id(ocean.id) && booking?.status === 'RELEASED',
`oceanBookingId=${id(booking?.id) || 'missing'}, status=${booking?.status || 'missing'}`, OPTIONS)
const carrierResponse = await context.request('GET',
const carrierResponse = await requestWithTransientRetry(context, 'GET',
'/api/platform/v1/suppliers?tenantId=0&category=TRUCKING&status=ACTIVE&page=1&size=20')
const carrierPage = context.apiSuccess(MODULE, '读取 ACTIVE TRUCKING 承运商事实源', carrierResponse, '', OPTIONS)
const carrier = carrierPage?.records?.find?.((item) => item?.id && item?.name)
+43 -109
View File
@@ -239,9 +239,6 @@ CREATE TABLE IF NOT EXISTS ll_inbound_plan (
UNIQUE KEY uk_ll_inbound_plan_container (tenant_id,container_no)
) ENGINE=InnoDB COMMENT='尾程入库计划表:每个租户的每个集装箱生成一张入库单';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan' AND column_name='version'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT ''入库计划乐观锁版本'' AFTER updated_at'));
PREPARE ll_inbound_plan_version_ddl FROM @ddl; EXECUTE ll_inbound_plan_version_ddl; DEALLOCATE PREPARE ll_inbound_plan_version_ddl;
CREATE TABLE IF NOT EXISTS ll_inbound_plan_line (
id BIGINT NOT NULL COMMENT '入库计划明细ID(雪花ID)',
plan_id BIGINT NOT NULL COMMENT '入库计划ID',
@@ -443,7 +440,6 @@ CREATE TABLE IF NOT EXISTS ll_order (
relabel_order_no VARCHAR(128) NULL COMMENT '换标单号',
relabel_status VARCHAR(32) NULL COMMENT '换标状态',
pallet_merge_flag VARCHAR(32) NULL COMMENT '合托标记',
pallet_merge_batch_id VARCHAR(64) NULL COMMENT '平台预标记合托分组号',
current_status VARCHAR(32) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '当前业务状态',
private_remark VARCHAR(1000) NULL COMMENT '备注(仅自己)',
customer_remark VARCHAR(1000) NULL COMMENT '备注(客户)',
@@ -461,9 +457,6 @@ CREATE TABLE IF NOT EXISTS ll_order (
KEY idx_ll_order_list (tenant_id,current_status,created_at)
) ENGINE=InnoDB COMMENT='尾程订单主表';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='pallet_merge_batch_id'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN pallet_merge_batch_id VARCHAR(64) NULL COMMENT ''平台预标记合托分组号'' AFTER pallet_merge_flag'));
PREPARE ll_order_merge_batch_ddl FROM @ddl; EXECUTE ll_order_merge_batch_ddl; DEALLOCATE PREPARE ll_order_merge_batch_ddl;
CREATE TABLE IF NOT EXISTS ll_order_detail (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单明细主键',
order_id BIGINT NOT NULL COMMENT '尾程订单ID',
@@ -1393,7 +1386,7 @@ CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
product_specification VARCHAR(500) NULL COMMENT '产品规格', product_url VARCHAR(2000) NULL COMMENT '产品链接', purchase_quantity BIGINT NOT NULL COMMENT '采购数量',
budget_unit_price DECIMAL(18,2) NULL COMMENT '预算单价', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', receiving_address VARCHAR(1000) NULL COMMENT '收货地址',
contact_name VARCHAR(128) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(64) NOT NULL COMMENT '联系人电话', purchase_warehouse_id VARCHAR(128) NULL COMMENT '采购入仓仓库ID',
purchase_warehouse_name VARCHAR(255) NULL COMMENT '采购入仓仓库名称', inbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '累计入库数量', outbound_details JSON NULL COMMENT '出库明细', outbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '累计出库数量',
purchase_warehouse_name VARCHAR(255) NULL COMMENT '采购入仓仓库名称', inbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '累计入库数量', outbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '累计出库数量',
domestic_carrier VARCHAR(128) NULL COMMENT '境内承运商', domestic_tracking_no VARCHAR(128) NULL COMMENT '境内物流单号', overseas_carrier VARCHAR(128) NULL COMMENT '海外承运商',
overseas_tracking_no VARCHAR(128) NULL COMMENT '海外物流单号', estimated_arrival_at DATETIME(3) NULL COMMENT '预计到达时间', status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '业务状态',
remark VARCHAR(1000) NULL COMMENT '备注', idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容哈希',
@@ -3202,8 +3195,6 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_overseas_procurement_order' AND column_name='inbound_quantity' AND COALESCE(column_comment,'') <> '累计入库数量'), 'ALTER TABLE ll_overseas_procurement_order MODIFY COLUMN inbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT ''累计入库数量''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_overseas_procurement_order' AND column_name='outbound_details'), 'SELECT 1', 'ALTER TABLE ll_overseas_procurement_order ADD COLUMN outbound_details JSON NULL COMMENT ''出库明细'' AFTER inbound_quantity'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_overseas_procurement_order' AND column_name='outbound_quantity' AND COALESCE(column_comment,'') <> '累计出库数量'), 'ALTER TABLE ll_overseas_procurement_order MODIFY COLUMN outbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT ''累计出库数量''', 'SELECT 1'));
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_overseas_procurement_order' AND column_name='domestic_carrier' AND COALESCE(column_comment,'') <> '境内承运商'), 'ALTER TABLE ll_overseas_procurement_order MODIFY COLUMN domestic_carrier VARCHAR(128) NULL COMMENT ''境内承运商''', 'SELECT 1'));
@@ -3815,106 +3806,49 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
'SELECT 1', 'ALTER TABLE ll_cargo_discard_application ADD COLUMN review_idempotency_key VARCHAR(255) NULL COMMENT ''审核幂等键'' AFTER request_hash'));
PREPARE phase4_discard_review_stmt FROM @ddl; EXECUTE phase4_discard_review_stmt; DEALLOCATE PREPARE phase4_discard_review_stmt;
-- 一件代发出库与 PDA 按托盘拣货出库。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status'));
PREPARE dropship_outbound_stmt FROM @ddl; EXECUTE dropship_outbound_stmt; DEALLOCATE PREPARE dropship_outbound_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='recipient_info'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN recipient_info VARCHAR(1000) NULL COMMENT ''一件代发收件信息'' AFTER outbound_type'));
PREPARE dropship_outbound_stmt FROM @ddl; EXECUTE dropship_outbound_stmt; DEALLOCATE PREPARE dropship_outbound_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='picked_package_count'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN picked_package_count INT NOT NULL DEFAULT 0 COMMENT ''已拣箱数'' AFTER package_count'));
PREPARE dropship_outbound_stmt FROM @ddl; EXECUTE dropship_outbound_stmt; DEALLOCATE PREPARE dropship_outbound_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='status'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN status VARCHAR(32) NOT NULL DEFAULT ''WAIT_PICK'' COMMENT ''拣货状态'' AFTER picked_package_count'));
PREPARE dropship_outbound_stmt FROM @ddl; EXECUTE dropship_outbound_stmt; DEALLOCATE PREPARE dropship_outbound_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''一件代发SKU'' AFTER status'));
PREPARE dropship_outbound_stmt FROM @ddl; EXECUTE dropship_outbound_stmt; DEALLOCATE PREPARE dropship_outbound_stmt;
ALTER TABLE ll_outbound_order MODIFY COLUMN dispatch_order_id BIGINT NULL COMMENT '派车单ID,一件代发为空', MODIFY COLUMN dispatch_no VARCHAR(64) NULL COMMENT '派车单号,一件代发为空';
CREATE TABLE IF NOT EXISTS ll_outbound_pda_task (id BIGINT NOT NULL COMMENT '主键ID',customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',outbound_order_id BIGINT NOT NULL COMMENT '出库单ID',task_no VARCHAR(64) NOT NULL COMMENT 'PDA任务号',status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态',pushed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '推送时间',completed_at DATETIME(3) NULL COMMENT '完成时间',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 '更新时间',PRIMARY KEY(id),UNIQUE KEY uk_ll_outbound_pda_task_order(customer_id,outbound_order_id),UNIQUE KEY uk_ll_outbound_pda_task_no(customer_id,task_no)) ENGINE=InnoDB COMMENT='一件代发出库PDA拣货任务';
CREATE TABLE IF NOT EXISTS ll_outbound_pda_pick (id BIGINT NOT NULL COMMENT '主键ID',customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',task_id BIGINT NOT NULL COMMENT 'PDA任务ID',outbound_pallet_item_id BIGINT NOT NULL COMMENT '出库托盘明细ID',inventory_position_id BIGINT NOT NULL COMMENT '库存仓位ID',packages BIGINT NOT NULL COMMENT '拣货箱数',idempotency_key VARCHAR(255) NOT NULL COMMENT '拣货幂等键',picked_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拣货时间',PRIMARY KEY(id),UNIQUE KEY uk_ll_outbound_pda_pick_key(customer_id,idempotency_key)) ENGINE=InnoDB COMMENT='一件代发出库PDA托盘拣货记录';
-- ============================================================================
-- 2026-09-02 / 031
-- 海外业务费用来源与写操作幂等审计;入仓码为可选业务引用。
-- ============================================================================
CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
id BIGINT NOT NULL COMMENT '费用版本记录ID', source_fee_id BIGINT NOT NULL COMMENT '跨版本稳定费用来源ID',
source_version INT NOT NULL COMMENT '费用来源版本,从1递增', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键',
customer_name VARCHAR(200) NULL COMMENT '客户名称快照', source_type VARCHAR(64) NOT NULL COMMENT '海外费用来源类型',
source_no VARCHAR(128) NOT NULL COMMENT '来源业务编号或入仓码快照', business_type VARCHAR(32) NOT NULL COMMENT '来源业务类型',
business_id BIGINT NULL COMMENT '来源业务记录ID', business_no VARCHAR(128) NOT NULL COMMENT '来源业务编号',
order_id BIGINT NULL COMMENT '关联订单ID', inbound_id BIGINT NULL COMMENT '关联海外入仓计划或入仓记录ID',
inbound_code VARCHAR(64) NULL COMMENT '关联入仓码;并非所有费用必填', container_id BIGINT NULL COMMENT '关联柜次ID',
container_booking_id BIGINT NULL COMMENT '来源订舱柜ID', container_no VARCHAR(64) NULL COMMENT '真实集装箱号快照',
warehouse_id BIGINT NULL COMMENT '关联海外仓ID', fee_item_code VARCHAR(64) NOT NULL COMMENT '费用项目编码快照',
fee_item_name VARCHAR(200) NOT NULL COMMENT '费用项目名称快照', direction VARCHAR(16) NOT NULL COMMENT 'RECEIVABLE应收或PAYABLE应付',
supplier_id BIGINT NULL COMMENT '应付供应商ID', supplier_name VARCHAR(200) NULL COMMENT '应付供应商名称快照',
supplier_category VARCHAR(32) NULL COMMENT '应付供应商类别约束', settlement_mode VARCHAR(16) NULL COMMENT 'ON_ACCOUNT账期应付或DIRECT_PAID业务已付',
paid_at DATETIME(3) NULL COMMENT '直接已付时间', payment_method VARCHAR(32) NULL COMMENT '直接已付方式',
payment_reference_no VARCHAR(128) NULL COMMENT '直接已付流水号', payment_voucher_file_ids JSON NULL COMMENT '直接已付凭证文件ID快照',
business_date DATE NOT NULL COMMENT '费用业务日期', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '财务结算币种,当前固定CNY',
amount DECIMAL(20,6) NOT NULL COMMENT '费用金额', review_status VARCHAR(16) NOT NULL COMMENT 'DRAFT、PENDING、APPROVED或REJECTED',
publication_status VARCHAR(16) NOT NULL COMMENT 'UNPUBLISHED或PUBLISHED', source_status VARCHAR(16) NOT NULL COMMENT 'ACTIVE、SUPERSEDED或VOIDED',
review_reason VARCHAR(500) NULL COMMENT '审核或作废原因', reviewer_id VARCHAR(128) NULL COMMENT '审核人ID', reviewed_at DATETIME(3) NULL COMMENT '审核时间',
created_by VARCHAR(128) NOT NULL COMMENT '创建人ID', updated_by VARCHAR(128) NOT NULL COMMENT '最后更新人ID',
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_version(source_fee_id,source_version),
KEY idx_ll_finance_fee_customer(customer_id,source_status,created_at), KEY idx_ll_finance_fee_business(business_type,business_no,created_at),
CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0), CONSTRAINT chk_ll_finance_fee_currency CHECK(currency='CNY'),
CONSTRAINT chk_ll_finance_fee_direction CHECK(direction IN('RECEIVABLE','PAYABLE')),
CONSTRAINT chk_ll_finance_fee_review CHECK(review_status IN('DRAFT','PENDING','APPROVED','REJECTED')),
CONSTRAINT chk_ll_finance_fee_publication CHECK(publication_status IN('UNPUBLISHED','PUBLISHED')),
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256', source_fee_id BIGINT NOT NULL COMMENT '稳定费用来源ID',
source_version INT NOT NULL COMMENT '命令结果版本', created_at DATETIME(3) NOT NULL COMMENT '命令创建时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_command(operator_id,action,idempotency_key),
KEY idx_ll_finance_fee_command_source(source_fee_id,source_version)
) ENGINE=InnoDB COMMENT='海外费用来源写操作幂等审计';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_cargo_discard_application' AND column_name='review_request_hash'),
'SELECT 1', 'ALTER TABLE ll_cargo_discard_application ADD COLUMN review_request_hash CHAR(64) NULL COMMENT ''审核请求内容哈希'' AFTER review_idempotency_key'));
PREPARE phase4_discard_review_stmt FROM @ddl; EXECUTE phase4_discard_review_stmt; DEALLOCATE PREPARE phase4_discard_review_stmt;
CREATE TABLE IF NOT EXISTS ll_order_fee (
id BIGINT NOT NULL COMMENT '费用明细主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
order_id BIGINT NOT NULL COMMENT '尾程订单ID', fee_item VARCHAR(128) NOT NULL COMMENT '费用项目',
currency VARCHAR(3) NOT NULL COMMENT '币种代码', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额',
status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '费用状态:PENDING_REVIEW待审核、APPROVED审核通过、REJECTED已驳回',
supplier_description VARCHAR(500) NULL COMMENT '供应商或费用说明', created_by VARCHAR(128) NOT NULL COMMENT '创建操作人',
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 '更新时间',
PRIMARY KEY(id), KEY idx_ll_order_fee_customer_order(customer_id,order_id,created_at)
) ENGINE=InnoDB COMMENT='尾程订单费用明细';
CREATE TABLE IF NOT EXISTS ll_order_operation_log (
id BIGINT NOT NULL COMMENT '操作记录主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
order_id BIGINT NOT NULL COMMENT '尾程订单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点',
operator_name VARCHAR(128) NOT NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
PRIMARY KEY(id), KEY idx_ll_order_operation_customer_order(customer_id,order_id,created_at)
) ENGINE=InnoDB COMMENT='尾程订单操作记录';
-- 派送单列表与详情快照、转派送费用及操作记录。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='overseas_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN overseas_warehouse_name VARCHAR(255) NULL COMMENT ''海外仓名称快照'', ADD COLUMN customer_name VARCHAR(255) NULL COMMENT ''客户名称快照'', ADD COLUMN waybill_no VARCHAR(128) NULL COMMENT ''承运运单号'', ADD COLUMN delivery_method VARCHAR(32) NULL COMMENT ''派送方式'', ADD COLUMN carrier_name VARCHAR(255) NULL COMMENT ''派送承运商'', ADD COLUMN pickup_method VARCHAR(64) NULL COMMENT ''提货方式'', ADD COLUMN loading_type VARCHAR(64) NULL COMMENT ''装车类型'', ADD COLUMN vehicle_type VARCHAR(64) NULL COMMENT ''车辆类型'', ADD COLUMN vehicle_size VARCHAR(64) NULL COMMENT ''车辆大小'', ADD COLUMN vehicle_plate VARCHAR(64) NULL COMMENT ''车牌号'', ADD COLUMN driver_name VARCHAR(128) NULL COMMENT ''司机或车队名称'', ADD COLUMN driver_phone VARCHAR(64) NULL COMMENT ''车队联系电话'', ADD COLUMN appointment_account VARCHAR(128) NULL COMMENT ''预约账号'', ADD COLUMN seller_name VARCHAR(128) NULL COMMENT ''卖家姓名'', ADD COLUMN total_weight_kg DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''总重量(千克)'', ADD COLUMN total_volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''总体积(立方米)'', ADD COLUMN planned_delivery_at DATETIME(3) NULL COMMENT ''预计派送时间'', ADD COLUMN dispatched_at DATETIME(3) NULL COMMENT ''实际派送时间'', ADD COLUMN expected_arrival_at DATETIME(3) NULL COMMENT ''预计到达时间'', ADD COLUMN signed_at DATETIME(3) NULL COMMENT ''签收时间'', ADD COLUMN remark VARCHAR(1000) NULL COMMENT ''派送备注'''));
PREPARE delivery_detail_stmt FROM @ddl; EXECUTE delivery_detail_stmt; DEALLOCATE PREPARE delivery_detail_stmt;
CREATE TABLE IF NOT EXISTS ll_delivery_fee (id BIGINT NOT NULL COMMENT '派送费用主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', fee_direction VARCHAR(16) NOT NULL DEFAULT 'PAYABLE' COMMENT '应收应付方向', fee_type VARCHAR(64) NOT NULL COMMENT '费用类型', currency VARCHAR(8) NOT NULL COMMENT '币种', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', remark VARCHAR(500) NULL COMMENT '费用备注', created_by VARCHAR(128) NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), KEY idx_ll_delivery_fee_order(customer_id,delivery_order_id,id)) ENGINE=InnoDB COMMENT='派送单费用明细';
CREATE TABLE IF NOT EXISTS ll_delivery_operation (id BIGINT NOT NULL COMMENT '派送操作主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', operator_id VARCHAR(128) NULL COMMENT '操作人ID', operator_name VARCHAR(128) NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间', PRIMARY KEY(id), KEY idx_ll_delivery_operation_order(customer_id,delivery_order_id,created_at,id)) ENGINE=InnoDB COMMENT='派送单操作记录';
CREATE TABLE IF NOT EXISTS ll_exception_order (
id BIGINT NOT NULL COMMENT '异常订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', exception_no VARCHAR(64) NOT NULL COMMENT '异常订单号',
exception_type VARCHAR(32) NOT NULL COMMENT '异常类型', status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '异常处理状态', reason VARCHAR(1000) NOT NULL COMMENT '异常原因',
last_mile_order_id BIGINT NULL COMMENT '关联尾程订单ID', last_mile_order_no VARCHAR(64) NULL COMMENT '关联尾程订单号', inbound_order_id BIGINT NULL COMMENT '关联入仓单ID', inbound_code VARCHAR(128) NULL COMMENT '关联入仓码',
delivery_order_id BIGINT NULL COMMENT '关联派送单ID', delivery_no VARCHAR(64) NULL COMMENT '关联派送单号', outbound_no VARCHAR(64) NULL COMMENT '关联出库单号',
customer_name VARCHAR(255) NULL COMMENT '客户名称快照', overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外仓ID快照', overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照',
sku_code VARCHAR(128) NULL COMMENT '异常货物SKU', package_count INT NULL COMMENT '异常箱数', return_direction VARCHAR(32) NULL COMMENT '退回方向', return_logistics_no VARCHAR(128) NULL COMMENT '退回物流单号',
handling_action VARCHAR(500) NULL COMMENT '异常处理措施', handler_id VARCHAR(128) NULL COMMENT '处理人ID', handler_name VARCHAR(128) NULL COMMENT '处理人名称',
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT '异常来源类型', created_by VARCHAR(128) NULL COMMENT '创建人ID',
exception_at DATETIME(3) NOT NULL COMMENT '异常发生时间', 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 '更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_no(customer_id,exception_no),
UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type),
KEY idx_ll_exception_list(customer_id,status,exception_at), KEY idx_ll_exception_tail(customer_id,last_mile_order_no),
KEY idx_ll_exception_inbound(customer_id,inbound_code)
) ENGINE=InnoDB COMMENT='海外仓异常订单';
CREATE TABLE IF NOT EXISTS ll_customer_visit (
id BIGINT NOT NULL COMMENT '客户回访记录ID',
customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
delivery_order_id BIGINT NOT NULL COMMENT '关联派送单ID',
delivery_no VARCHAR(64) NOT NULL COMMENT '关联派送单号',
customer_name VARCHAR(255) NULL COMMENT '客户名称快照',
contact_name VARCHAR(128) NULL COMMENT '客户联系人姓名快照',
contact_phone VARCHAR(64) NULL COMMENT '客户联系人电话快照',
destination VARCHAR(500) NULL COMMENT '派送目的仓编码汇总快照',
delivery_completed_at DATETIME(3) NOT NULL COMMENT '派送完成时间',
status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '回访状态:PENDING待回访或COMPLETED已回访',
selected_last_mile_order_id BIGINT NULL COMMENT '本次回访选择的尾程订单ID',
selected_last_mile_order_no VARCHAR(64) NULL COMMENT '本次回访选择的尾程订单号',
selected_inbound_order_id BIGINT NULL COMMENT '本次回访选择的入仓单ID',
selected_inbound_code VARCHAR(128) NULL COMMENT '本次回访选择的入仓码',
visit_result VARCHAR(32) NULL COMMENT '最近一次回访结果',
customer_feedback VARCHAR(2000) NULL COMMENT '最近一次客户反馈',
remark VARCHAR(1000) NULL COMMENT '最近一次回访备注',
attachment_json JSON NULL COMMENT '当前回访资料文件元数据列表',
visited_by VARCHAR(128) NULL COMMENT '最近一次回访操作人ID',
visited_at DATETIME(3) NULL COMMENT '最近一次回访时间',
visit_count INT NOT NULL DEFAULT 0 COMMENT '累计保存回访次数',
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 '更新时间',
PRIMARY KEY(id),
UNIQUE KEY uk_ll_customer_visit_delivery(customer_id,delivery_order_id),
KEY idx_ll_customer_visit_list(customer_id,status,delivery_completed_at),
KEY idx_ll_customer_visit_delivery_no(customer_id,delivery_no)
) ENGINE=InnoDB COMMENT='派送单维度客户回访记录';
-- ============================================================================
-- 2026-08-27 / local legacy inventory-ledger cleanup
-- ============================================================================
USE baiyue_lastleg;
-- These quantity columns are absent from the current ledger contract. Legacy
-- NOT NULL definitions without defaults block package-only ledger writes.
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_ledger' AND column_name='before_quantity'), 'ALTER TABLE ll_inventory_ledger DROP COLUMN before_quantity', 'SELECT 1'));
PREPARE lastleg_ledger_legacy_001 FROM @ddl; EXECUTE lastleg_ledger_legacy_001; DEALLOCATE PREPARE lastleg_ledger_legacy_001;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_ledger' AND column_name='delta_quantity'), 'ALTER TABLE ll_inventory_ledger DROP COLUMN delta_quantity', 'SELECT 1'));
PREPARE lastleg_ledger_legacy_002 FROM @ddl; EXECUTE lastleg_ledger_legacy_002; DEALLOCATE PREPARE lastleg_ledger_legacy_002;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_ledger' AND column_name='after_quantity'), 'ALTER TABLE ll_inventory_ledger DROP COLUMN after_quantity', 'SELECT 1'));
PREPARE lastleg_ledger_legacy_003 FROM @ddl; EXECUTE lastleg_ledger_legacy_003; DEALLOCATE PREPARE lastleg_ledger_legacy_003;
@@ -59,6 +59,33 @@ test('entry accepts a separate overseas Gateway without exposing token state', (
assert.match(source, /forbiddenValues = \[this\.options\.password, this\.options\.merchantPassword,[\s\S]*this\.token, this\.refreshToken, this\.overseasToken\]/)
})
test('overseas business keeps its marker-scoped customer fixture with reusable credentials configured', () => {
const previousUsername = process.env.MERCHANT_TEST_USERNAME
const previousPassword = process.env.MERCHANT_TEST_PASSWORD
process.env.MERCHANT_TEST_USERNAME = 'reusable-client'
process.env.MERCHANT_TEST_PASSWORD = 'reusable-password'
try {
const modules = parseArguments(['--modules', 'phase4-overseas-business']).modules
assert.ok(modules.includes('merchant-qualification-fixture'))
assert.ok(modules.indexOf('merchant-qualification-fixture') < modules.indexOf('phase4-overseas-business'))
} finally {
if (previousUsername === undefined) delete process.env.MERCHANT_TEST_USERNAME
else process.env.MERCHANT_TEST_USERNAME = previousUsername
if (previousPassword === undefined) delete process.env.MERCHANT_TEST_PASSWORD
else process.env.MERCHANT_TEST_PASSWORD = previousPassword
}
})
test('a fixed origin route does not require randomized order fixtures', () => {
const options = parseArguments(['--origin-warehouse-name', '东莞仓', '--modules', 'phase4-overseas-pda'])
assert.equal(options.originWarehouseName, '东莞仓')
assert.equal(options.routeMode, 'fixed')
assert.equal(options.orderProfileMode, 'fixed')
assert.match(source, /else if \(this\.options\.originWarehouseName \|\| this\.options\.channelName \|\| this\.options\.channelCode\)/)
assert.match(source, /this\.context\.waitSplitRoute = \{[\s\S]*randomize: false/)
assert.match(source, /this\.context\.allowAnyExistingPallet = true/)
})
test('dual-token refresh replaces the whole session only after a complete response', async () => {
const run = new AcceptanceRun({ gateway: 'http://localhost:9000', overseasGateway: 'http://localhost:9300',
username: 'contract-user', password: 'contract-password', modules: [], output: '' })