# 统一用户管理 — 设计方案 > 所属端:超管端 > 状态:已实现(迭代中) > 创建日期: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` 增加 `` 条件,使用 `` 遍历 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` 增加 `` 条件,使用 `t.code = #{params.tenantCodeFilter}` 过滤 **验证结果:** - 后端编译成功 - 公众/评委用户列表查询应按租户 code 正确过滤