diff --git a/pc/src/stores/README.md b/pc/src/stores/README.md deleted file mode 100644 index 181c29c..0000000 --- a/pc/src/stores/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# 菜单 Store 使用说明 - -## 问题描述 - -之前菜单 API 在多个地方被重复调用: -1. `CommonHeader.vue` - 面包屑导航需要菜单数据 -2. `CommonAside.vue` - 侧边栏菜单需要菜单数据 -3. `router/index.js` - 动态路由加载需要菜单数据 - -这导致访问页面时会发送多次相同的 API 请求,浪费资源。 - -## 解决方案 - -使用 Pinia Store 统一管理菜单数据,实现: -- **单例加载**:确保同一时间只发送一次 API 请求 -- **缓存机制**:5分钟缓存,减少重复请求 -- **响应式更新**:所有组件自动同步更新 - -## 使用方法 - -### 1. 在组件中使用 - -```javascript -import { useMenuStore } from '@/stores'; - -const menuStore = useMenuStore(); - -// 获取菜单(会自动使用缓存,避免重复请求) -await menuStore.fetchMenus(); - -// 强制刷新菜单(跳过缓存) -await menuStore.refreshMenus(); - -// 使用菜单数据 -const menus = menuStore.menus; // 响应式数据 -const isLoading = menuStore.loading; -``` - -### 2. 计算属性 - -```javascript -import { computed } from 'vue'; -import { useMenuStore } from '@/stores'; - -const menuStore = useMenuStore(); - -// 自动响应菜单数据变化 -const menuList = computed(() => menuStore.menus); -``` - -### 3. 登出时重置 - -```javascript -// 在登出时调用 -menuStore.resetMenus(); -``` - -## Store API - -### 状态 -- `menus` - 菜单列表(响应式) -- `loading` - 加载状态 -- `error` - 错误信息 -- `isLoaded` - 是否已加载 - -### 方法 -- `fetchMenus(forceRefresh = false)` - 获取菜单(优先使用缓存) -- `refreshMenus()` - 强制刷新菜单(跳过缓存) -- `resetMenus()` - 重置菜单状态(登出时使用) -- `clearCache()` - 清除缓存 - -## 缓存机制 - -- 缓存 key 基于用户类型和角色ID:`menu_cache_{loginType}_{roleId}` -- 缓存有效期:5分钟 -- 自动过期:超过5分钟自动失效 -- 后台更新:首次加载使用缓存,同时在后台更新 - -## 性能优化 - -1. **避免重复请求**:多个组件同时请求时,只发送一次 API 请求 -2. **智能缓存**:5分钟内使用缓存,减少服务器压力 -3. **响应式更新**:所有组件自动同步,无需手动刷新 - diff --git a/pc/src/stores/README_OA.md b/pc/src/stores/README_OA.md deleted file mode 100644 index 5ddec7d..0000000 --- a/pc/src/stores/README_OA.md +++ /dev/null @@ -1,192 +0,0 @@ -# OA 基础数据 Store 使用说明 - -## 概述 - -`useOAStore` 是一个专门用于管理 OA(办公自动化)模块基础数据的 Pinia Store,包括部门、职位、角色等数据。它提供了缓存机制,避免重复请求,大幅减少资源占用。 - -## 主要特性 - -### 1. 智能缓存机制 -- **缓存时间**:默认 5 分钟(可配置) -- **自动判断**:如果缓存有效,直接返回缓存数据,不发起网络请求 -- **缓存隔离**:部门、职位、角色数据独立缓存 - -### 2. 并发请求控制 -- **防重复请求**:如果数据正在加载中,后续请求会等待加载完成 -- **并行加载**:`fetchAllBaseData` 方法会并行请求所有基础数据 - -### 3. 统一数据管理 -- 所有 OA 相关页面共享同一份数据 -- 数据更新后,所有使用该数据的组件自动响应 - -## 使用方法 - -### 基本使用 - -```javascript -import { useOAStore } from '@/stores/oa'; - -const oaStore = useOAStore(); - -// 获取部门列表(带缓存) -await oaStore.fetchDepartments(); - -// 获取职位列表(带缓存) -await oaStore.fetchPositions(); - -// 获取角色列表(带缓存) -await oaStore.fetchRoles(); - -// 批量获取所有基础数据(推荐用于页面初始化) -await oaStore.fetchAllBaseData(); -``` - -### 强制刷新 - -```javascript -// 强制刷新部门数据(忽略缓存) -await oaStore.fetchDepartments(true); - -// 或者使用 refresh 方法 -await oaStore.refreshDepartments(); - -// 刷新所有基础数据 -await oaStore.refreshAll(); -``` - -### 获取特定数据 - -```javascript -// 根据ID获取部门信息 -const department = oaStore.getDepartmentById(1); - -// 根据ID获取职位信息 -const position = oaStore.getPositionById(1); - -// 根据ID获取角色信息 -const role = oaStore.getRoleById(1); -``` - -### 在组件中使用响应式数据 - -```javascript -import { computed } from 'vue'; -import { useOAStore } from '@/stores/oa'; - -const oaStore = useOAStore(); - -// 响应式的部门列表 -const departments = computed(() => oaStore.departments); - -// 响应式的部门树 -const departmentTree = computed(() => oaStore.departmentTree); - -// 响应式的职位列表 -const positions = computed(() => oaStore.positions); - -// 响应式的角色列表 -const roles = computed(() => oaStore.roles); - -// 加载状态 -const loadingDepartments = computed(() => oaStore.loadingDepartments); -const loadingPositions = computed(() => oaStore.loadingPositions); -const loadingRoles = computed(() => oaStore.loadingRoles); -``` - -### 缓存管理 - -```javascript -// 清除所有缓存 -oaStore.clearCache(); - -// 清除特定缓存 -oaStore.clearDepartmentsCache(); -oaStore.clearPositionsCache(); -oaStore.clearRolesCache(); -``` - -## 性能优化效果 - -### 优化前 -- 每次进入员工管理页面,都会发起 4 个独立的网络请求: - 1. 获取部门列表 - 2. 获取职位列表 - 3. 获取角色列表 - 4. 获取员工列表 - -- 如果用户频繁切换页面,会产生大量重复请求 - -### 优化后 -- **首次访问**:发起 4 个请求(并行请求,速度更快) -- **缓存有效期内再次访问**:只发起 1 个请求(员工列表) -- **缓存过期后**:自动刷新缓存,再次进入缓存有效期 - -### 性能提升 -- **请求次数减少**:75% 减少(从 4 次减少到 1 次) -- **响应速度提升**:缓存命中时,几乎瞬时响应 -- **服务器压力降低**:减少 75% 的基础数据请求 - -## 最佳实践 - -### 1. 页面初始化 -```javascript -onMounted(async () => { - // 使用批量获取方法,利用缓存机制 - await oaStore.fetchAllBaseData(); - // 然后获取业务数据 - await fetchEmployees(); -}); -``` - -### 2. 数据更新后 -```javascript -// 如果修改了部门、职位或角色,需要刷新相关缓存 -await addDepartment(newDepartment); -await oaStore.refreshDepartments(); // 刷新部门缓存 -``` - -### 3. 编辑时确保数据最新 -```javascript -const handleEdit = async (item) => { - // 确保基础数据已加载(使用缓存) - await oaStore.fetchAllBaseData(); - // 然后加载业务数据 - // ... -}; -``` - -## 注意事项 - -1. **缓存时间**:默认 5 分钟,如果需要更频繁的更新,可以修改 `cacheTime` 常量 -2. **租户隔离**:缓存是基于当前租户的,不同租户的数据不会混淆 -3. **数据一致性**:如果后端数据更新频繁,可以在更新操作后调用 `refresh` 方法 -4. **内存占用**:缓存数据存储在内存中,页面刷新后会清空 - -## API 参考 - -### 状态 -- `departments`: 部门列表(扁平结构) -- `departmentTree`: 部门树结构 -- `positions`: 职位列表 -- `roles`: 角色列表 -- `loadingDepartments`: 部门加载状态 -- `loadingPositions`: 职位加载状态 -- `loadingRoles`: 角色加载状态 - -### 方法 -- `fetchDepartments(forceRefresh)`: 获取部门列表 -- `fetchPositions(departmentId, forceRefresh)`: 获取职位列表 -- `fetchRoles(forceRefresh)`: 获取角色列表 -- `fetchAllBaseData(forceRefresh)`: 批量获取所有基础数据 -- `getDepartmentById(id)`: 根据ID获取部门 -- `getPositionById(id)`: 根据ID获取职位 -- `getRoleById(id)`: 根据ID获取角色 -- `refreshDepartments()`: 刷新部门数据 -- `refreshPositions()`: 刷新职位数据 -- `refreshRoles()`: 刷新角色数据 -- `refreshAll()`: 刷新所有数据 -- `clearCache()`: 清除所有缓存 -- `clearDepartmentsCache()`: 清除部门缓存 -- `clearPositionsCache()`: 清除职位缓存 -- `clearRolesCache()`: 清除角色缓存 - diff --git a/pc/src/views/apps/oa/workbench/index.vue b/pc/src/views/apps/oa/workbench/index.vue new file mode 100644 index 0000000..bc27406 --- /dev/null +++ b/pc/src/views/apps/oa/workbench/index.vue @@ -0,0 +1,2396 @@ + + + + + + + + + + + + + + 审批 + + + + + + + + + + + + + + + + + + {{ app.name }} + + {{ app.count > 99 ? "99+" : app.count }} + + + + + + + + + + + 知识库 + + + + + + + + + + + + + + + + + + + {{ knowledge.title }} + + {{ + knowledge.category + }} + {{ knowledge.date }} + + + + + + + + + + + + + + + 我的待办 + + + + + + 待审批 + + {{ task.title }} + + 处理节点: {{ task.node }} + 申请时间: {{ task.applyTime }} + + + + + + + + + + + + 常用单据 + + + + + + + {{ form }} + + + + + + + + + + + + + 行业资讯 + + 查看更多 + + + + + + {{ news.title }} + + {{ news.source }} + {{ news.date }} + + + + + + + + + + + + + + + + 我的日程 + + + 新增 + 查看 + + + + + + + + + + + {{ currentMonth }} + + + + + + + + + + {{ day }} + + + + + + + {{ getDayNumber(date) }} + + + + + + + + {{ formatDate(date) }} + + + + 添加日程 + + + + + + + + + + + {{ schedule.time }} + + + + {{ schedule.title }} + + + {{ schedule.description }} + + + + {{ schedule.typeLabel }} + + + + + + + + + + + + + + + + + 帮助与服务 + + 查看更多 + + + + + + {{ help.tag }} + + {{ help.title }} + + + + + + + + + + + diff --git a/server/conf/app.conf b/server/conf/app.conf index 3322320..cdf9ebe 100644 --- a/server/conf/app.conf +++ b/server/conf/app.conf @@ -9,12 +9,12 @@ mysqlpass = 2nZhRdMPCNZrdzsd mysqlurls = 212.64.112.158:3388 mysqldb = gotest -# SQLite -#sqlitepath = ./yunzer.db - # ORM配置 orm = mysql # 配置静态文件目录 -StaticDir = /static:../front/dist # 映射 /static 路径到前端 dist 目录 -StaticDir = /uploads:../uploads # 映射 /uploads 路径到上传文件目录(项目根目录下的 uploads 文件夹) \ No newline at end of file +# 映射 /static 路径到前端 dist 目录 +StaticDir = /static:../front/dist + +# 映射 /uploads 路径到上传文件目录(项目根目录下的 uploads 文件夹) +StaticDir = /uploads:../uploads \ No newline at end of file diff --git a/server/models/permission.go b/server/models/permission.go index 180b5a0..e175e6f 100644 --- a/server/models/permission.go +++ b/server/models/permission.go @@ -10,7 +10,7 @@ import ( "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"` @@ -19,115 +19,79 @@ type RoleMenu struct { 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"` // 权限标识列表 + 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接口 + MenuType int `json:"menu_type"` // 1页面菜单, 2API Permission string `json:"permission"` // 权限标识 ParentId int `json:"parent_id"` - Default int8 `json:"default"` // 默认可见性:0-全局,1-平台用户,2-租户用户 + Default int8 `json:"default"` // 0全局, 1平台用户, 2租户用户 } func init() { orm.RegisterModel(new(RoleMenu)) } -// GetRoleMenus 获取指定角色的所有菜单权限(从JSON字段读取) +// 获取指定角色的菜单ID列表 func GetRoleMenus(roleId int) ([]int, error) { o := orm.NewOrm() var menuIdsJson sql.NullString var menuIdsStr string - // 方法1: 尝试使用 JSON_UNQUOTE 读取 JSON 字段 + // 优先尝试解析JSON字段 err := o.Raw("SELECT IFNULL(JSON_UNQUOTE(JSON_EXTRACT(menu_ids, '$')), '[]') FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&menuIdsStr) if err == nil && menuIdsStr != "" && menuIdsStr != "[]" { menuIdsJson = sql.NullString{String: menuIdsStr, Valid: true} - fmt.Printf("GetRoleMenus: 方法1成功,角色 %d 的 menu_ids: %s\n", roleId, menuIdsStr[:min(100, len(menuIdsStr))]) } else { - // 方法1失败,尝试方法2: 直接 CAST - if err != nil { - fmt.Printf("GetRoleMenus: 方法1失败 (%v),尝试方法2\n", err) - } else { - fmt.Printf("GetRoleMenus: 方法1结果为空,尝试方法2\n") - } - - // 重置变量 + // 若失败用CAST再次尝试,依然失败直接返回空 menuIdsStr = "" err2 := o.Raw("SELECT CAST(IFNULL(menu_ids, '[]') AS CHAR) FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&menuIdsStr) if err2 != nil { - // 如果角色不存在,返回空数组而不是错误(兼容性处理) if err2 == orm.ErrNoRows { - fmt.Printf("GetRoleMenus: 角色 %d 不存在\n", roleId) return []int{}, nil } - fmt.Printf("GetRoleMenus: 方法2也失败,角色 %d 的 menu_ids 读取失败: %v\n", roleId, err2) - return []int{}, nil // 返回空数组而不是错误,保持兼容性 + return []int{}, nil } - if menuIdsStr != "" && menuIdsStr != "[]" && menuIdsStr != "null" { menuIdsJson = sql.NullString{String: menuIdsStr, Valid: true} - fmt.Printf("GetRoleMenus: 方法2成功,角色 %d 的 menu_ids: %s\n", roleId, menuIdsStr[:min(100, len(menuIdsStr))]) } else { - fmt.Printf("GetRoleMenus: 方法2结果也为空,角色 %d 的 menu_ids 为空或 null\n", roleId) return []int{}, nil } } - // 如果 menuIdsJson 无效或为空,返回空数组 if !menuIdsJson.Valid || menuIdsJson.String == "" { - fmt.Printf("GetRoleMenus: 角色 %d 的 menu_ids 最终为空或无效\n", roleId) return []int{}, nil } - // 清理可能的空白字符和换行符 jsonStr := strings.TrimSpace(menuIdsJson.String) jsonStr = strings.ReplaceAll(jsonStr, "\n", "") jsonStr = strings.ReplaceAll(jsonStr, "\r", "") - jsonStr = strings.ReplaceAll(jsonStr, " ", "") // 移除所有空格 - - // 调试:输出原始 JSON 字符串 - fmt.Printf("角色 %d 的 menu_ids 原始值: %s (长度: %d)\n", roleId, jsonStr, len(jsonStr)) + jsonStr = strings.ReplaceAll(jsonStr, " ", "") if jsonStr == "" || jsonStr == "[]" || jsonStr == "null" || jsonStr == "NULL" { - fmt.Printf("角色 %d 的 menu_ids 为空数组或 null\n", roleId) return []int{}, nil } var menuIds []int err = json.Unmarshal([]byte(jsonStr), &menuIds) if err != nil { - // 如果解析失败,记录详细错误信息用于调试 - fmt.Printf("错误:解析角色 %d 的菜单ID失败: %v\n", roleId, err) - fmt.Printf("原始值: %s\n", jsonStr) - fmt.Printf("原始值长度: %d\n", len(jsonStr)) - // 尝试打印前200个字符用于调试 - if len(jsonStr) > 200 { - fmt.Printf("原始值前200字符: %s\n", jsonStr[:200]) - } return []int{}, nil } - // 调试输出:成功解析的菜单ID数量 - fmt.Printf("成功解析角色 %d 的菜单ID,共 %d 个\n", roleId, len(menuIds)) - if len(menuIds) > 0 { - fmt.Printf("前10个菜单ID: %v\n", menuIds[:min(10, len(menuIds))]) - } - return menuIds, nil } @@ -139,45 +103,32 @@ func min(a, b int) int { return b } -// GetRolePermissions 获取角色的详细权限信息(包括菜单和API权限) -// 主要基于 yz_roles.menu_ids 字段来获取权限 +// 获取角色的详细权限(菜单和API权限) func GetRolePermissions(roleId int) (*RolePermission, error) { o := orm.NewOrm() - - // 直接使用 GetRoleById 获取角色信息,因为它已经正确实现了 JSON 字段的读取 role, err := GetRoleById(roleId) if err != nil { return nil, fmt.Errorf("角色不存在: %v", err) } - // 从角色对象中获取菜单ID列表(已经从 menu_ids JSON字段解析) menuIds := role.MenuIds if menuIds == nil { menuIds = []int{} } - // 调试输出 - fmt.Printf("GetRolePermissions: 角色 %d (%s) 的 menu_ids: %v (共 %d 个)\n", roleId, role.RoleName, menuIds, len(menuIds)) - fmt.Printf("GetRolePermissions: role.MenuIdsJson.Valid=%v, role.MenuIdsJson.String=%s\n", role.MenuIdsJson.Valid, role.MenuIdsJson.String) - - // 3. 根据菜单ID列表获取权限标识列表(从菜单的 permission 字段获取) - // 权限标识来源于 yz_menus 表的 permission 字段 - permissions := []string{} // 初始化为空数组,避免返回 null + 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 } - // 查询所有菜单的权限标识(包括页面菜单和API接口,且未删除的) query := fmt.Sprintf("SELECT DISTINCT permission FROM yz_menus WHERE id IN (%s) AND delete_time IS NULL 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) } - // 确保 permissions 不为 nil if permissions == nil { permissions = []string{} } @@ -186,17 +137,15 @@ func GetRolePermissions(roleId int) (*RolePermission, error) { return &RolePermission{ RoleId: role.RoleId, RoleName: role.RoleName, - MenuIds: menuIds, // 来自 yz_roles.menu_ids - Permissions: permissions, // 来自 yz_menus.permission(基于 menu_ids) + MenuIds: menuIds, + Permissions: permissions, }, nil } -// 获取所有菜单权限列表(用于分配权限时展示,未删除的) +// 获取全部菜单权限信息(未删除) func GetAllMenuPermissions() ([]*MenuPermission, error) { o := orm.NewOrm() - - // 查询菜单(菜单表没有default字段,直接使用0作为默认值) - var resultsWithoutDefault []struct { + var results []struct { MenuId int MenuName string Path string @@ -204,21 +153,20 @@ func GetAllMenuPermissions() ([]*MenuPermission, error) { Permission sql.NullString ParentId int } - _, err := o.Raw("SELECT id as menu_id, name as menu_name, path, menu_type, permission, parent_id FROM yz_menus WHERE delete_time IS NULL ORDER BY parent_id, `order`").QueryRows(&resultsWithoutDefault) + _, err := o.Raw("SELECT id as menu_id, name as menu_name, path, menu_type, permission, parent_id FROM yz_menus WHERE delete_time IS NULL ORDER BY parent_id, `order`").QueryRows(&results) if err != nil { return nil, fmt.Errorf("获取菜单列表失败: %v", err) } - // 转换为MenuPermission结构,default字段设为0(全局可见) - menus := make([]*MenuPermission, 0, len(resultsWithoutDefault)) - for _, r := range resultsWithoutDefault { + menus := make([]*MenuPermission, 0, len(results)) + for _, r := range results { menu := &MenuPermission{ MenuId: r.MenuId, MenuName: r.MenuName, Path: r.Path, MenuType: r.MenuType, ParentId: r.ParentId, - Default: 0, // 默认值为0(全局可见),因为菜单表没有default字段 + Default: 0, } if r.Permission.Valid { menu.Permission = r.Permission.String @@ -227,22 +175,14 @@ func GetAllMenuPermissions() ([]*MenuPermission, error) { } menus = append(menus, menu) } - return menus, nil } -// GetAllMenuPermissionsForUser 根据当前登录用户的权限获取可分配的菜单列表 -// userType: "user" 表示平台用户(可以看到所有菜单),"employee" 表示租户员工 -// roleId: 可选的角色ID,如果提供则根据该角色的default值过滤菜单 -// 设计说明: -// - 平台用户:可以看到所有菜单,可以给任何角色分配任何菜单 -// - 租户员工:在权限分配界面(提供roleId时)只能看到平台管理员已经分配给自己的菜单(包括父菜单) -// - 租户员工:在菜单显示时(不提供roleId时)只看到自己有权限的菜单 +// 获取可分配菜单列表,支持平台/租户员工 func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*MenuPermission, error) { o := orm.NewOrm() - // 如果提供了roleId,获取角色的default值用于过滤菜单 - var roleDefault int8 = 0 // 0表示全局,不进行过滤 + var roleDefault int8 = 0 if roleId > 0 { role, err := GetRoleById(roleId) if err == nil && role != nil { @@ -250,57 +190,41 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M } } - // 如果是平台用户,返回所有菜单(根据roleDefault过滤) if userType == "user" { allMenus, err := GetAllMenuPermissions() if err != nil { return nil, err } - - // 如果roleDefault>0,根据角色的default值过滤菜单 if roleDefault > 0 { filteredMenus := make([]*MenuPermission, 0) for _, menu := range allMenus { - // 角色default=1(平台用户角色):只能分配default=1或default=0的菜单 - // 角色default=2(租户用户角色):只能分配default=2或default=0的菜单 if menu.Default == 0 || menu.Default == roleDefault { filteredMenus = append(filteredMenus, menu) } } return filteredMenus, nil } - return allMenus, nil } - // 如果是租户员工 if userType == "employee" { - // 获取员工信息 var employee Employee err := o.Raw("SELECT * FROM yz_tenant_employees WHERE id = ? AND delete_time IS NULL", userId).QueryRow(&employee) if err != nil { return nil, fmt.Errorf("员工不存在: %v", err) } - - // 如果员工没有角色,返回空列表 if employee.Role == 0 { return []*MenuPermission{}, nil } - - // 获取员工角色的菜单ID列表(这是平台管理员分配给该员工的菜单) menuIds, err := GetRoleMenus(employee.Role) if err != nil { return nil, fmt.Errorf("获取角色菜单失败: %v", err) } - - // 如果没有权限,返回空列表 if len(menuIds) == 0 { return []*MenuPermission{}, nil } - // 如果提供了roleId(权限分配界面),需要包含父菜单 - // 如果没有提供roleId(菜单显示),也需要包含父菜单(但这里已经在GetTenantMenus中处理了) - // 为了性能优化,一次性查询所有菜单的父子关系 + // 获取全部菜单父子关系,内存递归收集所有父菜单ID type menuParent struct { Id int ParentId int @@ -310,14 +234,10 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M if err != nil { return nil, fmt.Errorf("获取菜单父子关系失败: %v", err) } - - // 构建菜单ID到父菜单ID的映射 menuParentMap := make(map[int]int) for _, mp := range allMenuParents { menuParentMap[mp.Id] = mp.ParentId } - - // 递归查找所有父菜单ID(使用内存中的映射,避免数据库查询) parentIds := make(map[int]bool) var findParents func(pid int) findParents = func(pid int) { @@ -329,15 +249,11 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M findParents(parentId) } } - - // 为每个菜单查找其父菜单 for _, menuId := range menuIds { if parentId, exists := menuParentMap[menuId]; exists && parentId > 0 { findParents(parentId) } } - - // 合并原始菜单ID和父菜单ID allMenuIds := make(map[int]bool) for _, id := range menuIds { allMenuIds[id] = true @@ -346,12 +262,10 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M allMenuIds[pid] = true } - // 构建IN查询的占位符和参数 finalMenuIds := make([]int, 0, len(allMenuIds)) for id := range allMenuIds { finalMenuIds = append(finalMenuIds, id) } - placeholders := make([]string, len(finalMenuIds)) args := make([]interface{}, len(finalMenuIds)) for i, id := range finalMenuIds { @@ -359,7 +273,6 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M args[i] = id } - // 查询菜单(包括父菜单) type menuResult struct { MenuId int MenuName string @@ -368,7 +281,6 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M Permission sql.NullString ParentId int } - var results []menuResult query := fmt.Sprintf("SELECT id as menu_id, name as menu_name, path, menu_type, permission, parent_id FROM yz_menus WHERE id IN (%s) AND delete_time IS NULL ORDER BY parent_id, `order`", strings.Join(placeholders, ",")) _, err = o.Raw(query, args...).QueryRows(&results) @@ -376,7 +288,6 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M return nil, fmt.Errorf("获取菜单列表失败: %v", err) } - // 转换为MenuPermission结构 menus := make([]*MenuPermission, 0, len(results)) for _, r := range results { menu := &MenuPermission{ @@ -385,46 +296,35 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M Path: r.Path, MenuType: r.MenuType, ParentId: r.ParentId, - Default: 0, // 默认值为0(全局可见),因为菜单表没有default字段 + Default: 0, } - - // 处理permission字段 if r.Permission.Valid { menu.Permission = r.Permission.String } else { menu.Permission = "" } - menus = append(menus, menu) } - // 如果roleDefault>0,根据角色的default值进一步过滤菜单 - // 但由于菜单表没有default字段,所有菜单都是default=0,所以这里实际上不会过滤 + // 菜单表没有default字段,下面逻辑实际上不会筛选掉任何菜单,仅保留 if roleDefault > 0 { filteredMenus := make([]*MenuPermission, 0) for _, menu := range menus { - // 角色default=1(平台用户角色):只能分配default=1或default=0的菜单 - // 角色default=2(租户用户角色):只能分配default=2或default=0的菜单 - // 由于菜单表没有default字段,所有菜单都是default=0,所以所有菜单都可以分配 if menu.Default == 0 || menu.Default == roleDefault { filteredMenus = append(filteredMenus, menu) } } return filteredMenus, nil } - return menus, nil } - // 未知的用户类型,返回空列表 return []*MenuPermission{}, nil } -// 为角色分配权限(菜单)- 更新JSON字段 +// 分配角色权限,更新role菜单ID func AssignRolePermissions(roleId int, menuIds []int, createBy string) error { o := orm.NewOrm() - - // 将菜单ID数组序列化为JSON var jsonData []byte var err error if len(menuIds) == 0 { @@ -435,28 +335,21 @@ func AssignRolePermissions(roleId int, menuIds []int, createBy string) error { return fmt.Errorf("序列化菜单ID失败: %v", err) } } - - // 更新角色表的menu_ids字段 _, err = o.Raw("UPDATE yz_roles SET menu_ids = ?, update_by = ?, update_time = NOW() WHERE role_id = ?", string(jsonData), createBy, roleId).Exec() if err != nil { return fmt.Errorf("更新角色权限失败: %v", 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, @@ -465,33 +358,27 @@ func GetUserPermissions(userId int) (*RolePermission, error) { Permissions: []string{}, }, nil } - - // 获取角色权限 return GetRolePermissions(user.Role) } -// CheckUserPermission 检查用户是否拥有指定权限 +// 检查用户是否拥有指定权限 func CheckUserPermission(userId int, permission string) (bool, error) { if permission == "" { - return true, nil // 空权限标识表示不需要权限控制 + 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"` @@ -508,34 +395,26 @@ type MenuTreeNode struct { 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 { @@ -547,8 +426,6 @@ func GetUserMenuTree(userId int) ([]*MenuTreeNode, error) { if err != nil { return nil, fmt.Errorf("获取菜单列表失败: %v", err) } - - // 转换为MenuTreeNode var nodes []*MenuTreeNode for _, m := range menus { nodes = append(nodes, &MenuTreeNode{ @@ -567,21 +444,17 @@ func GetUserMenuTree(userId int) ([]*MenuTreeNode, error) { 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/models/role.go b/server/models/role.go index aabe7cd..5afa6bf 100644 --- a/server/models/role.go +++ b/server/models/role.go @@ -3,24 +3,23 @@ package models import ( "database/sql" "encoding/json" - "fmt" "strings" "time" "github.com/beego/beego/v2/client/orm" ) -// Role 角色模型 +// Role 角色 type Role struct { RoleId int `orm:"pk;auto;column(role_id)" json:"roleId"` TenantId int `orm:"column(tenant_id)" json:"tenantId"` - Default int8 `orm:"column(default);default(2)" json:"default"` // 角色默认分配:1-只给租户1,2-所有租户可用,3-租户专属 + Default int8 `orm:"column(default);default(2)" json:"default"` // 1-租户1, 2-所有租户, 3-租户专属 RoleCode string `orm:"size(50);unique" json:"roleCode"` RoleName string `orm:"size(100)" json:"roleName"` Description string `orm:"type(text);null" json:"description"` - MenuIds []int `orm:"-" json:"menuIds"` // 前端使用的菜单ID数组(不存储在数据库) - MenuIdsJson sql.NullString `orm:"column(menu_ids);type(json);null" json:"-"` // 数据库存储的JSON字段 - Status int8 `orm:"default(1)" json:"status"` // 1:启用 0:禁用 + MenuIds []int `orm:"-" json:"menuIds"` // 前端菜单ID + MenuIdsJson sql.NullString `orm:"column(menu_ids);type(json);null" json:"-"` // 库JSON字段 + Status int8 `orm:"default(1)" json:"status"` // 1启用 0禁用 SortOrder int `orm:"default(0)" json:"sortOrder"` // 排序 CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"createTime"` UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"updateTime"` @@ -29,32 +28,22 @@ type Role struct { UpdateBy string `orm:"size(50);null" json:"updateBy"` } -// AfterRead 读取数据后解析JSON字段 +// AfterRead 解析MenuIdsJson func (r *Role) AfterRead() { - // 调试输出 - fmt.Printf("AfterRead: MenuIdsJson.Valid=%v, MenuIdsJson.String=%s\n", r.MenuIdsJson.Valid, r.MenuIdsJson.String) - if r.MenuIdsJson.Valid && r.MenuIdsJson.String != "" && r.MenuIdsJson.String != "[]" { - // 清理可能的空白字符 jsonStr := strings.TrimSpace(r.MenuIdsJson.String) jsonStr = strings.ReplaceAll(jsonStr, "\n", "") jsonStr = strings.ReplaceAll(jsonStr, "\r", "") - err := json.Unmarshal([]byte(jsonStr), &r.MenuIds) if err != nil { - // 如果解析失败,记录错误但使用空数组 - fmt.Printf("AfterRead: JSON解析失败: %v, 原始值: %s\n", err, jsonStr) r.MenuIds = []int{} - } else { - fmt.Printf("AfterRead: 成功解析 %d 个菜单ID\n", len(r.MenuIds)) } } else { - fmt.Printf("AfterRead: MenuIdsJson 无效或为空\n") r.MenuIds = []int{} } } -// BeforeInsert 插入前序列化JSON字段 +// BeforeInsert 序列化MenuIds到MenuIdsJson func (r *Role) BeforeInsert() { if len(r.MenuIds) > 0 { jsonData, _ := json.Marshal(r.MenuIds) @@ -64,7 +53,7 @@ func (r *Role) BeforeInsert() { } } -// BeforeUpdate 更新前序列化JSON字段 +// BeforeUpdate 序列化MenuIds到MenuIdsJson func (r *Role) BeforeUpdate() { if len(r.MenuIds) > 0 { jsonData, _ := json.Marshal(r.MenuIds) @@ -82,12 +71,10 @@ func init() { orm.RegisterModel(new(Role)) } -// GetRoleById 根据ID获取角色 +// GetRoleById 按ID获取角色 func GetRoleById(roleId int) (*Role, error) { o := orm.NewOrm() - // 使用Raw查询以正确读取JSON字段 - // 定义一个临时结构体来接收查询结果 type roleResult struct { RoleId int TenantId int @@ -106,7 +93,6 @@ func GetRoleById(roleId int) (*Role, error) { } var result roleResult - // 先读取其他字段(不包括 menu_ids),因为 Beego ORM 可能无法直接读取 JSON 类型 err := o.Raw("SELECT role_id, tenant_id, `default`, role_code, role_name, description, status, sort_order, create_time, update_time, delete_time, create_by, update_by FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow( &result.RoleId, &result.TenantId, &result.Default, &result.RoleCode, &result.RoleName, &result.Description, &result.Status, &result.SortOrder, &result.CreateTime, &result.UpdateTime, @@ -116,39 +102,24 @@ func GetRoleById(roleId int) (*Role, error) { return nil, err } - // 单独读取 menu_ids JSON 字段,使用 JSON_UNQUOTE 确保正确读取 var menuIdsStr string err2 := o.Raw("SELECT IFNULL(JSON_UNQUOTE(JSON_EXTRACT(menu_ids, '$')), '[]') FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&menuIdsStr) if err2 != nil { - fmt.Printf("GetRoleById: JSON_UNQUOTE 读取失败: %v,尝试 CAST\n", err2) - // 如果 JSON_UNQUOTE 失败,尝试直接 CAST err3 := o.Raw("SELECT CAST(IFNULL(menu_ids, '[]') AS CHAR) FROM yz_roles WHERE role_id = ? AND delete_time IS NULL", roleId).QueryRow(&menuIdsStr) if err3 != nil { - fmt.Printf("GetRoleById: CAST 也失败: %v,使用空数组\n", err3) menuIdsStr = "[]" } } - - // 设置 MenuIdsJson if menuIdsStr != "" && menuIdsStr != "[]" && menuIdsStr != "null" { result.MenuIdsJson = sql.NullString{String: menuIdsStr, Valid: true} - // 只打印前100个字符,避免日志过长 - preview := menuIdsStr - if len(preview) > 100 { - preview = preview[:100] + "..." - } - fmt.Printf("GetRoleById: 角色 %d 的 menu_ids 读取成功: %s (总长度: %d)\n", roleId, preview, len(menuIdsStr)) } else { result.MenuIdsJson = sql.NullString{String: "[]", Valid: true} - fmt.Printf("GetRoleById: 角色 %d 的 menu_ids 为空,使用空数组\n", roleId) } - // 检查是否已删除(虽然SQL已经过滤了,但为了安全还是检查一下) if result.DeleteTime != nil { return nil, orm.ErrNoRows } - // 构建Role对象 role := &Role{ RoleId: result.RoleId, TenantId: result.TenantId, @@ -165,21 +136,15 @@ func GetRoleById(roleId int) (*Role, error) { CreateBy: result.CreateBy, UpdateBy: result.UpdateBy, } - - // 解析JSON字段 role.AfterRead() - return role, nil } -// GetAllRoles 获取所有角色(未删除的) -// tenantId: 租户ID,0表示所有租户 -// userType: 用户类型,"user"表示平台用户,"employee"表示租户员工 +// GetAllRoles 获取所有角色 func GetAllRoles(tenantId int, userType string) ([]*Role, error) { o := orm.NewOrm() var roles []*Role - // 使用Raw查询以正确读取JSON字段 var results []struct { RoleId int TenantId int @@ -197,27 +162,17 @@ func GetAllRoles(tenantId int, userType string) ([]*Role, error) { UpdateBy string } - // 构建查询条件 var query string var args []interface{} - // 如果是平台用户(user),可以看到所有角色 if userType == "user" { query = "SELECT role_id, tenant_id, `default`, role_code, role_name, description, CAST(IFNULL(menu_ids, '[]') AS CHAR) as menu_ids, status, sort_order, create_time, update_time, delete_time, create_by, update_by FROM yz_roles WHERE delete_time IS NULL ORDER BY sort_order ASC, role_id ASC" args = []interface{}{} } else { - // 如果是租户员工(employee),根据 tenant_id 和 default 过滤 - // 规则: - // 1. default=0: 全局角色,所有租户可见 - // 2. default=1: 平台用户角色,租户员工不可见 - // 3. default=2: 租户用户角色,只有对应租户可见 - // 4. tenant_id=0: 全局角色 - // 5. tenant_id=当前租户ID: 当前租户的角色 if tenantId > 0 { query = "SELECT role_id, tenant_id, `default`, role_code, role_name, description, CAST(IFNULL(menu_ids, '[]') AS CHAR) as menu_ids, status, sort_order, create_time, update_time, delete_time, create_by, update_by FROM yz_roles WHERE delete_time IS NULL AND ((`default` = 0) OR (`default` = 2 AND (tenant_id = ? OR tenant_id = 0))) ORDER BY sort_order ASC, role_id ASC" args = []interface{}{tenantId} } else { - // tenantId=0,只返回全局角色(default=0) query = "SELECT role_id, tenant_id, `default`, role_code, role_name, description, CAST(IFNULL(menu_ids, '[]') AS CHAR) as menu_ids, status, sort_order, create_time, update_time, delete_time, create_by, update_by FROM yz_roles WHERE delete_time IS NULL AND `default` = 0 ORDER BY sort_order ASC, role_id ASC" args = []interface{}{} } @@ -252,12 +207,11 @@ func GetAllRoles(tenantId int, userType string) ([]*Role, error) { return roles, nil } -// GetRoleByTenantId 根据租户ID获取角色列表(未删除的) +// GetRoleByTenantId 按租户ID获取角色 func GetRoleByTenantId(tenantId int) ([]*Role, error) { o := orm.NewOrm() var roles []*Role - // 使用Raw查询以正确读取JSON字段 var results []struct { RoleId int TenantId int @@ -304,11 +258,10 @@ func GetRoleByTenantId(tenantId int) ([]*Role, error) { return roles, nil } -// GetRoleByCode 根据角色代码获取角色 +// GetRoleByCode 按角色代码获取角色 func GetRoleByCode(roleCode string) (*Role, error) { o := orm.NewOrm() - // 使用Raw查询以正确读取所有字段,包括 default 和 menu_ids type roleResult struct { RoleId int TenantId int @@ -336,7 +289,6 @@ func GetRoleByCode(roleCode string) (*Role, error) { return nil, err } - // 手动读取 menu_ids JSON 字段 var menuIdsStr string err2 := o.Raw("SELECT IFNULL(JSON_UNQUOTE(JSON_EXTRACT(menu_ids, '$')), '[]') FROM yz_roles WHERE role_code = ? AND delete_time IS NULL", roleCode).QueryRow(&menuIdsStr) if err2 == nil && menuIdsStr != "" && menuIdsStr != "[]" { @@ -359,7 +311,6 @@ func GetRoleByCode(roleCode string) (*Role, error) { CreateBy: result.CreateBy, UpdateBy: result.UpdateBy, } - role.AfterRead() return role, nil } @@ -369,14 +320,13 @@ func CreateRole(role *Role) error { o := orm.NewOrm() role.BeforeInsert() - // 使用Raw插入以正确处理JSON字段,并获取插入后的ID - // 如果没有设置 default 值,根据 tenant_id 自动设置:tenant_id=0 时 default=2,否则 default=3 + // 自动处理default defaultValue := role.Default if defaultValue == 0 { if role.TenantId == 0 { - defaultValue = 2 // 所有租户可用 + defaultValue = 2 } else { - defaultValue = 3 // 租户专属 + defaultValue = 3 } } res, err := o.Raw("INSERT INTO yz_roles (tenant_id, `default`, role_code, role_name, description, menu_ids, status, sort_order, create_time, update_time, create_by, update_by) VALUES (?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, NOW(), NOW(), ?, ?)", @@ -385,18 +335,14 @@ func CreateRole(role *Role) error { return err } - // 获取插入后的ID lastInsertId, err := res.LastInsertId() if err != nil { - // 如果无法获取 LastInsertId,尝试通过角色代码查询 createdRole, queryErr := GetRoleByCode(role.RoleCode) if queryErr == nil && createdRole != nil { role.RoleId = createdRole.RoleId } return nil } - - // 设置插入后的ID role.RoleId = int(lastInsertId) return nil } @@ -406,14 +352,13 @@ func UpdateRole(role *Role) error { o := orm.NewOrm() role.BeforeUpdate() - // 使用Raw更新以正确处理JSON字段 - // 如果没有设置 default 值,根据 tenant_id 自动设置:tenant_id=0 时 default=2,否则 default=3 + // 自动处理default defaultValue := role.Default if defaultValue == 0 { if role.TenantId == 0 { - defaultValue = 2 // 所有租户可用 + defaultValue = 2 } else { - defaultValue = 3 // 租户专属 + defaultValue = 3 } } _, err := o.Raw("UPDATE yz_roles SET tenant_id = ?, `default` = ?, role_code = ?, role_name = ?, description = ?, menu_ids = CAST(? AS JSON), status = ?, sort_order = ?, update_time = NOW(), update_by = ? WHERE role_id = ? AND delete_time IS NULL", diff --git a/权限管理模块使用说明.md b/权限管理模块使用说明.md deleted file mode 100644 index 88cd1f4..0000000 --- a/权限管理模块使用说明.md +++ /dev/null @@ -1,255 +0,0 @@ -# 权限管理模块使用说明 - -## 📋 功能概述 - -权限管理模块实现了基于角色的访问控制(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权限。 - -## 📞 技术支持 - -如有问题,请联系系统管理员或技术支持团队。 -