library-picturebook-activity/docs/design/super-admin/unified-user-management.md

340 lines
15 KiB
Markdown
Raw Normal View History

# 统一用户管理 — 设计方案
> 所属端:超管端
> 状态:已实现(迭代中)
> 创建日期2026-03-27
> 最后更新2026-03-30
---
## 1. 背景与问题
超管端"用户中心"下有两个用户管理页面:"用户管理"和"公众用户管理",存在以下问题:
| 问题 | 说明 |
|------|------|
| 超管看不到全局用户 | "用户管理"只显示 super 租户的用户,看不到 gdlib、judge、public 等租户的用户 |
| 用户类型无法区分 | 表格里没有"所属租户"或"用户类型"列,不知道用户属于哪个机构 |
| 两个页面职责重叠又割裂 | "用户管理"管 super 用户,"公众用户"管 public 用户,机构用户和评委谁都管不到 |
| 公众用户缺少管理操作 | 不能禁用异常账号、不能编辑信息 |
| 用户管理缺少关键字段 | 没有手机号、来源、城市等字段展示 |
| 没有搜索功能 | "用户管理"页面无任何筛选手段 |
**目标**:将两个页面合并为一个统一的用户管理页面,超管可以在全局视角下查看、筛选、管理所有类型的用户。
---
## 2. 现状分析
### 2.1 用户数据模型
系统通过 **租户归属 + 角色 + 来源** 三层组合区分用户类型:
```
用户类型推导规则:
├── tenant.isSuper === 1 → 平台用户(运营团队)
├── tenant.code === 'public' → 公众用户(家长/独立参与者)
├── tenant.code === 'judge' → 评委
└── 其他租户 → 机构用户
```
User 模型关键字段:`tenantId`, `username`, `nickname`, `phone`, `city`, `birthday`, `gender`, `userSource`, `status`, `organization`
### 2.2 现有页面
**用户管理**`views/system/users/Index.vue`
- 数据来源:`GET /api/users`,按 `req.tenantId` 过滤,超管只看到 super 租户用户
- 表格列ID、用户名、昵称、邮箱、角色、状态、创建时间
- 操作:新增、编辑、改密、删除
- 无搜索功能
**公众用户管理**`views/system/public-users/Index.vue`
- 数据来源:`GET /api/public/users`,专查 public 租户用户
- 表格列:用户信息(头像+昵称)、手机号、城市、子女数、报名数、状态、注册时间
- 操作查看详情Drawer 展示子女+报名记录)
- 有关键词搜索,但不能禁用/编辑
### 2.3 现有后端接口
```
GET /api/users — 按 tenantId 隔离,返回当前租户的用户
GET /api/users/:id — 用户详情
POST /api/users — 创建用户
PATCH /api/users/:id — 更新用户
DELETE /api/users/:id — 删除用户
GET /api/public/users — 公众用户列表(独立接口)
GET /api/public/users/:id — 公众用户详情(含子女+报名)
```
---
## 3. 设计方案
### 3.1 整体思路
合并"用户管理"和"公众用户管理"为一个页面,超管端通过统计卡片 + 筛选条件实现分类查看,详情 Drawer 根据用户类型展示不同内容。菜单层面移除"公众用户管理"入口。
### 3.2 页面设计
#### 页面结构
```
┌─ 标题卡片 ─────────────────────────────────────────────┐
│ 用户管理 │
└────────────────────────────────────────────────────────┘
┌─ 统计卡片(一行,可点击切换)──────────────────────────────┐
│ [全部 128] [平台 3] [机构 15] [评委 8] [公众 102] │
└────────────────────────────────────────────────────────┘
┌─ 筛选栏 ───────────────────────────────────────────────┐
│ 关键词:[_______] 所属机构:[下拉] 用户来源:[下拉] │
│ 状态:[下拉] [搜索] [重置] │
└────────────────────────────────────────────────────────┘
┌─ 数据表格 ─────────────────────────────────────────────┐
│ 用户信息 | 用户类型 | 所属机构 | 手机号 | 城市 | │
│ | 角色 | 来源 | 注册时间 | 操作 | │
└────────────────────────────────────────────────────────┘
```
#### 统计卡片
- 5 张卡片横排,每张显示:类型图标 + 类型名称 + 数量
- 类型命名:全部 / **运营团队** / 机构 / 评委 / 公众(~~平台~~ → 运营团队2026-03-30 更名)
- 选中的卡片高亮(主色边框),点击 = 设置 userType 筛选
- 点"全部"清除类型筛选
- 数据来源:`GET /api/users/stats`
#### 筛选栏
| 筛选项 | 组件 | 可选值 | 联动逻辑 |
|--------|------|--------|---------|
| 关键词 | Input | 用户名/昵称/手机号 | — |
| 所属机构 | Select | 租户下拉列表 | 仅 userType 为空或 "org" 时显示 |
| 用户来源 | Select | 管理员创建 / 自主注册 | — |
| 状态 | Select | 正常 / 禁用 | — |
#### 表格列
| 列 | 宽度 | 渲染方式 |
|----|------|----------|
| 用户信息 | 220 | 头像 + 昵称 + @用户名(竖排布局)|
| 用户类型 | 90 | Tag颜色区分蓝=平台,绿=机构,橙=评委,紫=公众 |
| 所属机构 | 140 | tenant.name公众/平台用户显示 "-" |
| 手机号 | 130 | phone 或 "-" |
| 城市 | 100 | city 或 "-" |
| 角色 | 120 | 角色名 Tag 列表 |
| 来源 | 90 | Tag管理创建/ 自主注册(蓝)|
| 注册时间 | 160 | YYYY-MM-DD HH:mm |
| 操作 | 140 | 查看详情 / 更多下拉(禁用/启用、重置密码)|
#### 操作设计
| 操作 | 说明 | 权限 |
|------|------|------|
| 查看详情 | 打开右侧 Drawer | 所有用户可查看 |
| 禁用/启用 | 切换 status 字段 | 不能禁用自己,不能禁用其他租户的唯一管理员 |
| 重置密码 | 弹窗输入新密码 | — |
注意:超管不提供"新增用户"和"删除用户"操作。新增用户应在各自端完成(机构管理端创建机构用户,公众端自主注册),删除用户风险过高,只提供禁用。
#### 详情 Drawer按用户类型适配内容
**通用区域**(所有类型):
```
基本信息 — Descriptions 组件
├── 昵称 / 用户名 / 手机号 / 邮箱
├── 城市 / 性别 / 所属单位
├── 所属机构 / 角色 / 用户来源
└── 注册时间 / 状态
```
**公众用户额外区域**
```
子女账号N个— 基于 UserParentChild 关系,子女为独立 User
├── 头像 / 昵称 / @用户名 / 性别 / 城市 / 关系(父亲/母亲/监护人) / 状态
报名记录近20条
├── 活动名称 / 报名状态 / 参与者(本人/子女名) / 报名时间
```
**评委额外区域**
```
评审活动
├── 活动名称 / 评审状态 / 已评/总数
```
**机构用户额外区域**
```
权限概览
├── 拥有角色 / 权限数量
```
### 3.3 后端改动
#### 3.3.1 改造 GET /api/users扩展查询参数
超管isSuper=1调用时
- 不按 `req.tenantId` 过滤,查全部用户
- 新增查询参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `userType` | string? | `platform` / `org` / `judge` / `public`,映射到租户条件 |
| `filterTenantId` | number? | 指定机构筛选 |
| `userSource` | string? | `admin_created` / `self_registered` |
| `status` | string? | `enabled` / `disabled` |
| `keyword` | string? | 搜索范围增加 phone 字段 |
- 返回字段增加:
- `tenant: { id, name, code, tenantType, isSuper }`(用于前端推导用户类型)
- `_count: { parentRelations, contestRegistrations }`(公众用户的子女账号数和报名数)
普通租户调用时:保持现有逻辑不变。
userType 映射逻辑:
```
platform → tenant.isSuper = 1
org → tenant.isSuper = 0 AND tenant.code NOT IN ('public', 'judge')
judge → tenant.code = 'judge'
public → tenant.code = 'public'
```
#### 3.3.2 新增 GET /api/users/stats
仅超管可访问,返回各类型用户数量:
```json
{
"total": 128,
"platform": 3,
"org": 15,
"judge": 8,
"public": 102
}
```
实现方式:分别 count 查询,按上述 userType 映射条件。
#### 3.3.3 改造 GET /api/users/:id
超管调用时:
- 不做 tenantId 过滤
- 公众用户额外返回:`parentRelations`(子女账号列表,含 child User 信息)、`contestRegistrations`近20条报名记录含活动名和子女名
- 评委额外返回:`contestJudges`(参与的评审活动列表)
#### 3.3.4 新增 PATCH /api/users/:id/status
专门用于禁用/启用,区分于通用的 update 接口:
```json
// Request
{ "status": "enabled" | "disabled" }
// 校验规则
// - 不能操作自己
// - 不能禁用其他租户的唯一管理员(查该租户下 tenant_admin 角色用户数)
```
### 3.4 前端改动
| 文件 | 操作 | 说明 |
|------|------|------|
| `frontend/src/api/users.ts` | 修改 | 扩展 UserQueryParams 类型,新增 stats 和 updateStatus 接口 |
| `frontend/src/views/system/users/Index.vue` | 重写 | 统一用户管理页面 |
| `frontend/src/views/system/public-users/Index.vue` | 废弃 | 功能合并到用户管理,文件保留但不再使用 |
| 后端菜单配置 | 修改 | 超管端移除"公众用户管理"菜单项 |
---
## 4. 改动范围
### 后端
| 文件 | 改动类型 |
|------|---------|
| `backend/src/users/users.service.ts` | 修改findAll 增加跨租户查询逻辑、新增 getStats 方法、findOne 增加关联数据 |
| `backend/src/users/users.controller.ts` | 修改findAll 增加查询参数、新增 stats 和 updateStatus 端点 |
| `backend/src/users/dto/create-user.dto.ts` | 修改:新增查询参数 DTO |
### 前端
| 文件 | 改动类型 |
|------|---------|
| `frontend/src/api/users.ts` | 修改:扩展类型和接口 |
| `frontend/src/views/system/users/Index.vue` | 重写 |
| `frontend/src/views/system/public-users/Index.vue` | 废弃(保留文件不删除)|
### 菜单配置
| 改动 | 说明 |
|------|------|
| 超管端菜单 | 用户中心下移除"公众用户管理"子菜单 |
---
## 5. 实施记录
### 2026-03-27 — 首次实现
**后端改动3 个文件):**
- `backend/src/users/users.service.ts` — findAll 重构为参数对象模式,超管跨租户查询;新增 getStats()、updateStatus() 方法findOne 超管调用时返回子女/报名/评审关联数据
- `backend/src/users/users.controller.ts` — 新增 GET /api/users/stats、PATCH /api/users/:id/status 端点findAll 增加 userType/filterTenantId/userSource/status 查询参数
- `backend/src/auth/strategies/jwt.strategy.ts` — JWT validate 时查租户 isSuper 字段,注入 isSuperTenant 到 req.user
**前端改动2 个文件):**
- `frontend/src/api/users.ts` — 扩展 UserQueryParams、User、UserStats 类型;新增 getUserStats()、updateUserStatus() 接口;保留 usersApi 兼容导出
- `frontend/src/views/system/users/Index.vue` — 完全重写:统计卡片 + 筛选栏 + 统一表格 + 详情 Drawer按用户类型适配+ 禁用/启用/重置密码操作
**待手动操作:**
- 超管端菜单管理中删除"公众用户管理"菜单项(数据在数据库中,非 menus.json
**验证结果:**
- 后端 TSC 编译通过NestJS 启动成功,/api/users/stats 和 /api/users/:id/status 路由注册正常
- 前端无新增 TS 错误(原有错误均为已有代码)
### 2026-03-30 — 命名优化 + 子女账号独立化适配
**问题:**
1. 统计卡片和 Tag 中「平台」命名易误解为"平台全部用户",实际指运营管理人员
2. 公众用户详情仍展示旧版 `Child` 模型(姓名/年级/学校),子女已独立为 `User` 后应使用 `UserParentChild` 关系
**改动3 个文件):**
- `backend/src/users/users.service.ts` — findOne 详情查询:`children`(旧 Child 表)→ `parentRelations`UserParentChild + child User列表 `_count.children``_count.parentRelations`
- `frontend/src/api/users.ts` — User 类型定义:`children` 数组 → `parentRelations` 数组(含 child 独立用户信息 + relationship + controlMode
- `frontend/src/views/system/users/Index.vue` — 统计卡片和 Tag 标签:「平台」→「运营团队」;详情 Drawer 子女区域:旧版姓名/年级/学校列表 → 新版子女账号卡片(头像+昵称+用户名+关系+状态)
**验证结果:**
- 后端重启成功,编译无错误
- 前端 HMR 热更新生效,无新增 TS 错误
### 2026-04-02 — 机构用户过滤逻辑修复
**问题:**
- 前端点击"机构"统计卡片时,后端 `findAll(userType=org)` 返回 0 条数据,与统计接口返回不一致
- 原因:`case "org"` 分支没有设置任何过滤参数Mapper XML 中也没有对应的处理逻辑
**改动2 个文件):**
- `backend/.../SysUserServiceImpl.java``findAll()` 方法:`case "org"` 分支增加调用 `getOrgTenantIds()` 获取机构租户 ID 列表,传递 `orgTenantIdsFilter` 参数;新增 `getOrgTenantIds()` 私有方法
- `backend/.../SysUserMapper.xml``selectUserPage` 增加 `<if test="params.orgTenantIdsFilter != null">` 条件,使用 `<foreach>` 遍历 ID 列表进行 `IN` 查询
**验证结果:**
- 后端编译成功Maven 编译无错误
- 机构用户列表查询应正确排除超管、公众、评委租户
### 2026-04-02 — 公众/评委用户过滤逻辑修复
**问题:**
- 前端点击"公众"卡片时,传递 `userType=public`,但返回空数据(统计显示 9 条)
- 原因:后端 `case "public"``tenant_type = 'public'` 过滤,但数据库中 public 租户的 `tenant_type = 'platform'`(不是 `'public'`
- 同理,`case "judge"` 按 `tenant_type = 'judge_pool'` 过滤,但 judge 租户的 `tenant_type = 'other'`
**改动2 个文件):**
- `backend/.../SysUserServiceImpl.java``findAll()` 方法:`case "public"` 和 `case "judge"` 改为传递 `tenantCodeFilter` 参数(值分别为 `"public"``"judge"`),按租户 code 过滤而非 tenant_type
- `backend/.../SysUserMapper.xml``selectUserPage` 增加 `<if test="params.tenantCodeFilter != null">` 条件,使用 `t.code = #{params.tenantCodeFilter}` 过滤
**验证结果:**
- 后端编译成功
- 公众/评委用户列表查询应按租户 code 正确过滤