统一前后端代码格式化配置
This commit is contained in:
@@ -34,10 +34,7 @@ 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'),
|
||||
])
|
||||
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
|
||||
@@ -65,13 +62,21 @@ if (args.provider && providerPresets[String(args.provider)]) {
|
||||
console.log(` 预设环境: ${preset.description}`)
|
||||
}
|
||||
} else {
|
||||
baseUrl = String(args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000').replace(/\/+$/, '')
|
||||
baseUrl = String(
|
||||
args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000',
|
||||
).replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
// 签名配置:CLI 参数 > 环境变量 > 默认值
|
||||
const appKey = (String(args.appKey || '') || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
|
||||
const signSecret = (String(args.signSecret || '') || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim()
|
||||
const signMethod = (String(args.signMethod || 'MD5').trim().toUpperCase())
|
||||
const signSecret = (
|
||||
String(args.signSecret || '') ||
|
||||
process.env.KUASHOU_INDUSTRY_SIGN_SECRET ||
|
||||
''
|
||||
).trim()
|
||||
const signMethod = String(args.signMethod || 'MD5')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
|
||||
const isLoadTest = Boolean(args.load)
|
||||
const targetTps = normalizePositiveInteger(args.tps, 100)
|
||||
@@ -119,24 +124,29 @@ if (!isLoadTest) {
|
||||
let failed = 0
|
||||
|
||||
// 1. 通知商家发码
|
||||
const sendParams = buildSignedParams(appKey, {
|
||||
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,
|
||||
}, signMethod)
|
||||
const sendParams = buildSignedParams(
|
||||
appKey,
|
||||
{
|
||||
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,
|
||||
},
|
||||
signMethod,
|
||||
)
|
||||
|
||||
const r1 = await post('/send-code', sendParams, { oid: testOid })
|
||||
if (assertResult(1, '通知商家发码', r1)) passed++; else failed++
|
||||
if (assertResult(1, '通知商家发码', r1)) passed++
|
||||
else failed++
|
||||
if (r1.ok) {
|
||||
const len = r1.data?.data?.etickets?.length || 0
|
||||
const firstId = r1.data?.data?.etickets?.[0]?.id
|
||||
@@ -145,12 +155,16 @@ if (!isLoadTest) {
|
||||
|
||||
// 2. 重复发码(幂等性)
|
||||
const r2 = await post('/send-code', sendParams, { oid: testOid })
|
||||
if (assertResult(2, '重复发码(幂等性)', r2)) passed++; else failed++
|
||||
if (assertResult(2, '重复发码(幂等性)', r2)) passed++
|
||||
else failed++
|
||||
|
||||
// 3. 查询全部卡券
|
||||
const qBiz: JsonObject = { oid: testOid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }
|
||||
const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), { oid: testOid })
|
||||
if (assertResult(3, '查询全部卡券', r3)) passed++; else failed++
|
||||
const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), {
|
||||
oid: testOid,
|
||||
})
|
||||
if (assertResult(3, '查询全部卡券', r3)) passed++
|
||||
else failed++
|
||||
if (r3.ok) {
|
||||
console.log(` 卡券数: ${r3.data?.data?.etickets?.length || 0}`)
|
||||
r3.data?.data?.etickets?.forEach((e: JsonObject, i: number) => {
|
||||
@@ -161,13 +175,31 @@ if (!isLoadTest) {
|
||||
// 4. 查询单个卡券
|
||||
const firstEticketId = r3.data?.data?.etickets?.[0]?.id
|
||||
if (firstEticketId) {
|
||||
const r4 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }, signMethod), { oid: testOid })
|
||||
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++; else failed++
|
||||
const r4 = await post(
|
||||
'/query-code',
|
||||
buildSignedParams(
|
||||
appKey,
|
||||
{
|
||||
oid: testOid,
|
||||
eticketId: String(firstEticketId),
|
||||
sendType: 'VIRTUAL',
|
||||
eticketType: 'DINING_OPEN_TICKET',
|
||||
},
|
||||
signMethod,
|
||||
),
|
||||
{ oid: testOid },
|
||||
)
|
||||
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++
|
||||
else failed++
|
||||
}
|
||||
|
||||
// 5. 查询不存在的订单
|
||||
const fakeOid = `FAKE_${Date.now()}`
|
||||
const r5 = await post('/query-code', buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod), { oid: fakeOid })
|
||||
const r5 = await post(
|
||||
'/query-code',
|
||||
buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod),
|
||||
{ oid: fakeOid },
|
||||
)
|
||||
const expect4012002 = r5.data?.result === 4012002
|
||||
if (expect4012002) {
|
||||
passed++
|
||||
@@ -179,24 +211,52 @@ if (!isLoadTest) {
|
||||
|
||||
// 6. 销毁指定卡券
|
||||
if (firstEticketId) {
|
||||
const r6 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'USER_APPLY_REFUND', etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }] }, signMethod), { oid: testOid })
|
||||
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++; else failed++
|
||||
const r6 = await post(
|
||||
'/destroy-code',
|
||||
buildSignedParams(
|
||||
appKey,
|
||||
{
|
||||
oid: testOid,
|
||||
reason: 'USER_APPLY_REFUND',
|
||||
etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }],
|
||||
},
|
||||
signMethod,
|
||||
),
|
||||
{ oid: testOid },
|
||||
)
|
||||
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++
|
||||
else failed++
|
||||
|
||||
// 7. 验证销毁后状态
|
||||
const r7 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' }, signMethod), { oid: testOid })
|
||||
const r7 = await post(
|
||||
'/query-code',
|
||||
buildSignedParams(
|
||||
appKey,
|
||||
{ oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' },
|
||||
signMethod,
|
||||
),
|
||||
{ oid: testOid },
|
||||
)
|
||||
const destroyed = r7.data?.result === 1 && r7.data?.data?.etickets?.[0]?.status === 'DESTROYED'
|
||||
if (destroyed) {
|
||||
passed++
|
||||
console.log(` ✅ [7] 销毁后验证: status=DESTROYED`)
|
||||
} else {
|
||||
failed++
|
||||
console.log(` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`)
|
||||
console.log(
|
||||
` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 整单销毁
|
||||
const r8 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod), { oid: testOid })
|
||||
if (assertResult(8, '整单销毁', r8)) passed++; else failed++
|
||||
const r8 = await post(
|
||||
'/destroy-code',
|
||||
buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod),
|
||||
{ oid: testOid },
|
||||
)
|
||||
if (assertResult(8, '整单销毁', r8)) passed++
|
||||
else failed++
|
||||
|
||||
// 9. 签名错误测试
|
||||
const badParams = buildSignedParams(appKey, { oid: testOid, sendType: 'VIRTUAL' }, signMethod)
|
||||
@@ -213,7 +273,9 @@ if (!isLoadTest) {
|
||||
|
||||
console.log('')
|
||||
console.log('╔══════════════════════════════════════════════════════╗')
|
||||
console.log(`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`)
|
||||
console.log(
|
||||
`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`,
|
||||
)
|
||||
console.log('╚══════════════════════════════════════════════════════╝')
|
||||
console.log('')
|
||||
process.exit(failed > 0 ? 1 : 0)
|
||||
@@ -222,35 +284,39 @@ if (!isLoadTest) {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 压力测试
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const endpointsToTest = testEndpoint
|
||||
? [testEndpoint]
|
||||
: ['send-code', 'query-code', 'destroy-code']
|
||||
const endpointsToTest = testEndpoint ? [testEndpoint] : ['send-code', 'query-code', 'destroy-code']
|
||||
|
||||
const loadTestOid = `LOAD_${Date.now()}`
|
||||
|
||||
// 先发码创建订单以供查询/销毁
|
||||
console.log('')
|
||||
console.log(` 准备测试数据: 通知商家发码 (oid=${loadTestOid})...`)
|
||||
const prepParams = buildSignedParams(appKey, {
|
||||
oid: loadTestOid,
|
||||
sellerId: '2174425348',
|
||||
num: 10,
|
||||
itemId: '20842040339391',
|
||||
skuId: '87457167264391',
|
||||
itemTitle: '压测商品',
|
||||
sendType: 'VIRTUAL',
|
||||
eticketType: 'DINING_OPEN_TICKET',
|
||||
token: `load-test-token-${loadTestOid}`,
|
||||
certExpireType: 3,
|
||||
certExpDays: 30,
|
||||
certActualStartTime: Date.now(),
|
||||
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||
}, signMethod)
|
||||
const prepParams = buildSignedParams(
|
||||
appKey,
|
||||
{
|
||||
oid: loadTestOid,
|
||||
sellerId: '2174425348',
|
||||
num: 10,
|
||||
itemId: '20842040339391',
|
||||
skuId: '87457167264391',
|
||||
itemTitle: '压测商品',
|
||||
sendType: 'VIRTUAL',
|
||||
eticketType: 'DINING_OPEN_TICKET',
|
||||
token: `load-test-token-${loadTestOid}`,
|
||||
certExpireType: 3,
|
||||
certExpDays: 30,
|
||||
certActualStartTime: Date.now(),
|
||||
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
signMethod,
|
||||
)
|
||||
const prepResult = await post('/send-code', prepParams, {})
|
||||
if (prepResult.data?.result === 1) {
|
||||
console.log(` 数据准备完成 (${prepResult.ms}ms)`)
|
||||
} else {
|
||||
console.log(` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`)
|
||||
console.log(
|
||||
` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`,
|
||||
)
|
||||
console.log(` 请确认服务端已配置正确的 appKey/signSecret 且未触发限流`)
|
||||
}
|
||||
|
||||
@@ -258,14 +324,18 @@ for (const endpoint of endpointsToTest) {
|
||||
console.log('')
|
||||
console.log('╔══════════════════════════════════════════════════════╗')
|
||||
console.log(`║ 压测: ${endpoint.padEnd(46)}║`)
|
||||
console.log(`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`)
|
||||
console.log(
|
||||
`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`,
|
||||
)
|
||||
console.log('╚══════════════════════════════════════════════════════╝')
|
||||
|
||||
const result = await runLoadTest(endpoint, targetTps, testDuration, loadTestOid)
|
||||
console.log(` 成功 TPS : ${result.successTps.toFixed(1)} (业务 result=1)`)
|
||||
console.log(` 总请求数 : ${result.totalReqs}`)
|
||||
console.log(` 业务成功 : ${result.successes}`)
|
||||
console.log(` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`)
|
||||
console.log(
|
||||
` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`,
|
||||
)
|
||||
console.log(` 被限流 : ${result.rateLimited}`)
|
||||
console.log(` 平均耗时 : ${result.avgMs.toFixed(1)} ms`)
|
||||
console.log(` P50 : ${result.p50Ms.toFixed(1)} ms`)
|
||||
@@ -275,7 +345,9 @@ for (const endpoint of endpointsToTest) {
|
||||
console.log(` 最大耗时 : ${result.maxMs.toFixed(1)} ms`)
|
||||
|
||||
if (result.rateLimited > result.totalReqs * 0.1) {
|
||||
console.log(` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`)
|
||||
console.log(
|
||||
` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +383,11 @@ async function post(
|
||||
})
|
||||
status = res.status
|
||||
const text = await res.text()
|
||||
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = { raw: text }
|
||||
}
|
||||
ok = res.ok && data.result === 1
|
||||
} catch (err) {
|
||||
data = { error: String(err) }
|
||||
@@ -455,9 +531,7 @@ function signKuaishouIndustry(params: JsonObject, method: string): string {
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignVal(value)}`)
|
||||
.join('&')
|
||||
const queryString = entries.map(([key, value]) => `${key}=${stringifySignVal(value)}`).join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
@@ -472,23 +546,30 @@ function stringifySignVal(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as JsonObject).sort())
|
||||
if (typeof value === 'object')
|
||||
return JSON.stringify(value, Object.keys(value as JsonObject).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// ─────────────────── 工具 ───────────────────
|
||||
|
||||
function assertResult(num: number, label: string, r: { ok: boolean; status: number; data: JsonObject; ms: number }) {
|
||||
function assertResult(
|
||||
num: number,
|
||||
label: string,
|
||||
r: { ok: boolean; status: number; data: JsonObject; ms: number },
|
||||
) {
|
||||
if (r.ok) {
|
||||
console.log(` ✅ [${num}] ${label}: result=1 (${r.ms}ms)`)
|
||||
return true
|
||||
}
|
||||
console.log(` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`)
|
||||
console.log(
|
||||
` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function avg(arr: number[]): number {
|
||||
@@ -512,10 +593,16 @@ function parseArgs(rawArgs: string[]): JsonObject {
|
||||
for (const rawArg of rawArgs) {
|
||||
const arg = String(rawArg || '').trim()
|
||||
if (!arg) continue
|
||||
if (arg === '--help' || arg === '-h') { parsed.help = true; 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 }
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user