yunzer_go/server/models/permission.go
2025-11-06 23:10:17 +08:00

588 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package models
import (
"database/sql"
"encoding/json"
"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"`
Default int8 `json:"default"` // 默认可见性0-全局1-平台用户2-租户用户
}
func init() {
orm.RegisterModel(new(RoleMenu))
}
// GetRoleMenus 获取指定角色的所有菜单权限从JSON字段读取
func GetRoleMenus(roleId int) ([]int, error) {
o := orm.NewOrm()
var menuIdsJson sql.NullString
var menuIdsStr string
// 方法1: 尝试使用 JSON_UNQUOTE 读取 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")
}
// 重置变量
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 // 返回空数组而不是错误,保持兼容性
}
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))
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
}
// min 辅助函数
func min(a, b int) int {
if a < b {
return a
}
return b
}
// GetRolePermissions 获取角色的详细权限信息包括菜单和API权限
// 主要基于 yz_roles.menu_ids 字段来获取权限
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
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{}
}
}
return &RolePermission{
RoleId: role.RoleId,
RoleName: role.RoleName,
MenuIds: menuIds, // 来自 yz_roles.menu_ids
Permissions: permissions, // 来自 yz_menus.permission基于 menu_ids
}, nil
}
// 获取所有菜单权限列表(用于分配权限时展示,未删除的)
func GetAllMenuPermissions() ([]*MenuPermission, error) {
o := orm.NewOrm()
// 查询菜单菜单表没有default字段直接使用0作为默认值
var resultsWithoutDefault []struct {
MenuId int
MenuName string
Path string
MenuType int
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)
if err != nil {
return nil, fmt.Errorf("获取菜单列表失败: %v", err)
}
// 转换为MenuPermission结构default字段设为0全局可见
menus := make([]*MenuPermission, 0, len(resultsWithoutDefault))
for _, r := range resultsWithoutDefault {
menu := &MenuPermission{
MenuId: r.MenuId,
MenuName: r.MenuName,
Path: r.Path,
MenuType: r.MenuType,
ParentId: r.ParentId,
Default: 0, // 默认值为0全局可见因为菜单表没有default字段
}
if r.Permission.Valid {
menu.Permission = r.Permission.String
} else {
menu.Permission = ""
}
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表示全局不进行过滤
if roleId > 0 {
role, err := GetRoleById(roleId)
if err == nil && role != nil {
roleDefault = role.Default
}
}
// 如果是平台用户返回所有菜单根据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中处理了
// 为了性能优化,一次性查询所有菜单的父子关系
type menuParent struct {
Id int
ParentId int
}
var allMenuParents []menuParent
_, err = o.Raw("SELECT id, parent_id FROM yz_menus WHERE delete_time IS NULL").QueryRows(&allMenuParents)
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) {
if pid == 0 || parentIds[pid] {
return
}
parentIds[pid] = true
if parentId, exists := menuParentMap[pid]; exists && parentId > 0 {
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
}
for pid := range parentIds {
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 {
placeholders[i] = "?"
args[i] = id
}
// 查询菜单(包括父菜单)
type menuResult struct {
MenuId int
MenuName string
Path string
MenuType int
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)
if err != nil {
return nil, fmt.Errorf("获取菜单列表失败: %v", err)
}
// 转换为MenuPermission结构
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字段
}
// 处理permission字段
if r.Permission.Valid {
menu.Permission = r.Permission.String
} else {
menu.Permission = ""
}
menus = append(menus, menu)
}
// 如果roleDefault>0根据角色的default值进一步过滤菜单
// 但由于菜单表没有default字段所有菜单都是default=0所以这里实际上不会过滤
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字段
func AssignRolePermissions(roleId int, menuIds []int, createBy string) error {
o := orm.NewOrm()
// 将菜单ID数组序列化为JSON
var jsonData []byte
var err error
if len(menuIds) == 0 {
jsonData = []byte("[]")
} else {
jsonData, err = json.Marshal(menuIds)
if err != nil {
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,
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 delete_time IS NULL 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
}