Files

185 lines
5.0 KiB
TypeScript

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<string, unknown>
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 回调地址,默认空
`)
}