import { mkdir, readFile, writeFile } from 'node:fs/promises' import { parseEnv } from 'node:util' import { resolve, dirname } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { AcceptanceRun } from './admin-test-env-acceptance.mjs' import { run as prepareCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs' import { verifyProcurementSample } from './admin-test-env-modules/procurement-sample-fulfillment.mjs' const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64') export function options(args) { const value = name => { const index = args.indexOf(name); return index < 0 ? '' : args[index + 1] || '' } if (!args.includes('--execute')) return { execute: false } const runId = value('--run-id'), warehouseId = value('--warehouse-id') if (value('--environment') !== 'TEST' || !/^[A-Za-z0-9-]{1,32}$/.test(runId) || !/^[1-9]\d*$/.test(warehouseId)) throw new Error('须显式指定 TEST、唯一 run-id 和已核对的有效收货仓') return { execute: true, runId, warehouseId } } export function accountForCustomer(data, customerId) { if (!Array.isArray(data)) throw new Error('Finance账户响应须为正式数组结构') const account = data.find(row => String(row.customerId) === String(customerId)) if (!account) throw new Error('账户响应未包含本轮独立客户') return account } export async function execute(args = process.argv.slice(2), execArgs = process.execArgv) { const config = options(args) if (!config.execute) return { mode: 'PLAN', writes: ['创建独立客户并审核资质', '仅为本轮客户充值10元', '上传样品凭证和面单', '执行采购两箱完整正常流程'], sql: false, deploy: false, cleanup: '保留样本,退出自身会话;未知结果不自动重跑' } const flag = execArgs.find(value => value.startsWith('--env-file=')) const envPath = flag ? resolve(flag.slice('--env-file='.length)) : '' if (!envPath.replaceAll('\\','/').endsWith('/.codex/env/chongchong-global/test-acceptance.env')) throw new Error('脚本前必须显式选择授权 TEST 环境文件') const env = parseEnv(await readFile(envPath, 'utf8')) if (env.CHONGCHONG_ENV !== 'test' || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw new Error('TEST身份配置缺失') const gateway = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL) if (!['http:', 'https:'].includes(gateway.protocol) || gateway.pathname.replace(/\/$/,'') !== '/api' || gateway.username || gateway.password || gateway.search || gateway.hash) throw new Error('TEST统一入口格式不符') const repository = resolve(dirname(fileURLToPath(import.meta.url)), '..') if (!repository.replaceAll('\\','/').endsWith('/chongchong-global/baiyue-firstleg')) throw new Error('只能在 TEST 工作区执行') const output = resolve(repository, `reports/procurement-sample-${config.runId}/report.json`) await mkdir(dirname(output), { recursive: true }) const context = new AcceptanceRun({ gateway: gateway.origin, environment: 'test', quiet: true, modules: [] }) context.marker = `PROC-${config.runId}` const report = { marker: context.marker, status: 'PREPARING', startedAt: new Date().toISOString(), requests: [], sessions: [], resources: context.createdResources, steps: context.steps } const secrets = new Set([env.CHONGCHONG_ADMIN_PASSWORD]) const sessions = [] function safe(value) { if (Array.isArray(value)) return value.map(safe) if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key,item]) => [key, /password|token|loginId/i.test(key) ? '[REDACTED]' : safe(item)])) if (typeof value === 'string') { for (const secret of secrets) if (secret && secret.length >= 8) value = value.replaceAll(secret, '[REDACTED]') } return value } async function checkpoint(first = false) { await writeFile(output, JSON.stringify(safe(report), null, 2) + '\n', { encoding: 'utf8', flag: first ? 'wx' : 'w' }) } await checkpoint(true) context.checkpoint = checkpoint const rawRequest = context.request.bind(context), rawForm = context.requestWithBody.bind(context) async function recorded(send, method, path, body, token, headers) { const entry = { method, path, body: body instanceof FormData ? { multipart: true, fields: [...body.keys()] } : safe(body), status: 'SENDING' } report.requests.push(entry); await checkpoint() let response try { response = await send(method, path, body, token, headers) } catch (error) { entry.status = 'UNKNOWN_RESULT'; await checkpoint(); throw error } for (const field of ['accessToken','refreshToken','temporaryPassword']) if (response?.body?.data?.[field]) secrets.add(response.body.data[field]) const unknown = response?.http === 0 || response?.http >= 500 || !response?.body Object.assign(entry, { status: unknown ? 'UNKNOWN_RESULT' : 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await checkpoint() if (unknown) throw new Error('请求结果未知,已保留原请求,禁止自动重跑') return response } context.request = (method,path,body,token=context.token,headers={}) => recorded(rawRequest,method,path,body,token,headers) context.requestWithBody = (method,path,body,token=context.token,headers={}) => recorded(rawForm,method,path,body,token,headers) const requireSuccess = response => { if (response?.http !== 200 || String(response?.body?.code) !== '200') throw new Error(`业务请求失败:${response?.http}/${response?.body?.code}`) return response.body.data } async function login(username,password,actor) { const data = requireSuccess(await context.request('POST','/api/auth/login',{loginId:username,password},'')) if (!data?.accessToken || !data?.refreshToken) throw new Error('登录响应缺少会话引用') sessions.push({ actor, accessToken: data.accessToken, refreshToken: data.refreshToken }) report.sessions.push({ actor, login: 'SUCCESS', logout: 'NOT_STARTED' }); await checkpoint() return data.accessToken } let failed = false try { context.token = await login(env.CHONGCHONG_ADMIN_USERNAME,env.CHONGCHONG_ADMIN_PASSWORD,'platform') const warehouses = requireSuccess(await context.request('GET','/api/platform/v1/last-leg/overseas-procurements/warehouse-options')) if (!Array.isArray(warehouses) || !warehouses.some(row => String(row.warehouseId) === config.warehouseId)) throw new Error('指定收货仓不在有效采购仓候选中') await prepareCustomer(context,{publishEnvironment:false}) if (context.steps.some(step => !step.passed)) throw new Error('独立客户准备检查未全部通过') const fixture = context.context.merchantQualificationFixture const customerId = String(fixture.tenantId), customerToken = await login(fixture.username,fixture.password,'customer') const readWallet = async () => { const page = requireSuccess(await context.request('GET',`/api/platform/finance/v1/accounts?page=1&pageSize=20&customerId=${customerId}`)) return accountForCustomer(page, customerId) } const before = await readWallet() requireSuccess(await context.request('POST',`/api/platform/finance/v1/accounts/${customerId}/adjustments`,{ type:'RECHARGE', amount:'10.00', occurredDate:new Date().toISOString().slice(0,10), paymentMethod:'BANK_TRANSFER', referenceNo:context.marker, remark:context.marker, expectedAccountVersion:before.accountVersion },context.token,{'Idempotency-Key':`${context.marker}-fund`})) const upload = async (orderNo,category) => requireSuccess(await context.requestMultipart('POST',`/api/platform/v1/last-leg/overseas-procurements/${encodeURIComponent(orderNo)}/files/${category}?customerId=${customerId}`,{ content:PNG,type:'image/png',name:`${category.toLowerCase()}.png` })) report.status = 'RUNNING'; await checkpoint() await verifyProcurementSample(context,{ marker:context.marker,customerId,customerName:fixture.tenantName,customerToken,warehouseId:config.warehouseId,readWallet,upload }) report.status = 'PASSED_NORMAL_FLOW' } catch (error) { failed = true; report.status = 'FAILED'; report.failure = safe(error.message) } finally { for (const session of sessions.reverse()) { const state = report.sessions.find(item => item.actor === session.actor) try { const response = await rawRequest('POST','/api/auth/logout',{refreshToken:session.refreshToken},session.accessToken) state.logout = response?.http === 200 && String(response?.body?.code) === '200' ? 'SUCCESS' : 'FAILED' state.http = response?.http; state.code = response?.body?.code if (state.logout !== 'SUCCESS') { failed = true; report.status = 'FAILED' } } catch { state.logout = 'UNKNOWN_RESULT'; failed = true; report.status = 'FAILED' } } report.finishedAt = new Date().toISOString(); await checkpoint() } return { output,status:report.status,failed } } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { execute().then(result => { console.log(JSON.stringify(result)); process.exitCode = result.failed ? 1 : 0 }) .catch(() => { console.error('采购验收未启动或报告保存失败;请核对TEST配置、唯一批次和既有证据。'); process.exitCode = 1 }) }