在后台管理系统开发中,菜单管理与角色权限控制是核心模块。一个设计合理的菜单权限系统不仅能保证系统安全性,还能提升用户体验。本文将结合实际代码,分享一套菜单与角色权限管理系统的实现方案,包括核心结构设计、树形菜单处理、权限控制逻辑等关键技术点。
一、系统核心功能概述
本系统主要实现两大核心功能:
- 菜单管理:支持菜单的增删改查,树形结构展示,父子级关联维护
- 角色管理:角色的创建、编辑、删除,以及角色与菜单的关联授权
系统采用 "角色 - 菜单" 的权限模型,即每个角色关联一组菜单 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(仅菜单列表需要)
}
优化效果:通过抽取共性字段到基础类,消除了原MenuModel、ListModel等重复类,减少代码冗余,后续维护只需修改基础类即可。
/// <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 };
}
五、总结与扩展
本文实现的菜单与角色权限管理系统具有以下特点:
- 结构清晰:通过基础类消除 DTO 冗余,树形结构统一使用
BaseMenuNode - 逻辑严谨:包含菜单父子级验证、循环引用检查、权限菜单补全等细节处理
- 可扩展性:支持菜单隐藏、排序、纯目录等特性,可根据需求扩展更多字段(如权限标识)
后续可扩展方向:
- 增加按钮级权限控制(在菜单基础上关联按钮权限)
- 实现菜单缓存机制(减少数据库查询)
- 支持角色继承(简化权限配置)
通过这套方案,可以快速搭建一个功能完善的菜单权限管理模块,满足大多数后台系统的需求。
2367

被折叠的 条评论
为什么被折叠?



