www_site/docs/phase-4-report.md

118 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 前缀重定向/404Next 恢复 `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 Chromiumadmin_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-05VIP 授予/撤销(含有效期)、额度明细、调整记录+原因+幂等+余额约束。
- P4-06TTS 任务按用户/状态/时间筛选并显示计量/耗时/错误/结算、用量汇总与流水一致、审计不含用户文本且不显示秘密。
- P4-07真实浏览器桌面 1440x900 / 移动 390x844完成查用户、禁用/恢复、授予/撤销 VIP、调整额度、查询任务与审计普通用户页面与接口均无权限受控测试账户验证 VIP 权益反映到账户并约束 TTS 提交。
### 遗留项
- `/cms/api/products/:slug``id` 路径查询返回 500Payload 行为);公网展示走 `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`),未纳入仓库依赖管理。