library-picturebook-activity/docs/design/super-admin/unified-user-management.md
zhonghua c5fad30849 fix: 修复用户管理页面所属机构字段显示及列表过滤逻辑
1. 前端所属机构字段改为使用后端返回的平铺 tenantName 字段
   - users.ts: 添加 tenantName, tenantCode, tenantType, tenantIsSuper 平铺字段
   - Index.vue: 表格列和详情 Drawer 使用 record.tenantName/detailData.tenantName

2. 后端修复机构用户 (org) 过滤逻辑
   - SysUserServiceImpl: case "org" 分支增加 getOrgTenantIds() 调用,传递 orgTenantIdsFilter 参数
   - SysUserMapper.xml: 增加 orgTenantIdsFilter 参数处理,使用 IN 查询过滤

3. 后端修复公众 (public) 和评委 (judge) 用户过滤逻辑
   - 数据库中 public 租户的 tenant_type='platform',judge 租户的 tenant_type='other'
   - case "public"/"judge" 改为传递 tenantCodeFilter 参数,按租户 code 过滤
   - SysUserMapper.xml: 增加 tenantCodeFilter 参数处理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:06:09 +08:00

340 lines
15 KiB
Markdown
Raw 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.

# 统一用户管理 — 设计方案
> 所属端:超管端
> 状态:已实现(迭代中)
> 创建日期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 正确过滤