397 lines
16 KiB
TypeScript
397 lines
16 KiB
TypeScript
/**
|
||
* 快手行业电子凭证接口一键测试脚本
|
||
*
|
||
* 用法:
|
||
* npm run test:industry
|
||
* npm run test:industry -- --oid=自定义订单号 --num=5
|
||
*
|
||
* 前提:
|
||
* 1. PostgreSQL 已启动且迁移已完成 (npm run db:migrate)
|
||
* 2. .env 中配置了 KUASHOU_INDUSTRY_APP_KEY 和 KUASHOU_INDUSTRY_SIGN_SECRET
|
||
* (未配置时使用测试默认值,可与 DB 中已插入的数据联调)
|
||
*/
|
||
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import process from 'node:process'
|
||
|
||
import { loadEnvFiles } from '../src/config/runtime-env.js'
|
||
|
||
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'),
|
||
])
|
||
|
||
process.env.KUASHOU_INDUSTRY_APP_KEY = process.env.KUASHOU_INDUSTRY_APP_KEY || 'test_industry_app_key'
|
||
process.env.KUASHOU_INDUSTRY_SIGN_SECRET = process.env.KUASHOU_INDUSTRY_SIGN_SECRET || 'test_industry_sign_secret'
|
||
process.env.KUASHOU_INDUSTRY_SHOP_ID = process.env.KUASHOU_INDUSTRY_SHOP_ID || 'test_shop'
|
||
|
||
const args = parseArgs(process.argv.slice(2))
|
||
|
||
if (args.help) {
|
||
printHelp()
|
||
process.exit(0)
|
||
}
|
||
|
||
const [
|
||
{ handleSendCode },
|
||
{ handleQueryCode },
|
||
{ handleDestroyCode },
|
||
{ signKuaishouIndustryPayload },
|
||
{ getKuaishouIndustryConfig },
|
||
{ closeDb },
|
||
] = await Promise.all([
|
||
import('../src/services/platforms/kuaishou-industry/send-code-service.js'),
|
||
import('../src/services/platforms/kuaishou-industry/query-code-service.js'),
|
||
import('../src/services/platforms/kuaishou-industry/destroy-code-service.js'),
|
||
import('../src/services/platforms/kuaishou-industry/crypto.js'),
|
||
import('../src/services/platforms/kuaishou-industry/config.js'),
|
||
import('../src/db/client.js'),
|
||
])
|
||
|
||
const config = getKuaishouIndustryConfig()
|
||
const testOid = String(args.oid || `TEST_KS${Date.now()}`).trim()
|
||
const testNum = normalizePositiveInteger(args.num, 3)
|
||
const signMethod = String(args.signMethod || 'MD5').trim().toUpperCase()
|
||
|
||
console.log('')
|
||
console.log('╔══════════════════════════════════════════════════════╗')
|
||
console.log('║ 快手行业电子凭证 接口一键测试 ║')
|
||
console.log('╠══════════════════════════════════════════════════════╣')
|
||
console.log(`║ appKey : ${config.appKey.padEnd(36)}║`)
|
||
console.log(`║ signSecret : ${'*'.repeat(12).padEnd(36)}║`)
|
||
console.log(`║ signMethod : ${signMethod.padEnd(36)}║`)
|
||
console.log(`║ 测试订单号 : ${testOid.padEnd(36)}║`)
|
||
console.log(`║ 发码数量 : ${String(testNum).padEnd(36)}║`)
|
||
console.log('╚══════════════════════════════════════════════════════╝')
|
||
console.log('')
|
||
|
||
let passed = 0
|
||
let failed = 0
|
||
let total = 0
|
||
|
||
try {
|
||
// ─────────────────────────────────────────────────────────
|
||
// 1. 通知商家发码
|
||
// ─────────────────────────────────────────────────────────
|
||
total++
|
||
printStep(1, '通知商家发码 POST /api/v1/open/kuaishou-industry/send-code')
|
||
|
||
const sendCodeBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
sellerId: '2174425348',
|
||
num: testNum,
|
||
itemId: '20842040339391',
|
||
skuId: '87457167264391',
|
||
itemTitle: '测试电子凭证商品',
|
||
sendType: 'VIRTUAL',
|
||
eticketType: 'DINING_OPEN_TICKET',
|
||
token: `test-token-${testOid}`,
|
||
certExpireType: 3,
|
||
certExpDays: 30,
|
||
certActualStartTime: Date.now(),
|
||
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||
}
|
||
|
||
const sendCodeParams = buildRequestParams(config.appKey, sendCodeBizParams, signMethod)
|
||
const sendCodeResult = await handleSendCode(sendCodeParams)
|
||
|
||
printResult('通知商家发码', sendCodeResult)
|
||
if (sendCodeResult.result === 1) {
|
||
passed++
|
||
console.log(` 返回卡券数: ${sendCodeResult.data?.etickets?.length || 0}`)
|
||
const firstTicket = sendCodeResult.data?.etickets?.[0]
|
||
if (firstTicket) {
|
||
console.log(` 首张卡券 ID: ${firstTicket.id}, 状态: ${firstTicket.status}`)
|
||
}
|
||
} else {
|
||
failed++
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 2. 重复发码(幂等性测试)
|
||
// ─────────────────────────────────────────────────────────
|
||
total++
|
||
printStep(2, '重复发码(幂等性测试)')
|
||
|
||
const sendCodeRepeatResult = await handleSendCode(sendCodeParams)
|
||
|
||
printResult('重复发码', sendCodeRepeatResult)
|
||
if (sendCodeRepeatResult.result === 1) {
|
||
passed++
|
||
console.log(` 返回卡券数: ${sendCodeRepeatResult.data?.etickets?.length || 0}`)
|
||
} else {
|
||
failed++
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 3. 查询商家卡券(不传 eticketId,查全部)
|
||
// ─────────────────────────────────────────────────────────
|
||
total++
|
||
printStep(3, '查询商家卡券 POST /api/v1/open/kuaishou-industry/query-code')
|
||
|
||
const queryCodeBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
sendType: 'VIRTUAL',
|
||
eticketType: 'DINING_OPEN_TICKET',
|
||
}
|
||
|
||
const queryCodeParams = buildRequestParams(config.appKey, queryCodeBizParams, signMethod)
|
||
const queryCodeResult = await handleQueryCode(queryCodeParams)
|
||
|
||
printResult('查询全部卡券', queryCodeResult)
|
||
if (queryCodeResult.result === 1) {
|
||
passed++
|
||
console.log(` 已发货数量: ${queryCodeResult.data?.sendNum || 0}`)
|
||
console.log(` 卡券列表数: ${queryCodeResult.data?.etickets?.length || 0}`)
|
||
queryCodeResult.data?.etickets?.forEach((eticket: Record<string, unknown>, index: number) => {
|
||
console.log(` [${index + 1}] id=${eticket.id} status=${eticket.status} num=${eticket.num}`)
|
||
})
|
||
} else {
|
||
failed++
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 4. 查询单个卡券(传 eticketId)
|
||
// ─────────────────────────────────────────────────────────
|
||
if (queryCodeResult.data?.etickets?.length > 0) {
|
||
total++
|
||
const targetEticketId = queryCodeResult.data.etickets[0].id as string
|
||
printStep(4, `查询单个卡券(eticketId=${targetEticketId})`)
|
||
|
||
const querySingleBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
eticketId: targetEticketId,
|
||
sendType: 'VIRTUAL',
|
||
eticketType: 'DINING_OPEN_TICKET',
|
||
}
|
||
|
||
const querySingleParams = buildRequestParams(config.appKey, querySingleBizParams, signMethod)
|
||
const querySingleResult = await handleQueryCode(querySingleParams)
|
||
|
||
printResult('查询单个卡券', querySingleResult)
|
||
if (querySingleResult.result === 1) {
|
||
passed++
|
||
console.log(` 卡券 ID: ${querySingleResult.data?.etickets?.[0]?.id}`)
|
||
console.log(` 状态 : ${querySingleResult.data?.etickets?.[0]?.status}`)
|
||
} else {
|
||
failed++
|
||
}
|
||
} else {
|
||
console.log(' ⚠ 跳过(无卡券可查询)')
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 5. 查询不存在的订单
|
||
// ─────────────────────────────────────────────────────────
|
||
total++
|
||
const fakeOid = `FAKE_NOT_EXIST_${Date.now()}`
|
||
printStep(5, '查询不存在的订单(错误码验证)')
|
||
|
||
const queryMissingBizParams: Record<string, unknown> = {
|
||
oid: fakeOid,
|
||
sendType: 'VIRTUAL',
|
||
}
|
||
|
||
const queryMissingParams = buildRequestParams(config.appKey, queryMissingBizParams, signMethod)
|
||
const queryMissingResult = await handleQueryCode(queryMissingParams)
|
||
|
||
if (queryMissingResult.result === 4012002) {
|
||
passed++
|
||
console.log(` └─ ✅ 查询不存在订单: result=4012002 (订单不存在,符合预期)`)
|
||
console.log(` 错误信息: ${queryMissingResult.error_msg}`)
|
||
} else {
|
||
failed++
|
||
console.log(` └─ ❌ 查询不存在订单: 期望 result=4012002,实际 result=${queryMissingResult.result}`)
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 6. 通知商家销毁(指定卡券 ID)
|
||
// ─────────────────────────────────────────────────────────
|
||
if (queryCodeResult.data?.etickets?.length > 0) {
|
||
total++
|
||
const destroyTargetIds = queryCodeResult.data.etickets.slice(0, 1).map((e: Record<string, unknown>) => ({
|
||
id: String(e.id),
|
||
num: 1,
|
||
goodsValue: 100,
|
||
}))
|
||
printStep(6, `通知商家销毁(指定卡券 id=${destroyTargetIds[0].id})`)
|
||
|
||
const destroyBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
reason: 'USER_APPLY_REFUND',
|
||
etickets: destroyTargetIds,
|
||
}
|
||
|
||
const destroyParams = buildRequestParams(config.appKey, destroyBizParams, signMethod)
|
||
const destroyResult = await handleDestroyCode(destroyParams)
|
||
|
||
printResult('通知商家销毁', destroyResult)
|
||
if (destroyResult.result === 1) {
|
||
passed++
|
||
} else {
|
||
failed++
|
||
}
|
||
|
||
// 销毁后再次查询,验证卡券状态变为 DESTROYED
|
||
total++
|
||
printStep(7, '销毁后验证卡券状态')
|
||
|
||
const verifyDestroyBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
eticketId: destroyTargetIds[0].id,
|
||
sendType: 'VIRTUAL',
|
||
eticketType: 'DINING_OPEN_TICKET',
|
||
}
|
||
|
||
const verifyDestroyParams = buildRequestParams(config.appKey, verifyDestroyBizParams, signMethod)
|
||
const verifyDestroyResult = await handleQueryCode(verifyDestroyParams)
|
||
|
||
printResult('销毁后查询', verifyDestroyResult)
|
||
const verifyStatus = verifyDestroyResult.data?.etickets?.[0]?.status
|
||
if (verifyDestroyResult.result === 1 && verifyStatus === 'DESTROYED') {
|
||
passed++
|
||
console.log(` 卡券状态: ${verifyStatus}`)
|
||
} else {
|
||
failed++
|
||
console.log(` 期望 status=DESTROYED,实际 status=${verifyStatus}, result=${verifyDestroyResult.result}`)
|
||
}
|
||
} else {
|
||
console.log(' ⚠ 跳过(无卡券可销毁)')
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// 8. 整单销毁(不传 etickets,销毁剩余全部)
|
||
// ─────────────────────────────────────────────────────────
|
||
total++
|
||
printStep(8, '整单销毁(不传 etickets,关闭全部剩余卡券)')
|
||
|
||
const destroyAllBizParams: Record<string, unknown> = {
|
||
oid: testOid,
|
||
reason: 'ETICKET_EXPIRED',
|
||
}
|
||
|
||
const destroyAllParams = buildRequestParams(config.appKey, destroyAllBizParams, signMethod)
|
||
const destroyAllResult = await handleDestroyCode(destroyAllParams)
|
||
|
||
printResult('整单销毁', destroyAllResult)
|
||
if (destroyAllResult.result === 1) {
|
||
passed++
|
||
} else {
|
||
failed++
|
||
}
|
||
|
||
} finally {
|
||
await closeDb()
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
console.log('')
|
||
console.log('╔══════════════════════════════════════════════════════╗')
|
||
console.log(`║ 测试完成: ${String(passed).padStart(2)}/${total} 通过, ${String(failed).padStart(2)}/${total} 失败 ║`)
|
||
console.log('╚══════════════════════════════════════════════════════╝')
|
||
console.log('')
|
||
|
||
if (failed > 0) {
|
||
process.exitCode = 1
|
||
}
|
||
|
||
// ─────────────────── helpers ───────────────────
|
||
|
||
function buildRequestParams(
|
||
appKey: string,
|
||
bizParams: Record<string, unknown>,
|
||
method: string,
|
||
): Record<string, unknown> {
|
||
const paramStr = JSON.stringify(bizParams)
|
||
|
||
const params: Record<string, unknown> = {
|
||
appkey: appKey,
|
||
version: '1',
|
||
timestamp: Date.now(),
|
||
signMethod: method,
|
||
access_token: `test-access-token-${testOid}`,
|
||
method: 'integration.virtual.eticket.callback',
|
||
param: paramStr,
|
||
}
|
||
|
||
const sign = signKuaishouIndustryPayload(params, method as 'MD5' | 'HMAC_SHA256')
|
||
params.sign = sign
|
||
|
||
return params
|
||
}
|
||
|
||
function normalizePositiveInteger(value: unknown, fallback: number): number {
|
||
const parsed = Number(value)
|
||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||
}
|
||
|
||
function printStep(stepNum: number, title: string) {
|
||
console.log('')
|
||
console.log(` ┌─ [${stepNum}] ${title}`)
|
||
}
|
||
|
||
function printResult(label: string, result: Record<string, unknown>) {
|
||
const ok = result.result === 1
|
||
const icon = ok ? '✅' : '❌'
|
||
const resultText = ok ? `result=${result.result}` : `result=${result.result} error_msg="${result.error_msg}"`
|
||
console.log(` └─ ${icon} ${label}: ${resultText}`)
|
||
}
|
||
|
||
function parseArgs(rawArgs: string[]) {
|
||
const parsed: Record<string, unknown> = {}
|
||
|
||
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 eqIdx = normalized.indexOf('=')
|
||
if (eqIdx < 0) {
|
||
parsed[normalized] = true
|
||
continue
|
||
}
|
||
|
||
const key = normalized.slice(0, eqIdx).trim()
|
||
const value = normalized.slice(eqIdx + 1).trim()
|
||
if (key) {
|
||
parsed[key] = value
|
||
}
|
||
}
|
||
|
||
return parsed
|
||
}
|
||
|
||
function printHelp() {
|
||
console.log(`
|
||
快手行业电子凭证接口一键测试
|
||
|
||
用法:
|
||
npm run test:industry
|
||
npm run test:industry -- --oid=自定义订单号 --num=5 --signMethod=HMAC_SHA256
|
||
|
||
参数:
|
||
--oid 测试订单号,默认自动生成 (TEST_KS + 时间戳)
|
||
--num 发码数量,默认 3
|
||
--signMethod 签名算法 MD5 或 HMAC_SHA256,默认 MD5
|
||
|
||
测试内容:
|
||
1. 通知商家发码 — send-code
|
||
2. 重复发码(幂等性) — send-code (same oid)
|
||
3. 查询全部卡券 — query-code
|
||
4. 查询单个卡券 — query-code (指定 eticketId)
|
||
5. 查询不存在订单 — query-code (错误码 4012002)
|
||
6. 通知商家销毁(指定卡券)— destroy-code (指定 etickets)
|
||
7. 销毁后验证卡券状态 — query-code (验证 DESTROYED)
|
||
8. 整单销毁(关闭订单) — destroy-code (不传 etickets)
|
||
`)
|
||
}
|