更新oa工作台
This commit is contained in:
parent
d2806311bb
commit
e2874cf9f0
@ -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. **响应式更新**:所有组件自动同步,无需手动刷新
|
||||
|
||||
@ -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()`: 清除角色缓存
|
||||
|
||||
2396
pc/src/views/apps/oa/workbench/index.vue
Normal file
2396
pc/src/views/apps/oa/workbench/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 文件夹)
|
||||
# 映射 /static 路径到前端 dist 目录
|
||||
StaticDir = /static:../front/dist
|
||||
|
||||
# 映射 /uploads 路径到上传文件目录(项目根目录下的 uploads 文件夹)
|
||||
StaticDir = /uploads:../uploads
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
255
权限管理模块使用说明.md
255
权限管理模块使用说明.md
@ -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权限。
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请联系系统管理员或技术支持团队。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user