增加测试

This commit is contained in:
yml2213
2026-07-02 12:09:24 +08:00
parent f38efa2edf
commit 664a7b03f8
7 changed files with 1088 additions and 1 deletions
@@ -0,0 +1,396 @@
/**
* 快手行业电子凭证接口一键测试脚本
*
* 用法:
* 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)
`)
}