增加测试
This commit is contained in:
@@ -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)
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user