Files
Genarrative/docs/technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md
kdletters a2c71fcb3a
Some checks failed
CI / verify (push) Has been cancelled
chore: remove maincloud configuration
2026-05-02 17:04:11 +08:00

16 KiB
Raw Blame History

后台管理独立前端工程技术方案

日期:2026-04-30

对应 PRD后台管理独立前端工程 PRD

落地状态:2026-04-30 已创建 apps/admin-web 独立前端工程包含登录、总览、API 调试、兑换码管理和注册邀请码管理首版页面;根工程已补 admin-web:* 转发脚本。2026-05-01 起,根构建与 Ubuntu 发布包会同步构建后台前端,并在发布包 Web 网关中以同域 /admin/ 暴露。

1. 结论

后台管理端采用独立前端工程,路径固定为 apps/admin-web。它只负责 UI 表现、输入采集、请求发起和结果渲染所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 server-rs/crates/api-server

本方案接管旧 api-server 内嵌 HTML/CSS/JS 页面Rust api-server 直连时旧 GET /admin 不再挂载。部署态后台入口由发布包内 web-server.mjs 承接:/admin/ 返回独立前端静态产物,/admin/api/* 继续反代到 api-server

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
      │  └─ AdminInviteCodePage.tsx
      └─ styles/
         └─ admin.css

首版可使用独立 package.json,不要求立刻把根工程改成 npm workspace。后续如果根工程统一 workspace再把 apps/admin-web 纳入统一脚本。

3. 技术栈

  1. React + TypeScript + Vite。
  2. 图标使用 lucide-react
  3. 样式首版使用普通 CSS 或 CSS Modules不引入新的大型 UI 组件库。
  4. 请求使用浏览器 fetch 封装,不新增状态管理库。
  5. 不引入 SpacetimeDB TypeScript SDK管理端不直连 SpacetimeDB。

4. API 边界

4.1 基础约定

所有管理端请求使用同一个 adminApiClient

  1. base URL 由 VITE_ADMIN_API_BASE_URL 配置。
  2. 未配置时默认同源空前缀。
  3. 有 token 时附加 Authorization: Bearer <token>
  4. 后端统一响应 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
创建/更新注册邀请码 POST /admin/api/profile/invite-codes 管理员 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 AdminUpsertProfileInviteCodeRequest {
  inviteCode: string;
  metadata?: Record<string, unknown>;
}

export interface ProfileRedeemCodeAdminResponse {
  code: string;
  mode: ProfileRedeemCodeMode;
  rewardPoints: number;
  maxUses: number;
  globalUsedCount: number;
  enabled: boolean;
  allowedUserIds: string[];
  createdBy: string;
  createdAt: string;
  updatedAt: string;
}

export interface ProfileInviteCodeAdminResponse {
  userId: string;
  inviteCode: string;
  metadata: Record<string, unknown>;
  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 返回:

  1. servicebindHostbindPortjwtIssueradminEnabledspacetimeServerUrlspacetimeDatabase
  2. databasedatabaseIdentityownerIdentityhostTypeschemaTableNamestableStatsfetchErrors

后端读取 SpacetimeDB schema 时必须请求 /v1/database/{database}/schema?version=9。SpacetimeDB 2.x schema HTTP API 缺少 version query 会返回 400 missing field version,后台页面只能展示读取异常,不能拿到真实表名。

schemaTableNamestableStats 必须采用同一份 schema 表清单生成不能再用硬编码关键表白名单补齐统计项。后台右上角显示的表数量必须等于统计表格实际行数schema 读取失败时两者均为空,并通过 fetchErrors 暴露读取失败原因。

后端读取表行数时必须按 SpacetimeDB 2.x /sql 响应解析:接口返回 statement result 数组,单条结果内的 schema.elements 描述列名,rows 是按列顺序排列的数组行,例如 rows: [[0]]。后台服务不能再假设响应是 { rows: [{ row_count: 0 }] } 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。

tableStats 中单表失败必须展示 errorMessage不能让整页变成空白。SpacetimeDB private 表或当前身份不可见的表在 /sql 下可能返回 no such table / marked private后台服务必须将这类错误归一为“不可统计private 或当前身份不可见)”,避免把预期的访问边界展示成原始 HTTP 400 故障。

线上如果大量表都显示“不可统计private 或当前身份不可见)”,优先检查 api-server 启动环境中的 GENARRATIVE_SPACETIME_TOKEN 是否存在且属于目标库 owner。Jenkins 覆盖发布包时必须保留部署目录已有运行 token只带迁移 token 不能让后台概览读取 private 表。

4.6 API 调试 contract

请求:

{
  "method": "GET",
  "path": "/healthz",
  "headers": [],
  "body": ""
}

限制由后端执行:

  1. path 只允许同源相对路径。
  2. 禁止绝对 URL。
  3. 禁止调试 /admin/api/login
  4. 禁止覆盖危险请求头。
  5. 请求体大小和超时由后端收口。

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 为准。

4.8 邀请码管理 contract

创建/更新请求:

{
  "inviteCode": "SPRING2026",
  "metadata": {
    "batch": "spring"
  }
}

成功返回邀请码记录:

{
  "userId": "admin",
  "inviteCode": "SPRING2026",
  "metadata": {
    "batch": "spring"
  },
  "createdAt": "2026-04-30T00:00:00Z",
  "updatedAt": "2026-04-30T00:00:00Z"
}

邀请码页的 metadata 输入必须先在前端解析为 JSON 对象;空字符串按 {} 处理,数组、字符串、数字等非对象值直接提示错误。最终标准化、长度限制和邀请码合法性以 server-rs 为准。

5. 鉴权与会话

  1. token key 固定为 genarrative_admin_token
  2. token 首版存 localStorage。
  3. 应用启动时如果存在 token先调用 GET /admin/api/me
  4. 401 时清空 token 并回到登录页。
  5. 403 时展示无权限状态,不自动重试。
  6. 退出登录只清理本地 token首版没有后台 refresh session 和服务端会话吊销。

6. 页面实现要点

  1. AdminShell 承载导航、当前管理员、退出按钮和页面容器。
  2. 登录页不进入 AdminShell,避免未登录时展示后台导航。
  3. 总览页加载失败时展示后端错误,不吞掉 fetchErrors
  4. API 调试页的 headers 使用键值行编辑,提交前转为 [{ name, value }]
  5. 兑换码页的 mode=private 时展示允许用户输入区;其他模式提交空数组。
  6. 邀请码页只提交 inviteCode 与 JSON 对象 metadata不在前端复制后端邀请码规则。
  7. 所有按钮的 loading 状态必须锁定重复提交。
  8. 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。

7. 部署与联调

7.1 本地联调

  1. 启动后端:npm run api-server
  2. 启动后台前端:在 apps/admin-web 执行 npm run dev
  3. 后台 dev server 通过 Vite proxy 转发 /admin/apiADMIN_API_TARGET;未配置时默认 http://127.0.0.1:3100
  4. 若使用非 3100 端口,在仓库根目录 .env.local 设置 ADMIN_API_TARGET=http://127.0.0.1:<api-server-port>,并重启后台前端 dev server。
  5. GENARRATIVE_API_PORT 控制 Rust api-server 监听端口;ADMIN_API_TARGET 只控制后台前端 dev proxy 目标,二者需要指向同一个端口。

7.2 构建部署

当前发布形态固定为同域 /admin/

  1. 本地单独执行 npm run admin-web:build 时,后台构建产物默认输出到 apps/admin-web/dist
  2. 根工程执行 npm run build 时,会先构建主前端,再构建后台前端;任一构建失败或输出 warning 都会让构建门禁失败。
  3. Ubuntu 发布包执行 npm run deploy:rust:remote 时,后台前端以 Vite --base /admin/ 构建到发布包 web/admin/
  4. 发布包 web-server.mjs/admin 返回 301 到 /admin/,对 /admin//admin/* 提供后台 SPA fallback/admin/api/* 优先反代到 api-server

该形态不新增后台静态端口和后台专用后端。server-rs 仍然是唯一管理 API 后端,后台前端不直连 SpacetimeDB。

7.3 后台工程脚本

apps/admin-web/package.json 首版至少提供以下脚本:

{
  "scripts": {
    "dev": "vite --host 127.0.0.1",
    "build": "node ../../scripts/admin-web-build.mjs build",
    "typecheck": "node ../../scripts/admin-web-build.mjs typecheck",
    "preview": "vite preview --host 127.0.0.1"
  }
}

如果后续接入根 npm workspace再在根 package.json 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。

当前工程没有启用 npm workspace因此后台构建脚本必须从仓库根目录调用 root toolchain。scripts/admin-web-build.mjs 统一执行 tsc --noEmit -p apps/admin-web/tsconfig.json 与 Vite 构建,避免 npm --prefix apps/admin-web 在子目录找不到 tsc

当前根工程同步提供以下转发脚本:

  1. npm run admin-web:dev
  2. npm run admin-web:typecheck
  3. npm run admin-web:build
  4. npm run admin-web:preview

8. 测试计划

  1. apps/admin-web
    • 登录成功和失败。
    • token 恢复、过期清理、退出登录。
    • 总览页正常数据、部分表统计失败、整体请求失败。
    • API 调试成功访问 /healthz,绝对 URL 被后端拒绝。
    • 兑换码 public/unique/private 表单提交和停用。
    • 邀请码表单提交、metadata JSON 对象校验和结果展示。
  2. 根工程:
    • npm run check:encoding
    • 后续接入根 workspace 后,补充后台工程 build/typecheck 脚本。
  3. 后端:
    • 继续保留 cargo test -p api-server --manifest-path server-rs/Cargo.toml admin
    • 修改后端管理 API 后必须运行 npm run api-server 并手动验证 /admin 为 404、/admin/api/login 可用。

9. 后续扩展边界

后续新增用户管理、作品审核、资产审核、订单/充值管理时,必须先补对应 PRD 和技术方案,并在 server-rs 增加受保护管理 API。不要让 apps/admin-web 直接读取 SpacetimeDB 或复制业务规则。

10. 实施顺序

  1. 先创建 apps/admin-web 工程骨架,确保空应用可 dev/build
  2. 再实现 adminApiTypesadminApiClient,用 /admin/api/login 做第一条真实链路。
  3. 接入 adminAuthStore 和启动恢复逻辑,确认 401 会清理本地 token。
  4. 完成 AdminShell 与四页路由再分别接入总览、API 调试、兑换码和邀请码接口。
  5. 最后补测试、运行 npm run check:encoding,并确认 GET /admin 仍由 api-server 返回 404

当前实现已完成第 1 至第 4 步。验证以实际命令输出为准。