118 lines
12 KiB
Markdown
118 lines
12 KiB
Markdown
# Phase 4 CMS 基础与管理后台报告
|
||
|
||
日期:2026-09-10
|
||
范围:P4-01 至 P4-07
|
||
状态:CMS 数据边界、模型、公共内容 API、可复现迁移、受控管理员、访问控制与真实内容闭环,以及用户/VIP/额度/TTS/审计管理后台均已验收通过。
|
||
|
||
## 1. 已完成
|
||
|
||
### 数据边界与模型
|
||
- 新增独立 Payload CMS 应用:`apps/cms`。
|
||
- CMS 使用独立 PostgreSQL 数据库 `kaotings_cms` 和独立数据库账号,不读取业务 API 用户表。
|
||
- 已配置 systemd 服务 `kaotings-cms`,仅监听 `127.0.0.1:3001`。
|
||
- Caddy 保留 `/cms` 外部边界,不向公网开放 CMS 回环端口。
|
||
- 建立模型:`Users`、`Products`、`Media`、`SiteSettings`、`HomeContent`。
|
||
- Product 支持 `draft`、`published`、`unpublished`,slug、分类、摘要、描述、媒体、features、specs、SEO 和排序字段。
|
||
- Media 为受控上传集合,普通访客只读媒体,写入需要 CMS 用户。
|
||
|
||
### 公共内容 API 与 Caddy 前缀
|
||
- Next.js basePath 为 `/cms`,Caddy 用 `handle /cms/*` 原样透传给 CMS 服务(不剥离前缀),避免资源地址/重定向丢失 `/cms`。
|
||
- `/cms/admin`、`/cms/admin/create-first-user` 可正常渲染 Payload 管理界面(200),资源以 `/cms/_next/...` 加载正常,无重定向循环。
|
||
- 公共 Products API `GET /cms/api/products` 返回 `200` 和空集合,未泄露草稿。
|
||
- Website 内容适配器已切换到 CMS 公共 API;`lib/content.ts` 的 `getProducts` 以 `?where[status][equals]=published` 拉取,`cache: no-store`,CMS 未发布产品时显示明确空状态。
|
||
|
||
### 数据库初始化固化(P4-01)
|
||
- 核对执行记录:`payload_migrations` 仅含 `20260909_100120_initial`(batch 1),与现有 `kaotings_cms` 结构一致。
|
||
- 已重新生成初始迁移并修正类型导入,命名为 `20260909_100120_initial` 以对齐已应用记录,避免重复应用。
|
||
- 迁移文件提交进仓库:`apps/cms/src/migrations/20260909_100120_initial.ts`。
|
||
- 使用独立临时库 `kaotings_cms_verify` 从仓库 `payload migrate` 重建,得到 14 张表,列类型比对 `SCHEMA_IDENTICAL`;随后删除临时库,现有 `kaotings_cms` 未清空。
|
||
- 迁移类型导入已修正(`MigrateUpArgs/MigrateDownArgs` 用 `import type`,`sql` 用值导入),本地与服务端构建均通过。
|
||
|
||
### 受控初始化 CMS 管理员(P4-02)
|
||
- 通过一次性受控路由 `apps/cms/src/app/api/seed-admin/route.ts` 初始化管理员,凭据来自 `/etc/kaotings/cms-admin.secrets`(root-only 0600),未写入 Git/报告/日志。
|
||
- 路由以 `x-cms-seed-token` 守卫,且仅在用户表为空时创建;已存在用户时返回 `409 {"ok":false,"error":"user exists"}`,防止重复初始化。
|
||
- 管理员:`tech@kaotings.com`(`displayName: CMS Admin`)。登录验证 `200 {"message":"Authentication Passed"}`。
|
||
- 修正:seed 路由不得在 Next 运行时调用 `payload.destroy()`(会销毁共享数据库连接池,导致后续请求 500)。
|
||
|
||
### 访问控制验证(P4-02)
|
||
- 匿名 `POST /cms/api/users/first-register` → `403`(已有用户后 create-first-user 失效)。
|
||
- 匿名 `POST /cms/api/users`(注册)→ `403`。
|
||
- 匿名 `GET /cms/api/users`、`GET /cms/api/users/1`(读取用户资料)→ `403`。
|
||
- 匿名 `PATCH /cms/api/users/1`(提权/修改)→ `403`。
|
||
- 匿名 `GET /cms/api/access`:`canAccessAdmin:false`(枚举到字段级权限,非仅状态码)。
|
||
- 管理员 `GET /cms/api/access`:`canAccessAdmin:true`。
|
||
- 普通网站用户属独立数据库 `kaotings`(角色 `kaotings_app`),CMS 使用 `kaotings_cms` 且签发自有 `payload-token` JWT,网站 `kaotings_session` cookie 不会被 CMS 接受,跨库无法获取 CMS 管理会话。
|
||
|
||
### 真实内容闭环(P4-03)
|
||
- 以管理员登录后创建明确标注的测试产品(slug `p4-test-draft-product`):
|
||
- 草稿:公共列表/API `docs:[]`,官网 `/products` 与详情不显示。
|
||
- 发布:`GET /cms/api/products` 返回该产品;官网 `/products` 列表显示,详情页渲染正确。
|
||
- 修改:更新 summary/name 后,公共列表与官网详情立即反映新内容(`no-store`,无缓存陈旧)。
|
||
- 下架:`status:unpublished` 后,公共列表/API 为空,官网详情 `404 Not Found`。
|
||
- 测试产品已下架(当前 `status:unpublished`),不对公网提供内容。
|
||
|
||
## 2. 修复的集成问题
|
||
|
||
- `/cms/admin` 前缀重定向/404:Next 恢复 `basePath: "/cms"`,Caddy 改用 `handle /cms/*` 透传,不再由 Caddy 剥离前缀。
|
||
- Website `CMS_PUBLIC_API_URL` 需指向 `http://127.0.0.1:3001/cms`(此前缺 `/cms` 前缀导致官网 `/products` 500):已更新 `kaotings-web.service` 环境变量。
|
||
- Payload Admin 的 serverFunction/importMap 接线:改用生成的真实 `importMap` 和 `@payloadcms/next/layouts` 的 `handleServerFunctions`。
|
||
- 产品 slug 查询统一使用已验证的筛选方式:`GET /cms/api/products?where[slug][equals]=<slug>`(返回 200)。Payload 的 `GET /cms/api/products/:slug` 是按 `id` 处理的路由,不是 slug 路由,任何单产品按 slug 取数都必须用 `where[slug][equals]=` 筛选,不得把按 `id` 的路由当作按 slug 使用。
|
||
|
||
### 管理后台(P4-04 至 P4-07)
|
||
- 复用既有 FastAPI 管理接口,不新建用户或会员体系,全部基于 `services/api` 与 `kaotings` 库。
|
||
- **P4-04 用户管理**:`GET /admin/users` 支持分页 + 邮箱/手机号/状态筛选(`ILIKE`),`GET /admin/users/{id}` 返回详情(用户、有效 Plan、当前会员、额度、会员历史、调整记录)。`PATCH /admin/users/{id}/status` 支持禁用/恢复,禁用时撤销该用户全部会话(`sessions_revoked`),并保留最后一个可用管理员保护(`LAST_ADMIN_PROTECTED`)。
|
||
- **P4-05 VIP 与额度**:`GET /admin/users/{id}/membership` 返回有效 Plan 与当前授予(含生效/到期时间);`PUT` 授予/变更(校验有效期);`POST /membership/revoke` 撤销 VIP 并回退为 free。`GET /admin/users/{id}/quota` 返回额度周期、上限、已用、冻结、调整、可用与调整记录;`POST /admin/users/{id}/quota-adjustments` 要求原因、幂等(重复提交 `409`)、且不允许调整后低于已用(`QUOTA_BALANCE_INVALID`)。
|
||
- **P4-06 TTS 与审计**:`GET /admin/tts/tasks` 按用户/状态/时间范围分页查询,返回计量(text_length)、耗时(duration_ms)、错误(error_code)、结算(consumed/released/pending);`GET /admin/usage/summary` 汇总额度与任务,账本合计与 `quota_accounts.used` 口径一致;`GET /admin/audit-logs` 分页返回审计,默认不含完整用户文本,也未暴露任何密钥/令牌/密码字段。
|
||
- 移除了一次性 seed-admin 路由,初始化令牌 `CMS_SEED_TOKEN` 已撤销;管理员恢复流程见 `docs/cms-admin-recovery.md`。
|
||
- 产品 slug 查询统一使用已验证的 `where[slug][equals]=` 筛选;不把 Payload 的按 `id` 路由当作按 slug 路由(见下)。
|
||
|
||
## 3. 范围边界
|
||
|
||
- CMS 不管理业务用户、Role/Plan、额度、TTS 任务或认证会话(这些由 `services/api` 管理)。
|
||
- P4-04 至 P4-07 面向业务 API 的管理后台已完成;CMS 管理界面(用户/VIP/额度/TTS)不重复实现于 CMS。
|
||
- 旧 Big-TTS Web 不参与 CMS,推理服务继续由新网站 API 调用。
|
||
- 本轮不修改 TTS 与 Worker,不新增未经规划的强制重试/取消/上游配置功能。
|
||
|
||
## 4. 验证记录
|
||
|
||
### 空库初始化 / 迁移(P4-01)
|
||
- 迁移 `20260909_100120_initial` 已入库并命名与原执行记录一致,避免重复应用。
|
||
- 独立临时库 `kaotings_cms_verify` 从仓库 `payload migrate` 重建得出 14 张表,列类型 `SCHEMA_IDENTICAL`;随后删除临时库,现有 `kaotings_cms` 未清空。
|
||
|
||
### 管理员访问控制(P4-02)
|
||
- 匿名 `first-register` / `POST users` / `GET users` / `GET users/1` / `PATCH users/1` → 全部 `403`。
|
||
- 匿名 `GET /cms/api/access` → `canAccessAdmin:false`;管理员 → `true`。
|
||
- 普通网站用户属独立库 `kaotings`,CMS 用自有 `payload-token` JWT,跨库无法拿 CMS 会话。
|
||
|
||
### 真实内容闭环(P4-03)
|
||
- 测试产品 `p4-test-draft-product`:草稿不可读、发布可读、修改即时、下架后详情 `404`;完成后已下架(当前 `unpublished`)。
|
||
|
||
### 管理后台 API 与浏览器(P4-04 至 P4-07)
|
||
- `services/api/tests/phase4_admin.py`:分页/筛选/详情、禁用+会话失效、最后管理员保护、VIP 授予/撤销、额度调整幂等与余额约束、TTS 任务筛选、用量汇总一致、审计不泄密、普通用户越权拦截——全部通过。
|
||
- `services/api/tests/phase4_browser.py`(Playwright headless Chromium):admin_login、admin_dashboard、users_table_renders、users_empty_state、tts_tasks_renders、audit_renders、mobile_users_paginates、normal_user_denied、entitlement_vip_reflected、entitlement_tts_202——全部通过。
|
||
- 受控测试账户 `p4.controlled@example.com`(only in server secrets)验证:授予 VIP 后账户页显示 VIP,且 TTS 提交受权益约束;测试结束后已禁用。
|
||
|
||
## 5. 后续
|
||
|
||
- 本命中遗留的按 slug 详情 REST `500`(`/cms/api/products/:slug`)已确认是 Payload 把该路径当作按 `id` 处理所致;公网产品展示闭环走 `where[status][equals]=published` 列表与按 slug 筛选,不受影响,后续不得引入按 `id` 路由取 slug。
|
||
- 迁移、seed 路由与集成修复已提交仓库并通过本地/服务端构建;CMS 现有数据库未清空,有受控备份(`infra/scripts/backup-cms-db.sh`)。
|
||
- 管理后台需 root-only 的 `/etc/kaotings/p4-ops.secrets` 与 `/etc/kaotings/p4-ctrl.secrets` 注入管理员与受控账户(0600),未写入仓库。Phase 3 尚未通过的异常矩阵继续保留,不因 Phase 4 完成而关闭。
|
||
|
||
## 6. 实际通过项与遗留项
|
||
|
||
### 实际通过项
|
||
- P4-01:迁移固化、仓库可复现重建、未清空现有库。
|
||
- P4-02:受控管理员初始化、匿名无法创建首个管理员、匿名无法注册/读/改用户、canAccessAdmin 权限枚举、跨库会话隔离。
|
||
- P4-03:草稿/发布/修改/下架内容闭环,测试产品已下架。
|
||
- P4-04:用户分页/筛选/详情、禁用恢复+会话撤销、最后管理员保护、普通用户越权拦截。
|
||
- P4-05:VIP 授予/撤销(含有效期)、额度明细、调整记录+原因+幂等+余额约束。
|
||
- P4-06:TTS 任务按用户/状态/时间筛选并显示计量/耗时/错误/结算、用量汇总与流水一致、审计不含用户文本且不显示秘密。
|
||
- P4-07:真实浏览器(桌面 1440x900 / 移动 390x844)完成查用户、禁用/恢复、授予/撤销 VIP、调整额度、查询任务与审计;普通用户页面与接口均无权限;受控测试账户验证 VIP 权益反映到账户并约束 TTS 提交。
|
||
|
||
### 遗留项
|
||
- `/cms/api/products/:slug` 按 `id` 路径查询返回 500(Payload 行为);公网展示走 `where[status][equals]=published` 与 `where[slug][equals]=`,不受影响,但未提供按 slug 的 REST 详情路由。
|
||
- CMS 未配置邮件适配器,管理员密码恢复依赖本地受控脚本(`docs/cms-admin-recovery.md`),SMTP 通道未接入。
|
||
- Phase 3 异常矩阵(并发争用、Worker 重启恢复、存储失败、上游超时等)仍为部分完成,未关闭;无公网 HTTP 上游与音频保留/备份的运营决策。
|
||
- 管理后台未做浏览器截图留档;本命中以 Playwright 无头 Chromium 的功能断言为准。移动端已覆盖表单/分页/空状态/错误反馈,但未针对每个页面单独截图。
|
||
- 前后端 tests 目录下的 playwright 依赖安装于测试机(root 运行需 `PLAYWRIGHT_BROWSERS_PATH=/home/flym/.cache/ms-playwright`),未纳入仓库依赖管理。
|