From fa6f1e5c58255858831bab055164c390b2328ff8 Mon Sep 17 00:00:00 2001 From: bujinyuan Date: Mon, 2 Mar 2026 15:40:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=92=E8=89=B2=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E4=BD=93=E7=B3=BB=E4=B8=8E=E6=9D=83=E9=99=90=E7=AE=A1=E6=8E=A7?= =?UTF-8?q?=E5=AE=9E=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库:auth_roles 表新增 level 字段(1=超管, 10=管理员, 100=普通用户) - 后端:新增 GetAllRoles/GetUserMaxLevel/SetUserRoles/FindByCodeList DAO 方法 - 后端:SetRolesRequest 替换 AssignRoleRequest,AdminUserInfo.Roles 改为 []RoleInfo - 后端:所有管理操作(禁用/启用/角色分配/创建用户)强制层级权限校验 - 后端:PUT /users/:id/roles(批量设置角色)+ GET /roles(角色列表) - 前端:角色管理改为 Checkbox Group 多选,高等级角色禁用 - 前端:禁用/启用按钮受角色层级约束,列表页操作按钮权限管控 - 前端:创建用户对话框角色选项根据操作者等级动态过滤 - 文档:API 文档、设计方案、集成规范、进度文档同步更新 Made-with: Cursor --- .cursor/rules/project-context.mdc | 5 +- admin/src/api/user.js | 32 ++- admin/src/views/user/detail.vue | 261 ++++++++++-------- admin/src/views/user/list.vue | 110 +++++--- .../controller/user_manage_controller.go | 77 ++++-- backend/go-service/app/admin/router.go | 5 +- .../app/admin/service/user_manage_service.go | 205 ++++++++++---- backend/go-service/app/auth/dao/role_dao.go | 85 ++++++ backend/go-service/app/auth/model/role.go | 1 + backend/go-service/app/dto/admin_dto.go | 42 +-- deploy/docker/postgres/init.sql | 12 +- design-system/echochat/MASTER.md | 12 +- .../echochat/pages/admin-user-detail.md | 52 ++++ docs/api/admin/user.md | 77 +++++- .../frontend-backend-integration.md | 47 ++++ .../2026-02-27-echochat-system-design.md | 21 +- .../2026-02-27-phase1-foundation-and-auth.md | 1 + docs/progress/CURRENT_STATUS.md | 5 +- 18 files changed, 771 insertions(+), 279 deletions(-) create mode 100644 design-system/echochat/pages/admin-user-detail.md diff --git a/.cursor/rules/project-context.mdc b/.cursor/rules/project-context.mdc index c1c93cd..d9f3e10 100644 --- a/.cursor/rules/project-context.mdc +++ b/.cursor/rules/project-context.mdc @@ -27,14 +27,15 @@ EchoChat 是一个实时音视频通讯平台,包含三个子项目: ## 核心开发规则 -1. **前端设计**:必须使用 `ui-ux-pro-max` 技能包(`~/.agent/skills/ui-ux-pro-max/scripts/search.py`),禁止手动设计 +1. **前端设计**:必须使用 openskills 安装的 `ui-ux-pro-max` 技能包,脚本绝对路径为 `/Users/bojinyuan/.agent/skills/ui-ux-pro-max/scripts/search.py`。**严禁使用** `.cursor/skills/ui-ux-pro-max.bak/` 目录下的任何文件,该目录已废弃。禁止手动设计系统 2. **工作流**:使用 superpowers 流程控制开发节奏 3. **两端差异**:`frontend/` 和 `admin/` 是完全独立的项目,技术栈不需要统一 4. **模块系统**:前端统一使用 ESM(`export`/`import`),禁止 CommonJS 5. **Go 常量命名**:camelCase(`UserStatusActive`),非大写下划线 6. **API 响应**:统一 `{ "code": 0, "message": "success", "data": ... }` 7. **JWT 策略**:有状态 JWT,Token 存 Redis(按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响 -8. **代码注释**:所有公开函数、组件、Store 必须有详细注释 +8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行层级权限校验 +9. **代码注释**:所有公开函数、组件、Store 必须有详细注释 9. **文档同步**:代码变更后必须同步更新 docs/ 下相关文档 10. **验证方式**:使用 Playwright MCP 进行页面自动化验证 11. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性 diff --git a/admin/src/api/user.js b/admin/src/api/user.js index 5567fcd..6b7cc78 100644 --- a/admin/src/api/user.js +++ b/admin/src/api/user.js @@ -1,15 +1,16 @@ /** * 用户管理 API 模块 * - * 对应后端路由:/api/v1/admin/users + * 对应后端路由:/api/v1/admin/users, /api/v1/admin/roles * 所有接口需要 JWT + admin 角色权限 * * 接口列表: - * - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选) - * - GET /api/v1/admin/users/:id 用户详情 + * - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选) + * - GET /api/v1/admin/users/:id 用户详情 * - PUT /api/v1/admin/users/:id/status 更新用户状态 - * - PUT /api/v1/admin/users/:id/role 分配角色 - * - POST /api/v1/admin/users 管理员创建用户 + * - PUT /api/v1/admin/users/:id/roles 批量设置角色 + * - POST /api/v1/admin/users 管理员创建用户 + * - GET /api/v1/admin/roles 获取所有角色列表 */ import request from '@/utils/request' @@ -57,16 +58,27 @@ export function updateUserStatus(id, status) { } /** - * 分配角色 + * 批量设置用户角色(先清后设) * @param {number} id - 用户 ID - * @param {string} roleCode - 角色代码:user/admin/super_admin + * @param {string[]} roleCodes - 角色代码列表 * @returns {Promise} */ -export function assignUserRole(id, roleCode) { +export function setUserRoles(id, roleCodes) { return request({ - url: `/api/v1/admin/users/${id}/role`, + url: `/api/v1/admin/users/${id}/roles`, method: 'put', - data: { role_code: roleCode } + data: { role_codes: roleCodes } + }) +} + +/** + * 获取所有角色列表(含 level 等级信息) + * @returns {Promise<{data: Array<{code: string, name: string, level: number}>}>} + */ +export function getAllRoles() { + return request({ + url: '/api/v1/admin/roles', + method: 'get' }) } diff --git a/admin/src/views/user/detail.vue b/admin/src/views/user/detail.vue index 1836e19..7b11696 100644 --- a/admin/src/views/user/detail.vue +++ b/admin/src/views/user/detail.vue @@ -5,14 +5,15 @@ 功能: - 基本信息展示(ID、用户名、邮箱、昵称、性别、手机、状态、注册/更新时间) - - 角色管理(查看当前角色 + 分配新角色) - - 状态切换(启用/禁用) + - 角色管理(Checkbox Group 多选,权限等级管控) + - 状态切换(启用/禁用,受角色层级约束) - 会议记录 Tab(占位,后续完善) 对应后端 API: - GET /api/v1/admin/users/:id - PUT /api/v1/admin/users/:id/status - - PUT /api/v1/admin/users/:id/role + - PUT /api/v1/admin/users/:id/roles + - GET /api/v1/admin/roles --> @@ -185,8 +187,12 @@ - - + @@ -207,12 +213,14 @@ * 2. 搜索/筛选/分页变化时重新加载 * 3. 操作(禁用/启用/创建)成功后刷新列表 */ -import { ref, reactive, onMounted } from 'vue' +import { ref, reactive, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { getUserList, updateUserStatus, createUser } from '@/api/user' +import { getUserList, updateUserStatus, createUser, getAllRoles } from '@/api/user' +import { useUserStore } from '@/store/user' const router = useRouter() +const userStore = useUserStore() /** 加载状态 */ const loading = ref(false) @@ -233,6 +241,36 @@ const pagination = reactive({ total: 0 }) +/** 系统所有角色列表(从 API 获取,含 level) */ +const allRoles = ref([]) + +/** 当前操作者的最高权限等级(最小 level 值) */ +const adminMaxLevel = computed(() => { + const adminRoleCodes = userStore.roles || [] + if (!allRoles.value.length || !adminRoleCodes.length) return 999 + let minLevel = 999 + for (const r of allRoles.value) { + if (adminRoleCodes.includes(r.code) && r.level < minLevel) { + minLevel = r.level + } + } + return minLevel +}) + +/** 创建用户对话框中可选的角色列表(过滤掉高于操作者等级的角色) */ +const assignableRoles = computed(() => { + return allRoles.value.filter(r => r.level > adminMaxLevel.value) +}) + +/** + * 判断操作者是否有权管理目标用户(level 严格小于) + * @param {Object} row - 用户行数据(含 max_level) + */ +const canManageRow = (row) => { + const targetLevel = (row.max_level == null) ? 999 : row.max_level + return adminMaxLevel.value < targetLevel +} + /** 创建用户对话框显示状态 */ const showCreateDialog = ref(false) @@ -276,15 +314,6 @@ const getRoleTagType = (role) => { return map[role] || 'info' } -/** - * 获取角色中文名称 - * @param {string} role - 角色代码 - */ -const getRoleName = (role) => { - const map = { super_admin: '超级管理员', admin: '管理员', user: '普通用户' } - return map[role] || role -} - /** * 获取状态 Tag 类型 * @param {number} status - 状态码 @@ -364,7 +393,7 @@ const handleToggleStatus = async (row, newStatus) => { ElMessage.success(`${action}成功`) fetchUserList() } catch (err) { - if (err !== 'cancel') { + if (err !== 'cancel' && err !== 'close') { console.error(`${action}用户失败:`, err) } } @@ -405,8 +434,19 @@ const resetCreateForm = () => { createForm.role_code = 'user' } +/** 获取角色列表(用于权限判断和创建用户的角色选项) */ +const fetchRoles = async () => { + try { + const res = await getAllRoles() + allRoles.value = res.data || [] + } catch (err) { + console.error('获取角色列表失败:', err) + } +} + onMounted(() => { fetchUserList() + fetchRoles() }) diff --git a/backend/go-service/app/admin/controller/user_manage_controller.go b/backend/go-service/app/admin/controller/user_manage_controller.go index 57de5b3..08579ea 100644 --- a/backend/go-service/app/admin/controller/user_manage_controller.go +++ b/backend/go-service/app/admin/controller/user_manage_controller.go @@ -119,6 +119,8 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) { utils.ResponseBadRequest(c, "不能禁用自己的账号") case service.ErrInvalidStatus: utils.ResponseBadRequest(c, "无效的用户状态") + case service.ErrInsufficientPermission: + utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户") default: logs.Error(ctx, funcName, "更新用户状态失败", zap.Error(err)) utils.ResponseError(c, "更新用户状态失败") @@ -129,10 +131,10 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) { utils.ResponseOK(c, nil) } -// AssignRole 分配角色 -// PUT /api/v1/admin/users/:id/role -func (ctl *UserManageController) AssignRole(c *gin.Context) { - funcName := "controller.user_manage_controller.AssignRole" +// SetRoles 批量设置用户角色 +// PUT /api/v1/admin/users/:id/roles +func (ctl *UserManageController) SetRoles(c *gin.Context) { + funcName := "controller.user_manage_controller.SetRoles" ctx := c.Request.Context() userID, err := strconv.ParseInt(c.Param("id"), 10, 64) @@ -141,27 +143,38 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) { return } - var req dto.AssignRoleRequest + var req dto.SetRolesRequest if err := c.ShouldBindJSON(&req); err != nil { logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err)) utils.ResponseBadRequest(c, "参数错误: "+err.Error()) return } - logs.Info(ctx, funcName, "分配角色", - zap.Int64("user_id", userID), - zap.String("role_code", req.RoleCode), + adminUserID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + logs.Info(ctx, funcName, "设置用户角色", + zap.Int64("target_user_id", userID), + zap.Strings("role_codes", req.RoleCodes), + zap.Int64("admin_user_id", adminUserID), ) - if err := ctl.userManageService.AssignUserRole(ctx, userID, req.RoleCode); err != nil { + if err := ctl.userManageService.SetUserRoles(ctx, userID, req.RoleCodes, adminUserID); err != nil { switch err { case service.ErrUserNotFound: utils.ResponseNotFound(c, "用户不存在") case service.ErrInvalidRole: - utils.ResponseBadRequest(c, "无效的角色代码") + utils.ResponseBadRequest(c, "包含无效的角色代码") + case service.ErrInsufficientPermission: + utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户") + case service.ErrCannotAssignHigherRole: + utils.ResponseForbidden(c, "不能分配高于自身等级的角色") default: - logs.Error(ctx, funcName, "分配角色失败", zap.Error(err)) - utils.ResponseError(c, "分配角色失败") + logs.Error(ctx, funcName, "设置用户角色失败", zap.Error(err)) + utils.ResponseError(c, "设置用户角色失败") } return } @@ -169,6 +182,24 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) { utils.ResponseOK(c, nil) } +// GetAllRoles 获取所有角色列表 +// GET /api/v1/admin/roles +func (ctl *UserManageController) GetAllRoles(c *gin.Context) { + funcName := "controller.user_manage_controller.GetAllRoles" + ctx := c.Request.Context() + + logs.Info(ctx, funcName, "获取所有角色列表") + + roles, err := ctl.userManageService.GetAllRoles(ctx) + if err != nil { + logs.Error(ctx, funcName, "获取角色列表失败", zap.Error(err)) + utils.ResponseError(c, "获取角色列表失败") + return + } + + utils.ResponseOK(c, roles) +} + // CreateUser 管理员手动创建用户 // POST /api/v1/admin/users func (ctl *UserManageController) CreateUser(c *gin.Context) { @@ -182,19 +213,31 @@ func (ctl *UserManageController) CreateUser(c *gin.Context) { return } + adminUserID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + logs.Info(ctx, funcName, "管理员创建用户", zap.String("username", req.Username), zap.String("email", logs.MaskEmail(req.Email)), + zap.Int64("admin_user_id", adminUserID), ) - userInfo, err := ctl.userManageService.CreateUser(ctx, &req) + userInfo, err := ctl.userManageService.CreateUser(ctx, &req, adminUserID) if err != nil { - if err == service.ErrUserExists { + switch err { + case service.ErrUserExists: utils.ResponseBadRequest(c, "用户名或邮箱已被注册") - return + case service.ErrInvalidRole: + utils.ResponseBadRequest(c, "无效的角色代码") + case service.ErrCannotAssignHigherRole: + utils.ResponseForbidden(c, "不能分配高于自身等级的角色") + default: + logs.Error(ctx, funcName, "创建用户失败", zap.Error(err)) + utils.ResponseError(c, "创建用户失败") } - logs.Error(ctx, funcName, "创建用户失败", zap.Error(err)) - utils.ResponseError(c, "创建用户失败") return } diff --git a/backend/go-service/app/admin/router.go b/backend/go-service/app/admin/router.go index 8922148..5d1f2d9 100644 --- a/backend/go-service/app/admin/router.go +++ b/backend/go-service/app/admin/router.go @@ -23,7 +23,10 @@ func RegisterRoutes( adminGroup.GET("/users", ctrl.GetUserList) adminGroup.GET("/users/:id", ctrl.GetUserDetail) adminGroup.PUT("/users/:id/status", ctrl.UpdateUserStatus) - adminGroup.PUT("/users/:id/role", ctrl.AssignRole) + adminGroup.PUT("/users/:id/roles", ctrl.SetRoles) adminGroup.POST("/users", ctrl.CreateUser) + + // 角色管理 + adminGroup.GET("/roles", ctrl.GetAllRoles) } } diff --git a/backend/go-service/app/admin/service/user_manage_service.go b/backend/go-service/app/admin/service/user_manage_service.go index f62f9cf..fba0583 100644 --- a/backend/go-service/app/admin/service/user_manage_service.go +++ b/backend/go-service/app/admin/service/user_manage_service.go @@ -4,6 +4,7 @@ package service import ( "context" "errors" + "math" authDAO "github.com/echochat/backend/app/auth/dao" "github.com/echochat/backend/app/auth/model" @@ -17,11 +18,13 @@ import ( ) var ( - ErrUserNotFound = errors.New("用户不存在") - ErrUserExists = errors.New("用户名或邮箱已被注册") - ErrInvalidStatus = errors.New("无效的用户状态") - ErrInvalidRole = errors.New("无效的角色代码") - ErrCannotDisableSelf = errors.New("不能禁用自己的账号") + ErrUserNotFound = errors.New("用户不存在") + ErrUserExists = errors.New("用户名或邮箱已被注册") + ErrInvalidStatus = errors.New("无效的用户状态") + ErrInvalidRole = errors.New("无效的角色代码") + ErrCannotDisableSelf = errors.New("不能禁用自己的账号") + ErrInsufficientPermission = errors.New("权限不足,无法操作更高等级的用户") + ErrCannotAssignHigherRole = errors.New("不能分配高于自身等级的角色") ) // UserManageService 管理端用户管理服务 @@ -56,11 +59,11 @@ func (s *UserManageService) GetUserList(ctx context.Context, req *dto.UserListRe list := make([]dto.AdminUserInfo, 0, len(users)) for _, user := range users { - roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID) if roleErr != nil { logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表", zap.Int64("user_id", user.ID), zap.Error(roleErr)) - roles = []string{} + roles = []model.Role{} } list = append(list, *s.buildAdminUserInfo(&user, roles)) } @@ -84,17 +87,17 @@ func (s *UserManageService) GetUserDetail(ctx context.Context, userID int64) (*d return nil, err } - roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID) if roleErr != nil { logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表", zap.Int64("user_id", user.ID), zap.Error(roleErr)) - roles = []string{} + roles = []model.Role{} } return s.buildAdminUserInfo(user, roles), nil } // UpdateUserStatus 启用/禁用用户 -// adminUserID 用于防止管理员禁用自己 +// adminUserID 用于防止管理员禁用自己;同时校验操作者权限等级必须高于目标用户 func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error { funcName := "service.user_manage_service.UpdateUserStatus" logs.Info(ctx, funcName, "更新用户状态", @@ -111,7 +114,6 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, return ErrInvalidStatus } - // 确认用户存在 _, err := s.userDAO.FindByID(ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -120,6 +122,10 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, return err } + if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil { + return err + } + if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil { return err } @@ -131,15 +137,16 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, return nil } -// AssignUserRole 分配角色给用户 -func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, roleCode string) error { - funcName := "service.user_manage_service.AssignUserRole" - logs.Info(ctx, funcName, "分配角色", - zap.Int64("user_id", userID), - zap.String("role_code", roleCode), +// SetUserRoles 批量设置用户角色(事务内先清后设) +// adminUserID 用于校验操作者等级,防止越权分配高等级角色 +func (s *UserManageService) SetUserRoles(ctx context.Context, userID int64, roleCodes []string, adminUserID int64) error { + funcName := "service.user_manage_service.SetUserRoles" + logs.Info(ctx, funcName, "设置用户角色", + zap.Int64("target_user_id", userID), + zap.Strings("role_codes", roleCodes), + zap.Int64("admin_user_id", adminUserID), ) - // 验证用户存在 _, err := s.userDAO.FindByID(ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -148,33 +155,106 @@ func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, ro return err } - // 查找角色 - role, err := s.roleDAO.FindByCode(ctx, roleCode) + if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil { + return err + } + + roles, err := s.roleDAO.FindByCodeList(ctx, roleCodes) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrInvalidRole + return err + } + if len(roles) != len(roleCodes) { + return ErrInvalidRole + } + + adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID) + if err != nil { + return err + } + for _, r := range roles { + if r.Level <= adminLevel { + logs.Warn(ctx, funcName, "尝试分配高于自身等级的角色", + zap.String("role_code", r.Code), + zap.Int("role_level", r.Level), + zap.Int("admin_level", adminLevel), + ) + return ErrCannotAssignHigherRole } + } + + roleIDs := make([]int, 0, len(roles)) + for _, r := range roles { + roleIDs = append(roleIDs, r.ID) + } + if err := s.roleDAO.SetUserRoles(ctx, userID, roleIDs); err != nil { return err } - // 分配角色(RoleDAO.AssignRole 内部会处理重复分配) - if err := s.roleDAO.AssignRole(ctx, userID, role.ID); err != nil { - return err - } - - logs.Info(ctx, funcName, "角色分配成功", + logs.Info(ctx, funcName, "用户角色设置成功", zap.Int64("user_id", userID), - zap.String("role_code", roleCode), + zap.Strings("role_codes", roleCodes), ) return nil } +// GetAllRoles 获取所有角色列表 +func (s *UserManageService) GetAllRoles(ctx context.Context) ([]dto.RoleInfo, error) { + funcName := "service.user_manage_service.GetAllRoles" + logs.Info(ctx, funcName, "获取所有角色列表") + + roles, err := s.roleDAO.GetAllRoles(ctx) + if err != nil { + return nil, err + } + + result := make([]dto.RoleInfo, 0, len(roles)) + for _, r := range roles { + result = append(result, dto.RoleInfo{ + Code: r.Code, + Name: r.Name, + Level: r.Level, + }) + } + return result, nil +} + +// checkPermissionLevel 校验操作者的权限等级是否高于目标用户 +// 操作者 level 必须 < 目标用户 level(数值更小 = 权限更高) +func (s *UserManageService) checkPermissionLevel(ctx context.Context, adminUserID, targetUserID int64) error { + funcName := "service.user_manage_service.checkPermissionLevel" + + adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID) + if err != nil { + logs.Error(ctx, funcName, "获取操作者权限等级失败", zap.Error(err)) + return err + } + + targetLevel, err := s.roleDAO.GetUserMaxLevel(ctx, targetUserID) + if err != nil { + logs.Error(ctx, funcName, "获取目标用户权限等级失败", zap.Error(err)) + return err + } + + if adminLevel >= targetLevel { + logs.Warn(ctx, funcName, "权限不足", + zap.Int64("admin_user_id", adminUserID), + zap.Int("admin_level", adminLevel), + zap.Int64("target_user_id", targetUserID), + zap.Int("target_level", targetLevel), + ) + return ErrInsufficientPermission + } + return nil +} + // CreateUser 管理员手动创建用户 -func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest) (*dto.AdminUserInfo, error) { +// adminUserID 用于校验操作者权限等级,防止越权分配高等级角色 +func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest, adminUserID int64) (*dto.AdminUserInfo, error) { funcName := "service.user_manage_service.CreateUser" logs.Info(ctx, funcName, "管理员创建用户", zap.String("username", req.Username), zap.String("email", logs.MaskEmail(req.Email)), + zap.Int64("admin_user_id", adminUserID), ) existing, findErr := s.userDAO.FindByUsername(ctx, req.Username) @@ -192,7 +272,28 @@ func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreate return nil, ErrUserExists } - // 加密密码 + roleCode := req.RoleCode + if roleCode == "" { + roleCode = constants.RoleUser + } + role, roleErr := s.roleDAO.FindByCode(ctx, roleCode) + if roleErr != nil { + return nil, ErrInvalidRole + } + + adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID) + if err != nil { + return nil, err + } + if role.Level <= adminLevel { + logs.Warn(ctx, funcName, "尝试为新用户分配高于自身等级的角色", + zap.String("role_code", roleCode), + zap.Int("role_level", role.Level), + zap.Int("admin_level", adminLevel), + ) + return nil, ErrCannotAssignHigherRole + } + hashedPassword, err := utils.HashPassword(req.Password) if err != nil { return nil, err @@ -214,35 +315,34 @@ func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreate return nil, err } - roleCode := req.RoleCode - if roleCode == "" { - roleCode = constants.RoleUser - } - role, roleErr := s.roleDAO.FindByCode(ctx, roleCode) - if roleErr != nil { - logs.Warn(ctx, funcName, "角色查找失败,将跳过角色分配", - zap.String("role_code", roleCode), zap.Error(roleErr)) - } else { - if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil { - logs.Warn(ctx, funcName, "角色分配失败", - zap.Int64("user_id", user.ID), zap.Error(assignErr)) - } + if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil { + logs.Warn(ctx, funcName, "角色分配失败", + zap.Int64("user_id", user.ID), zap.Error(assignErr)) } - roles, rolesErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID) + userRoles, rolesErr := s.roleDAO.GetUserRoles(ctx, user.ID) if rolesErr != nil { logs.Warn(ctx, funcName, "获取新建用户角色失败", zap.Int64("user_id", user.ID), zap.Error(rolesErr)) - roles = []string{} + userRoles = []model.Role{} } logs.Info(ctx, funcName, "管理员创建用户成功", zap.Int64("user_id", user.ID)) - return s.buildAdminUserInfo(user, roles), nil + return s.buildAdminUserInfo(user, userRoles), nil } -// buildAdminUserInfo 从 model.User 构建 dto.AdminUserInfo -func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string) *dto.AdminUserInfo { - if roles == nil { - roles = []string{} +// buildAdminUserInfo 从 model.User + []model.Role 构建 dto.AdminUserInfo +func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []model.Role) *dto.AdminUserInfo { + roleInfos := make([]dto.RoleInfo, 0, len(roles)) + minLevel := math.MaxInt32 + for _, r := range roles { + roleInfos = append(roleInfos, dto.RoleInfo{ + Code: r.Code, + Name: r.Name, + Level: r.Level, + }) + if r.Level < minLevel { + minLevel = r.Level + } } info := &dto.AdminUserInfo{ @@ -254,7 +354,8 @@ func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string) Gender: user.Gender, Status: user.Status, StatusText: constants.UserStatusMap[user.Status], - Roles: roles, + Roles: roleInfos, + MaxLevel: minLevel, CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), } diff --git a/backend/go-service/app/auth/dao/role_dao.go b/backend/go-service/app/auth/dao/role_dao.go index 842724d..afa19f8 100644 --- a/backend/go-service/app/auth/dao/role_dao.go +++ b/backend/go-service/app/auth/dao/role_dao.go @@ -2,6 +2,7 @@ package dao import ( "context" + "math" "github.com/echochat/backend/app/auth/model" "github.com/echochat/backend/pkg/logs" @@ -117,3 +118,87 @@ func (d *RoleDAO) RemoveUserRoles(ctx context.Context, userID int64) error { Where("user_id = ?", userID). Delete(&model.UserRole{}).Error } + +// GetAllRoles 获取所有角色列表(按 level 升序排列) +func (d *RoleDAO) GetAllRoles(ctx context.Context) ([]model.Role, error) { + funcName := "dao.role_dao.GetAllRoles" + logs.Debug(ctx, funcName, "获取所有角色列表") + + var roles []model.Role + err := d.db.WithContext(ctx).Order("level ASC").Find(&roles).Error + if err != nil { + logs.Error(ctx, funcName, "获取所有角色失败", zap.Error(err)) + return nil, err + } + return roles, nil +} + +// GetUserMaxLevel 获取用户的最高权限等级(最小 level 值) +// 如果用户无角色,返回 math.MaxInt32 表示无权限 +func (d *RoleDAO) GetUserMaxLevel(ctx context.Context, userID int64) (int, error) { + funcName := "dao.role_dao.GetUserMaxLevel" + logs.Debug(ctx, funcName, "获取用户最高权限等级", zap.Int64("user_id", userID)) + + var minLevel *int + err := d.db.WithContext(ctx). + Model(&model.UserRole{}). + Joins("JOIN auth_roles ON auth_roles.id = auth_user_roles.role_id"). + Where("auth_user_roles.user_id = ?", userID). + Select("MIN(auth_roles.level)"). + Scan(&minLevel).Error + if err != nil { + logs.Error(ctx, funcName, "获取用户权限等级失败", zap.Error(err)) + return 0, err + } + if minLevel == nil { + return math.MaxInt32, nil + } + return *minLevel, nil +} + +// SetUserRoles 事务内先清除再批量设置用户角色 +func (d *RoleDAO) SetUserRoles(ctx context.Context, userID int64, roleIDs []int) error { + funcName := "dao.role_dao.SetUserRoles" + logs.Info(ctx, funcName, "设置用户角色", + zap.Int64("user_id", userID), + zap.Ints("role_ids", roleIDs), + ) + + return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", userID).Delete(&model.UserRole{}).Error; err != nil { + logs.Error(ctx, funcName, "清除用户角色失败", zap.Error(err)) + return err + } + + if len(roleIDs) == 0 { + return nil + } + + userRoles := make([]model.UserRole, 0, len(roleIDs)) + for _, rid := range roleIDs { + userRoles = append(userRoles, model.UserRole{ + UserID: userID, + RoleID: rid, + }) + } + if err := tx.Create(&userRoles).Error; err != nil { + logs.Error(ctx, funcName, "批量设置用户角色失败", zap.Error(err)) + return err + } + return nil + }) +} + +// FindByCodeList 按角色代码列表批量查询角色 +func (d *RoleDAO) FindByCodeList(ctx context.Context, codes []string) ([]model.Role, error) { + funcName := "dao.role_dao.FindByCodeList" + logs.Debug(ctx, funcName, "批量查询角色", zap.Strings("codes", codes)) + + var roles []model.Role + err := d.db.WithContext(ctx).Where("code IN ?", codes).Find(&roles).Error + if err != nil { + logs.Error(ctx, funcName, "批量查询角色失败", zap.Error(err)) + return nil, err + } + return roles, nil +} diff --git a/backend/go-service/app/auth/model/role.go b/backend/go-service/app/auth/model/role.go index fa096f6..3aa6626 100644 --- a/backend/go-service/app/auth/model/role.go +++ b/backend/go-service/app/auth/model/role.go @@ -8,6 +8,7 @@ type Role struct { ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键 Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin(对应 constants.RoleCode*) Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」 + Level int `json:"level" gorm:"not null;default:100"` // 角色等级,值越小权限越高:1=超管, 10=管理员, 100=普通用户 Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示 CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒 } diff --git a/backend/go-service/app/dto/admin_dto.go b/backend/go-service/app/dto/admin_dto.go index 2403843..63bf78c 100644 --- a/backend/go-service/app/dto/admin_dto.go +++ b/backend/go-service/app/dto/admin_dto.go @@ -17,20 +17,28 @@ type UserListResponse struct { // AdminUserInfo 管理端用户信息(比 UserInfo 更详细,包含管理字段) type AdminUserInfo struct { - ID int64 `json:"id"` // 用户 ID - Username string `json:"username"` // 用户名 - Email string `json:"email"` // 邮箱 - Nickname string `json:"nickname"` // 昵称 - Avatar string `json:"avatar"` // 头像 URL - Gender int `json:"gender"` // 性别:0=未知, 1=男, 2=女 - Phone string `json:"phone,omitempty"` // 手机号 - Status int `json:"status"` // 账号状态 - StatusText string `json:"status_text"` // 状态中文描述 - Roles []string `json:"roles"` // 角色代码列表 - LastLoginAt string `json:"last_login_at,omitempty"` // 最后登录时间 - LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP - CreatedAt string `json:"created_at"` // 注册时间 - UpdatedAt string `json:"updated_at"` // 更新时间 + ID int64 `json:"id"` // 用户 ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Nickname string `json:"nickname"` // 昵称 + Avatar string `json:"avatar"` // 头像 URL + Gender int `json:"gender"` // 性别:0=未知, 1=男, 2=女 + Phone string `json:"phone,omitempty"` // 手机号 + Status int `json:"status"` // 账号状态 + StatusText string `json:"status_text"` // 状态中文描述 + Roles []RoleInfo `json:"roles"` // 角色详情列表(含 code/name/level) + MaxLevel int `json:"max_level"` // 用户最高权限等级(最小 level 值) + LastLoginAt string `json:"last_login_at,omitempty"` // 最后登录时间 + LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP + CreatedAt string `json:"created_at"` // 注册时间 + UpdatedAt string `json:"updated_at"` // 更新时间 +} + +// RoleInfo 角色信息(用于前端角色列表展示和角色分配) +type RoleInfo struct { + Code string `json:"code"` // 角色代码 + Name string `json:"name"` // 角色中文名称 + Level int `json:"level"` // 角色等级,值越小权限越高 } // UpdateUserStatusRequest 更新用户状态请求 @@ -38,9 +46,9 @@ type UpdateUserStatusRequest struct { Status int `json:"status" binding:"required,oneof=1 2"` // 目标状态:1=正常, 2=禁用 } -// AssignRoleRequest 分配角色请求 -type AssignRoleRequest struct { - RoleCode string `json:"role_code" binding:"required"` // 角色代码:user/admin/super_admin +// SetRolesRequest 批量设置用户角色请求(替换原有的单角色分配) +type SetRolesRequest struct { + RoleCodes []string `json:"role_codes" binding:"required,min=1"` // 角色代码列表 } // AdminCreateUserRequest 管理员手动创建用户请求 diff --git a/deploy/docker/postgres/init.sql b/deploy/docker/postgres/init.sql index 5e89877..45f7ee2 100644 --- a/deploy/docker/postgres/init.sql +++ b/deploy/docker/postgres/init.sql @@ -46,6 +46,7 @@ CREATE TABLE auth_roles ( id SERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, + level INT NOT NULL DEFAULT 100, description VARCHAR(200) DEFAULT '', created_at TIMESTAMP(0) NOT NULL DEFAULT NOW() ); @@ -54,14 +55,15 @@ COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有 COMMENT ON COLUMN auth_roles.id IS '角色唯一标识,自增主键'; COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员'; COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"'; +COMMENT ON COLUMN auth_roles.level IS '角色等级,值越小权限越高:1=超管, 10=管理员, 100=普通用户,预留间隔便于扩展'; COMMENT ON COLUMN auth_roles.description IS '角色描述说明'; COMMENT ON COLUMN auth_roles.created_at IS '创建时间'; --- 预置角色数据 -INSERT INTO auth_roles (code, name, description) VALUES - ('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'), - ('admin', '管理员', '后台管理员,可以管理用户、监控会议等'), - ('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置'); +-- 预置角色数据(level 值越小权限越高) +INSERT INTO auth_roles (code, name, level, description) VALUES + ('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'), + ('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'), + ('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置'); -- ============================================================ -- auth_user_roles: 用户角色关联表 diff --git a/design-system/echochat/MASTER.md b/design-system/echochat/MASTER.md index 88eeacc..6efc57f 100644 --- a/design-system/echochat/MASTER.md +++ b/design-system/echochat/MASTER.md @@ -7,7 +7,7 @@ --- **Project:** EchoChat -**Generated:** 2026-03-02 10:27:26 +**Generated:** 2026-03-02 15:16:13 **Category:** Analytics Dashboard --- @@ -163,10 +163,14 @@ ### Page Pattern -**Pattern Name:** Data-Dense + Drill-Down +**Pattern Name:** Horizontal Scroll Journey -- **CTA Placement:** Above fold -- **Section Order:** Hero > Features > CTA +- **Conversion Strategy:** Immersive product discovery. High engagement. Keep navigation visible. +28,Bento Grid Showcase,bento, grid, features, modular, apple-style, showcase", 1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA, Floating Action Button or Bottom of Grid, Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark., Hover card scale (1.02), video inside cards, tilt effect, staggered reveal, Scannable value props. High information density without clutter. Mobile stack. +29,Interactive 3D Configurator,3d, configurator, customizer, interactive, product", 1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase, Inside Configurator UI + Sticky Bottom Bar, Neutral studio background. Product: Realistic materials. UI: Minimal overlay., Real-time rendering, material swap animation, camera rotate/zoom, light reflection, Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart. +30,AI-Driven Dynamic Landing,ai, dynamic, personalized, adaptive, generative", 1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop, Input Field (Hero) + 'Try it' Buttons, Adaptive to user input. Dark mode for compute feel. Neon accents., Typing text effects, shimmering generation loaders, morphing layouts, Immediate value demonstration. 'Show, don't tell'. Low friction start. +- **CTA Placement:** Floating Sticky CTA or End of Horizontal Track +- **Section Order:** 1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer --- diff --git a/design-system/echochat/pages/admin-user-detail.md b/design-system/echochat/pages/admin-user-detail.md new file mode 100644 index 0000000..05ebeda --- /dev/null +++ b/design-system/echochat/pages/admin-user-detail.md @@ -0,0 +1,52 @@ +# Admin User Detail Page Overrides + +> **PROJECT:** EchoChat +> **Generated:** 2026-03-02 15:16:13 +> **Page Type:** Dashboard / Data View + +> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`). +> Only deviations from the Master are documented here. For all other rules, refer to the Master. + +--- + +## Page-Specific Rules + +### Layout Overrides + +- **Max Width:** 1200px (standard) +- **Layout:** Full-width sections, centered content +- **Sections:** 1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer + +### Spacing Overrides + +- No overrides — use Master spacing + +### Typography Overrides + +- No overrides — use Master typography + +### Color Overrides + +- **Strategy:** Continuous palette transition. Chapter colors. Progress bar #000000. + +### Component Overrides + +- Avoid: Use for flat single-level sites +- Avoid: Ignore accessibility motion settings +- Avoid: Validate only on submit + +--- + +## Page-Specific Components + +- No unique components for this page + +--- + +## Recommendations + +- Effects: Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill +- Navigation: Use for sites with 3+ levels of depth +- Animation: Check prefers-reduced-motion media query +- Forms: Validate on blur for most fields +- CTA Placement: Floating Sticky CTA or End of Horizontal Track diff --git a/docs/api/admin/user.md b/docs/api/admin/user.md index 82e8981..93d1756 100644 --- a/docs/api/admin/user.md +++ b/docs/api/admin/user.md @@ -11,9 +11,10 @@ |------|------|------|------| | GET | /api/v1/admin/users | admin | 获取用户列表 | | GET | /api/v1/admin/users/:id | admin | 获取用户详情 | -| PUT | /api/v1/admin/users/:id/status | admin | 更新用户状态 | -| PUT | /api/v1/admin/users/:id/role | admin / super_admin | 分配用户角色 | +| PUT | /api/v1/admin/users/:id/status | admin | 更新用户状态(受角色等级约束) | +| PUT | /api/v1/admin/users/:id/roles | admin / super_admin | 批量设置用户角色(受角色等级约束) | | POST | /api/v1/admin/users | admin | 管理员创建用户 | +| GET | /api/v1/admin/roles | admin | 获取所有角色列表(含 level) | | GET | /api/v1/admin/users/:id/meetings | admin | 获取用户会议记录(Phase 3 实现) | --- @@ -51,7 +52,10 @@ "phone": "13800138000", "status": 1, "status_text": "正常", - "roles": ["user"], + "roles": [ + { "code": "user", "name": "普通用户", "level": 100 } + ], + "max_level": 100, "last_login_at": "2026-02-27 10:00:00", "last_login_ip": "192.168.1.100", "created_at": "2026-02-20 08:00:00", @@ -85,7 +89,10 @@ "phone": "13800138000", "status": 1, "status_text": "正常", - "roles": ["user"], + "roles": [ + { "code": "user", "name": "普通用户", "level": 100 } + ], + "max_level": 100, "last_login_at": "2026-02-27 10:00:00", "last_login_ip": "192.168.1.100", "created_at": "2026-02-20 08:00:00", @@ -111,6 +118,7 @@ **说明:** - 禁用用户后,该用户的所有活跃 Token 将被清除 - 不能禁用自己的账号(返回 400) +- 操作者权限等级必须高于目标用户(level 数值更小),否则返回 403 - 禁用后用户尝试登录将返回 403 **成功响应:** @@ -125,9 +133,9 @@ --- -## 4. 分配用户角色 +## 4. 批量设置用户角色 -`PUT /api/v1/admin/users/:id/role` +`PUT /api/v1/admin/users/:id/roles` **权限:** admin / super_admin @@ -135,11 +143,49 @@ | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| -| role_code | string | 是 | 角色代码:user / admin / super_admin | +| role_codes | string[] | 是 | 角色代码列表,如 `["user", "admin"]` | + +**权限管控规则:** +- 操作者权限等级(level)必须严格高于目标用户(数值更小),否则返回 403 +- 不能分配等级高于或等于操作者自身的角色,否则返回 403 +- 角色列表为全量覆盖(先清除再设置) +- 注:`super_admin` 角色(level=1)不可通过 API 分配(任何人的 level 都 >= 1),超管只能通过数据库直接创建 + +**成功响应:** +```json +{ + "code": 0, + "message": "success" +} +``` + +**错误响应:** +- 403:`权限不足,无法操作更高等级的用户` 或 `不能分配高于自身等级的角色` --- -## 5. 管理员创建用户 +## 5. 获取所有角色列表 + +`GET /api/v1/admin/roles` + +**权限:** admin + +**成功响应:** +```json +{ + "code": 0, + "message": "success", + "data": [ + { "code": "super_admin", "name": "超级管理员", "level": 1 }, + { "code": "admin", "name": "管理员", "level": 10 }, + { "code": "user", "name": "普通用户", "level": 100 } + ] +} +``` + +--- + +## 6. 管理员创建用户 `POST /api/v1/admin/users` @@ -155,6 +201,14 @@ | nickname | string | 否 | 昵称(默认使用用户名) | | role_code | string | 否 | 指定角色,默认为 user | +**权限管控规则:** +- 不能为新用户分配等级高于或等于操作者自身的角色,否则返回 403 +- 例:admin(level=10)只能分配 level > 10 的角色(如 user),不能分配 admin 或 super_admin + +**错误响应:** +- 400:`用户名或邮箱已被注册` / `无效的角色代码` +- 403:`不能分配高于自身等级的角色` + **成功响应:** ```json { @@ -169,7 +223,10 @@ "gender": 0, "status": 1, "status_text": "正常", - "roles": ["user"], + "roles": [ + { "code": "user", "name": "普通用户", "level": 100 } + ], + "max_level": 100, "created_at": "2026-03-02 11:12:33", "updated_at": "2026-03-02 11:12:33" }, @@ -180,7 +237,7 @@ --- -## 6. 获取用户会议记录 +## 7. 获取用户会议记录 `GET /api/v1/admin/users/:id/meetings` diff --git a/docs/conventions/frontend-backend-integration.md b/docs/conventions/frontend-backend-integration.md index 501171e..eb9dffb 100644 --- a/docs/conventions/frontend-backend-integration.md +++ b/docs/conventions/frontend-backend-integration.md @@ -265,3 +265,50 @@ JWT Token 的 Claims 中包含 `client_type` 字段,用于: - [ ] JWT Claims 中包含 client_type 字段 - [ ] 登出时只删除对应 clientType 的 Token - [ ] Token 刷新时保持原 clientType 不变 + +--- + +## 7. 角色等级与权限管控规范 + +### 7.1 角色等级设计 + +`auth_roles` 表 `level` 字段,值越小权限越高,预留间隔: + +| 角色 | Code | Level | 说明 | +|------|------|-------|------| +| 超级管理员 | super_admin | 1 | 最高权限 | +| 管理员 | admin | 10 | 后台管理 | +| 普通用户 | user | 100 | 基础权限 | + +用户可拥有多个角色,取**最小 level 值**作为有效权限等级。 + +### 7.2 权限管控规则 + +**核心原则:操作者的 level 必须严格小于目标用户的 level,才能执行管理操作。** + +1. **更新用户状态(禁用/启用)**:操作者 level < 目标用户 level +2. **设置用户角色**:操作者 level < 目标用户 level,且不能分配 level <= 自身的角色 +3. **前端管控**:禁用/启用按钮对高等级用户隐藏,高等级角色 checkbox 禁用 + +### 7.3 API 响应变更 + +`AdminUserInfo` 的 `roles` 字段从 `[]string` 改为 `[]RoleInfo`: + +```json +{ + "roles": [ + { "code": "admin", "name": "管理员", "level": 10 }, + { "code": "user", "name": "普通用户", "level": 100 } + ], + "max_level": 10 +} +``` + +### 7.4 检查清单 + +涉及用户管理操作时,必须确认: + +- [ ] 后端 Service 层调用 `checkPermissionLevel` 进行等级校验 +- [ ] 权限不足时返回 403(`ErrInsufficientPermission`) +- [ ] 前端通过比较 `adminMaxLevel` 和 `targetMaxLevel` 控制 UI 可见性 +- [ ] 角色分配使用全量覆盖模式(`SetUserRoles`),非追加模式 diff --git a/docs/plans/2026-02-27-echochat-system-design.md b/docs/plans/2026-02-27-echochat-system-design.md index cc92461..2118f6f 100644 --- a/docs/plans/2026-02-27-echochat-system-design.md +++ b/docs/plans/2026-02-27-echochat-system-design.md @@ -81,6 +81,8 @@ - 前台用户和后台管理员共用 `auth_users` 表 - 通过 `auth_roles` 和 `auth_user_roles` 区分角色(user / admin / super_admin) +- `auth_roles.level` 字段实现角色等级层级管控(值越小权限越高:1=超管, 10=管理员, 100=用户) +- 管理操作强制执行"高等级管理低等级"规则(操作者 level 必须 < 目标用户 level) - 前台 API 路由:`/api/v1/auth/*` — 仅需 JWT 验证 - 后台 API 路由:`/api/v1/admin/*` — JWT 验证 + 角色检查双重中间件 @@ -235,6 +237,7 @@ CREATE TABLE auth_roles ( id SERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, + level INT NOT NULL DEFAULT 100, description VARCHAR(200) DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT NOW() ); @@ -243,14 +246,15 @@ COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有 COMMENT ON COLUMN auth_roles.id IS '角色唯一标识,自增主键'; COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员'; COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"'; +COMMENT ON COLUMN auth_roles.level IS '角色等级,值越小权限越高:1=超管, 10=管理员, 100=普通用户,预留间隔便于扩展'; COMMENT ON COLUMN auth_roles.description IS '角色描述说明'; COMMENT ON COLUMN auth_roles.created_at IS '创建时间'; --- 预置角色数据 -INSERT INTO auth_roles (code, name, description) VALUES - ('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'), - ('admin', '管理员', '后台管理员,可以管理用户、监控会议等'), - ('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置'); +-- 预置角色数据(level 值越小权限越高) +INSERT INTO auth_roles (code, name, level, description) VALUES + ('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'), + ('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'), + ('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置'); -- ============================================================ -- auth_user_roles: 用户角色关联表 @@ -265,8 +269,8 @@ CREATE TABLE auth_user_roles ( #### 权限扩展规划(后期迭代) -> 当前 MVP 阶段使用粗粒度 3 角色(user / admin / super_admin)即可满足需求。 -> 后期管理员增多、需要区分职责时,引入细粒度权限点机制,扩展如下两张表: +> 当前已实现:3 角色 + level 等级层级管控(super_admin=1, admin=10, user=100),所有管理操作受层级约束。 +> 后期管理员增多、需要区分职责时,引入细粒度权限点机制,扩展如下两张表(level 字段天然支持排序和层级融合): ```sql -- 预留:权限点表(后期扩展) @@ -714,8 +718,9 @@ POST /api/v1/admin/auth/login GET /api/v1/admin/users GET /api/v1/admin/users/:id PUT /api/v1/admin/users/:id/status -PUT /api/v1/admin/users/:id/role +PUT /api/v1/admin/users/:id/roles POST /api/v1/admin/users +GET /api/v1/admin/roles GET /api/v1/admin/users/:id/meetings # 会议管理 diff --git a/docs/plans/2026-02-27-phase1-foundation-and-auth.md b/docs/plans/2026-02-27-phase1-foundation-and-auth.md index 67df510..096f22b 100644 --- a/docs/plans/2026-02-27-phase1-foundation-and-auth.md +++ b/docs/plans/2026-02-27-phase1-foundation-and-auth.md @@ -3,6 +3,7 @@ > **状态:✅ Phase 1 全部完成**(2026-03-02) > 11 个 Task 全部通过端到端验证和代码审查。 > 分支:`feature/phase1-foundation-and-auth` +> **补充:角色等级体系与权限管控已实施**(2026-03-02)— `auth_roles.level` 字段 + 层级权限 + 多选角色分配 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. diff --git a/docs/progress/CURRENT_STATUS.md b/docs/progress/CURRENT_STATUS.md index bf50a04..a3d804c 100644 --- a/docs/progress/CURRENT_STATUS.md +++ b/docs/progress/CURRENT_STATUS.md @@ -1,6 +1,6 @@ # EchoChat 项目开发进度 -> **最后更新**:2026-03-02(前后端错误处理规范统一 + 安全加固) +> **最后更新**:2026-03-02(角色等级体系与权限管控实施) > **当前阶段**:Phase 1 - 基础设施与用户认证 > **当前分支**:`feature/phase1-foundation-and-auth` > **实施计划**:`docs/plans/2026-02-27-phase1-foundation-and-auth.md` @@ -35,6 +35,7 @@ 5. **API 响应格式**:统一 `{ "code": 0, "message": "success", "data": ... }` 6. **常量命名**:Go camelCase(`UserStatusActive`),非大写下划线 7. **模块路由**:模块内自注册 + 中央 router 聚合 +8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行"高等级管理低等级"规则 ### 前台用户端(frontend/) 1. **框架**:uni-app 3.0(Vue 3.4.21 框架锁定) @@ -151,7 +152,7 @@ cd frontend && npm run dev:h5 ### 可测试功能 -- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用 → 分配角色 → 创建用户 +- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用(受角色等级约束) → 批量设置角色(Checkbox 多选 + 等级管控) → 创建用户 - **前台 H5**:注册 → 登录 → 个人中心 → 修改资料 → 退出登录 - **API**:`GET http://localhost:8085/health`(健康检查)