优化
This commit is contained in:
@@ -8,6 +8,8 @@ import process from 'node:process'
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', '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-customs', 'phase3-port-schedules', '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', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
|
||||
const MODULE_DEPENDENCIES = new Map([
|
||||
['suppliers', ['merchant-qualification-fixture']],
|
||||
['supplemental', ['merchant-qualification-fixture']],
|
||||
['orders', ['merchant-qualification-fixture']],
|
||||
['inbound-measurement', ['orders']],
|
||||
['phase3-container-loading', ['inbound-measurement']],
|
||||
@@ -232,11 +234,20 @@ function usage() {
|
||||
--username <name> 必填,管理员账号
|
||||
--password <value> 必填,管理员密码
|
||||
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
|
||||
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
|
||||
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
|
||||
--merchant-password <value> 复用客户账号密码,不写入报告
|
||||
--origin-warehouse-name <name> 固定国内起运仓名称
|
||||
--channel-name <name> 固定物流渠道名称
|
||||
--channel-code <code> 固定物流渠道编码
|
||||
--order-profile-mode <mode> 订单档案模式:fixed 或 randomized,默认fixed
|
||||
--wait-split-count <n> 同一客户创建的 WAIT_SPLIT 订单数,1-100,默认1
|
||||
--output <path> JSON 报告路径,默认 reports/
|
||||
--help 显示帮助
|
||||
|
||||
也可使用环境变量: ADMIN_TEST_GATEWAY, ADMIN_OVERSEAS_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD,
|
||||
ADMIN_TEST_WEIGHT_IMAGE_PATH`)
|
||||
ADMIN_TEST_WEIGHT_IMAGE_PATH, ADMIN_TEST_CUSTOMER_NAME, MERCHANT_TEST_USERNAME,
|
||||
MERCHANT_TEST_PASSWORD, ADMIN_WAIT_SPLIT_COUNT`)
|
||||
}
|
||||
|
||||
export function parseArguments(argv) {
|
||||
@@ -255,12 +266,27 @@ export function parseArguments(argv) {
|
||||
values.set(name, next); index += 1
|
||||
}
|
||||
}
|
||||
const merchantUsername = values.get('merchant-username') || process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const merchantPassword = values.get('merchant-password') || process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const modules = normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean))
|
||||
if (merchantUsername && merchantPassword) {
|
||||
const fixtureIndex = modules.indexOf('merchant-qualification-fixture')
|
||||
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1)
|
||||
}
|
||||
return {
|
||||
gateway: (values.get('gateway') || process.env.ADMIN_TEST_GATEWAY || '').replace(/\/$/, ''),
|
||||
overseasGateway: (values.get('overseas-gateway') || process.env.ADMIN_OVERSEAS_GATEWAY || '').replace(/\/$/, ''),
|
||||
username: values.get('username') || process.env.ADMIN_TEST_USERNAME || '',
|
||||
password: values.get('password') || process.env.ADMIN_TEST_PASSWORD || '',
|
||||
modules: normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean)),
|
||||
modules,
|
||||
customerName: values.get('customer-name') || process.env.ADMIN_TEST_CUSTOMER_NAME || '',
|
||||
merchantUsername,
|
||||
merchantPassword,
|
||||
originWarehouseName: values.get('origin-warehouse-name') || '',
|
||||
channelName: values.get('channel-name') || '',
|
||||
channelCode: values.get('channel-code') || '',
|
||||
orderProfileMode: values.get('order-profile-mode') || 'fixed',
|
||||
waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT || 1),
|
||||
output: values.get('output') || '',
|
||||
}
|
||||
}
|
||||
@@ -273,6 +299,32 @@ export function validateExecutionOptions(options) {
|
||||
&& (!options.overseasGateway || options.overseasGateway.trim() === '')) {
|
||||
throw new Error('四期海外模块缺少必填执行参数: overseas-gateway')
|
||||
}
|
||||
if (!Number.isInteger(options.waitSplitCount) || options.waitSplitCount < 1 || options.waitSplitCount > 100) {
|
||||
throw new Error('wait-split-count必须是1-100的整数')
|
||||
}
|
||||
if (options.customerName && options.customerName.trim().length > 128) {
|
||||
throw new Error('customer-name不能超过128个字符')
|
||||
}
|
||||
if (Boolean(options.merchantUsername) !== Boolean(options.merchantPassword)) {
|
||||
throw new Error('merchant-username和merchant-password必须同时提供')
|
||||
}
|
||||
for (const [name, value] of [['origin-warehouse-name', options.originWarehouseName],
|
||||
['channel-name', options.channelName], ['channel-code', options.channelCode]]) {
|
||||
if (value && value.trim().length > 128) throw new Error(`${name}不能超过128个字符`)
|
||||
}
|
||||
if (!['fixed', 'randomized'].includes(options.orderProfileMode)) {
|
||||
throw new Error('order-profile-mode必须为fixed或randomized')
|
||||
}
|
||||
if (options.orderProfileMode === 'randomized'
|
||||
&& (!options.originWarehouseName || !options.channelName || !options.channelCode)) {
|
||||
throw new Error('randomized订单档案必须同时固定起运仓、渠道名称和渠道编码')
|
||||
}
|
||||
if (options.waitSplitCount > 1) {
|
||||
const fixtureModules = new Set(['merchant-qualification-fixture', 'orders', 'inbound-measurement'])
|
||||
if (options.modules.some((module) => !fixtureModules.has(module))) {
|
||||
throw new Error('批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement')
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -555,6 +607,124 @@ export class AcceptanceRun {
|
||||
this.modules.push({ name, startedAt, finishedAt: new Date().toISOString(), success: fatal === null && steps.every((s) => s.passed), passed: steps.filter((s) => s.passed).length, failed: steps.filter((s) => !s.passed).length, fatal })
|
||||
}
|
||||
|
||||
captureWaitSplitFixture(sequence) {
|
||||
const order = this.createdResources.orders || {}
|
||||
const measurement = this.createdResources.inboundMeasurement || {}
|
||||
const ready = Boolean(order.orderId && measurement.inboundId
|
||||
&& String(order.tenantId) === String(measurement.tenantId)
|
||||
&& measurement.processStatus === 'WAIT_SPLIT' && measurement.finalStatus === 'APPROVED')
|
||||
this.assert('wait-split-fixtures', `固定客户第 ${sequence} 单停在待分柜`, null, ready,
|
||||
`tenantId=${measurement.tenantId || 'missing'}, orderId=${order.orderId || 'missing'}, inboundId=${measurement.inboundId || 'missing'}, processStatus=${measurement.processStatus || 'missing'}, reviewStatus=${measurement.finalStatus || 'missing'}`,
|
||||
{ scenario: 'fixed-customer-wait-split', deploymentBlocker: true })
|
||||
if (!ready) return false
|
||||
this.createdResources.waitSplitFixtures ??= []
|
||||
this.createdResources.waitSplitFixtures.push({
|
||||
sequence, tenantId: String(measurement.tenantId), orderId: String(order.orderId),
|
||||
orderNo: order.orderNo || null, inboundId: String(measurement.inboundId),
|
||||
inboundCode: order.inboundCode || null, processStatus: measurement.processStatus,
|
||||
reviewStatus: measurement.finalStatus, skuCount: order.skuLines?.length || null,
|
||||
totalPackageCount: (order.skuLines || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0),
|
||||
palletCount: measurement.palletCount || null,
|
||||
actualGrossWeightKg: measurement.actualGrossWeightKg || null,
|
||||
actualNetWeightKg: measurement.actualNetWeightKg || null,
|
||||
measurementProfile: this.context.waitSplitVariant?.measurementProfile || null,
|
||||
receivingWarehouseId: order.receivingWarehouseId || null,
|
||||
receivingWarehouseName: order.receivingWarehouseName || null,
|
||||
destinationWarehouseId: order.destinationWarehouseId || null,
|
||||
destinationWarehouseCode: order.destinationWarehouseCode || null,
|
||||
destinationWarehouseName: order.destinationWarehouseName || null,
|
||||
channelCode: order.channelCode || null,
|
||||
channelName: order.channelName || null,
|
||||
pricingTaxMode: order.pricingTaxMode || null,
|
||||
destinationType: order.destinationType || null,
|
||||
fulfillmentType: order.fulfillmentType || null,
|
||||
pickupMethod: order.pickupMethod || null,
|
||||
taxMode: order.taxMode || null,
|
||||
containerMode: order.containerMode || null,
|
||||
customsInspection: order.customsInspection ?? null,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
waitSplitVariant(sequence) {
|
||||
const dimensionProfiles = [
|
||||
{ code: 'SMALL', first: { length: 29.1, width: 19.1, height: 18.1 }, replacement: { length: 29.5, width: 19.5, height: 18.5 }, outer: { length: 34, width: 24, height: 24 } },
|
||||
{ code: 'MEDIUM', first: { length: 39.1, width: 29.1, height: 28.1 }, replacement: { length: 39.5, width: 29.5, height: 28.5 }, outer: { length: 44, width: 34, height: 34 } },
|
||||
{ code: 'LARGE', first: { length: 49.1, width: 39.1, height: 38.1 }, replacement: { length: 49.5, width: 39.5, height: 38.5 }, outer: { length: 54, width: 44, height: 44 } },
|
||||
{ code: 'EXTRA_LARGE', first: { length: 59.1, width: 49.1, height: 48.1 }, replacement: { length: 59.5, width: 49.5, height: 48.5 }, outer: { length: 64, width: 54, height: 54 } },
|
||||
]
|
||||
const skuCount = 1 + ((sequence - 1) % 4)
|
||||
const packageCountPerSku = 1 + (Math.floor((sequence - 1) / 4) % 5)
|
||||
const palletCount = 1 + ((sequence - 1) % 3)
|
||||
const grossWeightPerPalletKg = 45 + ((sequence - 1) % 6) * 15
|
||||
return {
|
||||
sequence, skuCount, packageCountPerSku, palletCount, grossWeightPerPalletKg,
|
||||
measurementProfile: dimensionProfiles[(sequence - 1) % dimensionProfiles.length],
|
||||
}
|
||||
}
|
||||
|
||||
randomizedOrderProfile(sequence) {
|
||||
const combinations = [
|
||||
['FBA', 'STANDARD'],
|
||||
['OVERSEAS', 'STANDARD'],
|
||||
['PRIVATE', 'STANDARD'],
|
||||
['FBA', 'ONE_CLICK_DROPSHIP'],
|
||||
['OVERSEAS', 'ONE_CLICK_DROPSHIP'],
|
||||
['PRIVATE', 'ONE_CLICK_DROPSHIP'],
|
||||
]
|
||||
const [destinationType, fulfillmentType] = combinations[(sequence * 5 + 1) % combinations.length]
|
||||
const pickupMethods = ['PICKUP', 'SELF_EXPRESS', 'SELF_TRUCK']
|
||||
return {
|
||||
destinationType,
|
||||
fulfillmentType,
|
||||
pickupMethod: pickupMethods[(sequence * 7 + 2) % pickupMethods.length],
|
||||
taxMode: sequence % 2 === 0 ? 'TAX_REFUND' : 'BUY_DECLARATION',
|
||||
containerMode: sequence % 2 === 0 ? 'FULL_CONTAINER' : 'LCL',
|
||||
customsInspection: sequence % 4 === 0,
|
||||
}
|
||||
}
|
||||
|
||||
applyWaitSplitVariant(sequence) {
|
||||
const variant = this.waitSplitVariant(sequence)
|
||||
this.context.waitSplitVariant = variant
|
||||
this.context.phase3LoadingIncrementalDetailCount = variant.skuCount
|
||||
this.context.phase3LoadingIncrementalPackageCount = variant.packageCountPerSku
|
||||
const profile = this.options.orderProfileMode === 'randomized'
|
||||
? this.randomizedOrderProfile(sequence)
|
||||
: { destinationType: 'FBA', fulfillmentType: 'STANDARD', pickupMethod: 'PICKUP',
|
||||
taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false }
|
||||
this.context.orderProfile = profile
|
||||
this.context.orderDestinationType = profile.destinationType
|
||||
this.context.orderFulfillmentType = profile.fulfillmentType
|
||||
this.context.waitSplitRoute = {
|
||||
originWarehouseName: this.options.originWarehouseName || '',
|
||||
channelName: this.options.channelName || '',
|
||||
channelCode: this.options.channelCode || '',
|
||||
destinationIndex: (sequence * 17 + 11),
|
||||
taxModeIndex: sequence - 1,
|
||||
}
|
||||
}
|
||||
|
||||
async createAdditionalWaitSplitFixtures() {
|
||||
const total = this.options.waitSplitCount
|
||||
if (total <= 1) return
|
||||
const baseMarker = this.marker
|
||||
const orders = await import(new URL('./admin-test-env-modules/orders.mjs', import.meta.url))
|
||||
const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url))
|
||||
try {
|
||||
for (let sequence = 2; sequence <= total; sequence += 1) {
|
||||
this.marker = `${baseMarker}-WS${String(sequence).padStart(2, '0')}`
|
||||
this.applyWaitSplitVariant(sequence)
|
||||
await this.executeModule('orders', () => orders.run(this))
|
||||
if (this.modules.at(-1)?.success !== true) break
|
||||
await this.executeModule('inbound-measurement', () => measurement.run(this))
|
||||
if (this.modules.at(-1)?.success !== true || !this.captureWaitSplitFixture(sequence)) break
|
||||
}
|
||||
} finally {
|
||||
this.marker = baseMarker
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSession(reason) {
|
||||
const response = await this.request('POST', '/api/auth/refresh',
|
||||
{ refreshToken: this.refreshToken }, '')
|
||||
@@ -1184,6 +1354,11 @@ export class AcceptanceRun {
|
||||
}
|
||||
|
||||
async run() {
|
||||
if (this.options.merchantUsername && this.options.merchantPassword) {
|
||||
process.env.MERCHANT_TEST_USERNAME = this.options.merchantUsername
|
||||
process.env.MERCHANT_TEST_PASSWORD = this.options.merchantPassword
|
||||
}
|
||||
if (this.options.waitSplitCount > 1) this.applyWaitSplitVariant(1)
|
||||
const loginResponse = await this.request('POST', '/api/auth/login', { username: this.options.username, password: this.options.password }, '')
|
||||
const login = this.apiSuccess('authentication', '平台管理员登录', loginResponse)
|
||||
if (!login?.accessToken) this.fatal = '平台管理员登录失败,未执行业务模块'
|
||||
@@ -1270,6 +1445,9 @@ export class AcceptanceRun {
|
||||
} finally {
|
||||
if (restoreFixture) restoreFixture()
|
||||
}
|
||||
if (module === 'inbound-measurement' && this.captureWaitSplitFixture(1)) {
|
||||
await this.createAdditionalWaitSplitFixtures()
|
||||
}
|
||||
const laterOverseasModule = this.options.modules.slice(moduleIndex + 1)
|
||||
.some((name) => name.startsWith('phase4-overseas-'))
|
||||
if (module === 'system' && laterOverseasModule) {
|
||||
@@ -1290,7 +1468,8 @@ export class AcceptanceRun {
|
||||
deploymentBlockers: this.deploymentBlockers, coverageGaps: this.coverageGaps,
|
||||
createdResources: this.createdResources,
|
||||
})
|
||||
const forbiddenValues = [this.options.password, this.token, this.refreshToken, this.overseasToken]
|
||||
const forbiddenValues = [this.options.password, this.options.merchantPassword,
|
||||
this.token, this.refreshToken, this.overseasToken]
|
||||
.filter((value) => typeof value === 'string' && value.length >= 8)
|
||||
const sensitivePatterns = [
|
||||
/"(?:password|newPassword|temporaryPassword|accessToken|refreshToken|authorization)"\s*:/i,
|
||||
|
||||
Reference in New Issue
Block a user