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
+21
View File
@@ -11,6 +11,7 @@ import {
} from 'react-router'
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
import { isRedeemPath } from '@/utils/redeem'
import { isWorkerSite, WORKER_SITE_ORIGIN } from '@/utils/site'
import { hasWorkerSession } from '@/utils/worker-auth'
import { resolveLegacyWorkerPath } from '@/utils/worker-routes'
@@ -36,6 +37,7 @@ const AdminWorkerPlatformPage = lazy(() => import('@/pages/admin/AdminWorkerPlat
const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage'))
const CollectPage = lazy(() => import('@/pages/collect/CollectPage'))
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
const RedeemPage = lazy(() => import('@/pages/redeem/RedeemPage'))
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
const WorkerOrdersPage = lazy(() => import('@/pages/worker/WorkerOrdersPage'))
@@ -88,6 +90,12 @@ export default function App() {
return <WorkerSiteRedirect path={legacyWorkerPath} />
}
// 固定兑换链接是路径式(`https://<域名>/redeem`,不带 `#`),必须早于站点判断,
// 用独立的 BrowserRouter 分支承载,主站 HashRouter 不受影响。
if (isRedeemPath(window.location.pathname)) {
return <RedeemApplication />
}
if (isWorkerSite()) {
return <WorkerApplication />
}
@@ -137,6 +145,19 @@ function AdminApplication() {
)
}
function RedeemApplication() {
return (
<BrowserRouter>
<Suspense fallback={<RouteLoading />}>
<Routes>
<Route path="/redeem" element={<RedeemPage />} />
<Route path="*" element={<Navigate to="/redeem" replace />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
function WorkerApplication() {
return (
<BrowserRouter>