2026-08-30 17:02:19 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为已授权的非生产环境尽力生成 IT测试/义乌仓 WAIT_SPLIT 数据。
|
|
|
|
|
*
|
|
|
|
|
* 每票随机 1-300 箱(可通过参数缩小),失败会记录后继续;不会自动清理已写入数据。
|
|
|
|
|
* 测量前置只复用 TP-001/YW-TP-002,不创建托盘;库位由正式仓储接口分配,
|
|
|
|
|
* 普通位不足时由服务端落公共位 A0,脚本不创建库位。
|
|
|
|
|
* 账号、密码和 Gateway 只从 admin-test-env-acceptance.mjs 支持的环境变量读取。
|
|
|
|
|
*/
|
2026-08-27 09:21:01 +08:00
|
|
|
import { randomInt } from 'node:crypto'
|
|
|
|
|
import { appendFile, mkdir, writeFile } from 'node:fs/promises'
|
|
|
|
|
import { resolve } from 'node:path'
|
|
|
|
|
import process from 'node:process'
|
|
|
|
|
|
|
|
|
|
import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs'
|
|
|
|
|
|
|
|
|
|
function argumentsMap(argv) {
|
|
|
|
|
const values = new Map()
|
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
|
|
|
const argument = argv[index]
|
|
|
|
|
if (!argument.startsWith('--')) throw new Error(`无法识别的参数: ${argument}`)
|
|
|
|
|
const name = argument.slice(2)
|
|
|
|
|
const value = argv[index + 1]
|
|
|
|
|
if (!value || value.startsWith('--')) throw new Error(`参数 --${name} 缺少值`)
|
|
|
|
|
values.set(name, value)
|
|
|
|
|
index += 1
|
|
|
|
|
}
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function positiveInteger(value, fallback, name) {
|
|
|
|
|
const parsed = Number(value ?? fallback)
|
|
|
|
|
if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${name}必须是正整数`)
|
|
|
|
|
return parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function increment(record, key) {
|
|
|
|
|
const normalized = String(key || 'UNKNOWN')
|
|
|
|
|
record[normalized] = Number(record[normalized] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
const values = argumentsMap(process.argv.slice(2))
|
|
|
|
|
const count = positiveInteger(values.get('count'), 8000, 'count')
|
|
|
|
|
const batchSize = positiveInteger(values.get('batch-size'), 100, 'batch-size')
|
|
|
|
|
const startIndex = positiveInteger(values.get('start-index'), 1, 'start-index')
|
|
|
|
|
const boxMin = positiveInteger(values.get('box-min'), 1, 'box-min')
|
|
|
|
|
const boxMax = positiveInteger(values.get('box-max'), 300, 'box-max')
|
|
|
|
|
if (boxMax < boxMin || boxMax > 300) throw new Error('箱数范围必须满足1 <= box-min <= box-max <= 300')
|
|
|
|
|
const customerName = values.get('customer-name') || 'IT测试'
|
|
|
|
|
const originWarehouseName = values.get('origin-warehouse-name') || '义乌仓'
|
|
|
|
|
const workerId = values.get('worker-id') || 'worker-1'
|
|
|
|
|
const timestamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14)
|
|
|
|
|
const outputDirectory = resolve(values.get('output-dir') || `reports/it-test-wait-split-8000-${timestamp}`)
|
|
|
|
|
const journalPath = resolve(outputDirectory, 'results.jsonl')
|
|
|
|
|
const summaryPath = resolve(outputDirectory, 'summary.json')
|
|
|
|
|
const failureDirectory = resolve(outputDirectory, 'failures')
|
|
|
|
|
await mkdir(failureDirectory, { recursive: true })
|
|
|
|
|
|
|
|
|
|
const summary = {
|
|
|
|
|
workerId, customerName, originWarehouseName, requested: count, startIndex, batchSize,
|
|
|
|
|
boxMin, boxMax, attempted: 0, succeeded: 0, failed: 0, totalPackages: 0, totalPallets: 0,
|
|
|
|
|
minPackages: null, maxPackages: null, channels: {}, destinationTypes: {},
|
|
|
|
|
processId: process.pid, startedAt: new Date().toISOString(),
|
|
|
|
|
updatedAt: new Date().toISOString(), outputDirectory,
|
|
|
|
|
}
|
|
|
|
|
await writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
|
|
|
|
console.log(`OUTPUT_DIR=${outputDirectory}`)
|
|
|
|
|
|
|
|
|
|
for (let offset = 0; offset < count; offset += 1) {
|
|
|
|
|
const sequence = startIndex + offset
|
|
|
|
|
const packageCount = randomInt(boxMin, boxMax + 1)
|
|
|
|
|
const randomValue = randomInt(1, 2_147_483_647)
|
|
|
|
|
const batchNo = Math.floor(offset / batchSize) + 1
|
|
|
|
|
const planned = { event: 'planned', sequence, batchNo, packageCount, randomValue,
|
|
|
|
|
plannedAt: new Date().toISOString() }
|
|
|
|
|
await appendFile(journalPath, `${JSON.stringify(planned)}\n`, 'utf8')
|
|
|
|
|
|
|
|
|
|
let report = null
|
|
|
|
|
let thrown = null
|
|
|
|
|
try {
|
|
|
|
|
const options = validateExecutionOptions(parseArguments([
|
|
|
|
|
'--modules', 'orders,inbound-measurement',
|
|
|
|
|
'--customer-name', customerName,
|
|
|
|
|
'--origin-warehouse-name', originWarehouseName,
|
|
|
|
|
'--order-profile-mode', 'randomized',
|
|
|
|
|
'--route-mode', 'random',
|
|
|
|
|
'--wait-split-count', '1',
|
|
|
|
|
'--wait-split-package-count', String(packageCount),
|
|
|
|
|
'--wait-split-sequence', String(sequence),
|
|
|
|
|
'--wait-split-random-value', String(randomValue),
|
|
|
|
|
'--quiet', 'true',
|
|
|
|
|
]))
|
|
|
|
|
options.markerNamespace = workerId
|
|
|
|
|
report = await new AcceptanceRun(options).run()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
thrown = error instanceof Error ? error.message : String(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fixture = report?.createdResources?.waitSplitFixtures?.find?.(
|
|
|
|
|
(item) => Number(item.sequence) === sequence)
|
|
|
|
|
const completed = Boolean(fixture?.processStatus === 'WAIT_SPLIT'
|
|
|
|
|
&& fixture?.reviewStatus === 'APPROVED'
|
|
|
|
|
&& Number(fixture?.totalPackageCount) === packageCount
|
|
|
|
|
&& Number(fixture?.palletCount) === Math.ceil(packageCount / 30)
|
|
|
|
|
&& fixture?.receivingWarehouseName === originWarehouseName)
|
|
|
|
|
const result = {
|
|
|
|
|
event: 'result', sequence, batchNo, packageCount, completed,
|
|
|
|
|
orderId: fixture?.orderId || null, orderNo: fixture?.orderNo || null,
|
|
|
|
|
inboundId: fixture?.inboundId || null, inboundCode: fixture?.inboundCode || null,
|
|
|
|
|
palletCount: fixture?.palletCount || null,
|
|
|
|
|
channelCode: fixture?.channelCode || null, channelName: fixture?.channelName || null,
|
|
|
|
|
destinationType: fixture?.destinationType || null,
|
|
|
|
|
destinationWarehouseCode: fixture?.destinationWarehouseCode || null,
|
|
|
|
|
reportSuccess: report?.success ?? false, assertionFailures: report?.summary?.failed ?? null,
|
|
|
|
|
deploymentBlockers: report?.deploymentBlockers || [], error: thrown,
|
|
|
|
|
finishedAt: new Date().toISOString(),
|
|
|
|
|
}
|
|
|
|
|
await appendFile(journalPath, `${JSON.stringify(result)}\n`, 'utf8')
|
|
|
|
|
summary.attempted += 1
|
|
|
|
|
if (completed) {
|
|
|
|
|
summary.succeeded += 1
|
|
|
|
|
summary.totalPackages += packageCount
|
|
|
|
|
summary.totalPallets += Number(fixture.palletCount)
|
|
|
|
|
summary.minPackages = summary.minPackages === null ? packageCount : Math.min(summary.minPackages, packageCount)
|
|
|
|
|
summary.maxPackages = summary.maxPackages === null ? packageCount : Math.max(summary.maxPackages, packageCount)
|
|
|
|
|
increment(summary.channels, fixture.channelCode)
|
|
|
|
|
increment(summary.destinationTypes, fixture.destinationType)
|
|
|
|
|
} else {
|
|
|
|
|
summary.failed += 1
|
|
|
|
|
if (report) {
|
|
|
|
|
await writeFile(resolve(failureDirectory, `sequence-${String(sequence).padStart(5, '0')}.json`),
|
|
|
|
|
`${JSON.stringify(report, null, 2)}\n`, 'utf8')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
summary.updatedAt = new Date().toISOString()
|
|
|
|
|
summary.averagePackages = summary.succeeded > 0
|
|
|
|
|
? Number((summary.totalPackages / summary.succeeded).toFixed(3)) : null
|
|
|
|
|
summary.averagePallets = summary.succeeded > 0
|
|
|
|
|
? Number((summary.totalPallets / summary.succeeded).toFixed(3)) : null
|
|
|
|
|
if (summary.attempted % batchSize === 0 || !completed || summary.attempted === count) {
|
|
|
|
|
await writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
|
|
|
|
console.log(`PROGRESS attempted=${summary.attempted}/${count} succeeded=${summary.succeeded} failed=${summary.failed} batch=${batchNo}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
summary.finishedAt = new Date().toISOString()
|
|
|
|
|
await writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
|
|
|
|
console.log(`RESULT attempted=${summary.attempted} succeeded=${summary.succeeded} failed=${summary.failed}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
console.error(error instanceof Error ? error.message : String(error))
|
|
|
|
|
process.exitCode = 1
|
|
|
|
|
})
|