菜单与角色权限管理系统的设计与实现(递归/树)

在后台管理系统开发中,菜单管理与角色权限控制是核心模块。一个设计合理的菜单权限系统不仅能保证系统安全性,还能提升用户体验。本文将结合实际代码,分享一套菜单与角色权限管理系统的实现方案,包括核心结构设计、树形菜单处理、权限控制逻辑等关键技术点。

一、系统核心功能概述

本系统主要实现两大核心功能:

  • 菜单管理:支持菜单的增删改查,树形结构展示,父子级关联维护
  • 角色管理:角色的创建、编辑、删除,以及角色与菜单的关联授权

系统采用 "角色 - 菜单" 的权限模型,即每个角色关联一组菜单 ID,用户通过所属角色获得对应的菜单访问权限。

二、核心数据结构设计

1. 实体类设计(数据库映射)

菜单实体(SysMenuEntity)
/// <summary>
/// 菜单实体
/// </summary>
public class SysMenuEntity
{
    public string Id { get; set; } = ""; // 菜单唯一标识
    public string Name { get; set; } = ""; // 菜单编码(用于标识)
    public string Text { get; set; } = ""; // 菜单显示文本
    public bool IsHide { get; set; } // 是否隐藏菜单
    public bool IsPure { get; set; } // 是否为纯目录(无实际链接)
    public string Link { get; set; } = ""; // 菜单链接地址
    public string FileUrl { get; set; } = ""; // 图标/资源路径
    public int Sort { get; set; } // 排序号(控制显示顺序)
    public string ParentId { get; set; } = ""; // 父菜单ID(顶级菜单为"")
    public bool Enable { get; set; } = true; // 是否启用
}
角色实体(SysRoleEntity)
/// <summary>
/// 角色实体
/// </summary>
public class SysRoleEntity
{
    public string Id { get; set; } = ""; // 角色ID
    public string RoleName { get; set; } = ""; // 角色名称
    public string MenuIdsList { get; set; } = ""; // 关联菜单ID集合(JSON格式存储)
    public bool Enable { get; set; } = true; // 是否启用
}

2. DTO 设计(数据传输对象)

基础树形节点类(统一共性字段)
/// <summary>
/// 菜单树形节点基础类(所有菜单相关DTO的父类)
/// </summary>
public class BaseMenuNode
{
    public string Id { get; set; } = "";
    public string Name { get; set; } = "";
    public string Text { get; set; } = "";
    public bool Hide { get; set; } 
    public bool Pure { get; set; } 
    public string Link { get; set; } = "";
    public string File { get; set; } = ""; 
    public int Sort { get; set; }
    public List<BaseMenuNode> Children { get; set; } = new(); // 子节点集合
}
业务 DTO(仅保留独有字段)
/// <summary>
/// 角色列表输出DTO
/// </summary>
public class RoleIndexOutput : BaseMenuNode
{
    public string Key { get; set; } = ""; // 角色ID别名(兼容前端)
    public string ListJson { get; set; } = ""; // 菜单ID集合的JSON字符串
}

/// <summary>
/// 菜单列表输出DTO
/// </summary>
public class MenuIndexOutput : BaseMenuNode
{
    public string? ParentId { get; set; } = ""; // 父菜单ID(仅菜单列表需要)
}

优化效果:通过抽取共性字段到基础类,消除了原MenuModelListModel等重复类,减少代码冗余,后续维护只需修改基础类即可。

/// <summary>
/// 角色实体
/// </summary>
public class SysRoleEntity
{
    public string Id { get; set; } = ""; // 角色ID
    public string RoleName { get; set; } = ""; // 角色名称
    public string MenuIdsList { get; set; } = ""; // 关联菜单ID集合(JSON格式存储)
    public bool Enable { get; set; } = true; // 是否启用
}

三、核心功能实现

1. 树形菜单构建(递归算法)

树形结构是菜单展示的核心,系统通过递归算法实现菜单的层级关系构建:

/// <summary>
/// 构建菜单树形结构
/// </summary>
/// <param name="allMenus">所有菜单列表</param>
/// <param name="parentId">父节点ID(顶级节点为"")</param>
private List<BaseMenuNode> BuildMenuTree(List<SysMenuEntity> allMenus, string parentId)
{
    // 1. 筛选当前父节点的直接子菜单
    var childMenus = allMenus
        .Where(menu => menu.ParentId == parentId)
        .OrderBy(menu => menu.Sort) // 按排序号升序
        .ToList();

    // 2. 递归构建子菜单树
    return childMenus.Select(menu => new BaseMenuNode
    {
        Id = menu.Id,
        Name = menu.Name,
        Text = menu.Text,
        Hide = menu.IsHide,
        Pure = menu.IsPure,
        Link = menu.Link,
        File = menu.FileUrl,
        Sort = menu.Sort,
        // 递归构建子节点
        Children = BuildMenuTree(allMenus, menu.Id)
    }).ToList();
}

核心思路

  • 先筛选出指定父 ID 的直接子菜单
  • 对每个子菜单递归调用自身,构建其子菜单树
  • 通过Sort字段控制同层级菜单的显示顺序

2. 角色与菜单关联逻辑

角色关联菜单时,需要存储菜单 ID 集合(JSON 格式),并在查询时解析为树形结构:

(1)角色关联菜单的保存
/// <summary>
/// 保存角色关联的菜单
/// </summary>
public BaseResultOutput AddRole(AddRoleInput input)
{
    // 验证菜单ID有效性(是否存在且启用)
    var (isValid, errorMsg) = ValidateMenuIds(input.MenuIds);
    if (!isValid)
    {
        return new BaseResultOutput { Code = 500, Message = errorMsg };
    }

    // 保存角色信息,菜单ID集合转为JSON存储
    _dbService.Add(new SysRoleEntity
    {
        Id = input.Key,
        RoleName = input.Name,
        MenuIdsList = JsonHelper.ToJson(input.MenuIds) // 序列化菜单ID列表
    });
    return new BaseResultOutput { Code = 200, Message = "添加成功" };
}
(2)角色关联菜单的解析(树形化)
/// <summary>
/// 将角色关联的菜单ID解析为树形结构
/// </summary>
private List<BaseMenuNode> ResolveRoleMenus(List<string> menuIds)
{
    if (!menuIds.Any()) return new List<BaseMenuNode>();

    // 1. 获取所有启用的菜单
    var allValidMenus = _dbService.Where<SysMenuEntity>(p => p.Enable).ToList();

    // 2. 筛选出角色关联的菜单,并补全父级(确保树形完整)
    var authorizedMenus = new HashSet<SysMenuEntity>(
        allValidMenus.Where(m => menuIds.Contains(m.Id))
    );
    // 补全父级菜单(子菜单必须有父级才能在树形中显示)
    foreach (var menu in authorizedMenus.ToList())
    {
        AddAllParents(menu.Id, allValidMenus, authorizedMenus);
    }

    // 3. 构建树形结构(从顶级菜单开始)
    var topMenus = authorizedMenus
        .Where(m => string.IsNullOrEmpty(m.ParentId))
        .OrderBy(m => m.Sort)
        .ToList();

    return topMenus.Select(menu => ConvertToMenuNode(menu, allValidMenus, authorizedMenus)).ToList();
}

/// <summary>
/// 递归补全父级菜单
/// </summary>
private void AddAllParents(string menuId, List<SysMenuEntity> allMenus, HashSet<SysMenuEntity> authorizedMenus)
{
    var currentMenu = allMenus.FirstOrDefault(m => m.Id == menuId);
    if (currentMenu == null || string.IsNullOrEmpty(currentMenu.ParentId)) return;

    var parentMenu = allMenus.FirstOrDefault(m => m.Id == currentMenu.ParentId);
    if (parentMenu != null && !authorizedMenus.Contains(parentMenu))
    {
        authorizedMenus.Add(parentMenu);
        AddAllParents(parentMenu.Id, allMenus, authorizedMenus); // 递归补全祖父级
    }
}

关键逻辑

  • 角色关联的菜单可能是子菜单,必须补全其所有父级才能正确显示树形结构
  • 通过HashSet存储授权菜单,避免重复添加
  • 递归补全父级菜单,确保从顶级到子级的完整层级

3. 菜单管理的核心操作

(1)菜单新增(含父级验证)
public BaseResultOutput AddMenu(AddMenuInput input)
{
    // 验证菜单名称唯一性
    if (_dbService.Exists<SysMenuEntity>(p => p.Enable && p.Name == input.Name))
    {
        return new BaseResultOutput { Code = 500, Message = $"菜单名称「{input.Name}」已存在" };
    }

    // 验证父菜单有效性(如果指定了父菜单)
    if (!string.IsNullOrEmpty(input.ParentId) && 
        !_dbService.Exists<SysMenuEntity>(p => p.Enable && p.Id == input.ParentId))
    {
        return new BaseResultOutput { Code = 500, Message = $"父菜单ID「{input.ParentId}」不存在" };
    }

    // 保存菜单
    _dbService.Add(new SysMenuEntity
    {
        Name = input.Name,
        Text = input.Text,
        ParentId = input.ParentId,
        Sort = input.Sort,
        // 其他字段赋值...
    });
    return new BaseResultOutput { Code = 200, Message = "添加成功" };
}
(2)菜单更新(含循环引用检查)
public BaseResultOutput UpdateMenu(UpdateMenuInput input)
{
    var menu = _dbService.FirstOrDefault<SysMenuEntity>(p => p.Enable && p.Id == input.Id);
    if (menu == null)
    {
        return new BaseResultOutput { Code = 500, Message = "菜单不存在" };
    }

    // 检查是否将自己设为父菜单
    if (input.ParentId == input.Id)
    {
        return new BaseResultOutput { Code = 500, Message = "不能将自己设为父菜单" };
    }

    // 检查是否产生循环引用(如A→B→C→A)
    if (!string.IsNullOrEmpty(input.ParentId) && IsCircularReference(input.ParentId, input.Id))
    {
        return new BaseResultOutput { Code = 500, Message = "修改会导致循环引用,请重新选择父菜单" };
    }

    // 更新菜单信息
    menu.Name = input.Name;
    menu.Text = input.Text;
    menu.ParentId = input.ParentId;
    // 其他字段更新...
    _dbService.Update(menu);
    return new BaseResultOutput { Code = 200, Message = "更新成功" };
}

/// <summary>
/// 检查父级链是否存在循环引用
/// </summary>
private bool IsCircularReference(string parentId, string currentId, HashSet<string>? visited = null)
{
    visited ??= new HashSet<string>();
    if (string.IsNullOrEmpty(parentId) || visited.Contains(parentId)) return false;

    // 父级ID等于当前ID,形成循环
    if (parentId == currentId) return true;

    visited.Add(parentId);
    var parentMenu = _dbService.FirstOrDefault<SysMenuEntity>(p => p.Enable && p.Id == parentId);
    if (parentMenu == null) return false;

    // 递归检查父级的父级
    return IsCircularReference(parentMenu.ParentId, currentId, visited);
}
(3)菜单删除(级联处理子菜单)
public BaseResultOutput DeleteMenu(string id)
{
    var menu = _dbService.FirstOrDefault<SysMenuEntity>(p => p.Id == id && p.Enable);
    if (menu == null)
    {
        return new BaseResultOutput { Code = 500, Message = "菜单不存在" };
    }

    // 级联禁用所有子菜单(逻辑删除)
    var childMenus = _dbService.Where<SysMenuEntity>(p => p.Enable && p.ParentId == id).ToList();
    foreach (var child in childMenus)
    {
        child.Enable = false;
        _dbService.Update(child);
    }

    // 禁用当前菜单
    menu.Enable = false;
    _dbService.Update(menu);
    return new BaseResultOutput { Code = 200, Message = "删除成功" };
}

四、权限控制实现

系统通过角色关联的菜单 ID 集合,控制用户可访问的菜单:

/// <summary>
/// 获取当前用户的权限菜单
/// </summary>
[Authorize]
public BaseResultOutput GetUserMenus()
{
    // 1. 获取当前用户所属角色
    var roleId = GetCurrentUserRoleId(); // 从登录信息中获取角色ID
    var role = _dbService.FirstOrDefault<SysRoleEntity>(p => p.Id == roleId);
    if (role == null)
    {
        return new BaseResultOutput { Code = 500, Message = "未分配角色" };
    }

    // 2. 解析角色关联的菜单ID
    var menuIds = JsonHelper.ToList<string>(role.MenuIdsList) ?? new List<string>();
    if (!menuIds.Any())
    {
        return new BaseResultOutput { Code = 200, Data = new List<BaseMenuNode>() };
    }

    // 3. 构建权限菜单树形结构
    var allMenus = _dbService.Where<SysMenuEntity>(p => p.Enable).ToList();
    var authorizedMenus = GetAuthorizedMenus(allMenus, menuIds);
    var menuTree = BuildMenuTree(authorizedMenus, "");

    return new BaseResultOutput { Code = 200, Data = menuTree };
}

五、总结与扩展

本文实现的菜单与角色权限管理系统具有以下特点:

  1. 结构清晰:通过基础类消除 DTO 冗余,树形结构统一使用BaseMenuNode
  2. 逻辑严谨:包含菜单父子级验证、循环引用检查、权限菜单补全等细节处理
  3. 可扩展性:支持菜单隐藏、排序、纯目录等特性,可根据需求扩展更多字段(如权限标识)

后续可扩展方向:

  • 增加按钮级权限控制(在菜单基础上关联按钮权限)
  • 实现菜单缓存机制(减少数据库查询)
  • 支持角色继承(简化权限配置)

通过这套方案,可以快速搭建一个功能完善的菜单权限管理模块,满足大多数后台系统的需求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值