This commit is contained in:
2026-08-27 09:21:01 +08:00
parent 817e356c21
commit 554b6c93b7
8 changed files with 419 additions and 27 deletions
+71 -16
View File
@@ -250,6 +250,10 @@ function usage() {
--channel-code <code> 固定物流渠道编码
--order-profile-mode <mode> 订单档案模式:fixed 或 randomized,默认fixed
--wait-split-count <n> 同一客户创建的 WAIT_SPLIT 订单数,1-100,默认1
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
--wait-split-sequence <n> 单票全局序号,正整数,默认1
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
--manual-split-plan-id <id> 人工操作产生的智能装柜计划 ID,仅只读核验模块使用
--manual-preload-source-report <path> 人工推送预装前置或核验来源报告路径
@@ -302,6 +306,11 @@ export function parseArguments(argv) {
orderProfileMode: values.get('order-profile-mode') || 'fixed',
waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT
|| (modules.includes('manual-order-splitting-fixture') ? 6 : 1)),
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
routeMode: values.get('route-mode') || 'fixed',
quiet: values.get('quiet') === 'true',
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
manualSplitPlanId: values.get('manual-split-plan-id') || '',
manualPreloadSourceReport: values.get('manual-preload-source-report') || '',
@@ -325,6 +334,16 @@ export function validateExecutionOptions(options) {
if (!Number.isInteger(options.waitSplitCount) || options.waitSplitCount < 1 || options.waitSplitCount > 100) {
throw new Error('wait-split-count必须是1-100的整数')
}
if (!Number.isInteger(options.waitSplitPackageCount) || options.waitSplitPackageCount < 0
|| options.waitSplitPackageCount > 1000) {
throw new Error('wait-split-package-count必须是1-1000的整数或0')
}
if (!Number.isInteger(options.waitSplitSequence) || options.waitSplitSequence < 1) {
throw new Error('wait-split-sequence必须是正整数')
}
if (!Number.isInteger(options.waitSplitRandomValue) || options.waitSplitRandomValue < 1) {
throw new Error('wait-split-random-value必须是正整数')
}
if (options.customerName && options.customerName.trim().length > 128) {
throw new Error('customer-name不能超过128个字符')
}
@@ -338,7 +357,13 @@ export function validateExecutionOptions(options) {
if (!['fixed', 'randomized'].includes(options.orderProfileMode)) {
throw new Error('order-profile-mode必须为fixed或randomized')
}
if (options.orderProfileMode === 'randomized'
if (!['fixed', 'random'].includes(options.routeMode)) {
throw new Error('route-mode必须为fixed或random')
}
if (options.routeMode === 'random' && !options.originWarehouseName) {
throw new Error('random线路必须固定起运仓名称')
}
if (options.orderProfileMode === 'randomized' && options.routeMode !== 'random'
&& (!options.originWarehouseName || !options.channelName || !options.channelCode)) {
throw new Error('randomized订单档案必须同时固定起运仓、渠道名称和渠道编码')
}
@@ -445,7 +470,8 @@ export function buildAcceptanceSummary(steps = [], modules = [], coverageGaps =
export class AcceptanceRun {
constructor(options) {
this.options = options
this.marker = `ADMIN-API-${new Date().toISOString().replace(/\D/g, '').slice(0, 17)}`
const markerNamespace = String(options.markerNamespace || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 12)
this.marker = `ADMIN-API-${new Date().toISOString().replace(/\D/g, '').slice(0, 17)}${markerNamespace ? `-${markerNamespace}` : ''}`
this.startedAt = new Date().toISOString()
this.steps = []
this.modules = []
@@ -620,7 +646,9 @@ export class AcceptanceRun {
}
this.steps.push(step)
if (step.deploymentBlocker) this.deploymentBlockers.push({ module, scenario: step.scenario, name, evidence, code: step.code, requestId: step.requestId, traceId: step.traceId })
console.log(`[${step.passed ? 'PASS' : 'FAIL'}] [${module}${step.scenario ? `/${step.scenario}` : ''}] ${name}${evidence ? ` - ${evidence}` : ''}`)
if (!this.options.quiet) {
console.log(`[${step.passed ? 'PASS' : 'FAIL'}] [${module}${step.scenario ? `/${step.scenario}` : ''}] ${name}${evidence ? ` - ${evidence}` : ''}`)
}
return step.passed
}
@@ -706,13 +734,33 @@ export class AcceptanceRun {
{ 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
const randomValue = this.options.waitSplitRandomValue || sequence
const requestedTotal = Number(this.options.waitSplitPackageCount || 0)
const skuCount = requestedTotal > 0
? 1 + (randomValue % Math.min(4, requestedTotal))
: 1 + ((sequence - 1) % 4)
const packageCountPerSku = requestedTotal > 0 ? 1 : 1 + (Math.floor((sequence - 1) / 4) % 5)
const packageCounts = []
if (requestedTotal > 0) {
let remaining = requestedTotal
let state = randomValue >>> 0
for (let index = 0; index < skuCount - 1; index += 1) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0
const maximum = remaining - (skuCount - index - 1)
const count = 1 + (state % maximum)
packageCounts.push(count)
remaining -= count
}
packageCounts.push(remaining)
}
const totalPackageCount = requestedTotal || skuCount * packageCountPerSku
const palletCount = requestedTotal > 0 ? Math.ceil(totalPackageCount / 30) : 1 + (randomValue % 3)
const grossWeightPerPalletKg = 45 + (randomValue % 6) * 15
return {
sequence, skuCount, packageCountPerSku, palletCount, grossWeightPerPalletKg,
measurementProfile: dimensionProfiles[(sequence - 1) % dimensionProfiles.length],
sequence, skuCount, packageCountPerSku, packageCounts,
totalPackageCount,
palletCount, grossWeightPerPalletKg,
measurementProfile: dimensionProfiles[randomValue % dimensionProfiles.length],
}
}
@@ -725,15 +773,16 @@ export class AcceptanceRun {
['OVERSEAS', 'ONE_CLICK_DROPSHIP'],
['PRIVATE', 'ONE_CLICK_DROPSHIP'],
]
const [destinationType, fulfillmentType] = combinations[(sequence * 5 + 1) % combinations.length]
const randomValue = this.options.waitSplitRandomValue || sequence
const [destinationType, fulfillmentType] = combinations[randomValue % 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,
pickupMethod: pickupMethods[Math.floor(randomValue / 7) % pickupMethods.length],
taxMode: randomValue % 2 === 0 ? 'TAX_REFUND' : 'BUY_DECLARATION',
containerMode: Math.floor(randomValue / 11) % 2 === 0 ? 'FULL_CONTAINER' : 'LCL',
customsInspection: Math.floor(randomValue / 13) % 4 === 0,
}
}
@@ -753,6 +802,8 @@ export class AcceptanceRun {
originWarehouseName: this.options.originWarehouseName || '',
channelName: this.options.channelName || '',
channelCode: this.options.channelCode || '',
randomize: this.options.routeMode === 'random',
randomIndex: this.options.waitSplitRandomValue || sequence,
destinationIndex: (sequence * 17 + 11),
taxModeIndex: sequence - 1,
}
@@ -1421,7 +1472,10 @@ export class AcceptanceRun {
process.env.MERCHANT_TEST_USERNAME = this.options.merchantUsername
process.env.MERCHANT_TEST_PASSWORD = this.options.merchantPassword
}
if (this.options.waitSplitCount > 1) this.applyWaitSplitVariant(1)
if (this.options.waitSplitCount > 1 || this.options.waitSplitPackageCount > 0
|| this.options.orderProfileMode === 'randomized' || this.options.routeMode === 'random') {
this.applyWaitSplitVariant(this.options.waitSplitSequence || 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 = '平台管理员登录失败,未执行业务模块'
@@ -1508,7 +1562,8 @@ export class AcceptanceRun {
} finally {
if (restoreFixture) restoreFixture()
}
if (module === 'inbound-measurement' && this.captureWaitSplitFixture(1)) {
if (module === 'inbound-measurement'
&& this.captureWaitSplitFixture(this.options.waitSplitSequence || 1)) {
await this.createAdditionalWaitSplitFixtures()
}
const laterOverseasModule = this.options.modules.slice(moduleIndex + 1)