This commit is contained in:
2026-10-27 17:44:41 +08:00
parent 57cd211973
commit db0e72daeb
37 changed files with 701 additions and 671 deletions
+49 -11
View File
@@ -4,6 +4,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
import { normalizeFixedCustomerId, requireFixedSecondaryCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs'
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'pda-releases', 'audit', 'dashboard', 'self-operated-analytics', 'master-data', 'suppliers', 'domestic-container', 'orders', 'inbound-hold', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'sailing-calendar', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'appointment-work', 'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-return-receipt', 'phase4-overseas-return-followups', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase5-terminal-drayage', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'fleet-management', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'phase4-overseas-wallet-and-pallet-cost', 'customer-operating-statistics']
const OPTIONAL_MODULES = ['phase5-root-fulfillment', 'manual-order-splitting-fixture', 'manual-order-splitting-verify',
@@ -324,6 +325,7 @@ function usage() {
--pda-username <name> 国内装柜链必填,实际领取任务的 PDA 作业员账号
--pda-password <value> 国内装柜链必填,PDA 作业员密码
--environment <name> 默认 test;gray 仅允许显式 FBA 定向闭环
--fixed-customer-id <id> 复用既有客户编号,灰度必填或配置 ADMIN_GRAY_FIXED_CUSTOMER_ID
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
--f40-tax-mode <mode> F40 税制:BUY_DECLARATION 或 TAX_REFUND
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
@@ -354,20 +356,39 @@ function usage() {
ADMIN_TEST_WEIGHT_IMAGE_PATH, ADMIN_TEST_CUSTOMER_NAME, MERCHANT_TEST_USERNAME,
MERCHANT_TEST_PASSWORD, ADMIN_PDA_USERNAME, ADMIN_PDA_PASSWORD, ADMIN_WAIT_SPLIT_COUNT。
gray 使用独立 ADMIN_GRAY_GATEWAY/USERNAME/PASSWORD、ADMIN_GRAY_PDA_USERNAME/PASSWORD、
ADMIN_GRAY_CUSTOMER_NAME、MERCHANT_GRAY_USERNAME/PASSWORD;不回落 TEST 凭据。`)
ADMIN_GRAY_FIXED_CUSTOMER_ID、MERCHANT_GRAY_USERNAME/PASSWORD;必须复用既有客户,不回落 TEST 凭据。`)
}
const fleetPoolGrayOptions = new WeakSet()
/** 灰度拼车只从本次显式配置读取两个既有客户,不借用TEST凭据或自动建客。 */
export function grayFixedCustomerOptions(env) {
const options = { fixedCustomerId: env.CHONGCHONG_ACCEPTANCE_CUSTOMER_ID,
merchantUsername: env.CHONGCHONG_MERCHANT_USERNAME, merchantPassword: env.CHONGCHONG_MERCHANT_PASSWORD,
fixedSecondaryCustomer: { customerId: env.CHONGCHONG_ACCEPTANCE_SECONDARY_CUSTOMER_ID,
username: env.CHONGCHONG_SECONDARY_MERCHANT_USERNAME, password: env.CHONGCHONG_SECONDARY_MERCHANT_PASSWORD } }
if (['fixedCustomerId', 'merchantUsername', 'merchantPassword'].some(key => typeof options[key] !== 'string' || !options[key].trim()))
throw Error('GRAY 必须配置 CHONGCHONG_ACCEPTANCE_CUSTOMER_ID 和 CHONGCHONG_MERCHANT_USERNAME/PASSWORD;禁止自动建客')
options.fixedCustomerId = normalizeFixedCustomerId(options.fixedCustomerId)
const secondary = requireFixedSecondaryCustomer(options)
options.fixedSecondaryCustomer = { ...secondary, customerId: normalizeFixedCustomerId(secondary.customerId) }
if (env.CHONGCHONG_ACCEPTANCE_FLEET_RESOURCES) options.fixedFleetResources = JSON.parse(env.CHONGCHONG_ACCEPTANCE_FLEET_RESOURCES)
if (env.CHONGCHONG_ACCEPTANCE_FIXED_ACTORS) options.fixedActors = JSON.parse(env.CHONGCHONG_ACCEPTANCE_FIXED_ACTORS)
if (env.CHONGCHONG_ACCEPTANCE_PDA_ACTORS) options.fixedPdaActors = JSON.parse(env.CHONGCHONG_ACCEPTANCE_PDA_ACTORS)
return options
}
/** Dedicated pool orchestrator transport only: no generic module execution or CLI scope switch. */
export function createFleetPoolGrayOptions(input) {
const keys = ['gateway', 'username', 'password', 'output', 'markerNamespace']
if (!input || Object.keys(input).some(key => !keys.includes(key))
const keys = ['gateway', 'username', 'password', 'output', 'markerNamespace', 'fixedCustomerId', 'merchantUsername', 'merchantPassword']
if (!input || Object.keys(input).some(key => ![...keys, 'fixedSecondaryCustomer', 'fixedFleetResources', 'fixedActors', 'fixedPdaActors'].includes(key))
|| keys.some(key => typeof input[key] !== 'string' || !input[key].trim())) throw Error('Explicit gray pool transport configuration required')
const customerId = normalizeFixedCustomerId(input.fixedCustomerId), secondary = requireFixedSecondaryCustomer(input)
const target = new URL(input.gateway)
if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash || target.pathname !== '/')
throw Error('Gray pool requires the unified HTTPS origin')
const options = Object.freeze({ ...input, environment: 'gray', executionScope: 'FLEET_CAPACITY_POOL_GRAY',
const options = Object.freeze({ ...input, fixedCustomerId: customerId,
fixedSecondaryCustomer: Object.freeze({ ...secondary, customerId: normalizeFixedCustomerId(secondary.customerId) }), environment: 'gray', executionScope: 'FLEET_CAPACITY_POOL_GRAY',
modules: Object.freeze([]), requestedModules: Object.freeze([]), quiet: true, waitSplitCount: 1, verifyUnloadEventRecovery: false })
fleetPoolGrayOptions.add(options)
return options
@@ -443,7 +464,7 @@ export function parseArguments(argv) {
modules,
requestedModules,
stopFile: values.get('stop-file') || '',
fixedCustomerId: environment === 'test' ? process.env.ADMIN_TEST_FIXED_CUSTOMER_ID || '' : '',
fixedCustomerId: values.get('fixed-customer-id') || envValue('ADMIN_TEST_FIXED_CUSTOMER_ID', 'ADMIN_GRAY_FIXED_CUSTOMER_ID'),
customerName: values.get('customer-name') || envValue('ADMIN_TEST_CUSTOMER_NAME', 'ADMIN_GRAY_CUSTOMER_NAME'),
merchantUsername,
merchantPassword,
@@ -484,6 +505,9 @@ export function validateExecutionOptions(options) {
if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash
|| target.pathname !== '/') throw new Error('gray gateway 必须为统一 HTTPS 源站,不带 /api、凭据或查询参数')
if (!options.output?.trim()) throw new Error('gray 必须指定独立 --output 报告路径')
if (!options.fixedCustomerId || !options.merchantUsername || !options.merchantPassword)
throw new Error('gray 必须配置 ADMIN_GRAY_FIXED_CUSTOMER_ID、MERCHANT_GRAY_USERNAME/PASSWORD;禁止自动创建客户')
normalizeFixedCustomerId(options.fixedCustomerId)
}
if (options.verifyUnloadEventRecovery && !options.modules.includes('phase4-overseas-pda')) {
throw new Error('卸柜事件恢复验收必须包含正式 phase4-overseas-pda 收货链')
@@ -655,6 +679,15 @@ export class AcceptanceRun {
constructor(options) {
assertGrayExecutionScope(options)
this.options = { ...options }
this.fixedPdaLeases = new Set()
if (options.environment === 'gray') {
this.options.fixedCustomerId ||= process.env.ADMIN_GRAY_FIXED_CUSTOMER_ID || ''
this.options.merchantUsername ||= process.env.MERCHANT_GRAY_USERNAME || ''
this.options.merchantPassword ||= process.env.MERCHANT_GRAY_PASSWORD || ''
this.options.fixedFleetResources ??= JSON.parse(process.env.ADMIN_GRAY_FLEET_RESOURCES || '[]')
this.options.fixedActors ??= JSON.parse(process.env.ADMIN_GRAY_FIXED_ACTORS || '{}')
this.options.fixedPdaActors ??= JSON.parse(process.env.ADMIN_GRAY_PDA_ACTORS || '[]')
}
if (options.environment === 'test') {
this.options.fixedCustomerId ||= process.env.ADMIN_TEST_FIXED_CUSTOMER_ID || ''
this.options.merchantUsername ||= process.env.MERCHANT_TEST_USERNAME || ''
@@ -662,6 +695,7 @@ export class AcceptanceRun {
this.options.fixedPdaActors ??= JSON.parse(process.env.ADMIN_TEST_OVERSEAS_PDA_ACTORS || '[]')
this.options.fixedDomesticDenyActor ??= JSON.parse(process.env.ADMIN_TEST_DOMESTIC_DENY_ACTOR || 'null')
this.options.fixedFleetResources ??= JSON.parse(process.env.ADMIN_TEST_FLEET_RESOURCES || '[]')
this.options.fixedActors ??= JSON.parse(process.env.ADMIN_TEST_FIXED_ACTORS || '{}')
if (!this.options.fixedSecondaryCustomer && process.env.ADMIN_TEST_SECONDARY_CUSTOMER_ID) {
this.options.fixedSecondaryCustomer = { customerId: process.env.ADMIN_TEST_SECONDARY_CUSTOMER_ID,
username: process.env.ADMIN_TEST_SECONDARY_MERCHANT_USERNAME || '', password: process.env.ADMIN_TEST_SECONDARY_MERCHANT_PASSWORD || '' }
@@ -686,12 +720,14 @@ export class AcceptanceRun {
}
assertCustomerWriteAllowed(method, path) {
if (this.options.environment !== 'test') return
const pathname = new URL(path, 'https://acceptance.invalid').pathname.replace(/\/$/, '')
if (String(method).toUpperCase() === 'POST'
&& /^\/api\/platform\/v1\/(iam\/(roles|users|permissions)|suppliers)$/.test(pathname))
throw new Error('固定资源验收禁止新增角色、账号、权限或供应商;请配置并复用既有资源')
if (String(method).toUpperCase() === 'POST' && (pathname === '/api/platform/v1/iam/tenants'
|| pathname === '/api/auth/register'
|| /^\/api\/platform\/v1\/iam\/tenants\/[^/]+\/(archive|submit-review)$/.test(pathname))) {
throw new Error('TEST 固定客户模式禁止创建、归档客户或重提资质;请复用已配置客户')
throw new Error(`${String(this.options.environment || 'acceptance').toUpperCase()} 固定客户模式禁止创建、归档客户或重提资质;请复用已配置客户`)
}
}
@@ -1888,12 +1924,13 @@ export class AcceptanceRun {
}
async run() {
if (this.options.environment === 'test' && !this.options.fixedCustomerId) {
throw new Error('TEST 验收必须配置 ADMIN_TEST_FIXED_CUSTOMER_ID;禁止自动创建测试客户')
if (['test', 'gray'].includes(this.options.environment) && !this.options.fixedCustomerId) {
throw new Error(`${this.options.environment.toUpperCase()} 验收必须配置固定客户;禁止自动创建客户`)
}
if (this.options.merchantUsername && this.options.merchantPassword) {
process.env.MERCHANT_TEST_USERNAME = this.options.merchantUsername
process.env.MERCHANT_TEST_PASSWORD = this.options.merchantPassword
const prefix = this.options.environment === 'gray' ? 'MERCHANT_GRAY' : 'MERCHANT_TEST'
process.env[`${prefix}_USERNAME`] = this.options.merchantUsername
process.env[`${prefix}_PASSWORD`] = this.options.merchantPassword
}
if (this.options.waitSplitCount > 1 || this.options.waitSplitPackageCount > 0
|| this.options.orderProfileMode === 'randomized' || this.options.routeMode === 'random'
@@ -2053,6 +2090,7 @@ export class AcceptanceRun {
createdResources: this.createdResources,
})
const forbiddenValues = [this.options.password, this.options.merchantPassword, this.options.fixedSecondaryCustomer?.password,
...Object.values(this.options.fixedActors || {}).map(actor => actor?.password),
this.token, this.refreshToken]
.filter((value) => typeof value === 'string' && value.length >= 8)
const sensitivePatterns = [