From 43c8b59c2429d8dbf5822770ef40fa255a8c285a Mon Sep 17 00:00:00 2001
From: linmeng <819991304@qq.com>
Date: Wed, 29 Jul 2026 13:13:47 +0800
Subject: [PATCH] fix: expose payment admin navigation
---
docs/BUG_HISTORY.md | 17 ++++++++++++++++-
frontend/src/components/admin/admin-app.tsx | 4 ++++
frontend/tests/admin-contracts.test.ts | 8 ++++++++
3 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md
index 81ff0854..3346c46d 100644
--- a/docs/BUG_HISTORY.md
+++ b/docs/BUG_HISTORY.md
@@ -1610,4 +1610,19 @@
- 防复发:活动运行配置和测试不得重新引入独立后台域名、`AUTH_ADMIN_ORIGIN`、`BETTER_AUTH_ADMIN_SECRET` 或浏览器 admin auth service;`viewer` 对后台入口、页面、读 API 和写 API 均必须为 `403`;入口可见性不能替代 route guard。
- 相关记录:BUG-010、BUG-083、BUG-084、BUG-087
- 复发自:BUG-087
-- 修复版本:本地实现,待 staging 验收
+- 修复版本:`435e628806390e7ae138363491e7bae63ee801d4`,staging 已验收
+
+## BUG-093 | 后台导航遗漏支付记录与支付配置入口
+
+- 状态:resolved
+- 首次发现:2026-07-29
+- 最近更新:2026-07-29
+- 影响面:后台 Refine 侧栏导航;支付页面与后台支付 API 本身不受影响。
+- 用户现象:管理员进入后台后只能看到兑换码、用户、积分流水、咨询和审计菜单,看不到支付记录与支付配置。
+- 触发条件:进入同域 `/admin` 后通过 Refine `resources` 生成后台导航。
+- 根因:`/admin/payments`、`/admin/packages` 页面及 `/api/admin/payments`、`/api/admin/packages` 接口已经存在,但 `AdminApp` 的 `resources` 数组没有注册这两个资源。
+- 修复:在后台导航中增加“支付记录”和“支付配置”,分别指向 `/admin/payments` 与 `/admin/packages`,并使用支付卡和设置图标;权限仍由统一的 admin-only 页面/API guard 执行。
+- 验证:后台合同测试锁定两个资源的名称、路径与图标;Next.js 生产构建通过;staging 部署和登录态验收记录在本次交付报告。
+- 防复发:新增独立后台页面时必须同步注册导航资源,后台合同测试必须覆盖页面路由与菜单资源的一致性。
+- 相关记录:BUG-087、BUG-092
+- 修复版本:待 staging 验收
diff --git a/frontend/src/components/admin/admin-app.tsx b/frontend/src/components/admin/admin-app.tsx
index 0f6620b1..d6363921 100644
--- a/frontend/src/components/admin/admin-app.tsx
+++ b/frontend/src/components/admin/admin-app.tsx
@@ -2,8 +2,10 @@
import {
AuditOutlined,
+ CreditCardOutlined,
GiftOutlined,
MessageOutlined,
+ SettingOutlined,
TeamOutlined,
TransactionOutlined,
} from "@ant-design/icons";
@@ -32,6 +34,8 @@ export function AdminApp({ children }: { children: ReactNode }) {
notificationProvider={notificationProvider}
resources={[
{ name: "codes", list: "/admin/codes", meta: { label: "兑换码", icon: } },
+ { name: "payments", list: "/admin/payments", meta: { label: "支付记录", icon: } },
+ { name: "packages", list: "/admin/packages", meta: { label: "支付配置", icon: } },
{ name: "users", list: "/admin/codes?resource=users", meta: { label: "用户资料", icon: } },
{ name: "credit-transactions", list: "/admin/codes?resource=credit-transactions", meta: { label: "积分流水", icon: } },
{ name: "consultations", list: "/admin/codes?resource=consultations", meta: { label: "咨询请求", icon: } },
diff --git a/frontend/tests/admin-contracts.test.ts b/frontend/tests/admin-contracts.test.ts
index 6036ea41..61911d3f 100644
--- a/frontend/tests/admin-contracts.test.ts
+++ b/frontend/tests/admin-contracts.test.ts
@@ -13,6 +13,7 @@ const codesRoute = readFileSync(new URL("../src/app/api/admin/codes/route.ts", i
const codeRoute = readFileSync(new URL("../src/app/api/admin/codes/[id]/route.ts", import.meta.url), "utf8");
const providers = readFileSync(new URL("../src/lib/admin/providers.ts", import.meta.url), "utf8");
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
+const adminApp = readFileSync(new URL("../src/components/admin/admin-app.tsx", import.meta.url), "utf8");
const adminRootRoute = readFileSync(new URL("../src/app/admin/route.ts", import.meta.url), "utf8");
const readonlyRoutes = ["users", "credit-transactions", "consultations", "audit-logs"].map((resource) =>
readFileSync(new URL(`../src/app/api/admin/${resource}/route.ts`, import.meta.url), "utf8"),
@@ -43,6 +44,13 @@ test("self-hosted account entry checks only the persisted admin role", () => {
assert.match(auth, /authorizeAdminAccess\(user, access\)/);
});
+test("admin navigation exposes payment records and payment configuration", () => {
+ assert.match(adminApp, /name: "payments", list: "\/admin\/payments", meta: \{ label: "支付记录"/);
+ assert.match(adminApp, /name: "packages", list: "\/admin\/packages", meta: \{ label: "支付配置"/);
+ assert.match(adminApp, /CreditCardOutlined/);
+ assert.match(adminApp, /SettingOutlined/);
+});
+
test("admin pages and root route are server-gated before rendering or redirecting", () => {
assert.match(adminLayout, /await requireAdminSession\("read"\)/);
assert.match(adminLayout, /error\.status === 401 \? "\/login" : "\/"/);