/** * 快手行业电子凭证接口一键测试脚本 * * 用法: * 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 = { 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 = { 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, 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 = { 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 = { 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) => ({ id: String(e.id), num: 1, goodsValue: 100, })) printStep(6, `通知商家销毁(指定卡券 id=${destroyTargetIds[0].id})`) const destroyBizParams: Record = { 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 = { 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 = { 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, method: string, ): Record { const paramStr = JSON.stringify(bizParams) const params: Record = { 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) { 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 = {} 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) `) }