---
title: "feat: Admin UI + Creator Review Status + Order Payment"
status: active
created: 2026-06-21
origin: docs/plans/2026-06-21-001-feat-admin-review-hermes-sync-plan.md (U7 前端缺失)
plan_depth: standard
---

# Plan: 管理员后台 UI + 创作者审核状态展示 + 订单付款流程

## Summary

补齐 EternalAI 平台的三个核心功能缺口：(1) 管理员后台 UI，让管理员可通过浏览器完成角色审核和 Hermes 同步发起；(2) 创作者角色卡片审核状态展示，让创作者看到自己角色的审核进度和二维码；(3) Order 订单后端 API + 前端付款流程对接，替换当前的纯前端 Mock。微信 OAuth 和收入/提现后端不在本次范围内。

---

## Problem Frame

当前平台存在三个阻断性缺口：

1. **管理员无法通过浏览器操作**：后端审核 API（`/api/admin/reviews/*`）、同步 API（`/api/admin/sync/*`）、配置 API（`/api/admin/config/*`）均已就绪，但 `index.html` 中无任何 admin 视图，`app.js` 中无 admin 相关代码。管理员只能通过 curl/Postman 操作，无法实际使用。

2. **创作者看不到审核状态**：后端 `GET /api/roles/my/roles` 已返回 `reviewStatus`、`qrCodeUrl`、`syncedAt`、`reviewNote`，但前端 `renderCreatorRoles()` 只显示 `status`（running/stopped），创作者无法知道自己角色处于待审核/已通过/已同步/被驳回哪个状态，也看不到驳回原因和同步后的二维码。

3. **付款流程是前端 Mock**：`prisma/schema.prisma` 定义了 Order 模型但无任何路由，`app.js` 的 `payRole()` 仅切换 DOM 显示，无 API 调用、无订单创建。用户"付款"后看不到真实二维码，平台无法记录订阅关系。

---

## Requirements

### 功能需求

- **R1**: 管理员可通过浏览器登录后台（独立登录入口，与用户登录隔离）
- **R2**: 管理员可查看待审核角色列表，支持按状态筛选
- **R3**: 管理员可查看角色详情，通过或驳回（驳回需填写原因）
- **R4**: 管理员可对已通过角色发起 Hermes 同步，填写 profile 名、model key、服务商等参数
- **R5**: 同步成功后管理员可看到二维码和 profileId
- **R6**: 管理员可查看同步状态列表（approved/syncing/synced/failed）
- **R7**: 管理员可配置系统参数（HERMES_WEBHOOK_URL 等）
- **R8**: 创作者角色卡片显示审核状态标签（待审核/已通过/同步中/已同步/同步失败/已驳回）
- **R9**: 被驳回角色卡片显示驳回原因
- **R10**: 已同步角色卡片显示二维码图片和「转发二维码」按钮
- **R11**: 后端提供 Order CRUD API（创建/查询）
- **R12**: 前端付款流程调用后端 API 创建订单，付款成功后展示真实二维码

### 非功能需求

- 管理员 token 与用户 token 隔离存储，不互相干扰
- 管理员 UI 遵循现有视图路由模式（showView + views 注册）
- Order API 需用户认证（authMiddleware）
- 付款流程暂不对接真实支付网关，创建订单即视为已付款（status=paid），返回角色二维码

### 范围边界

**在范围内**：
- 管理员后台 UI（登录、审核列表、审核详情、同步表单、同步状态、系统配置）
- 创作者角色卡片审核状态展示
- Order 后端 API + 前端付款流程对接

**不在范围内**（Deferred）：
- 微信 OAuth 真实接入（hermes-server/src/routes/bind.js 仍为占位符）
- 收入/提现后端 API（仍为前端 Mock）
- 真实支付网关对接（本次创建订单即视为已付款）
- 角色删除/上下架切换
- 定时任务调度逻辑（Hermes Agent 侧实现）

---

## Key Technical Decisions

### KTD1: 管理员 token 独立存储

管理员 token 存储在 `localStorage` 的 `eternal_ai_admin_token`，与用户 token（`eternal_ai_token`）隔离。管理员 state 存储在 `eternal_ai_admin_state`，与用户 state（`eternal_ai_state`）隔离。

**理由**: 管理员和用户可能是同一人，token 隔离避免互相覆盖。管理员 API 使用独立的 `ADMIN_JWT_SECRET`，token 不可混用。

### KTD2: 管理员入口通过 URL hash 访问

管理员后台不显示在底部 TabBar 中（普通用户不可见）。入口通过 URL hash `#admin` 直达，或在首页添加隐藏入口（如长按 logo）。

**理由**: 管理员后台是内部工具，不应暴露给普通用户。URL hash 方式简单直接，管理员可收藏书签。

### KTD3: Order 状态简化为 pending → paid

本次 Order 状态仅 `pending`（待付款）和 `paid`（已付款）。不对接真实支付网关，创建订单时前端直接标记为 `paid`。后续对接真实支付时扩展 `failed`/`refunded` 状态。

**理由**: 本次目标是让付款流程有后端记录，而非实现完整支付系统。简化状态机避免过度设计。

### KTD4: 付款后返回角色 qrCodeUrl

Order 创建（付款）成功后，后端返回角色的 `qrCodeUrl`（来自 Role 表的 `qrCodeUrl` 字段，由 Hermes 同步时写入）。前端展示该二维码供用户扫码绑定微信。

**理由**: 用户付款的目的是获取角色二维码以扫码绑定。`qrCodeUrl` 已在同步流程中存储到 Role 表，直接返回即可。若角色未同步（无 qrCodeUrl），则返回错误提示。

### KTD5: 创作者角色卡片状态基于 reviewStatus 优先

角色卡片状态标签优先显示 `reviewStatus`（pending_review/approved/rejected/syncing/synced/failed），仅当 `reviewStatus` 为 `synced` 时才显示 `status`（running/stopped）作为运行状态。

**理由**: `reviewStatus` 是角色的生命周期主状态，`status` 仅在同步完成后才有意义。避免两个状态标签混淆。

---

## High-Level Technical Design

### 管理员后台视图架构

```
用户访问 #admin
    ↓
检查 adminToken 是否有效（GET /api/admin-auth/me）
    ↓
有效 → 显示审核列表页          无效 → 显示管理员登录页
    ↓                              ↓
点击角色 → 审核详情页           登录成功 → 存储 adminToken → 显示审核列表页
    ↓
通过 → 返回列表页（状态更新）
驳回 → 弹窗填写原因 → 返回列表页
同步 → 同步表单页 → 提交 → 显示二维码 → 返回列表页
```

### Order 付款流程

```
用户在角色详情页点击「立即订阅」
    ↓
POST /api/orders { roleId, amount }
    ↓
后端校验角色已同步（reviewStatus=synced, qrCodeUrl 非空）
    ↓
创建 Order 记录（status=paid）
    ↓
返回 { order, role: { qrCodeUrl } }
    ↓
前端展示二维码
```

---

## Implementation Units

### U1. 管理员登录页 + 认证状态管理

**Goal**: 实现管理员登录页面和独立的 token/state 管理机制。

**Requirements**: R1

**Dependencies**: 无（后端 API 已就绪）

**Files**:
- `index.html` — 新增 `#admin-login` 视图和 `#admin-reviews` 视图骨架
- `app.js` — 新增管理员 state 管理、adminApi 封装、登录逻辑、hash 路由

**Approach**:
1. 在 `index.html` 中新增两个视图：
   - `#admin-login`：管理员登录表单（账号、密码、登录按钮）
   - `#admin-reviews`：管理员后台主视图（包含审核列表、详情、同步等子面板，通过内部 Tab 切换）
2. 在 `app.js` 中新增：
   - `adminState` 对象和 `loadAdminState()`/`saveAdminState()` 函数（key: `eternal_ai_admin_state`）
   - `getAdminToken()`/`setAdminToken()` 函数（key: `eternal_ai_admin_token`）
   - `adminApi(path, options)` 封装（自动添加管理员 Authorization 头）
   - `handleHashRoute()` 函数：监听 `hashchange`，当 hash 为 `#admin` 时检查管理员登录状态并显示对应视图
   - `handleAdminLogin(formData)` 函数：调用 `POST /api/admin-auth/login`，成功后存储 token 并跳转审核列表
3. 在 `views` 对象中注册 `'admin-login'` 和 `'admin-reviews'`
4. 在 `viewLabels` 中添加对应标签

**Patterns to follow**:
- 用户登录表单模式（`app.js:1189-1232` 行的 auth 表单提交）
- `api()` 封装模式（`app.js:4-42` 行），adminApi 镜照此模式但读取 adminToken
- 视图注册模式（`app.js:112-122` 行 views 对象）

**Test scenarios**:
- **Happy path**: 管理员访问 `#admin`，输入正确账号密码，登录成功跳转审核列表页
- **Error path**: 管理员输入错误密码，显示错误提示
- **Edge case**: 管理员已登录时访问 `#admin`，直接跳转审核列表页（不显示登录表单）
- **Edge case**: 非管理员用户访问 `#admin`，显示登录页（用户 token 不能用于管理员 API）
- **Integration**: 登录后调用 `GET /api/admin-auth/me` 验证 token 有效

**Verification**: 管理员可通过 `#admin` URL 访问登录页，登录成功后进入审核列表页，刷新页面后仍保持登录状态。

---

### U2. 管理员审核列表页 + 角色详情审核页

**Goal**: 实现审核列表（支持状态筛选）和角色详情审核页（通过/驳回）。

**Requirements**: R2, R3, R6

**Dependencies**: U1

**Files**:
- `index.html` — 在 `#admin-reviews` 视图中添加审核列表和详情面板的 HTML 结构
- `app.js` — 新增 `renderAdminReviews()`、`renderAdminReviewDetail()`、`handleApprove()`、`handleReject()` 函数
- `e2e/admin-sync-flow.spec.js` — 扩展 E2E 测试覆盖管理员审核 UI 流程（可选，已有 API 级测试）

**Approach**:
1. 审核列表页结构：
   - 顶部状态筛选 Tab（全部/待审核/已通过/已同步/同步失败/已驳回）
   - 角色卡片列表（头像、名称、创作者、审核状态标签、创建时间）
   - 点击卡片进入详情页
2. 详情审核页结构：
   - 角色完整信息（头像、名称、描述、人设字段）
   - 当前审核状态
   - 操作按钮区：
     - `pending_review` 状态：显示「通过」和「驳回」按钮
     - `approved` 状态：显示「发起同步」按钮（跳转 U3）
     - `synced` 状态：显示二维码图片和 profileId
     - `rejected` 状态：显示驳回原因
     - `failed` 状态：显示「重新同步」按钮
3. 驳回流程：点击「驳回」→ 弹窗输入原因 → 调用 `POST /api/admin/reviews/:roleId/reject`
4. 列表数据通过 `GET /api/admin/reviews?status=xxx` 获取，支持分页

**Patterns to follow**:
- 角色库列表渲染模式（`app.js:302-339` 行 `renderRoleLibrary()`）
- 创作者角色卡片渲染模式（`app.js:415-450` 行 `renderCreatorRoles()`）
- Tab 切换模式（`app.js:409-431` 行 creator-center 的 center-tabs）

**Test scenarios**:
- **Happy path**: 管理员登录后看到待审核角色列表，点击角色进入详情，点击「通过」后角色状态变为 approved
- **Happy path**: 管理员点击「驳回」，输入原因，角色状态变为 rejected，列表中显示驳回原因
- **Edge case**: 切换状态筛选 Tab，列表正确过滤
- **Error path**: 对已审核角色再次操作，后端返回 400，前端显示错误提示
- **Integration**: 审核通过后，角色出现在「已通过」筛选列表中

**Verification**: 管理员可查看各状态角色列表，可审核通过或驳回角色，操作后列表实时更新。

---

### U3. 管理员同步表单 + 二维码展示 + 系统配置

**Goal**: 实现同步发起表单、同步成功后二维码展示、系统配置页面。

**Requirements**: R4, R5, R7

**Dependencies**: U2

**Files**:
- `index.html` — 在 `#admin-reviews` 视图中添加同步表单面板和配置面板的 HTML
- `app.js` — 新增 `renderSyncForm(roleId)`、`handleSyncSubmit()`、`renderSyncResult()`、`renderAdminConfig()`、`handleConfigUpdate()` 函数

**Approach**:
1. 同步表单（在详情页中 `approved` 状态时显示）：
   - 字段：profile 名（必填）、主 model key（必填）、主服务商（必填，下拉选择 openrouter/siliconflow 等）、多媒体 model key、多媒体服务商、定时任务开关、webhook URL 覆盖（可选）
   - 提交按钮：调用 `POST /api/admin/sync/:roleId`
   - 提交后显示 loading 状态
2. 同步结果展示：
   - 成功：显示二维码图片（`qrCodeUrl`）、profileId、「复制二维码链接」按钮
   - 失败：显示错误信息、「重新同步」按钮
3. 系统配置页（管理员后台第三个 Tab）：
   - 列出所有系统配置（`GET /api/admin/config`）
   - 每项配置可编辑（`PUT /api/admin/config/:key`）
   - 敏感配置（SYNC_SECRET）显示 `***` 且不可编辑
   - 重点配置：HERMES_WEBHOOK_URL、HERMES_ADMIN_TOKEN 等

**Patterns to follow**:
- 角色创建表单的 stepper 模式（`app.js:633-936` 行）
- 设置表单提交模式（`app.js:1233-1255` 行 settings 表单）
- Hermes 部署指南弹窗模式（`app.js:546-617` 行 `showHermesDeployGuide()`）

**Test scenarios**:
- **Happy path**: 管理员在 approved 角色详情页填写同步参数，提交后显示二维码和 profileId
- **Error path**: 同步失败（Hermes 不可达），显示错误信息
- **Edge case**: webhook URL 留空时使用全局配置
- **Edge case**: 同步中状态（syncing）时按钮禁用
- **Happy path**: 管理员在配置页修改 HERMES_WEBHOOK_URL，保存成功
- **Error path**: 尝试修改 SYNC_SECRET，返回 403 禁止操作

**Verification**: 管理员可发起同步并看到二维码，可修改系统配置。

---

### U4. 创作者角色卡片审核状态展示

**Goal**: 修改创作者角色卡片，展示审核状态、驳回原因、二维码。

**Requirements**: R8, R9, R10

**Dependencies**: 无（后端已返回所需字段）

**Files**:
- `app.js` — 修改 `renderCreatorRoles()` 函数（约 415-450 行）
- `styles.css` — 新增审核状态标签样式
- `e2e/creator.spec.js` — 扩展测试覆盖审核状态展示

**Approach**:
1. 修改 `renderCreatorRoles()` 中的卡片渲染逻辑：
   - 根据 `reviewStatus` 渲染状态标签（优先于 `status`）：
     - `pending_review` → 灰色标签「待审核」
     - `approved` → 蓝色标签「已通过」
     - `rejected` → 红色标签「已驳回」+ 显示 `reviewNote`
     - `syncing` → 黄色标签「同步中」
     - `synced` → 绿色标签「已同步」+ 显示 `status`（运行中/已停止）
     - `failed` → 橙色标签「同步失败」
   - `synced` 状态角色卡片额外显示：
     - 二维码缩略图（`qrCodeUrl`）
     - 「查看二维码」按钮（点击放大显示）
     - 「转发二维码」按钮（复制链接到剪贴板）
   - `rejected` 状态角色卡片额外显示：
     - 驳回原因文本（`reviewNote`）
     - 「编辑」按钮高亮提示修改
2. 新增 `showQrCodeModal(qrCodeUrl)` 函数：弹窗显示二维码大图
3. 新增 `copyQrCodeLink(qrCodeUrl)` 函数：复制二维码链接到剪贴板

**Patterns to follow**:
- 现有角色卡片状态标签模式（`role-card__status--${statusClass}`）
- Hermes 部署指南弹窗模式（`app.js:546-617` 行）
- `escapeHtml()` 用于所有用户可控内容

**Test scenarios**:
- **Happy path**: 创作者创建角色后，卡片显示「待审核」标签
- **Happy path**: 管理员审核通过后，创作者卡片显示「已通过」标签
- **Happy path**: 同步完成后，创作者卡片显示「已同步」+ 二维码缩略图
- **Happy path**: 被驳回后，创作者卡片显示「已驳回」+ 驳回原因
- **Edge case**: 点击「查看二维码」弹窗显示大图
- **Edge case**: 点击「转发二维码」复制链接到剪贴板

**Verification**: 创作者可在角色列表看到每个角色的审核状态、驳回原因和二维码。

---

### U5. Order 订单后端 API

**Goal**: 实现 Order 模型的 CRUD 后端端点。

**Requirements**: R11

**Dependencies**: 无（Order 模型已在 schema 中定义）

**Files**:
- `src/routes/orders.js` — 新建订单路由文件
- `server.js` — 挂载 `/api/orders` 路由
- `e2e/orders.spec.js` — 新建订单 E2E 测试

**Approach**:
1. 新建 `src/routes/orders.js`，实现以下端点：
   - `POST /api/orders` — 创建订单（付款）
     - 认证：`authMiddleware`
     - 请求体：`{ roleId, amount }`
     - 逻辑：
       - 校验 roleId 存在且角色 `reviewStatus='synced'` 且 `qrCodeUrl` 非空
       - 校验 amount > 0
       - 创建 Order 记录（status='paid'，userId=当前用户）
       - 返回 `{ order: { id, roleId, amount, status, createdAt }, role: { qrCodeUrl, displayName } }`
   - `GET /api/orders` — 查询当前用户订单列表
     - 认证：`authMiddleware`
     - 返回：`{ orders: [{ id, roleId, role: { displayName, avatar }, amount, status, createdAt }] }`
   - `GET /api/orders/:id` — 查询订单详情
     - 认证：`authMiddleware`
     - 校验：订单属于当前用户
     - 返回：`{ order: { ...完整字段, role: { ...角色信息 } } }`
2. 在 `server.js` 中挂载路由：`app.use('/api/orders', require('./src/routes/orders'));`
3. 错误处理：
   - 角色未同步：返回 400 `{ error: '该角色尚未同步，无法订阅' }`
   - 角色不存在：返回 404
   - 订单不属于当前用户：返回 403

**Patterns to follow**:
- 现有路由模式（`src/routes/roles.js` 的 `authMiddleware` 使用）
- 现有错误处理模式（`res.status(400).json({ error: '...' })`）
- Prisma 查询模式（`prisma.order.findMany({ where: { userId }, include: { role: true } })`）

**Test scenarios**:
- **Happy path**: 用户对已同步角色发起付款，创建订单成功，返回 qrCodeUrl
- **Error path**: 用户对未同步角色发起付款，返回 400
- **Error path**: 未认证用户发起付款，返回 401
- **Edge case**: 用户查询自己的订单列表，只返回自己的订单
- **Edge case**: 用户查询他人订单，返回 403
- **Integration**: 创建订单后，订单列表中包含该订单

**Verification**: Order API 可创建、查询订单，角色未同步时拒绝创建。

---

### U6. 前端付款流程对接

**Goal**: 修改前端 `payRole()` 函数，调用后端 API 创建订单并展示真实二维码。

**Requirements**: R12

**Dependencies**: U5

**Files**:
- `app.js` — 修改 `payRole()` 函数（约 361-367 行），新增 `renderPaidState()` 函数
- `index.html` — 修改 `#role-detail` 视图中的付款后区域结构
- `e2e/roles.spec.js` — 扩展测试覆盖付款流程

**Approach**:
1. 修改 `payRole()` 函数：
   - 调用 `POST /api/orders` 传入 `{ roleId: currentRole.id, amount: currentRole.price }`
   - 成功后调用 `renderPaidState(data.role.qrCodeUrl)` 展示二维码
   - 失败时 `alert` 错误信息
   - 添加 loading 状态（按钮禁用 + 文字变为「处理中…」）
2. 新增 `renderPaidState(qrCodeUrl)` 函数：
   - 隐藏「立即订阅」按钮
   - 显示「已订阅」区域
   - 渲染二维码图片（`<img src="${qrCodeUrl}" alt="角色二维码" />`）
   - 添加「保存二维码」按钮（长按或下载）
3. 修改 `#role-detail` 视图 HTML：
   - `#detail-qr` 区域改为可容纳 `<img>` 的容器
   - 移除 `qr-placeholder` 占位符

**Patterns to follow**:
- 现有 `api()` 调用模式（`app.js:4-42` 行）
- 现有按钮 loading 模式（禁用 + 文字变化）
- `escapeHtml()` 用于所有动态内容

**Test scenarios**:
- **Happy path**: 用户点击「立即订阅」，调用 API 成功，显示真实二维码
- **Error path**: 角色未同步，API 返回 400，显示错误提示
- **Error path**: 网络错误，显示「无法连接服务器」
- **Edge case**: 重复点击「立即订阅」按钮，第二次点击被禁用（loading 状态）
- **Integration**: 付款后刷新页面，角色详情页仍显示已订阅状态（基于订单记录）

**Verification**: 用户付款后看到真实二维码，非占位符。

---

## Risks & Dependencies

### 风险

1. **管理员 UI 复杂度**: 管理员后台涉及多个面板（审核列表、详情、同步表单、配置），HTML 和 JS 代码量较大。**缓解**: 分三个实现单元（U1/U2/U3）逐步实现，每个单元可独立验证。

2. **Order 付款流程简化**: 本次创建订单即视为已付款，不对接真实支付。后续对接支付网关时需重构。**缓解**: Order 模型已预留 `status` 字段，后续扩展状态机即可。

3. **创作者卡片状态展示复杂性**: `reviewStatus` 和 `status` 两个状态字段的展示逻辑需仔细设计，避免混淆。**缓解**: KTD5 明确了优先级规则——`reviewStatus` 优先，仅 `synced` 时显示 `status`。

### 依赖

- 后端管理员 API 已全部就绪（U1-U3 依赖）
- 后端 `GET /api/roles/my/roles` 已返回所需字段（U4 依赖）
- Order 模型已在 Prisma schema 中定义（U5 依赖）
- 前端 `payRole()` 函数已存在（U6 修改对象）

---

## Deferred to Follow-Up Work

- 微信 OAuth 真实接入（hermes-server/src/routes/bind.js 的 `wechat_oauth_placeholder`）
- 收入/提现后端 API（当前仍为前端 Mock 数据）
- 真实支付网关对接（当前创建订单即视为已付款）
- 角色删除/上下架切换
- 定时任务调度逻辑（enableSchedule 标志已传递，但 Hermes Server 无实际调度）
- 管理员后台移动端适配优化（本次优先功能完整性）

---

## System-Wide Impact

- **用户**: 普通用户在角色详情页付款后能看到真实二维码（U6）
- **创作者**: 创作者可在角色列表看到审核状态和二维码（U4）
- **管理员**: 管理员可通过浏览器完成审核和同步操作（U1-U3）
- **数据库**: Order 表开始有数据写入（U5）
- **API**: 新增 `/api/orders` 路由（U5）

---

## Acceptance Examples

- **AE1**: 管理员访问 `#admin`，登录后看到待审核角色列表，点击「通过」→ 角色状态变为 approved → 点击「发起同步」→ 填写参数 → 显示二维码
- **AE2**: 创作者创建角色后，在角色列表看到「待审核」标签 → 管理员驳回后，看到「已驳回」+ 驳回原因 → 修改后重新提交，看到「待审核」
- **AE3**: 用户在角色详情页点击「立即订阅」→ 调用 API 创建订单 → 显示真实二维码 → 用户扫码绑定
