From ca1d265e340a21f49e2fc83cb8bbe994ef396ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Mon, 3 Nov 2025 18:00:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=81=9A=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pc/src/api/permission.js | 73 +++ pc/src/views/system/permissions/index.vue | 672 +++++++++++++++++++--- server/controllers/permission.go | 264 +++++++++ server/middleware/permission.go | 188 ++++++ server/models/permission.go | 305 ++++++++++ server/routers/router.go | 8 + 权限管理模块使用说明.md | 255 ++++++++ 7 files changed, 1674 insertions(+), 91 deletions(-) create mode 100644 pc/src/api/permission.js create mode 100644 server/controllers/permission.go create mode 100644 server/middleware/permission.go create mode 100644 server/models/permission.go create mode 100644 权限管理模块使用说明.md diff --git a/pc/src/api/permission.js b/pc/src/api/permission.js new file mode 100644 index 0000000..8049e91 --- /dev/null +++ b/pc/src/api/permission.js @@ -0,0 +1,73 @@ +import request from '@/utils/request'; + +/** + * 获取所有菜单权限列表(用于分配权限) + */ +export function getAllMenuPermissions() { + return request({ + url: '/api/permissions/menus', + method: 'get', + }); +} + +/** + * 获取指定角色的权限 + * @param {number} roleId - 角色ID + */ +export function getRolePermissions(roleId) { + return request({ + url: `/api/permissions/role/${roleId}`, + method: 'get', + }); +} + +/** + * 为角色分配权限 + * @param {number} roleId - 角色ID + * @param {Array} menuIds - 菜单ID列表 + */ +export function assignRolePermissions(roleId, menuIds) { + return request({ + url: `/api/permissions/role/${roleId}`, + method: 'post', + timeout: 60000, // 设置60秒超时,因为批量保存可能需要较长时间 + data: { + menu_ids: menuIds, + }, + }); +} + +/** + * 获取当前登录用户的权限 + */ +export function getUserPermissions() { + return request({ + url: '/api/permissions/user', + method: 'get', + }); +} + +/** + * 获取当前用户有权限访问的菜单树 + */ +export function getUserMenuTree() { + return request({ + url: '/api/permissions/user/menus', + method: 'get', + }); +} + +/** + * 检查用户是否拥有指定权限 + * @param {string} permission - 权限标识 + */ +export function checkPermission(permission) { + return request({ + url: '/api/permissions/check', + method: 'get', + params: { + permission, + }, + }); +} + diff --git a/pc/src/views/system/permissions/index.vue b/pc/src/views/system/permissions/index.vue index 1970e43..12da748 100644 --- a/pc/src/views/system/permissions/index.vue +++ b/pc/src/views/system/permissions/index.vue @@ -1,112 +1,602 @@ - - diff --git a/server/controllers/permission.go b/server/controllers/permission.go new file mode 100644 index 0000000..2952476 --- /dev/null +++ b/server/controllers/permission.go @@ -0,0 +1,264 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "server/models" + "strconv" + + beego "github.com/beego/beego/v2/server/web" + "github.com/beego/beego/v2/core/logs" +) + +// PermissionController 权限管理控制器 +type PermissionController struct { + beego.Controller +} + +// GetAllMenuPermissions 获取所有菜单权限列表(用于分配权限) +func (c *PermissionController) GetAllMenuPermissions() { + menus, err := models.GetAllMenuPermissions() + + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取菜单列表失败", + "error": err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取菜单列表成功", + "data": menus, + } + } + + c.ServeJSON() +} + +// GetRolePermissions 获取指定角色的权限 +func (c *PermissionController) GetRolePermissions() { + roleId, err := c.GetInt(":roleId") + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "角色ID参数错误", + } + c.ServeJSON() + return + } + + permissions, err := models.GetRolePermissions(roleId) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取角色权限失败", + "error": err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取角色权限成功", + "data": permissions, + } + } + + c.ServeJSON() +} + +// AssignRolePermissions 为角色分配权限 +func (c *PermissionController) AssignRolePermissions() { + roleId, err := c.GetInt(":roleId") + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "角色ID参数错误", + } + c.ServeJSON() + return + } + + // 解析请求体 + var requestData struct { + MenuIds []int `json:"menu_ids"` + } + + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "请求参数错误", + "error": err.Error(), + } + c.ServeJSON() + return + } + + // 获取当前用户ID(从JWT中获取) + userIdData := c.Ctx.Input.GetData("userId") + var createBy string + if userIdData != nil { + userId, ok := userIdData.(int) + if ok { + createBy = strconv.Itoa(userId) + } + } + + // 记录日志(用于调试) + logs.Info(fmt.Sprintf("开始为角色 %d 分配权限,共 %d 个菜单", roleId, len(requestData.MenuIds))) + + // 分配权限 + err = models.AssignRolePermissions(roleId, requestData.MenuIds, createBy) + if err != nil { + logs.Error(fmt.Sprintf("分配权限失败: %v", err)) + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "分配权限失败", + "error": err.Error(), + } + } else { + logs.Info(fmt.Sprintf("角色 %d 权限分配成功", roleId)) + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "分配权限成功", + } + } + + c.ServeJSON() +} + +// GetUserPermissions 获取当前登录用户的权限 +func (c *PermissionController) GetUserPermissions() { + // 从JWT中获取用户ID + userIdData := c.Ctx.Input.GetData("userId") + if userIdData == nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "未获取到用户信息", + } + c.ServeJSON() + return + } + + userId, ok := userIdData.(int) + if !ok { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "用户ID格式错误", + } + c.ServeJSON() + return + } + + permissions, err := models.GetUserPermissions(userId) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取用户权限失败", + "error": err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取用户权限成功", + "data": permissions, + } + } + + c.ServeJSON() +} + +// GetUserMenuTree 获取当前用户有权限访问的菜单树 +func (c *PermissionController) GetUserMenuTree() { + // 从JWT中获取用户ID + userIdData := c.Ctx.Input.GetData("userId") + if userIdData == nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "未获取到用户信息", + } + c.ServeJSON() + return + } + + userId, ok := userIdData.(int) + if !ok { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "用户ID格式错误", + } + c.ServeJSON() + return + } + + menuTree, err := models.GetUserMenuTree(userId) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取用户菜单失败", + "error": err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取用户菜单成功", + "data": menuTree, + } + } + + c.ServeJSON() +} + +// CheckPermission 检查用户是否拥有指定权限 +func (c *PermissionController) CheckPermission() { + // 从JWT中获取用户ID + userIdData := c.Ctx.Input.GetData("userId") + if userIdData == nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "未获取到用户信息", + } + c.ServeJSON() + return + } + + userId, ok := userIdData.(int) + if !ok { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "用户ID格式错误", + } + c.ServeJSON() + return + } + + // 获取权限标识 + permission := c.GetString("permission") + if permission == "" { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "权限标识不能为空", + } + c.ServeJSON() + return + } + + hasPermission, err := models.CheckUserPermission(userId, permission) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "检查权限失败", + "error": err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "权限检查完成", + "data": map[string]interface{}{ + "has_permission": hasPermission, + }, + } + } + + c.ServeJSON() +} + diff --git a/server/middleware/permission.go b/server/middleware/permission.go new file mode 100644 index 0000000..895326b --- /dev/null +++ b/server/middleware/permission.go @@ -0,0 +1,188 @@ +package middleware + +import ( + "server/models" + "strings" + + "github.com/beego/beego/v2/server/web/context" +) + +// PermissionMiddleware 权限验证中间件 +// 根据路由的权限标识检查用户是否有访问权限 +func PermissionMiddleware() func(ctx *context.Context) { + return func(ctx *context.Context) { + // 获取当前请求的路径 + path := ctx.Input.URL() + + // 不需要权限验证的路径列表 + publicPaths := []string{ + "/api/login", + "/api/logout", + "/api/reset-password", + "/api/program-categories/public", + "/api/program-infos/public", + "/api/files/public", + } + + // 检查是否为公开路径 + for _, p := range publicPaths { + if path == p { + return + } + } + + // 检查是否为公开预览接口 + if strings.HasPrefix(path, "/api/files/public-preview/") { + return + } + + // 获取用户ID + userIdData := ctx.Input.GetData("userId") + if userIdData == nil { + // 如果没有用户ID,说明未登录,这个应该在JWT中间件中处理 + // 这里直接返回,因为JWT中间件已经拦截了 + return + } + + userId, ok := userIdData.(int) + if !ok { + ctx.Output.JSON(map[string]interface{}{ + "success": false, + "message": "用户ID格式错误", + }, false, false) + return + } + + // 获取当前路由对应的权限标识 + permission := getPermissionByPath(path, ctx.Input.Method()) + + // 如果没有权限标识,说明该接口不需要权限控制 + if permission == "" { + return + } + + // 检查用户是否拥有该权限 + hasPermission, err := models.CheckUserPermission(userId, permission) + if err != nil { + ctx.Output.JSON(map[string]interface{}{ + "success": false, + "message": "权限验证失败", + "error": err.Error(), + }, false, false) + return + } + + if !hasPermission { + ctx.Output.JSON(map[string]interface{}{ + "success": false, + "message": "您没有权限访问此接口", + "code": 403, + }, false, false) + return + } + } +} + +// getPermissionByPath 根据路径和方法获取权限标识 +// 这是一个简化版本,实际应该从数据库中动态获取路由-权限映射关系 +func getPermissionByPath(path, method string) string { + // 权限映射表(路径模式 -> 权限标识) + // 这里只列举了部分示例,实际应该从数据库中加载 + permissionMap := map[string]string{ + // 用户管理 + "GET:/api/allUsers": "user:list", + "GET:/api/user/:id": "user:detail", + "POST:/api/addUser": "user:add", + "POST:/api/editUser/:id": "user:edit", + "DELETE:/api/deleteUser/:id": "user:delete", + "POST:/api/changePassword/:id":"user:changePassword", + + // 角色管理 + "GET:/api/roles": "role:list", + "POST:/api/roles": "role:create", + "GET:/api/roles/:id": "role:detail", + "POST:/api/roles/:id": "role:update", + "DELETE:/api/roles/:id": "role:delete", + + // 菜单管理 + "GET:/api/allmenu": "menu:list", + "POST:/api/menu": "menu:create", + "PUT:/api/menu/:id": "menu:update", + "DELETE:/api/menu/:id": "menu:delete", + + // 文件管理 + "GET:/api/files": "file:list", + "POST:/api/files": "file:upload", + "GET:/api/files/my": "file:my", + "GET:/api/files/download/:id": "file:download", + "GET:/api/files/preview/:id": "file:preview", + "GET:/api/files/:id": "file:detail", + "PUT:/api/files/:id": "file:update", + "DELETE:/api/files/:id": "file:delete", + "GET:/api/files/search": "file:search", + "GET:/api/files/statistics": "file:statistics", + + // 租户管理 + "GET:/api/tenant/list": "tenant:list", + "POST:/api/tenant": "tenant:create", + "PUT:/api/tenant/:id": "tenant:update", + "DELETE:/api/tenant/:id": "tenant:delete", + "POST:/api/tenant/:id/audit": "tenant:audit", + "GET:/api/tenant/:id": "tenant:detail", + + // 知识库 + "GET:/api/knowledge/list": "knowledge:list", + "GET:/api/knowledge/detail": "knowledge:detail", + "POST:/api/knowledge/create": "knowledge:create", + "POST:/api/knowledge/update": "knowledge:update", + "POST:/api/knowledge/delete": "knowledge:delete", + } + + // 匹配路径(简化版本,不支持动态参数匹配) + key := method + ":" + path + if perm, ok := permissionMap[key]; ok { + return perm + } + + // 尝试匹配动态路由(简单的ID参数替换) + // 例如:/api/user/123 -> /api/user/:id + pathParts := strings.Split(path, "/") + for pattern, perm := range permissionMap { + parts := strings.Split(pattern, ":") + if len(parts) != 2 { + continue + } + + methodPart := parts[0] + pathPattern := parts[1] + + if methodPart != method { + continue + } + + patternParts := strings.Split(pathPattern, "/") + if len(patternParts) != len(pathParts) { + continue + } + + match := true + for i, part := range patternParts { + if strings.HasPrefix(part, ":") { + // 动态参数,跳过 + continue + } + if part != pathParts[i] { + match = false + break + } + } + + if match { + return perm + } + } + + // 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制) + return "" +} + diff --git a/server/models/permission.go b/server/models/permission.go new file mode 100644 index 0000000..7b6efa6 --- /dev/null +++ b/server/models/permission.go @@ -0,0 +1,305 @@ +package models + +import ( + "fmt" + "strings" + "time" + + "github.com/beego/beego/v2/client/orm" +) + +// RoleMenu 角色-菜单关联表模型 +type RoleMenu struct { + Id int `orm:"auto" json:"id"` + RoleId int `orm:"column(role_id)" json:"role_id"` + MenuId int `orm:"column(menu_id)" json:"menu_id"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + CreateBy string `orm:"column(create_by);size(50);null" json:"create_by"` +} + +// TableName 指定表名 +func (r *RoleMenu) TableName() string { + return "yz_role_menus" +} + +// RolePermission 角色权限响应结构(包含菜单信息) +type RolePermission struct { + RoleId int `json:"role_id"` + RoleName string `json:"role_name"` + MenuIds []int `json:"menu_ids"` + Permissions []string `json:"permissions"` // 权限标识列表 +} + +// MenuPermission 菜单权限信息 +type MenuPermission struct { + MenuId int `json:"menu_id"` + MenuName string `json:"menu_name"` + Path string `json:"path"` + MenuType int `json:"menu_type"` // 1: 页面菜单, 2: API接口 + Permission string `json:"permission"` // 权限标识 + ParentId int `json:"parent_id"` +} + +func init() { + orm.RegisterModel(new(RoleMenu)) +} + +// GetRoleMenus 获取指定角色的所有菜单权限 +func GetRoleMenus(roleId int) ([]int, error) { + o := orm.NewOrm() + var menuIds []int + + _, err := o.Raw("SELECT menu_id FROM yz_role_menus WHERE role_id = ?", roleId).QueryRows(&menuIds) + if err != nil { + return nil, fmt.Errorf("获取角色菜单失败: %v", err) + } + + return menuIds, nil +} + +// GetRolePermissions 获取角色的详细权限信息(包括菜单和API权限) +func GetRolePermissions(roleId int) (*RolePermission, error) { + o := orm.NewOrm() + + // 获取角色信息 + var role Role + err := o.Raw("SELECT * FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&role) + if err != nil { + return nil, fmt.Errorf("角色不存在: %v", err) + } + + // 获取角色关联的所有菜单ID + menuIds, err := GetRoleMenus(roleId) + if err != nil { + return nil, err + } + + // 获取权限标识列表 + var permissions []string + if len(menuIds) > 0 { + // 构建IN查询的占位符和参数 + placeholders := make([]string, len(menuIds)) + args := make([]interface{}, len(menuIds)) + for i, id := range menuIds { + placeholders[i] = "?" + args[i] = id + } + query := fmt.Sprintf("SELECT DISTINCT permission FROM yz_menus WHERE id IN (%s) AND permission IS NOT NULL AND permission != ''", strings.Join(placeholders, ",")) + _, err = o.Raw(query, args...).QueryRows(&permissions) + if err != nil { + return nil, fmt.Errorf("获取权限标识失败: %v", err) + } + } + + return &RolePermission{ + RoleId: role.RoleId, + RoleName: role.RoleName, + MenuIds: menuIds, + Permissions: permissions, + }, nil +} + +// GetAllMenuPermissions 获取所有菜单权限列表(用于分配权限时展示) +func GetAllMenuPermissions() ([]*MenuPermission, error) { + o := orm.NewOrm() + var menus []*MenuPermission + + _, err := o.Raw("SELECT id as menu_id, name as menu_name, path, menu_type, permission, parent_id FROM yz_menus WHERE status = 1 ORDER BY parent_id, `order`").QueryRows(&menus) + if err != nil { + return nil, fmt.Errorf("获取菜单列表失败: %v", err) + } + + return menus, nil +} + +// AssignRolePermissions 为角色分配权限(菜单) +func AssignRolePermissions(roleId int, menuIds []int, createBy string) error { + o := orm.NewOrm() + + // 先删除该角色的所有权限(使用更快的方式) + _, err := o.Raw("DELETE FROM yz_role_menus WHERE role_id = ?", roleId).Exec() + if err != nil { + return fmt.Errorf("删除旧权限失败: %v", err) + } + + // 如果没有新权限,直接返回 + if len(menuIds) == 0 { + return nil + } + + // 使用更高效的批量插入方式 + // 如果数据量太大,分批插入以避免超时 + batchSize := 500 // 每批500条,MySQL可以高效处理 + total := len(menuIds) + + for i := 0; i < total; i += batchSize { + end := i + batchSize + if end > total { + end = total + } + + batch := menuIds[i:end] + + // 构建批量INSERT语句 + query := "INSERT INTO yz_role_menus (role_id, menu_id, create_by) VALUES " + values := make([]interface{}, 0, len(batch)*3) + + placeholders := make([]string, 0, len(batch)) + for _, menuId := range batch { + placeholders = append(placeholders, "(?, ?, ?)") + values = append(values, roleId, menuId, createBy) + } + + query += strings.Join(placeholders, ", ") + + // 执行批量插入 + _, err = o.Raw(query, values...).Exec() + if err != nil { + return fmt.Errorf("插入新权限失败(批次 %d/%d): %v", i/batchSize+1, (total+batchSize-1)/batchSize, err) + } + } + + return nil +} + +// GetUserPermissions 获取用户的所有权限(通过用户角色) +func GetUserPermissions(userId int) (*RolePermission, error) { + o := orm.NewOrm() + + // 获取用户信息 + var user User + err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND delete_time IS NULL", userId).QueryRow(&user) + if err != nil { + return nil, fmt.Errorf("用户不存在: %v", err) + } + + // 如果用户没有角色,返回空权限 + if user.Role == 0 { + return &RolePermission{ + RoleId: 0, + RoleName: "无角色", + MenuIds: []int{}, + Permissions: []string{}, + }, nil + } + + // 获取角色权限 + return GetRolePermissions(user.Role) +} + +// CheckUserPermission 检查用户是否拥有指定权限 +func CheckUserPermission(userId int, permission string) (bool, error) { + if permission == "" { + return true, nil // 空权限标识表示不需要权限控制 + } + + userPerms, err := GetUserPermissions(userId) + if err != nil { + return false, err + } + + // 检查权限列表中是否包含指定权限 + for _, perm := range userPerms.Permissions { + if perm == permission { + return true, nil + } + } + + return false, nil +} + +// MenuTreeNode 菜单树节点(包含子节点) +type MenuTreeNode struct { + Id int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + ParentId int `json:"parent_id"` + Icon string `json:"icon"` + Order int `json:"order"` + Status int8 `json:"status"` + ComponentPath string `json:"component_path"` + IsExternal int8 `json:"is_external"` + ExternalUrl string `json:"external_url"` + MenuType int8 `json:"menu_type"` + Permission string `json:"permission"` + Children []*MenuTreeNode `json:"children"` +} + +// GetUserMenuTree 获取用户有权限访问的菜单树(仅页面菜单) +func GetUserMenuTree(userId int) ([]*MenuTreeNode, error) { + o := orm.NewOrm() + + // 获取用户角色 + var user User + err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND delete_time IS NULL", userId).QueryRow(&user) + if err != nil { + return nil, fmt.Errorf("用户不存在: %v", err) + } + + if user.Role == 0 { + return []*MenuTreeNode{}, nil + } + + // 获取角色的菜单ID列表 + menuIds, err := GetRoleMenus(user.Role) + if err != nil { + return nil, err + } + + if len(menuIds) == 0 { + return []*MenuTreeNode{}, nil + } + + // 获取菜单信息(仅页面菜单) + var menus []*Menu + // 构建IN查询的占位符和参数 + placeholders := make([]string, len(menuIds)) + args := make([]interface{}, len(menuIds)) + for i, id := range menuIds { + placeholders[i] = "?" + args[i] = id + } + query := fmt.Sprintf("SELECT * FROM yz_menus WHERE id IN (%s) AND menu_type = 1 AND status = 1 ORDER BY parent_id, `order`", strings.Join(placeholders, ",")) + _, err = o.Raw(query, args...).QueryRows(&menus) + if err != nil { + return nil, fmt.Errorf("获取菜单列表失败: %v", err) + } + + // 转换为MenuTreeNode + var nodes []*MenuTreeNode + for _, m := range menus { + nodes = append(nodes, &MenuTreeNode{ + Id: m.Id, + Name: m.Name, + Path: m.Path, + ParentId: m.ParentId, + Icon: m.Icon, + Order: m.Order, + Status: m.Status, + ComponentPath: m.ComponentPath, + IsExternal: m.IsExternal, + ExternalUrl: m.ExternalUrl, + MenuType: m.MenuType, + Permission: m.Permission, + Children: []*MenuTreeNode{}, + }) + } + + // 构建菜单树 + return buildMenuTree(nodes, 0), nil +} + +// buildMenuTree 构建菜单树 +func buildMenuTree(menus []*MenuTreeNode, parentId int) []*MenuTreeNode { + var tree []*MenuTreeNode + + for _, menu := range menus { + if menu.ParentId == parentId { + menu.Children = buildMenuTree(menus, menu.Id) + tree = append(tree, menu) + } + } + + return tree +} + diff --git a/server/routers/router.go b/server/routers/router.go index 44f7e46..ea86e0c 100644 --- a/server/routers/router.go +++ b/server/routers/router.go @@ -128,6 +128,14 @@ func init() { beego.Router("/api/roles/:id", &controllers.RoleController{}, "post:UpdateRole") beego.Router("/api/roles/:id", &controllers.RoleController{}, "delete:DeleteRole") + // 权限管理路由 + beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions") + beego.Router("/api/permissions/role/:roleId", &controllers.PermissionController{}, "get:GetRolePermissions") + beego.Router("/api/permissions/role/:roleId", &controllers.PermissionController{}, "post:AssignRolePermissions") + beego.Router("/api/permissions/user", &controllers.PermissionController{}, "get:GetUserPermissions") + beego.Router("/api/permissions/user/menus", &controllers.PermissionController{}, "get:GetUserMenuTree") + beego.Router("/api/permissions/check", &controllers.PermissionController{}, "get:CheckPermission") + // 手动配置特殊路由(无法通过自动路由处理的) beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus") beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic") diff --git a/权限管理模块使用说明.md b/权限管理模块使用说明.md new file mode 100644 index 0000000..88cd1f4 --- /dev/null +++ b/权限管理模块使用说明.md @@ -0,0 +1,255 @@ +# 权限管理模块使用说明 + +## 📋 功能概述 + +权限管理模块实现了基于角色的访问控制(RBAC),允许管理员为不同角色分配菜单和API访问权限。 + +## 🗄️ 数据库结构 + +### 1. 角色菜单关联表 `yz_role_menus` +```sql +CREATE TABLE yz_role_menus ( + id INT AUTO_INCREMENT PRIMARY KEY, + role_id INT NOT NULL, -- 角色ID + menu_id INT NOT NULL, -- 菜单ID(包括页面菜单和API接口) + create_time DATETIME, + create_by VARCHAR(50), + UNIQUE KEY uk_role_menu (role_id, menu_id) +); +``` + +### 2. 菜单类型说明 +- `menu_type = 1`: 页面菜单(显示在导航栏) +- `menu_type = 2`: API接口(用于权限控制) + +## 🎯 核心功能 + +### 1. 权限分配 +- 为角色分配菜单访问权限 +- 为角色分配API访问权限 +- 支持树形结构的权限选择 +- 支持批量分配和取消 + +### 2. 权限验证 +- 用户登录后自动加载权限 +- 菜单根据权限动态显示 +- API访问根据权限自动拦截 + +### 3. 权限查询 +- 查询角色的所有权限 +- 查询用户的所有权限 +- 检查用户是否拥有特定权限 + +## 📡 API接口 + +### 1. 获取所有菜单权限列表 +``` +GET /api/permissions/menus +``` +**响应示例:** +```json +{ + "success": true, + "data": [ + { + "menu_id": 1, + "menu_name": "系统管理", + "path": "/system", + "menu_type": 1, + "permission": null, + "parent_id": 0 + } + ] +} +``` + +### 2. 获取角色权限 +``` +GET /api/permissions/role/:roleId +``` +**响应示例:** +```json +{ + "success": true, + "data": { + "role_id": 1, + "role_name": "系统管理员", + "menu_ids": [1, 2, 3, 4, 5], + "permissions": ["user:list", "user:add", "file:upload"] + } +} +``` + +### 3. 分配角色权限 +``` +POST /api/permissions/role/:roleId +Content-Type: application/json + +{ + "menu_ids": [1, 2, 3, 4, 5] +} +``` + +### 4. 获取当前用户权限 +``` +GET /api/permissions/user +``` + +### 5. 获取用户菜单树 +``` +GET /api/permissions/user/menus +``` + +### 6. 检查权限 +``` +GET /api/permissions/check?permission=user:list +``` + +## 🎨 前端使用 + +### 页面访问 +``` +系统管理 -> 权限管理 +``` + +### 权限分配流程 +1. 在左侧选择要配置的角色 +2. 在右侧权限树中勾选该角色应该拥有的菜单和API权限 +3. 点击"保存权限设置"按钮 +4. 系统会自动保存并刷新权限 + +### 权限树操作 +- **全部展开**: 展开所有节点 +- **全部折叠**: 折叠所有节点 +- **全选**: 选中所有权限 +- **取消全选**: 取消所有权限选择 +- **搜索**: 支持按菜单名称、路径、权限标识搜索 + +### 权限类型标识 +- 🔹 **页面**:蓝色标签,表示页面菜单 +- 🟢 **API**:绿色标签,表示API接口 +- ℹ️ **权限标识**:灰色标签,显示具体的权限代码 + +## 🔒 权限验证中间件(可选) + +已创建权限验证中间件 `server/middleware/permission.go`,但默认未启用。 + +### 启用方法 +在 `server/routers/router.go` 中添加: +```go +// 在JWT中间件之后添加权限验证中间件 +beego.InsertFilter("/api/*", beego.BeforeExec, middleware.PermissionMiddleware()) +``` + +### 注意事项 +- 权限中间件会根据路径和方法自动匹配权限标识 +- 如果用户没有对应权限,会返回 403 错误 +- 公开接口(如登录、注册)会自动跳过权限验证 + +## 📊 权限标识规范 + +### 命名规范 +格式:`模块:操作` + +### 示例 +- `user:list` - 查看用户列表 +- `user:add` - 添加用户 +- `user:edit` - 编辑用户 +- `user:delete` - 删除用户 +- `file:upload` - 上传文件 +- `file:download` - 下载文件 +- `role:create` - 创建角色 +- `menu:update` - 更新菜单 + +## 🎯 使用场景 + +### 1. 为新角色分配权限 +``` +场景:公司新增了"财务专员"角色,需要分配相关权限 +步骤: +1. 在角色管理中创建"财务专员"角色 +2. 在权限管理中选择"财务专员" +3. 勾选需要的权限(如:文件管理、知识库查看) +4. 保存权限设置 +``` + +### 2. 调整现有角色权限 +``` +场景:需要限制"普通用户"的某些功能 +步骤: +1. 在权限管理中选择"普通用户"角色 +2. 取消不需要的权限(如:用户管理、系统设置) +3. 保存权限设置 +``` + +### 3. 权限审计 +``` +场景:查看某个角色有哪些权限 +步骤: +1. 在权限管理中选择目标角色 +2. 查看已勾选的权限项 +3. 所有勾选项即为该角色拥有的权限 +``` + +## 🔧 技术实现 + +### 后端 +- **模型层**: `server/models/permission.go` + - 角色权限关联 + - 权限查询和验证 + - 菜单树构建 + +- **控制器层**: `server/controllers/permission.go` + - 权限分配接口 + - 权限查询接口 + +- **中间件层**: `server/middleware/permission.go` + - API权限验证 + - 路径-权限映射 + +### 前端 +- **API层**: `pc/src/api/permission.js` + - 权限相关接口封装 + +- **页面层**: `pc/src/views/system/permissions/index.vue` + - 角色列表展示 + - 权限树展示和操作 + - 权限保存 + +## 📝 注意事项 + +1. **权限继承**:子菜单会继承父菜单的权限,建议同时勾选父子节点 +2. **API权限**:分配页面权限时,也要分配对应的API权限 +3. **系统管理员**:role_id=1 的系统管理员已默认分配所有权限 +4. **权限缓存**:修改权限后,用户需要重新登录才能生效(可以实现实时刷新) +5. **权限粒度**:当前实现到API级别,可以根据需要扩展到字段级别 + +## 🚀 后续优化建议 + +1. **权限缓存**: 使用Redis缓存用户权限,提高查询效率 +2. **实时刷新**: WebSocket推送权限变更,无需重新登录 +3. **权限日志**: 记录权限变更历史,便于审计 +4. **数据权限**: 扩展到数据行级权限控制 +5. **权限模板**: 预定义常用权限组合,快速分配 + +## ❓ 常见问题 + +### Q1: 修改权限后不生效? +A: 用户需要重新登录,因为权限信息存储在JWT中。可以实现权限实时刷新机制。 + +### Q2: 如何给新用户分配权限? +A: 在用户管理中为用户分配角色,然后在权限管理中为该角色分配权限。 + +### Q3: 权限标识是什么? +A: 权限标识是API接口的唯一识别码,格式为"模块:操作",用于权限验证。 + +### Q4: 为什么看不到某些菜单? +A: 检查该角色是否被分配了对应的菜单权限,同时确保菜单状态为启用。 + +### Q5: API接口返回403错误? +A: 用户没有访问该接口的权限,需要在权限管理中为用户角色分配对应的API权限。 + +## 📞 技术支持 + +如有问题,请联系系统管理员或技术支持团队。 +