import crypto from 'node:crypto' import path from 'node:path' import { fileURLToPath } from 'node:url' import { loadEnvFiles } from '../src/config/runtime-env.js' type JsonObject = Record const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)) const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') loadEnvFiles([ path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env'), ]) const args = parseArgs(process.argv.slice(2)) if (args.help) { printHelp() process.exit(0) } const secret = readEnv('KAQUAN91_SECRET') const version = readEnv('KAQUAN91_VERSION') || '1.0' const baseUrl = String(args.baseUrl || process.env.OPEN91_MOCK_BASE_URL || 'http://127.0.0.1').replace(/\/$/, '') const mode = String(args.mode || 'create').trim() if (!secret || secret.length !== 32) { console.error('缺少 32 位 91 密钥:请配置 KAQUAN91_SECRET。') process.exit(1) } if (!['create', 'query'].includes(mode)) { console.error('mode 只能是 create 或 query。') process.exit(1) } const orderNo = String(args.orderNo || `MOCK91${Date.now()}`).trim() const timestamp = Number(args.timestamp || Math.floor(Date.now() / 1000)) const payload = mode === 'query' ? { orderNo, timestamp, version, } : { buyNum: normalizePositiveInteger(args.buyNum, 1), callbackUrl: String(args.callbackUrl || ''), maxAmount: String(args.maxAmount || '0.01'), orderNo, productNo: String(args.productNo || '套装-浪漫天命').trim(), timestamp, version, } const body = { ...payload, sign: signOpen91Payload(payload, secret), } const endpoint = `${baseUrl}/api/v1/open/91/orders/${mode}` console.log(`POST ${endpoint}`) console.log(JSON.stringify(maskPayload(body), null, 2)) const response = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify(body), }).catch((error) => { console.error('请求 91 mock 接口失败,请确认后端入口可访问。') console.error(`当前地址:${endpoint}`) console.error('Docker 开发环境通常使用 http://127.0.0.1;本地直启后端通常使用 http://127.0.0.1:3000。') throw error }) const responseText = await response.text() console.log(`HTTP ${response.status}`) try { console.log(JSON.stringify(JSON.parse(responseText), null, 2)) } catch { console.log(responseText) } function parseArgs(rawArgs: string[]) { const parsed: JsonObject = {} for (const rawArg of rawArgs) { const arg = String(rawArg || '').trim() if (!arg) continue if (arg === '--help' || arg === '-h') { parsed.help = true continue } const normalized = arg.startsWith('--') ? arg.slice(2) : arg const separatorIndex = normalized.indexOf('=') if (separatorIndex < 0) { parsed[normalized] = true continue } const key = normalized.slice(0, separatorIndex).trim() const value = normalized.slice(separatorIndex + 1).trim() if (key) { parsed[key] = value } } return parsed } function readEnv(...keys: string[]) { for (const key of keys) { const value = String(process.env[key] || '').trim() if (value) return value } return '' } function signOpen91Payload(payload: JsonObject, currentSecret: string) { return crypto .createHash('md5') .update(buildOpen91SignSource(payload, currentSecret), 'utf8') .digest('hex') .toUpperCase() } function buildOpen91SignSource(payload: JsonObject, currentSecret: string) { const queryString = Object.entries(payload) .filter(([key]) => key !== 'sign') .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => `${key}=${stringifySignValue(value)}`) .join('&') return `${currentSecret}${queryString}${currentSecret}` } function stringifySignValue(value: unknown) { if (typeof value === 'number' && Number.isFinite(value)) return String(value) if (typeof value === 'boolean') return value ? 'true' : 'false' if (value == null) return '' return String(value) } function normalizePositiveInteger(value: unknown, fallback: number) { const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback } function maskPayload(payload: JsonObject) { return { ...payload, sign: String(payload.sign || '').replace(/^(.{6}).+(.{4})$/, '$1***$2'), } } function printHelp() { console.log(` 模拟 91 卡券请求: npm run mock:open91 -- --productNo=套装-浪漫天命 npm run mock:open91 -- --productNo=荣耀勋章礼包(30个) --maxAmount=300 --buyNum=1 npm run mock:open91 -- --mode=query --orderNo=MOCK911779000000000 参数: --baseUrl 后端地址,默认 http://127.0.0.1 --mode create 或 query,默认 create --orderNo 91 订单号,默认自动生成 --productNo 91 商品名,默认 套装-浪漫天命 --buyNum 购买数量,默认 1 --maxAmount 最大金额/正常金额,默认 0.01 --callbackUrl 回调地址,默认空 `) }