Files
order_site/apps/backend/scripts/test-kuaishou-industry.ts
T
2026-07-02 12:09:24 +08:00

397 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 快手行业电子凭证接口一键测试脚本
*
* 用法:
* 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)
`)
}