This commit is contained in:
2026-08-24 19:58:35 +08:00
parent 9e0f41e78b
commit c93fdc44d5
23 changed files with 965 additions and 313 deletions
+102 -25
View File
@@ -1,18 +1,10 @@
#!/usr/bin/env node
// Test environment administrator / 测试环境管理员
// Username: platform_admin
// Password: ChangeMe123!
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
const DEFAULT_GATEWAY = 'http://8.163.30.128:9000'
const DEFAULT_OVERSEAS_GATEWAY = 'http://8.163.30.128:9300'
const DEFAULT_USERNAME = 'platform_admin'
const DEFAULT_PASSWORD = 'ChangeMe123!'
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-overseas-pda', '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']
export function normalizeModuleOrder(modules) {
@@ -65,6 +57,13 @@ export function normalizeModuleOrder(modules) {
&& !normalized.includes('merchant-qualification-fixture')) {
normalized.splice(normalized.indexOf('phase4-overseas-business'), 0, 'merchant-qualification-fixture')
}
if (normalized.includes('phase4-overseas-pda')) {
const prerequisites = ['merchant-qualification-fixture', 'phase3-loading', 'phase3-pda-convergence',
'phase3-evidence-gate', 'phase3-loading-finalize']
const targetIndex = normalized.indexOf('phase4-overseas-pda')
const missing = prerequisites.filter((name) => !normalized.includes(name))
if (missing.length > 0) normalized.splice(targetIndex, 0, ...missing)
}
if (normalized.includes('phase4-cross-region-events')) {
const prerequisites = ['system', 'merchant-qualification-fixture', 'orders', 'inbound-measurement',
'phase3-container-loading', 'phase3-loading', 'phase3-pda-convergence', 'phase3-evidence-gate',
@@ -130,7 +129,7 @@ export function normalizeModuleOrder(modules) {
}
const DEFAULT_MODULES = normalizeModuleOrder(CONFIGURED_MODULES)
const WEIGHT_IMAGE_PATH = 'C:/Users/BY-OY/Desktop/上传图片包/测量图片/测量图片/orig_1cf27695-b2c3-44b3-98e3-c0be8b80f79a.JPG'
const WEIGHT_IMAGE_PATH = process.env.ADMIN_TEST_WEIGHT_IMAGE_PATH || ''
const CUSTOMER_FIELDS = [
'id', 'tenantCode', 'tenantName', 'status', 'legalName', 'legalRepresentative',
'contactName', 'contactPhone', 'contactEmail', 'contactWechat',
@@ -148,21 +147,20 @@ const BOX_DEFAULTS = [
function usage() {
console.log(`后台管理测试环境 HTTP API 验收
测试管理员: ${DEFAULT_USERNAME} / ${DEFAULT_PASSWORD}
用法:
node scripts/admin-test-env-acceptance.mjs [options]
选项:
--gateway <url> Gateway 地址,默认 ${DEFAULT_GATEWAY}
--overseas-gateway <url> 海外 Gateway 地址,默认 ${DEFAULT_OVERSEAS_GATEWAY}
--username <name> 管理员账号,默认 ${DEFAULT_USERNAME}
--password <value> 管理员密码,默认使用上方测试密码
--gateway <url> 必填,国内 Gateway 地址
--overseas-gateway <url> 四期海外模块必填,海外 Gateway 地址
--username <name> 必填,管理员账号
--password <value> 必填,管理员密码
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
--output <path> JSON 报告路径,默认 reports/
--help 显示帮助
环境变量覆盖: ADMIN_TEST_GATEWAY, ADMIN_OVERSEAS_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD`)
也可使用环境变量: ADMIN_TEST_GATEWAY, ADMIN_OVERSEAS_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD,
ADMIN_TEST_WEIGHT_IMAGE_PATH`)
}
export function parseArguments(argv) {
@@ -182,15 +180,26 @@ export function parseArguments(argv) {
}
}
return {
gateway: (values.get('gateway') || process.env.ADMIN_TEST_GATEWAY || DEFAULT_GATEWAY).replace(/\/$/, ''),
overseasGateway: (values.get('overseas-gateway') || process.env.ADMIN_OVERSEAS_GATEWAY || DEFAULT_OVERSEAS_GATEWAY).replace(/\/$/, ''),
username: values.get('username') || process.env.ADMIN_TEST_USERNAME || DEFAULT_USERNAME,
password: values.get('password') || process.env.ADMIN_TEST_PASSWORD || DEFAULT_PASSWORD,
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)),
output: values.get('output') || '',
}
}
export function validateExecutionOptions(options) {
for (const [name, value] of [['gateway', options.gateway], ['username', options.username], ['password', options.password]]) {
if (typeof value !== 'string' || value.trim() === '') throw new Error(`缺少必填执行参数: ${name}`)
}
if (options.modules.some((module) => module.startsWith('phase4-overseas-'))
&& (!options.overseasGateway || options.overseasGateway.trim() === '')) {
throw new Error('四期海外模块缺少必填执行参数: overseas-gateway')
}
return options
}
function own(value, field) { return value !== null && typeof value === 'object' && Object.hasOwn(value, field) }
function hasFields(value, fields) { return fields.every((field) => own(value, field)) }
function numberEquals(actual, expected) { return Math.abs(Number(actual) - Number(expected)) < 0.0005 }
@@ -383,7 +392,13 @@ export class AcceptanceRun {
}
addCoverageGap(module, scenario, name, reason, category = 'not-implemented', metadata = {}) {
this.coverageGaps.push({ module, scenario, name, reason, category, ...metadata })
const nonBlocking = new Set(['device', 'external-third-party', 'visual'])
const blocking = metadata.blocking ?? !nonBlocking.has(category)
this.coverageGaps.push({ module, scenario, name, reason, category, blocking, ...metadata })
if (blocking) {
this.addStep(module, `覆盖阻断:${name}`, null, false, reason,
{ scenario, deploymentBlocker: true })
}
}
apiSuccess(module, name, response, evidence = '', options = {}) {
@@ -757,9 +772,9 @@ export class AcceptanceRun {
}
}
async preparePhase3LoadingFixture() {
async preparePhase3LoadingFixture(suffix = '') {
const originalMarker = this.marker
const fixtureMarker = `${originalMarker}-LOAD`
const fixtureMarker = `${originalMarker}-LOAD${suffix}`
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound',
'phase3LoadingRequireSharedOverflow', 'phase3LoadingIncrementalPackageCount']
@@ -798,7 +813,7 @@ export class AcceptanceRun {
internalRemark: fixtureOrder.internalRemark,
cargoRemark: fixtureOrder.cargoRemark,
} : { marker: originalMarker, sourceMarker: fixtureMarker, failed: true }
const incrementalMarker = `${originalMarker}-LOAD-INC`
const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC`
this.marker = incrementalMarker
this.context.phase3LoadingRequireSharedOverflow = false
this.context.phase3LoadingIncrementalPackageCount = 2
@@ -834,6 +849,59 @@ export class AcceptanceRun {
}
}
async runPhase4ProducerVariant(label, destinationType) {
const matrixMarker = this.marker
const variantMarker = `${matrixMarker}-P4-${label}`
const contextKeys = ['orderDestinationType', 'phase4ProducerMatrixMarker']
const createdKeys = ['phase3Loading', 'phase3LoadingCompletedProducer']
const contextSnapshot = new Map(contextKeys.map((key) => [key, {
present: Object.hasOwn(this.context, key), value: this.context[key],
}]))
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
present: Object.hasOwn(this.createdResources, key), value: this.createdResources[key],
}]))
let restoreFixture = null
this.marker = variantMarker
this.context.orderDestinationType = destinationType
this.context.phase4ProducerMatrixMarker = matrixMarker
try {
restoreFixture = await this.preparePhase3LoadingFixture()
const loading = await import(new URL('./admin-test-env-modules/phase3-loading.mjs', import.meta.url))
await this.executeModule(`phase3-loading-phase4-${label}`, () => loading.run(this))
const finalize = await import(new URL('./admin-test-env-modules/phase3-loading-finalize.mjs', import.meta.url))
await this.executeModule(`phase3-loading-finalize-phase4-${label}`, () => finalize.run(this))
} finally {
if (restoreFixture) restoreFixture()
this.marker = matrixMarker
for (const [key, snapshot] of contextSnapshot) {
if (snapshot.present) this.context[key] = snapshot.value
else delete this.context[key]
}
for (const [key, snapshot] of createdSnapshot) {
if (snapshot.present) this.createdResources[key] = snapshot.value
else delete this.createdResources[key]
}
}
}
async ensurePhase4PdaProducerMatrix() {
const readiness = () => this.createdResources.phase4InboundProducerFixtures?.readiness || {}
if (Number(readiness().fbaContainerCount || 0) < 1) {
await this.runPhase4ProducerVariant('FBA-PRIMARY', 'FBA')
}
if (Number(readiness().overseasContainerCount || 0) < 1) {
await this.runPhase4ProducerVariant('OVERSEAS-PRIMARY', 'OVERSEAS')
}
if (Number(readiness().distinctContainerCount || 0) < 3) {
await this.runPhase4ProducerVariant('SECONDARY', 'FBA')
}
const matrix = this.createdResources.phase4InboundProducerFixtures
this.assert('phase4-overseas-pda-fixture', '本轮三期跨区生产矩阵已就绪', null,
matrix?.marker === this.marker && matrix?.readiness?.readyForPhase4Pda === true,
`marker=${matrix?.marker || 'missing'}, FBA=${matrix?.readiness?.fbaContainerCount ?? 0}, OVERSEAS=${matrix?.readiness?.overseasContainerCount ?? 0}, containers=${matrix?.readiness?.distinctContainerCount ?? 0}`,
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
async disableAvailableLocationsForA0(warehouseId) {
const module = 'phase3-loading-fixture'
const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true }
@@ -912,6 +980,15 @@ export class AcceptanceRun {
continue
}
let restoreFixture = null
if (module === 'phase4-overseas-pda') {
try {
await this.ensurePhase4PdaProducerMatrix()
} catch (error) {
const reason = error instanceof Error ? error.message : String(error)
this.addStep('phase4-overseas-pda-fixture', '创建FBA/OVERSEAS/第二柜三期生产矩阵', null,
false, reason, { scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
}
const ocrIndex = this.options.modules.indexOf('inbound-weight-ocr')
if (module === 'inbound-measurement' && ocrIndex >= 0 && ocrIndex < moduleIndex) {
try {
@@ -1001,7 +1078,7 @@ export class AcceptanceRun {
}
async function main() {
const options = parseArguments(process.argv.slice(2))
const options = validateExecutionOptions(parseArguments(process.argv.slice(2)))
const timestamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14)
const outputPath = resolve(options.output || `reports/admin-test-env-acceptance-${timestamp}.json`)
const report = await new AcceptanceRun(options).run()