Merge branch 'codex/wonderq-admin-rbac'

This commit is contained in:
duanshuwen
2026-08-26 18:52:06 +08:00
73 changed files with 3963 additions and 788 deletions

View File

@@ -17,13 +17,13 @@
| --- | --- | --- | --- | --- | --- |
| 登录、刷新、退出 | `/api/admin/auth/login`、`refresh`、`logout`、`/api/admin/me` | 兼容旧 `token/user` 字段,401 自动刷新 | 登录页、内存 Access Token、HttpOnly Refresh Cookie | 无 | 自动化通过,待浏览器人工验收 |
| 仪表盘 | `/api/admin/dashboard` | 已有 | `DashboardPage` 已接入 | 无 | 读流程已接入 |
| 首页与站点配置 | `/api/admin/site-config` 及模块 CRUD、排序 | 已有 | `HomePage` 已读取全量配置;编辑/排序待迁移 | `/api/public/home`、`/api/public/site-config` | 读契约保持,编辑待验收 |
| 玩法分类与路线 | `/api/admin/wanfa/categories` 及分类/路线 CRUD | 已有 | `WanfaPage` 已读取分类和路线;写操作待迁移 | `/api/public/wanfa/categories` | 读流程已接入,写流程待迁移 |
| 路线详情 | `/api/admin/details` 及详情 CRUD | React 现有玩法流程依赖 | Vue API 类型和页面待补齐 | `/api/public/details/{key}` | 待迁移 |
| 管家 | `/api/admin/concierge/advisors` 及 CRUD | 已有 | `ConciergePage` 已读取顾问;写操作待迁移 | `/api/public/concierge/advisors`、详情顾问信息 | 读流程已接入,写流程待迁移 |
| 需求线索 | `/api/admin/leads`、状态更新 | 已有筛选和状态流转 | `LeadsPage` 已读取;状态更新/筛选待迁移 | `/api/public/leads` | 读流程已接入,写流程待迁移 |
| 媒体 | `/api/admin/media-assets`、上传 | React 已有上传 | Vue 页面和上传流程待迁移 | 通过站点配置间接依赖 | 待迁移 |
| 发布、重置 | `/api/admin/publish`、`reset-guizhou-content` | React 现有入口/能力 | Vue 按权限增加二次确认后迁移 | 发布结果影响 Public API | 待迁移,执行前需要人工确认 |
| 首页与站点配置 | `/api/admin/site-config` 及模块 CRUD、排序 | 已有 | `HomePage` 已迁移全部 7 类站点模块的读写、删除和可用模块排序;单例模块禁止重复创建 | `/api/public/home`、`/api/public/site-config` | 契约、表单校验、接口回归和页面入口已通过,真实数据写入待人工确认 |
| 玩法分类与路线 | `/api/admin/wanfa/categories` 及分类/路线 CRUD | 已有 | `WanfaPage` 已迁移分类/路线新增、编辑、删除、排序和非空分类删除保护 | `/api/public/wanfa/categories` | 契约、接口回归和页面入口已通过,真实数据写入待人工确认 |
| 路线详情 | `/api/admin/details` 及详情 CRUD | React 现有玩法流程依赖 | 已集成到 Vue 路线编辑器,支持详情字段、数组文案、管家关联、启用状态和图片上传/排序 | `/api/public/details/{key}` | 契约、接口回归和页面入口已通过,真实数据写入待人工确认 |
| 管家 | `/api/admin/concierge/advisors` 及 CRUD | 已有 | `ConciergePage` 已接入新增、编辑、启停、排序、删除和头像/二维码上传 | `/api/public/concierge/advisors`、详情顾问信息 | 契约、表单校验和页面入口已通过,真实写入待人工确认 |
| 需求线索 | `/api/admin/leads`、状态更新 | 已有筛选和状态流转 | `LeadsPage` 已接入类型、状态、关键词筛选、详情查看和六态状态写入 | `/api/public/leads` | 契约、有效会话下的列表/筛选/详情页面验收已通过,真实状态写入待人工确认 |
| 媒体 | `/api/admin/media-assets`、上传 | React 已有上传 | `MediaPage` 已接入素材列表、分组、批量上传和图片校验;玩法/管家继续复用上传入口 | 通过站点配置间接依赖 | 契约、列表和上传入口已通过,真实上传待人工确认 |
| 发布、重置 | `/api/admin/publish`、`reset-guizhou-content` | React 现有入口/能力 | `OperationsToolsPage` 已接入发布和重置,均有权限控制、进行中禁用和二次确认 | 发布结果影响 Public API | 入口和确认闸门已通过,执行前必须人工确认 |
| RBAC 用户 | `/api/admin/system/users` | 旧角色字段继续兼容 | 系统资源入口已接入读取 | 无 | 后端/读页面已接入,编辑待迁移 |
| RBAC 角色、菜单、部门 | `/api/admin/system/roles`、`menus`、`depts` | 旧端不删除 | Vue 动态菜单、组件白名单和系统资源读取已接入 | 无 | 后端/读页面已接入,完整按钮授权待验收 |
@@ -37,3 +37,12 @@
## 删除 React 的闸门
只有当矩阵中 Vue 列全部变为“已接入并人工验收”,并且登录刷新、权限、数据范围、Redis 故障、Public API 回归和 Docker 验证记录齐全后,才可以征求确认切换 `/admin` 与删除 `/admin-legacy`。
## 阶段验收记录(2026-08-26)
- 线索状态:已在 Vue 管理端将真实线索从“待处理”改为“已联系”,再恢复为“待处理”,写入和恢复均成功。
- 发布与重置:发布成功;重置成功,Public API 和 MiniAPP 已读取重置后的站点内容。
- RBAC:用户、角色、菜单、部门均完成真实编辑写入并恢复原值,编辑成功提示和最终列表值正常。
- 媒体上传:按当前验收顺序暂缓,放到最后;此前验证发现 OSS 返回 403,尚未形成素材记录。
- 自动化回归:Vue 15 个测试文件、25 个测试通过,TypeScript 检查和生产构建通过;后端 134 个测试通过,7 个既有 Alembic 兼容性测试失败,未涉及本轮站点重置和 RBAC 改动。
- React 删除:未执行。需待媒体上传、登录刷新、权限/数据范围、Redis 异常、Public API 和 Docker 验收全部完成,并经人工确认后再处理。

View File

@@ -0,0 +1,468 @@
# WonderQ Admin Vue 正式化实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在不删除 React 过渡端的前提下,将四组首页 CRUD 接入 Vue,补齐管理员踢下线、显式 403、五级数据范围和 `/admin`、`/admin-legacy` 正式代理,并完成可重复的三端验收。
**Architecture:** 保持 FastAPI + PostgreSQL + Redis 的现有模块化单体。后端继续以既有 Admin API 和 `{code,msg,data}` 契约为边界;Vue 通过独立的 API client、页面组件和静态路由注册实现新管理端;React 继续由独立 legacy 静态入口提供回滚能力。Redis 只保存会话记录、用户会话索引和权限缓存,不新增会话数据库表。
**Tech Stack:** Python 3、FastAPI、SQLAlchemy 2、Alembic、PostgreSQL、Redis、JWT、Vue 3、Element Plus、TypeScript、Pinia、Vue Router 4、React、Vite、Nginx、Docker Compose、pytest、Vitest。
---
## 文件边界与责任
### 后端
- 修改 `WonderQ-Admin/app/redis_session.py`:扩展会话存储协议,实现按用户索引、列出和批量撤销会话;不改变 Token 明文存储策略。
- 修改 `WonderQ-Admin/app/auth.py`:在登录创建和刷新轮换时维护用户会话索引;保留旧登录响应字段。
- 修改 `WonderQ-Admin/app/routers/system.py`:新增踢下线接口,执行权限、数据范围、当前会话保护和审计。
- 修改 `WonderQ-Admin/app/routers/admin.py`:为四组首页资源接入现有权限码和数据范围检查,保持既有路径与返回字段。
- 修改 `WonderQ-Admin/app/rbac.py`:固定数据范围和权限缓存故障的安全行为,确保资源列表与单条写入使用同一范围规则。
- 修改 `WonderQ-Admin/tests/test_home.py`、`tests/test_admin_rbac.py`:补充首页权限、范围和契约回归。
- 新建 `WonderQ-Admin/tests/test_admin_sessions.py`:覆盖 Redis 会话索引、刷新轮换、批量撤销和故障行为。
- 新建 `WonderQ-Admin/tests/test_data_scope_matrix.py`:覆盖五种数据范围的跨部门矩阵。
### Vue 管理端
- 修改 `WonderQ-Admin-UI-Vue/src/types.ts`:增加四组首页资源类型、403 错误类型、会话撤销结果类型。
- 修改 `WonderQ-Admin-UI-Vue/src/api/client.ts`:增加首页 CRUD、踢下线请求和显式 403 错误;不改变 API base、Cookie 或 Access Token 键。
- 新建 `WonderQ-Admin-UI-Vue/src/api/client-home.test.ts`:验证四组首页 API 方法、HTTP 方法和路径。
- 修改 `WonderQ-Admin-UI-Vue/src/api/client-system.test.ts`:验证踢下线 API。
- 新建 `WonderQ-Admin-UI-Vue/src/components/home/HomeCrudEditor.vue`:共享首页卡片表单、校验、图片 URL/状态字段和提交态。
- 新建 `WonderQ-Admin-UI-Vue/src/components/home/HomeCrudTable.vue`:共享列表、空态、错误态、重试、排序和权限按钮。
- 新建 `WonderQ-Admin-UI-Vue/src/lib/home-content.ts`:定义四组资源的字段元数据、默认值和 payload 归一化。
- 新建 `WonderQ-Admin-UI-Vue/src/pages/HomeContentPage.vue`:体验、团建、野奢三组 CRUD 页面。
- 新建 `WonderQ-Admin-UI-Vue/src/pages/HomeWanfaRecommendationPage.vue`:玩法推荐关联 CRUD 页面。
- 新建 `WonderQ-Admin-UI-Vue/src/pages/ForbiddenPage.vue`:登录态下的 403 页面。
- 修改 `WonderQ-Admin-UI-Vue/src/pages/HomePage.vue`、`src/router/index.ts`:把首页站点配置与四组业务入口拆开,并注册 403 和首页业务路由。
- 修改 `WonderQ-Admin-UI-Vue/src/pages/SystemResourcePage.vue`:在用户列表增加踢下线操作。
- 新建 `WonderQ-Admin-UI-Vue/src/router/router.test.ts`:验证 401、403、无权限页面和未知路径的分流。
### 部署与契约
- 修改 `docs/home-api.md`:补充 Vue 对接字段、统一错误和四组后台验收矩阵。
- 修改 `docs/admin-api-requirements.md`:补充踢下线、403、数据范围和 Redis 故障契约。
- 修改 `docs/integration-workflow.md`:补充双管理端、正式代理和回滚命令。
- 修改 `docs/admin-business-function-matrix.md`:记录每个切片的实际测试和人工验收结果。
- 新建 `WonderQ-Admin-UI-Vue/Dockerfile`、`WonderQ-Admin-UI-Vue/nginx.conf`:构建并提供 `/admin/` 静态资源。
- 修改 `WonderQ-Admin-UI/vite.config.ts`、`WonderQ-Admin-UI/nginx.conf`、`WonderQ-Admin-UI/Dockerfile`:支持 `/admin-legacy/` base 和 legacy 静态入口。
- 新建 `deploy/nginx/wonderq-admin.conf`:统一代理 `/admin/`、`/admin-legacy/` 和 `/api/`;不写入任何真实环境变量。
- 新建 `scripts/verify-admin-proxy.ps1`:检查两个构建产物和统一 Nginx 配置的入口、深链与 API 隔离。
- 修改 `docker-compose.yml`(仅在现有 Compose 确认包含前端服务时):加入两个前端构建入口和健康检查;若现有 Compose 不负责前端,则只增加部署文档,不扩大 Compose 责任。
## Task 1: 建立契约基线与失败测试入口
**Files:**
- Modify: `docs/home-api.md`
- Modify: `docs/admin-api-requirements.md`
- Modify: `docs/integration-workflow.md`
- Test: `WonderQ-Admin/tests/test_home.py`
- Test: `WonderQ-Admin-UI-Vue/src/api/client-home.test.ts`
- [ ] **Step 1: 记录四组首页的现有字段与接口响应**
从 `WonderQ-Admin/app/schemas.py`、`WonderQ-Admin/app/routers/admin.py`、`WonderQ-Admin-UI/src/api.ts` 和 `docs/home-api.md` 对照四组资源。契约表必须明确 `GET/POST/PATCH/DELETE/PATCH reorder`、成功状态码、空数组、排序完整 ID 列表和 403 响应,不改现有公共字段名。
- [ ] **Step 2: 先写 Vue API 失败测试**
在 `client-home.test.ts` 中为四组资源各调用一次列表、创建、更新、删除、排序方法,断言 `fetch` 收到以下请求:
```ts
expect(fetchMock.mock.calls.map(([url, options]) => [url, options?.method])).toEqual([
["/api/admin/home/experiences", undefined],
["/api/admin/home/experiences", "POST"],
["/api/admin/home/experiences/experience-1", "PATCH"],
["/api/admin/home/experiences/experience-1", "DELETE"],
["/api/admin/home/experiences/reorder", "PATCH"],
["/api/admin/home/team-buildings", undefined],
["/api/admin/home/team-buildings", "POST"],
["/api/admin/home/team-buildings/team-1", "PATCH"],
["/api/admin/home/team-buildings/team-1", "DELETE"],
["/api/admin/home/team-buildings/reorder", "PATCH"],
["/api/admin/home/wild-archives", undefined],
["/api/admin/home/wild-archives", "POST"],
["/api/admin/home/wild-archives/archive-1", "PATCH"],
["/api/admin/home/wild-archives/archive-1", "DELETE"],
["/api/admin/home/wild-archives/reorder", "PATCH"],
["/api/admin/home/play-recommendations", undefined],
["/api/admin/home/play-recommendations", "POST"],
["/api/admin/home/play-recommendations/recommendation-1", "PATCH"],
["/api/admin/home/play-recommendations/recommendation-1", "DELETE"],
["/api/admin/home/play-recommendations/reorder", "PATCH"],
]);
```
- [ ] **Step 3: 运行失败测试,确认缺口真实存在**
Run: `Set-Location WonderQ-Admin-UI-Vue; yarn test --run src/api/client-home.test.ts`
Expected: FAIL because the four Vue client methods and corresponding types do not exist yet.
- [ ] **Step 4: 同步契约文档并检查差异**
Run: `git diff --check -- docs/home-api.md docs/admin-api-requirements.md docs/integration-workflow.md`
Expected: no whitespace errors;文档中没有真实 Token、密码、OSS 凭证或生产地址。
## Task 2: 接入四组首页 API 与后端权限/范围
**Files:**
- Modify: `WonderQ-Admin-UI-Vue/src/types.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/api/client.ts`
- Modify: `WonderQ-Admin/app/routers/admin.py`
- Modify: `WonderQ-Admin/tests/test_home.py`
- Modify: `WonderQ-Admin-UI-Vue/src/api/client-home.test.ts`
- [ ] **Step 1: 写后端首页鉴权和范围失败测试**
为四组列表和至少一个更新接口增加测试:普通管理员没有 `admin:site-config` 时返回 403;有该权限时只能读取或更新 `is_within_data_scope` 为真的记录;完整排序请求缺 ID 或重复 ID 时返回 400。测试继续使用现有依赖覆盖方式,不创建真实生产数据。
- [ ] **Step 2: 运行后端失败测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_home.py -q`
Expected: new permission/scope assertions fail while existing home CRUD compatibility tests remain visible in the output。
- [ ] **Step 3: 增加 TypeScript 资源类型和 API 方法**
在 `src/types.ts` 中以 React 已有类型和后端 schema 为准定义 `HomeExperience`、`HomeTeamBuilding`、`HomeWildArchive`、`HomeWanfaRecommendation` 及对应 Create/Patch 类型;在 `src/api/client.ts` 中提供以下函数:
```ts
getHomeExperiences(); createHomeExperience(input); updateHomeExperience(id, input);
deleteHomeExperience(id); reorderHomeExperiences(itemIds);
getHomeTeamBuildings(); createHomeTeamBuilding(input); updateHomeTeamBuilding(id, input);
deleteHomeTeamBuilding(id); reorderHomeTeamBuildings(itemIds);
getHomeWildArchives(); createHomeWildArchive(input); updateHomeWildArchive(id, input);
deleteHomeWildArchive(id); reorderHomeWildArchives(itemIds);
getHomePlayRecommendations(); createHomePlayRecommendation(input);
updateHomePlayRecommendation(id, input); deleteHomePlayRecommendation(id);
reorderHomePlayRecommendations(itemIds);
```
每个方法只调用既有 `/api/admin/home/...` 路径,并由 `request<T>` 统一解包响应。
- [ ] **Step 4: 将后端四组路由切换到已有权限依赖并复用数据范围**
把当前四组首页路由的 `Depends(require_admin)` 替换为 `Depends(require_admin_permission("admin:site-config"))`,列表通过 `apply_data_scope`,单条更新、删除和排序前通过 `is_within_data_scope`;保留超级管理员兼容行为、既有审计事件和返回结构。写入记录继续使用现有创建人/部门回填逻辑。
- [ ] **Step 5: 运行后端与 Vue API 测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_home.py -q`
Run: `Set-Location ..\WonderQ-Admin-UI-Vue; yarn test --run src/api/client-home.test.ts`
Expected: new assertions PASS;如全量后端仍出现已知 Alembic 兼容失败,记录其具体测试名,不将其归因于首页切片。
## Task 3: 迁移首页三组内容 CRUD 页面
**Files:**
- Create: `WonderQ-Admin-UI-Vue/src/lib/home-content.ts`
- Create: `WonderQ-Admin-UI-Vue/src/components/home/HomeCrudEditor.vue`
- Create: `WonderQ-Admin-UI-Vue/src/components/home/HomeCrudTable.vue`
- Create: `WonderQ-Admin-UI-Vue/src/pages/HomeContentPage.vue`
- Modify: `WonderQ-Admin-UI-Vue/src/router/index.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/pages/HomePage.vue`
- Test: `WonderQ-Admin-UI-Vue/src/lib/home-content.test.ts`
- [ ] **Step 1: 写字段元数据和表单归一化失败测试**
测试必须分别检查三组资源的必填字段、空字符串裁剪、数字字段转换、`sortOrder` 默认值、`isActive` 默认值和错误文案。测试输入只使用测试文本和本地图片 URL。
- [ ] **Step 2: 运行失败测试**
Run: `Set-Location WonderQ-Admin-UI-Vue; yarn test --run src/lib/home-content.test.ts`
Expected: FAIL because the metadata module does not exist。
- [ ] **Step 3: 实现元数据模块**
`home-content.ts` 导出资源联合类型、资源定义、`createEmptyHomeDraft`、`toHomePayload` 和 `validateHomePayload`。所有字段名必须与 `docs/home-api.md` 和后端 Pydantic schema 一致;不把 base64 文件内容写入 CRUD payload,只保存媒体接口返回的 URL。
- [ ] **Step 4: 实现共享表格与编辑器**
`HomeCrudTable.vue` 接收 `items/loading/error/permission`,提供空态、错误重试、编辑、删除、上下移和提交中禁用;`HomeCrudEditor.vue` 接收资源定义和 draft,提供 Element Plus 表单校验、取消、提交中状态和 `save` 事件。按钮使用 `v-permission="'admin:site-config'"`,后端 403 仍由页面捕获显示错误。
- [ ] **Step 5: 实现三组 Vue 页面**
`HomeContentPage.vue` 根据路由参数选择 `experiences`、`team-buildings` 或 `wild-archives`,加载、创建、更新、删除和排序均调用 Task 2 的 API 方法。列表、编辑器和请求状态按资源隔离,避免修改一组数据时污染另外两组。页面提供加载骨架、空态、错误重试和窄屏表格滚动。
- [ ] **Step 6: 将首页入口与新页面注册到静态路由**
保留 `/admin/home` 作为站点配置页,在该页增加三个明确入口;在 `router/index.ts` 增加 `/home/content/:resource` 静态路由并绑定 `HomeContentPage`,权限使用 `admin:site-config`。组件仍必须来自本地预注册表,不接受后端任意组件路径。
- [ ] **Step 7: 运行 Vue 单元、类型和构建验证**
Run: `Set-Location WonderQ-Admin-UI-Vue; yarn test --run src/lib/home-content.test.ts src/api/client-home.test.ts`
Run: `yarn build`
Expected: tests and TypeScript build PASS;Vite 已有 chunk 大小或 `__dirname` warning 可记录,但不得出现编译错误。
## Task 4: 迁移玩法推荐关联 CRUD
**Files:**
- Create: `WonderQ-Admin-UI-Vue/src/pages/HomeWanfaRecommendationPage.vue`
- Modify: `WonderQ-Admin-UI-Vue/src/router/index.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/pages/HomePage.vue`
- Test: `WonderQ-Admin-UI-Vue/src/api/client-home.test.ts`
- Test: `WonderQ-Admin/tests/test_home.py`
- [ ] **Step 1: 写玩法推荐列表、关联和排序失败测试**
后端测试验证关联不存在的玩法分类返回 404/400、删除关联不删除分类、状态更新只改变关联、排序必须包含当前全部关联 ID。Vue API 测试验证 `play-recommendations` 的五个方法路径。
- [ ] **Step 2: 运行失败测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_home.py -q`
Run: `Set-Location ..\WonderQ-Admin-UI-Vue; yarn test --run src/api/client-home.test.ts`
Expected: new Vue methods and missing edge-case assertions fail。
- [ ] **Step 3: 实现 Vue 玩法推荐页面**
页面加载现有玩法分类作为选择项,单独显示已关联推荐;创建和编辑只提交后端允许的分类 ID、状态和排序字段;删除操作明确提示“只移除首页关联,不删除玩法分类”;复用 `HomeCrudTable` 的空态、错误态、排序和权限按钮。
- [ ] **Step 4: 运行玩法推荐切片验收**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_home.py -q`
Run: `Set-Location ..\WonderQ-Admin-UI-Vue; yarn test --run; yarn build`
Expected: 后端首页测试、Vue 现有 25 项基线测试和构建全部通过;失败项记录到业务矩阵后再进入下一阶段。
## Task 5: 实现管理员踢下线
**Files:**
- Modify: `WonderQ-Admin/app/redis_session.py`
- Modify: `WonderQ-Admin/app/auth.py`
- Modify: `WonderQ-Admin/app/routers/system.py`
- Modify: `WonderQ-Admin/tests/test_admin_rbac.py`
- Create: `WonderQ-Admin/tests/test_admin_sessions.py`
- Modify: `WonderQ-Admin-UI-Vue/src/types.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/api/client.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/api/client-system.test.ts`
- Modify: `WonderQ-Admin-UI-Vue/src/pages/SystemResourcePage.vue`
- [ ] **Step 1: 写会话索引失败测试**
在 `test_admin_sessions.py` 验证:
```python
store = InMemoryAdminSessionStore()
store.create(session_id="s-1", user_id="u-1", access_jti="a-1", refresh_hash="r-1", access_expires_at=future, refresh_expires_at=future)
store.create(session_id="s-2", user_id="u-1", access_jti="a-2", refresh_hash="r-2", access_expires_at=future, refresh_expires_at=future)
assert store.revoke_user_sessions("u-1", except_session_id="s-1") == 1
assert store.is_access_active("s-1", "a-1")
assert not store.is_access_active("s-2", "a-2")
```
同时验证 rotate 保留同一会话索引、revoke 删除索引、重复批量撤销返回 0、Redis 异常抛出 `RedisUnavailableError`。
- [ ] **Step 2: 运行会话失败测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_admin_sessions.py -q`
Expected: FAIL because the protocol and stores do not expose `revoke_user_sessions`。
- [ ] **Step 3: 扩展协议和两种存储实现**
在 `AdminSessionStore` 增加 `list_user_sessions(user_id)` 和 `revoke_user_sessions(user_id, except_session_id=None)`。InMemory 使用 `dict[user_id, set[session_id]]` 或从现有记录派生并在 create/rotate/revoke 中保持一致;Redis 使用 `wonderq:admin:user-sessions:{user_id}` Set,成员仅为 session ID,批量删除 session key、refresh key 和 Set 成员。所有 Redis 客户端异常统一为 `RedisUnavailableError`。
- [ ] **Step 4: 运行会话存储测试并修复轮换回归**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_admin_sessions.py tests/test_admin_rbac.py -q`
Expected: session index, JWT rotation, logout and existing legacy login fields all PASS。
- [ ] **Step 5: 写踢下线路由失败测试**
使用 FastAPI `TestClient` 和依赖覆盖验证:没有 `system:user:update` 返回 403;目标用户不存在返回 404;目标为当前用户返回 400;成功时排除当前 session、返回撤销数量并写入审计记录;Redis 不可用返回服务错误而不是继续放行。
- [ ] **Step 6: 实现 system 路由与前端 API**
新增 `POST /api/admin/system/users/{user_id}/sessions/revoke`,依赖 `require_permission("system:user:update")`,使用当前 JWT 的 `sid` 作为排除项,调用 `revoke_user_sessions`,审计内容只记录操作者、目标用户和数量。Vue 增加 `revokeSystemUserSessions(userId)`,系统用户行增加“踢下线”确认按钮、提交中禁用和结果提示。
- [ ] **Step 7: 运行踢下线全链路测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_admin_sessions.py tests/test_admin_rbac.py -q`
Run: `Set-Location ..\WonderQ-Admin-UI-Vue; yarn test --run src/api/client-system.test.ts`
Expected: backend and API client tests PASS;浏览器验收时用两个登录会话确认被踢会话的下一次请求收到 401 并回到登录页。
## Task 6: 增加显式 403 页面与错误分流
**Files:**
- Modify: `WonderQ-Admin-UI-Vue/src/api/client.ts`
- Create: `WonderQ-Admin-UI-Vue/src/pages/ForbiddenPage.vue`
- Modify: `WonderQ-Admin-UI-Vue/src/router/index.ts`
- Create: `WonderQ-Admin-UI-Vue/src/router/router.test.ts`
- [ ] **Step 1: 写 403 与 401 分流失败测试**
测试 `request` 收到 403 时抛出 `PermissionDeniedError`,不调用 `/auth/refresh`;收到 401 时仍只刷新一次;路由守卫对已登录无权限页面返回 `forbidden`,未登录返回 `login`,未知路径返回 `not-found`。
- [ ] **Step 2: 运行失败测试**
Run: `Set-Location WonderQ-Admin-UI-Vue; yarn test --run src/router/router.test.ts`
Expected: FAIL because 403 error class、forbidden route and explicit guard branch do not exist。
- [ ] **Step 3: 实现错误类与 403 页面**
新增 `PermissionDeniedError extends ApiRequestError`,状态码固定为 403;`request` 保持 403 原样解包并抛出该错误。新增 `/forbidden` 公共渲染页面,显示无权限、返回上一页和回到首页操作,不清除 Pinia 登录态。
- [ ] **Step 4: 修改路由守卫**
把当前 `return { name: "not-found" }` 的权限分支改为 `return { name: "forbidden", query: { from: to.fullPath } }`;保留动态路由只从 `componentRegistry` 注册。系统资源路由仍根据用户、角色、菜单、部门分别检查权限码。
- [ ] **Step 5: 运行 403、现有测试和构建**
Run: `Set-Location WonderQ-Admin-UI-Vue; yarn test --run; yarn build`
Expected: 403 不触发刷新、不误导为 404,现有测试和构建 PASS。
## Task 7: 完成五级数据范围自动化矩阵
**Files:**
- Modify: `WonderQ-Admin/app/rbac.py`
- Modify: `WonderQ-Admin/app/routers/admin.py`
- Modify: `WonderQ-Admin/tests/test_data_scope_matrix.py`
- Modify: `WonderQ-Admin/tests/test_admin_rbac.py`
- Modify: `docs/admin-api-requirements.md`
- [ ] **Step 1: 建立隔离测试数据**
测试 fixture 创建部门 `dept-a`、子部门 `dept-a-child`、`dept-b`,管理员 `user-a`、`user-b`,并创建带 `deptId` 和 `createdById` 的线索、玩法详情、管家、媒体和首页资源。fixture 每次测试独立回滚,不使用开发数据库真实业务数据。
- [ ] **Step 2: 写五种范围失败测试**
分别为 `all`、`dept`、`dept_and_children`、`custom_dept`、`self` 配置角色,断言列表只返回预期 ID;同时调用详情、更新、删除或状态写入,确认跨范围资源返回 404/403,不能只在前端隐藏。
- [ ] **Step 3: 运行范围矩阵失败测试**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_data_scope_matrix.py -q`
Expected: 对未覆盖资源或错误的异常状态出现失败,输出具体资源和范围编码。
- [ ] **Step 4: 统一范围过滤与写入检查**
确保 `apply_data_scope` 对目录列表使用部门/创建人 OR 规则;`is_within_data_scope` 用于详情、更新、删除、状态和排序;缺少归属字段的旧记录由既有回填规则处理,不把无归属资源错误暴露给非超级管理员。保留 `admin/super_admin` 的全量兼容行为。
- [ ] **Step 5: 固定 Redis 权限缓存故障语义**
权限缓存读取或写入失败时,允许数据库重新计算权限但不得把异常转换成放行;需要 Redis 会话校验的接口继续拒绝。为 `RedisUnavailableError` 增加测试,确认不能因 Redis 断开返回全量菜单或全量业务数据。
- [ ] **Step 6: 运行范围和后端回归**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest tests/test_data_scope_matrix.py tests/test_admin_rbac.py tests/test_home.py -q`
Expected: five-level matrix and affected business tests PASS;与本切片无关的既有 Alembic bootstrap 失败单独记录,不跳过新测试。
## Task 8: 建立 Docker 与正式双入口代理
**Files:**
- Create: `WonderQ-Admin-UI-Vue/Dockerfile`
- Create: `WonderQ-Admin-UI-Vue/nginx.conf`
- Modify: `WonderQ-Admin-UI/Dockerfile`
- Modify: `WonderQ-Admin-UI/nginx.conf`
- Modify: `WonderQ-Admin-UI/vite.config.ts`
- Create: `deploy/nginx/wonderq-admin.conf`
- Modify: `docs/integration-workflow.md`
- [ ] **Step 1: 写代理静态检查**
新建 `scripts/verify-admin-proxy.ps1`,断言 Vue 构建产物引用 `/admin/`,React legacy 构建产物引用 `/admin-legacy/`,Nginx 配置包含 `/api/` proxy、两个 SPA fallback 和独立 `/healthz`,且不存在把 `/api/` fallback 到前端 index 的规则。
- [ ] **Step 2: 运行代理失败检查**
Run: `rg -n "admin-legacy|/admin/|proxy_pass|try_files|location /api" WonderQ-Admin-UI WonderQ-Admin-UI-Vue deploy/nginx`
Expected: 当前 React 入口和 Vue Docker 入口缺少至少一项,检查失败并列出缺失路径。
- [ ] **Step 3: 配置 Vue 静态镜像**
Vue Dockerfile 使用 Node 构建 `yarn build`,Nginx 配置把 `/` 的容器根目录作为 Vue `/admin/` 资源根,并对 `/admin/` history fallback 到 `/admin/index.html`。健康检查使用 `/healthz`,不暴露环境变量。
- [ ] **Step 4: 配置 React legacy base 和镜像**
在 React Vite 配置中读取 `VITE_BASE_PATH`,默认开发仍为 `/`,正式 legacy 构建传入 `/admin-legacy/`;React Nginx 资源根、history fallback 和静态资源链接与该 base 一致。原有 API proxy 开发行为保持不变。
- [ ] **Step 5: 编写统一部署 Nginx 配置**
统一配置明确以下映射:
```nginx
location /api/ { proxy_pass http://wonderq-api:4000; }
location /admin/ { alias /srv/wonderq-vue/; try_files $uri $uri/ /admin/index.html; }
location /admin-legacy/ { alias /srv/wonderq-react/; try_files $uri $uri/ /admin-legacy/index.html; }
```
实际文件路径按镜像挂载目录调整,但必须保留三条逻辑边界,且 `/api/` 位于 SPA fallback 之前并单独转发。
- [ ] **Step 6: 构建并验证容器**
Run: `docker compose config`
Run: `docker build -t wonderq-admin-ui-vue:acceptance .\WonderQ-Admin-UI-Vue`
Run: `docker build --build-arg VITE_BASE_PATH=/admin-legacy/ -t wonderq-admin-ui-react-legacy:acceptance .\WonderQ-Admin-UI`
Run: `docker compose up -d postgres redis`
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m alembic upgrade head`
Expected: 两个前端镜像、PostgreSQL、Redis 构建/启动成功;`http://localhost:4000/health` 和两个静态入口可访问。若当前 Compose 不包含 API 服务,只执行现有 API 启动命令并记录边界。
## Task 9: 三端回归、人工验收与 React 删除闸门
**Files:**
- Modify: `docs/admin-business-function-matrix.md`
- Modify: `docs/integration-workflow.md`
- Read-only verification: `WonderQ-MiniAPP/src/lib/api.ts`, `WonderQ-Admin-UI/src/api.ts`
- [ ] **Step 1: 运行后端完整回归**
Run: `Set-Location WonderQ-Admin; .\.venv\Scripts\python.exe -m pytest -q`
Expected: 新增切片全部通过;既有 Alembic 兼容失败必须保留完整测试名和原因,不能用“部分通过”掩盖。
- [ ] **Step 2: 运行两套管理端回归**
Run: `Set-Location ..\WonderQ-Admin-UI-Vue; yarn test --run; yarn build`
Run: `Set-Location ..\WonderQ-Admin-UI; yarn build`
Expected: Vue 测试/构建和 React 构建通过;旧 React API 路径、登录响应字段和刷新逻辑没有被删改。
- [ ] **Step 3: 做 MiniAPP Public API 兼容检查**
确认 `WonderQ-MiniAPP/src/lib/api.ts` 仍只访问 `/api/public/...`,首页 `experiences`、团队共创和极境视界字段与 `docs/public-api.md` 一致。只有 Public API 或字段发生变化时才运行 MiniAPP 类型检查和测试。
- [ ] **Step 4: 人工验收顺序**
使用独立测试账号逐项完成:
1. 四组首页新建、编辑、删除、排序、启停和错误重试;
2. 两个登录会话验证踢下线;
3. 无页面权限 403、无按钮权限和直接 API 403;
4. 五种数据范围跨部门列表与写入;
5. Redis 异常、Docker、`/admin/`、`/admin-legacy/` 和深链刷新;
6. Public API/MiniAPP 回归;
7. 最后进行 OSS/媒体上传验收。
每项将实际 URL、操作结果和失败证据写入 `docs/admin-business-function-matrix.md`,不记录账号密码、Token 或密钥。
- [ ] **Step 5: 判断 React 删除闸门**
只有 Vue 功能矩阵、Public/Admin API 回归、RBAC 五级范围、会话/Redis、Docker/代理、OSS/媒体和人工验收全部通过,并收到用户明确“删除旧 React”确认,才另开删除计划。当前计划结束时保留 React 源码、依赖、构建入口和 `/admin-legacy/`。
## 验证与交付规则
- 每个 Task 完成后先运行其局部测试,再运行受影响端的构建;失败时先定位根因,不以扩大改动规避测试。
- 不执行 `git reset`、`git checkout --`、批量删除或覆盖用户现有改动。
- 不修改 `.env`、`.env.local`、真实 Docker secrets、OSS 凭证、生产数据库连接或生产部署变量。
- 不新增 Alembic 迁移;会话索引只使用 Redis。若实现过程中发现确需数据库结构变化,暂停并单独征求确认。
- 不自动提交或删除 React;阶段结果以 `git diff --check`、测试输出和业务矩阵记录作为检查点。

View File

@@ -0,0 +1,220 @@
# WonderQ Admin Vue 正式化与三端联调设计规格
## 1. 文档状态
- 日期:2026-08-26
- 状态:待用户评审
- 适用范围:WonderQ-Admin、WonderQ-Admin-UI-Vue、WonderQ-Admin-UI、WonderQ-MiniAPP 的本阶段正式化工作
- 目标:在保留 React 过渡端的前提下,补齐 Vue 管理端未覆盖的首页 CRUD,完成管理员会话踢下线、显式 403、五级数据范围与正式反向代理验收。
本阶段不删除 React,不切换生产默认入口,不处理 OSS/媒体上传验收。媒体上传仍作为最后一项验收内容。
## 2. 当前基线与缺口
现有后端已经提供以下首页管理接口,但 Vue 管理端尚未接入:
| 功能组 | 接口前缀 | Vue 现状 | React 现状 |
| --- | --- | --- | --- |
| 体验内容 | `/api/admin/home/experiences` | 未接入 | 已有列表、创建、编辑、删除、排序 |
| 团建内容 | `/api/admin/home/team-buildings` | 未接入 | 已有列表、创建、编辑、删除、排序 |
| 野奢档案 | `/api/admin/home/wild-archives` | 未接入 | 已有列表、创建、编辑、删除、排序 |
| 玩法推荐 | `/api/admin/home/play-recommendations` | 未接入 | 已有列表、创建、编辑、删除、排序 |
现有 Vue 首页只覆盖站点配置模块,不能据此判断旧 React 业务已经全部迁移。
当前还存在以下验收缺口:
1. Redis 会话只支持当前会话退出,没有按用户撤销全部会话的管理能力。
2. Vue 路由权限失败会落到 Not Found,API 客户端也没有统一的 403 反馈路径。
3. 五级数据范围已有后端基础模型与角色编辑入口,但缺少覆盖五种范围的完整自动化矩阵和非超级管理员真实验收记录。
4. React 与 Vue 目前没有统一的生产反向代理入口;React 仍按根路径构建,不能直接挂载到 `/admin-legacy/`。
## 3. 目标架构
```text
浏览器
├─ /admin/ -> Vue3 静态资源,正式管理端
├─ /admin-legacy/ -> React 静态资源,过渡管理端
└─ /api/ -> FastAPI,保持现有 Public API 与 Admin API
FastAPI
├─ PostgreSQL:业务数据、RBAC、审计
└─ Redis:管理员会话、会话索引、权限菜单缓存、限流
```
Vue 继续使用预注册组件加载动态菜单,不允许由后端返回任意前端组件路径。React 保持原有 API 响应格式和登录字段兼容,直到正式删除闸门满足并得到用户确认。
## 4. 分阶段实施设计
### 阶段一:四组首页 CRUD 垂直切片
每个功能组都同时完成 API 客户端、Vue 页面、权限按钮、错误态与测试,不先做一个只读总览页。
#### 4.1 API 与数据契约
在 Vue API 层增加四组资源的类型和请求函数,沿用后端既有路径、字段、`{code,msg,data}` 包裹和排序语义:
- `GET /api/admin/home/experiences`
- `POST /api/admin/home/experiences`
- `PATCH /api/admin/home/experiences/{id}`
- `DELETE /api/admin/home/experiences/{id}`
- `POST /api/admin/home/experiences/reorder`
- `GET/POST/PATCH/DELETE/POST /reorder` 对应 `team-buildings`
- `GET/POST/PATCH/DELETE/POST /reorder` 对应 `wild-archives`
- `GET/POST/PATCH/DELETE/POST /reorder` 对应 `play-recommendations`
实际字段以现有后端 schema 和 `docs/home-api.md` 为准;如果发现契约缺失,先补文档,再同步 TypeScript 类型和后端测试,不擅自改动旧字段名。
#### 4.2 Vue 页面拆分
不继续把四组业务逻辑堆进 `HomePage.vue`。按资源拆出可维护的页面或业务组件,并复用列表、编辑表单、排序操作、确认弹窗和权限按钮组件。每组至少覆盖:
- 列表加载、空态、错误态、重试;
- 新建、编辑、删除及提交中禁用态;
- 排序保存与失败回滚提示;
- 图片/链接/状态等现有字段的校验和展示;
- `home:*` 权限码控制按钮显示与接口 403 反馈;
- 桌面端和窄屏布局。
验收以 React 页面和后端路由为基线逐项核对,不因 Vue 页面视觉重排而遗漏业务字段或操作。
### 阶段二:踢下线
#### 4.3 Redis 会话索引
在现有会话存储基础上增加按管理员用户索引的 Redis Set,索引只保存随机会话 ID,不保存明文 Refresh Token。创建、刷新轮换、主动退出、过期清理和批量撤销都必须维护索引;Redis 不可用时不得绕过会话校验。
会话存储协议增加:
- 按用户列出会话 ID;
- 按用户撤销全部会话;
- 清理已不存在或已撤销的索引项。
不新增数据库表,不把 Refresh Token 写入 PostgreSQL,不在日志中记录 Token 或其完整哈希。
#### 4.4 管理接口与 UI
新增:
`POST /api/admin/system/users/{user_id}/sessions/revoke`
要求:
- 权限码:`system:user:update`;
- 只能由具备该按钮权限且通过数据范围校验的管理员调用;
- 默认撤销目标用户的全部管理员会话;
- 禁止通过该接口撤销当前操作会话,避免误操作造成管理流程中断;
- 目标用户不存在、无数据范围权限或 Redis 不可用时返回明确错误,不返回内部异常;
- 写入审计日志,记录操作者、目标用户、撤销数量和结果,不记录 Token;
- Vue 用户管理页提供按钮、确认提示、提交中状态和成功/失败反馈;
- 被踢出的浏览器在下一次 API 请求或刷新时收到 401,并回到登录页。
### 阶段三:显式 403
新增 Vue `ForbiddenPage.vue` 和 `/forbidden` 路由。路由守卫在用户已登录但无菜单/页面权限时进入 403 页面;未登录仍进入登录页,未知路径继续进入 Not Found。
API 客户端对 403 不刷新 Token、不清除有效登录态,统一转换为可识别的权限错误。页面层显示“无权限”与返回上一页/首页操作;按钮级权限仍由现有权限指令控制,但后端 403 始终视为最终边界。
至少验证:
- 无页面权限访问页面 URL;
- 有页面权限但无按钮权限;
- 直接调用被禁止的 Admin API;
- 401 与 403 不混淆;
- 刷新后权限缓存和动态路由行为一致。
### 阶段四:五级数据范围
保留现有范围编码和角色编辑能力,补充自动化测试和真实验收,不另造一套权限模型。五种范围必须分别验证:
1. 全部:可见全部归属资源;
2. 当前部门:只见当前部门资源;
3. 当前部门及子部门:可见当前部门和后代部门资源;
4. 自定义部门:只见角色明确配置的部门资源;
5. 本人:只见创建人/负责人为自己的资源。
测试使用至少两个部门、一个子部门、两个管理员和跨部门资源,覆盖列表、详情、更新、删除/状态写入等接口。每个切片都检查数据范围过滤在服务端生效,不能只依赖前端隐藏行。
权限或部门变更后必须使权限菜单缓存和相关数据范围缓存失效。Redis 不可用时拒绝需要会话或权限缓存的请求,不降级为全量数据。
### 阶段五:Docker 与正式反向代理
新增部署级 Nginx 配置和 Vue 静态镜像入口,统一提供:
- `/admin/`:Vue 正式管理端,使用 Vue 的 `/admin/` base;
- `/admin-legacy/`:React 过渡端,React 构建必须使用 `/admin-legacy/` base;
- `/api/`:反向代理到 FastAPI;
- 两个前端入口都支持 SPA history fallback,但不能把 `/api/` 错误回退到 `index.html`。
React 通过构建环境或独立 legacy 构建配置注入 base,不改变现有开发端口和 API 合同。正式代理切换采用可回滚配置:先同时发布两个入口,再切换默认链接到 `/admin/`;切换期间 `/admin-legacy/` 保持可访问。
Docker 验收包括前端镜像构建、PostgreSQL/Redis/API 启动、Alembic 升级、健康检查、两个管理入口、API 代理和刷新深链路。生产环境变量、密钥和现有部署配置不写入仓库或文档示例。
## 5. 文档同步
实现前或接口确认时同步以下契约文档:
- `docs/home-api.md`:四组首页 CRUD 的 Vue 对接字段、排序和错误语义;
- `docs/admin-api-requirements.md`:踢下线、403、数据范围和响应契约;
- `docs/integration-workflow.md`:三端启动、正式代理和验收顺序;
- `docs/admin-business-function-matrix.md`:每个垂直切片的迁移、测试和人工验收记录。
不修改 `docs/public-api.md`,除非实际发现 Public API 字段发生兼容性变化。MiniAPP 本阶段只做契约回归检查,不整体重写。
## 6. 测试与验收闸门
采用测试先行和垂直切片交付。每一片先增加会失败的测试,再写实现,最后运行与影响范围匹配的验证。
### 后端
- 四组 CRUD 的鉴权、参数校验、响应格式、排序和审计测试;
- 会话创建、刷新轮换、退出、按用户踢下线、重复撤销、Redis 不可用测试;
- 401/403 区分测试;
- 五级数据范围的跨部门/子部门/本人矩阵测试;
- 旧 React 登录字段、Public API、既有 Admin API 兼容测试;
- Alembic 空库初始化、现有数据库升级与 Docker 健康检查。
### Vue 与 React
- Vue TypeScript 检查、单元测试和生产构建;
- 四组首页 CRUD 的列表、表单、删除、排序、错误态和按钮权限;
- Vue 刷新、退出、踢下线、403、动态菜单;
- React 原有构建和登录刷新回归;
- 正式代理下 `/admin/`、`/admin-legacy/`、深链刷新和 `/api/` 转发。
### 人工验收顺序
1. 四组首页 CRUD;
2. 踢下线与 403;
3. 五级数据范围;
4. Docker/Redis/正式反向代理;
5. Public API 与 MiniAPP 回归;
6. 最后单独验收 OSS/媒体上传。
每个阶段完成后记录实际命令、结果、失败项和人工操作证据。任何一项未通过都不能把 Vue 标记为正式替代端。
## 7. 回滚与删除闸门
- 四组业务切片异常时,可保持 React 入口继续承载原页面,Vue 只回滚对应菜单/路由,不回滚数据库既有业务数据。
- 反向代理异常时保留 `/admin-legacy/`,恢复默认入口到 legacy 配置即可回退。
- 会话索引异常时拒绝管理请求并修复 Redis 索引,不允许切换到绕过会话校验的降级模式。
- 本阶段不删除 React 源码、依赖、构建入口或 legacy 代理。
只有同时满足以下条件,并由用户明确确认,才启动 React 删除阶段:
1. Vue 覆盖功能矩阵全部业务能力;
2. Public API 和现有 Admin API 无回归;
3. 角色、菜单、按钮权限、五级数据范围测试通过;
4. 登录刷新、退出、踢下线、Redis 异常通过;
5. Docker 和正式反向代理通过;
6. OSS/媒体上传通过;
7. 用户完成人工验收并明确确认删除旧 React。
## 8. 非目标
- 不整体重写 MiniAPP;
- 不引入 RuoYi 的代码生成、定时任务、监控、表单设计器等非核心模块;
- 不直接删除或重置业务数据;
- 不恢复 Alembic 已清理的无用 schema;
- 不把媒体上传提前到本阶段的首页 CRUD 验收之前;
- 不在本规格确认前改动生产环境变量、密钥和部署数据。