简化履约匹配并支持mock订单测试
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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 回调地址,默认空
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user