12 KiB
后台管理独立前端工程技术方案
日期:2026-04-30
对应 PRD:后台管理独立前端工程 PRD
落地状态:2026-04-30 已创建 apps/admin-web 独立前端工程,包含登录、总览、API 调试和兑换码管理首版页面;根工程已补 admin-web:* 转发脚本。
1. 结论
后台管理端采用独立前端工程,路径固定为 apps/admin-web。它只负责 UI 表现、输入采集、请求发起和结果渲染;所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 server-rs/crates/api-server。
本方案接管旧 api-server 内嵌 HTML/CSS/JS 页面,旧 GET /admin 不再挂载。后续后台入口由独立前端工程部署产物承接。
2. 工程结构
建议首版结构:
apps/
└─ admin-web/
├─ index.html
├─ package.json
├─ tsconfig.json
├─ vite.config.ts
└─ src/
├─ main.tsx
├─ app/
│ ├─ AdminApp.tsx
│ ├─ AdminShell.tsx
│ └─ adminRoutes.ts
├─ api/
│ ├─ adminApiClient.ts
│ └─ adminApiTypes.ts
├─ auth/
│ └─ adminAuthStore.ts
├─ pages/
│ ├─ AdminLoginPage.tsx
│ ├─ AdminOverviewPage.tsx
│ ├─ AdminDebugHttpPage.tsx
│ └─ AdminRedeemCodePage.tsx
└─ styles/
└─ admin.css
首版可使用独立 package.json,不要求立刻把根工程改成 npm workspace。后续如果根工程统一 workspace,再把 apps/admin-web 纳入统一脚本。
3. 技术栈
- React + TypeScript + Vite。
- 图标使用
lucide-react。 - 样式首版使用普通 CSS 或 CSS Modules,不引入新的大型 UI 组件库。
- 请求使用浏览器
fetch封装,不新增状态管理库。 - 不引入 SpacetimeDB TypeScript SDK;管理端不直连 SpacetimeDB。
4. API 边界
4.1 基础约定
所有管理端请求使用同一个 adminApiClient:
- base URL 由
VITE_ADMIN_API_BASE_URL配置。 - 未配置时默认同源空前缀。
- 有 token 时附加
Authorization: Bearer <token>。 - 后端统一响应 envelope 时,前端读取
data;错误优先读取error.details.message,再读error.message,最后回退到 HTTP 状态。
前端统一按以下响应形状解析,不在页面组件里重复拆 envelope:
export interface ApiSuccessEnvelope<T> {
data: T;
meta?: unknown;
}
export interface ApiErrorEnvelope {
error?: {
code?: string;
message?: string;
details?: {
message?: string;
[key: string]: unknown;
} | null;
};
meta?: unknown;
}
adminApiClient 暴露 request<T>()、get<T>()、post<T>() 三层即可。页面只拿到成功数据或抛出的中文错误消息,不直接处理 Response。
4.2 已有管理接口
| 功能 | 方法与路径 | 鉴权 |
|---|---|---|
| 管理员登录 | POST /admin/api/login |
无 |
| 当前管理员 | GET /admin/api/me |
管理员 Bearer |
| 服务与数据库概览 | GET /admin/api/overview |
管理员 Bearer |
| 受控 HTTP 调试 | POST /admin/api/debug/http |
管理员 Bearer |
| 创建/更新兑换码 | POST /admin/api/profile/redeem-codes |
管理员 Bearer |
| 停用兑换码 | POST /admin/api/profile/redeem-codes/disable |
管理员 Bearer |
4.3 前端类型命名
后台前端首版不引入自动生成 contract。为了避免字段漂移,apps/admin-web/src/api/adminApiTypes.ts 必须按 shared-contracts 的 camelCase JSON 字段命名:
export interface AdminSessionPayload {
subject: string;
username: string;
displayName: string;
roles: string[];
issuedAt: string;
expiresAt: string;
}
export interface AdminLoginResponse {
token: string;
admin: AdminSessionPayload;
}
export interface AdminOverviewResponse {
service: AdminServiceOverviewPayload;
database: AdminDatabaseOverviewPayload;
}
export interface AdminServiceOverviewPayload {
bindHost: string;
bindPort: number;
jwtIssuer: string;
adminEnabled: boolean;
spacetimeServerUrl: string;
spacetimeDatabase: string;
}
export interface AdminDatabaseOverviewPayload {
databaseIdentity: string | null;
ownerIdentity: string | null;
hostType: string | null;
schemaTableNames: string[];
tableStats: AdminDatabaseTableStatPayload[];
fetchErrors: string[];
}
export interface AdminDatabaseTableStatPayload {
tableName: string;
rowCount: number | null;
errorMessage: string | null;
}
export interface AdminDebugHeaderInput {
name: string;
value: string;
}
export interface AdminDebugHttpRequest {
method: string;
path: string;
headers?: AdminDebugHeaderInput[];
body?: string;
}
export interface AdminDebugHttpResponse {
status: number;
statusText: string;
headers: AdminDebugHeaderInput[];
bodyText: string;
bodyJson: unknown | null;
}
兑换码类型同样保持 camelCase:
export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
enabled: boolean;
allowedUserIds: string[];
allowedPublicUserCodes: string[];
}
export interface AdminDisableProfileRedeemCodeRequest {
code: string;
}
export interface ProfileRedeemCodeAdminResponse {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
globalUsedCount: number;
enabled: boolean;
allowedUserIds: string[];
createdBy: string;
createdAt: string;
updatedAt: string;
}
4.4 登录 contract
请求:
{
"username": "root",
"password": "secret123"
}
成功数据:
{
"token": "<admin bearer token>",
"admin": {
"subject": "admin:root",
"username": "root",
"displayName": "root",
"roles": ["admin"],
"issuedAt": "2026-04-30T00:00:00Z",
"expiresAt": "2026-04-30T04:00:00Z"
}
}
503 表示后台未启用;401 表示用户名或密码错误。
4.5 总览 contract
GET /admin/api/overview 返回:
service:bindHost、bindPort、jwtIssuer、adminEnabled、spacetimeServerUrl、spacetimeDatabase。database:databaseIdentity、ownerIdentity、hostType、schemaTableNames、tableStats、fetchErrors。
后端读取 SpacetimeDB schema 时必须请求 /v1/database/{database}/schema?version=9。SpacetimeDB 2.x schema HTTP API 缺少 version query 会返回 400 missing field version,后台页面只能展示读取异常,不能拿到真实表名。
后端读取表行数时必须按 SpacetimeDB 2.x /sql 响应解析:接口返回 statement result 数组,单条结果内的 schema.elements 描述列名,rows 是按列顺序排列的数组行,例如 rows: [[0]]。后台服务不能再假设响应是 { rows: [{ row_count: 0 }] } 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。
tableStats 中单表失败必须展示 errorMessage,不能让整页变成空白。
4.6 API 调试 contract
请求:
{
"method": "GET",
"path": "/healthz",
"headers": [],
"body": ""
}
限制由后端执行:
path只允许同源相对路径。- 禁止绝对 URL。
- 禁止调试
/admin/api/login。 - 禁止覆盖危险请求头。
- 请求体大小和超时由后端收口。
4.7 兑换码管理 contract
创建/更新请求:
{
"code": "WELCOME2026",
"mode": "public",
"rewardPoints": 100,
"maxUses": 1,
"enabled": true,
"allowedUserIds": [],
"allowedPublicUserCodes": []
}
停用请求:
兑换码管理页的最近一次接口返回记录由 AdminApp 维护为管理端会话态,并传入 AdminRedeemCodePage 渲染。页面页签通过 hash 切换时子页面会卸载,不能把最近记录只放在兑换码页面内部 useState 中,否则切换到其他页签再返回会展示“暂无记录”。该会话态只用于保留当前操作结果,不作为兑换码历史列表;退出登录或重新登录时清空。
{
"code": "WELCOME2026"
}
成功返回兑换码记录:
{
"code": "WELCOME2026",
"mode": "public",
"rewardPoints": 100,
"maxUses": 1,
"globalUsedCount": 0,
"enabled": true,
"allowedUserIds": [],
"createdBy": "admin:root",
"createdAt": "2026-04-30T00:00:00Z",
"updatedAt": "2026-04-30T00:00:00Z"
}
前端只做基础输入约束,最终标准化、私有码用户解析、次数和奖励合法性以 server-rs 为准。
5. 鉴权与会话
- token key 固定为
genarrative_admin_token。 - token 首版存 localStorage。
- 应用启动时如果存在 token,先调用
GET /admin/api/me。 401时清空 token 并回到登录页。403时展示无权限状态,不自动重试。- 退出登录只清理本地 token;首版没有后台 refresh session 和服务端会话吊销。
6. 页面实现要点
AdminShell承载导航、当前管理员、退出按钮和页面容器。- 登录页不进入
AdminShell,避免未登录时展示后台导航。 - 总览页加载失败时展示后端错误,不吞掉
fetchErrors。 - API 调试页的 headers 使用键值行编辑,提交前转为
[{ name, value }]。 - 兑换码页的
mode=private时展示允许用户输入区;其他模式提交空数组。 - 所有按钮的 loading 状态必须锁定重复提交。
- 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。
7. 部署与联调
7.1 本地联调
- 启动后端:
npm run api-server:maincloud。 - 启动后台前端:在
apps/admin-web执行npm run dev。 - 后台 dev server 通过 Vite proxy 转发
/admin/api到ADMIN_API_TARGET;未配置时默认http://127.0.0.1:3100。 - 若使用非 3100 端口,在仓库根目录
.env.local设置ADMIN_API_TARGET=http://127.0.0.1:<api-server-port>,并重启后台前端 dev server。 GENARRATIVE_API_PORT控制 Rustapi-server监听端口;ADMIN_API_TARGET只控制后台前端 dev proxy 目标,二者需要指向同一个端口。
7.2 构建部署
首版构建产物由独立后台工程输出到 apps/admin-web/dist。部署可以选择:
- 独立静态站点域名,例如
https://admin.example.com。 - 与主站同域不同路径,由网关把后台静态资源和
/admin/api/*分别路由到正确目标。
无论哪种方式,server-rs 仍然是唯一管理 API 后端。
7.3 后台工程脚本
apps/admin-web/package.json 首版至少提供以下脚本:
{
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit",
"preview": "vite preview --host 127.0.0.1"
}
}
如果后续接入根 npm workspace,再在根 package.json 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。
当前根工程同步提供以下转发脚本:
npm run admin-web:devnpm run admin-web:typechecknpm run admin-web:buildnpm run admin-web:preview
8. 测试计划
apps/admin-web:- 登录成功和失败。
- token 恢复、过期清理、退出登录。
- 总览页正常数据、部分表统计失败、整体请求失败。
- API 调试成功访问
/healthz,绝对 URL 被后端拒绝。 - 兑换码 public/unique/private 表单提交和停用。
- 根工程:
npm run check:encoding。- 后续接入根 workspace 后,补充后台工程 build/typecheck 脚本。
- 后端:
- 继续保留
cargo test -p api-server --manifest-path server-rs/Cargo.toml admin。 - 修改后端管理 API 后必须运行
npm run api-server:maincloud并手动验证/admin为 404、/admin/api/login可用。
- 继续保留
9. 后续扩展边界
后续新增用户管理、作品审核、资产审核、订单/充值管理时,必须先补对应 PRD 和技术方案,并在 server-rs 增加受保护管理 API。不要让 apps/admin-web 直接读取 SpacetimeDB 或复制业务规则。
10. 实施顺序
- 先创建
apps/admin-web工程骨架,确保空应用可dev/build。 - 再实现
adminApiTypes与adminApiClient,用/admin/api/login做第一条真实链路。 - 接入
adminAuthStore和启动恢复逻辑,确认401会清理本地 token。 - 完成
AdminShell与三页路由,再分别接入总览、API 调试和兑换码接口。 - 最后补测试、运行
npm run check:encoding,并确认GET /admin仍由api-server返回404。
当前实现已完成第 1 至第 4 步。验证以实际命令输出为准。