feat(redeem): 固定兑换链接 /redeem(输入订单号 → 状态分流 → 兑换)

- 路径式入口 `https://<域名>/redeem`(不带 #):主站是 HashRouter,
  故在 App.tsx 里早于 isWorkerSite() 插入独立 BrowserRouter 分支,
  主站与打手站路由均不受影响;Caddy 已有 try_files 兜底,无需改部署
- 页面按 7 种状态渲染:not_found / unpaid / cancelled / refunded /
  pending_certificate / redeemable / verified
  - not_found 保留用户输入的订单号,不清空
  - pending_certificate 只给显式「刷新」,不做自动轮询(公开限流接口,避免空转)
  - verified 只展示后端已掩码的券码,绝不渲染明文 certNo
  - confirm 不假设成功:一律按返回的 status 重渲染
- 纯函数抽到 utils/redeem.ts(无 React/antd、无 @ 别名),可直接跑单测
This commit is contained in:
yml2213
2026-10-09 12:01:35 +08:00
parent 080480f03b
commit 8c33ff62ec
7 changed files with 410 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
formatRedeemAmount,
formatRedeemTime,
isRedeemPath,
resolveRedeemStatusView,
} from '../src/utils/redeem.ts'
import type { RedeemOrderStatus } from '../src/types/redeem.ts'
/** 故意构造后端未来可能新增的状态,验证未知状态不会崩 */
const FUTURE_STATUS = 'reviewing' as unknown as RedeemOrderStatus
test('isRedeemPath:只认路径式 /redeem,容忍结尾斜杠', () => {
assert.equal(isRedeemPath('/redeem'), true)
assert.equal(isRedeemPath('/redeem/'), true)
assert.equal(isRedeemPath('/redeem//'), true)
})
test('isRedeemPath:/redeemx、/、空串都不算兑换路径', () => {
assert.equal(isRedeemPath('/redeemx'), false)
assert.equal(isRedeemPath('/'), false)
assert.equal(isRedeemPath('/admin/redeem'), false)
assert.equal(isRedeemPath(''), false)
assert.equal(isRedeemPath(null), false)
})
test('formatRedeemAmount:分转元保留两位小数', () => {
assert.equal(formatRedeemAmount(1990), '¥19.90')
assert.equal(formatRedeemAmount(0), '¥0.00')
assert.equal(formatRedeemAmount(5), '¥0.05')
})
test('resolveRedeemStatusView:7 个状态各有标题与色调', () => {
assert.deepEqual(resolveRedeemStatusView('not_found'), {
tone: 'warning',
title: '未找到订单',
})
assert.deepEqual(resolveRedeemStatusView('unpaid'), { tone: 'warning', title: '订单未支付' })
assert.deepEqual(resolveRedeemStatusView('cancelled'), { tone: 'error', title: '订单已取消' })
assert.deepEqual(resolveRedeemStatusView('refunded'), { tone: 'error', title: '订单已退款' })
assert.deepEqual(resolveRedeemStatusView('pending_certificate'), {
tone: 'info',
title: '兑换券准备中',
})
assert.deepEqual(resolveRedeemStatusView('redeemable'), {
tone: 'success',
title: '可以兑换',
})
assert.deepEqual(resolveRedeemStatusView('verified'), { tone: 'success', title: '已兑换' })
})
test('resolveRedeemStatusView:未知状态回退为中性提示', () => {
assert.deepEqual(resolveRedeemStatusView(FUTURE_STATUS), {
tone: 'info',
title: '订单状态未知',
})
})
test('formatRedeemTime:空值或非法时间返回空串,合法时间格式化为 YYYY-MM-DD HH:mm', () => {
assert.equal(formatRedeemTime(''), '')
assert.equal(formatRedeemTime(null), '')
assert.equal(formatRedeemTime(' '), '')
assert.equal(formatRedeemTime('不是时间'), '')
assert.match(formatRedeemTime('2026-09-17T10:00:00'), /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
})