一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

  • 一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)已关闭评论
  • 21 次浏览
  • A+
所属分类:.NET技术
摘要

  一、前言   这是一篇搭建权限管理系统的系列文章。   随着网络的发展,信息安全对应任何企业来说都越发的重要,而本系列文章将和大家一起一步一步搭建一个全新的权限管理系统。

  一、前言

  这是一篇搭建权限管理系统的系列文章。

  随着网络的发展,信息安全对应任何企业来说都越发的重要,而本系列文章将和大家一起一步一步搭建一个全新的权限管理系统。

  说明:由于搭建一个全新的项目过于繁琐,所有作者将挑选核心代码和核心思路进行分享。

  二、技术选择

一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

 

  三、开始设计

  1、自主搭建vue前端和.net core webapi后端,网上有很多搭建教程。

  这是我搭建的

 后端: 一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)  前端:一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

 

  搭建好之后,vue需要把基础配置做好,比如路由、响应请求等,网上都有教程。

   vue配置较为简单,webapi的框架我使用DDD领域启动设计方式,各个层的介绍如下下。

  • ProjectManageWebApi webapi接口层,属于启动项
  • Model 业务模型,代表着系统中的具体业务对象。
  • Infrastructure 仓储层,是数据存储层,提供持久化对象的方法。
  • Domain  领域层,是整个系统运行时核心业务对象的载体,是业务逻辑处理的领域。
  • Subdomain 子域,子域是领域层更加细微的划分,处理整个系统最核心业务逻辑。
  • Utility  工具层,存放系统的辅助工具类。

  2、搭建数据库

  菜单对于一个系统来说,是必不可少的,我们搭建权限管理系统就从这里开始

  任务:建立菜单表,并通过程序把菜单动态加载到页面,实现树形菜单。

  这是我的菜单表结构

  一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

 

  我采用的是一张表存储系统菜单,用id和pid存储上下级关系。当然这不是唯一的,根据情况可以把它拆分层多张表。

  3、创建基础仓储和菜单仓储

  在webapi中Infrastructure 仓储层创建基础仓储,以便提供持久化支持。

  我orm框架使用的是dapper来提共数据库和编程语言间的映射关系。

  首先需要建立一个增删改查的仓储接口,大致如下:

一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

/// <summary> /// 仓储接口定义 /// </summary> public interface IRepository {  }  /// <summary> /// 定义泛型仓储接口 /// </summary> /// <typeparam name="T">实体类型</typeparam> /// <typeparam name="object">主键类型</typeparam> public interface IRepository<T> : IRepository where T : class, new() {     /// <summary>     /// 新增     /// </summary>     /// <param name="entity">实体</param>     /// <param name="innserSql">新增sql</param>     /// <returns></returns>     int Insert(T entity, string innserSql);      /// <summary>     /// 修改     /// </summary>     /// <param name="entity">实体</param>     /// <param name="updateSql">更新sql</param>     /// <returns></returns>     int Update(T entity, string updateSql);      /// <summary>     /// 删除     /// </summary>     /// <param name="deleteSql">删除sql</param>     /// <returns></returns>     int Delete(string key,string deleteSql);      /// <summary>     /// 根据主键获取模型     /// </summary>     /// <param name="key">主键</param>     /// <param name="selectSql">查询sql</param>     /// <returns></returns>     T GetByKey(string key, string selectSql);      /// <summary>     /// 获取所有数据     /// </summary>     /// <param name="selectAllSql">查询sql</param>     /// <returns></returns>     List<T> GetAll(string selectAllSql);      /// <summary>     /// 根据唯一主键验证数据是否存在     /// </summary>     /// <param name="id">主键</param>     /// <param name="selectSql">查询sql</param>     /// <returns>返回true存在,false不存在</returns>     bool IsExist(string id, string selectSql);      /// <summary>     /// dapper通用分页方法     /// </summary>     /// <typeparam name="T">泛型集合实体类</typeparam>     /// <param name="pageResultModel">分页模型</param>     /// <returns></returns>     PageResultModel<T> GetPageList<T>(PageResultModel pageResultModel);  }

View Code

  然后实现这个仓储接口

一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

/// <summary> /// 仓储基类 /// </summary> /// <typeparam name="T">实体类型</typeparam> /// <typeparam name="TPrimaryKey">主键类型</typeparam> public abstract class Repository<T> : IRepository<T> where T : class, new() {     /// <summary>     /// 删除     /// </summary>     /// <param name="deleteSql">删除sql</param>     /// <returns></returns>     public int Delete(string key, string deleteSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         return connection.Execute(deleteSql, new { Key = key });     }      /// <summary>     /// 根据主键获取模型     /// </summary>     /// <param name="id">主键</param>     /// <param name="selectSql">查询sql</param>     /// <returns></returns>     public T GetByKey(string id, string selectSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         return connection.QueryFirstOrDefault<T>(selectSql, new { Key = id });     }      /// <summary>     /// 获取所有数据     /// </summary>     /// <param name="selectAllSql">查询sql</param>     /// <returns></returns>     public List<T> GetAll(string selectAllSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         return connection.Query<T>(selectAllSql).ToList();     }      /// <summary>     /// 新增     /// </summary>     /// <param name="entity">新增实体</param>     /// <param name="innserSql">新增sql</param>     /// <returns></returns>     public int Insert(T entity, string innserSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         return connection.Execute(innserSql, entity);     }      /// <summary>     /// 根据唯一主键验证数据是否存在     /// </summary>     /// <param name="id">主键</param>     /// <param name="selectSql">查询sql</param>     /// <returns>返回true存在,false不存在</returns>     public bool IsExist(string id, string selectSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         var count = connection.QueryFirst<int>(selectSql, new { Key = id });         if (count > 0)             return true;         else             return false;     }      /// <summary>     /// 更新     /// </summary>     /// <param name="entity">更新实体</param>     /// <param name="updateSql">更新sql</param>     /// <returns></returns>     public int Update(T entity, string updateSql)     {         using var connection = DataBaseConnectConfig.GetSqlConnection();         return connection.Execute(updateSql, entity);     }      /// <summary>     /// 分页方法     /// </summary>     /// <typeparam name="T">泛型集合实体类</typeparam>     /// <param name="pageResultModel">分页模型</param>     /// <returns></returns>     public PageResultModel<T> GetPageList<T>(PageResultModel pageResultModel)     {         PageResultModel<T> resultModel = new();         using var connection = DataBaseConnectConfig.GetSqlConnection();         int skip = 1;         var orderBy = string.Empty;         if (pageResultModel.pageIndex > 0)         {             skip = (pageResultModel.pageIndex - 1) * pageResultModel.pageSize + 1;         }         if (!string.IsNullOrEmpty(pageResultModel.orderByField))         {             orderBy = string.Format(" ORDER BY {0} {1} ", pageResultModel.orderByField, pageResultModel.sortType);         }         StringBuilder sb = new StringBuilder();         sb.AppendFormat("SELECT COUNT(1) FROM {0} where 1=1 {1};", pageResultModel.tableName, pageResultModel.selectWhere);         sb.AppendFormat(@"SELECT  *                             FROM(SELECT ROW_NUMBER() OVER( {3}) AS RowNum,{0}                                       FROM  {1}                                       where 1=1 {2}                                     ) AS result                             WHERE  RowNum >= {4}   AND RowNum <= {5}                              ", pageResultModel.tableField, pageResultModel.tableName, pageResultModel.selectWhere, orderBy, skip, pageResultModel.pageIndex * pageResultModel.pageSize);         using var reader = connection.QueryMultiple(sb.ToString());         resultModel.total = reader.ReadFirst<int>();         resultModel.data = reader.Read<T>().ToList();         return resultModel;     }  }

View Code

  以上两段代码就实现了对数据库的增删改查。当然在上述仓储接口中有一个分页查询接口,它对于的模型如下

/// <summary> /// 分页模型 /// </summary> public class PageResultModel {     /// <summary>     /// 当前页     /// </summary>     public int pageIndex { get; set; }      /// <summary>     /// 每页显示条数     /// </summary>     public int pageSize { get; set; }      /// <summary>     /// 查询表字段     /// </summary>     public string tableField { get; set; }      /// <summary>     /// 查询表     /// </summary>     public string tableName { get; set; }      /// <summary>     /// 查询条件     /// </summary>     public string selectWhere { get; set; }      /// <summary>     /// 查询条件json     /// </summary>     public string filterJson { get; set; }      /// <summary>     /// 当前菜单id     /// </summary>     public string menuId { get; set; }      /// <summary>     /// 排序字段(不能为空)     /// </summary>     public string orderByField { get; set; }      /// <summary>     /// 排序类型     /// </summary>     public string sortType { get; set; }      /// <summary>     /// 总数     /// </summary>     public int total { get; set; } }  /// <summary> /// 查询数据 /// </summary> /// <typeparam name="T"></typeparam> public class PageResultModel<T> : PageResultModel  {     /// <summary>     /// 数据     /// </summary>     public List<T> data { get; set; } }

上述代码解释:上述仓储接口中定义了所有基础接口,因为它们都是数据库操作最基本的存在,为了统一管理,降低耦合把它们定义到仓储中,以备后用。

  建立好基础仓储后,我们需要建立菜单表的仓储

  菜单仓储接口

 /// <summary>  /// 菜单仓储  /// </summary>  public interface ISysMenuRepository : IRepository<Menu>  {}

  菜单仓储接口实现

/// <summary> /// 菜单仓储 /// </summary> public class SysMenuRepository : Repository<Menu>, ISysMenuRepository {}

  上述代码解释:可以看见上述代码继承了IRepository和Repository,这说明菜单拥有了增删改查等功能。

   4、创建领域服务,递归组织树形菜单结构

  在Domain领域层创建领域服务接口和实现接口

  领域服务接口

 /// <summary>  /// 菜单服务接口  /// </summary>  public interface ISysMenuService  {       /// <summary>      /// 获取所有菜单--上下级关系      /// </summary>      /// <returns></returns>      List<MenuDao> GetAllChildren(); }

  领域接口实现

/// <summary> /// 菜单服务实现 /// </summary> public class SysMenuService : ISysMenuService {      //仓储接口     private readonly ISysMenuRepository _menuRepository;      /// <summary>     /// 构造函数 实现依赖注入     /// </summary>     /// <param name="userRepository">仓储对象</param>     public SysMenuService(ISysMenuRepository menuRepository)     {         _menuRepository = menuRepository;     }      /// <summary>     /// 获取菜单--上下级关系     /// </summary>     /// <returns></returns>     public List<MenuDao> GetAllChildren()     {         var list = _menuRepository.GetMenusList();         var menuDaoList = MenuCore.GetMenuDao(list);         return menuDaoList;     } }

  5、在Subdomain子域中创建菜单核心代码

  为什么在子域中创建菜单核心,应该菜单是整个系统的核心之一,考虑到之后系统会频繁使用,所以创建在子域中,以便提供给其他业务领域使用

  下面是递归菜单实现树形结构的核心

 public static class MenuCore  {      #region 用于菜单导航的树形结构       /// <summary>      /// 递归获取菜单结构--呈现上下级关系      /// 用于菜单的树形结构      /// </summary>      /// <returns></returns>      public static List<MenuDao> GetMenuDao(List<Menu> menuList)      {          List<MenuDao> list = new();          List<MenuDao> menuListDto = new();          foreach (var item in menuList)          {              MenuDao model = new()              {                  Title = item.MenuTitle,                  Icon = item.MenuIcon,                  Id = item.MenuUrl + "?MneuId=" + item.Id,                  MenuKey = item.Id,                  PMenuKey = item.Pid,                  Component = item.Component,                  Path = item.Path,                  RequireAuth = item.RequireAuth,                  Name = item.Name,                  Redirect = item.Redirect,                  IsOpen = item.IsOpen              };              list.Add(model);          }          foreach (var data in list.Where(f => f.PMenuKey == 0 && f.IsOpen))          {              var childrenList = GetChildrenMenu(list, data.MenuKey);              data.children = childrenList.Count == 0 ? null : childrenList;              menuListDto.Add(data);          }          return menuListDto;      }       /// <summary>      /// 实现递归      /// </summary>      /// <param name="moduleOutput"></param>      /// <param name="id"></param>      /// <returns></returns>      private static List<MenuDao> GetChildrenMenu(List<MenuDao> moduleOutput, int id)      {          List<MenuDao> sysShowTempMenus = new();          //得到子菜单          var info = moduleOutput.Where(w => w.PMenuKey == id && w.IsOpen).ToList();          //循环          foreach (var sysMenuInfo in info)          {              var childrenList = GetChildrenMenu(moduleOutput, sysMenuInfo.MenuKey);              //把子菜单放到Children集合里              sysMenuInfo.children = childrenList.Count == 0 ? null : childrenList;              //添加父级菜单              sysShowTempMenus.Add(sysMenuInfo);          }          return sysShowTempMenus;      } }

  以上便是后端实现动态菜单的核心代码,到这一节点,后端的工作基本完成。

  在Controller创建好接口后,运行后端代码,出现如图所示,便说明成功。

  一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

   6、vue 动态路由搭建

  配置vue动态路由前,需要看你选择的前端框架是什么,不同的框架,解析的字段不一样,我选择的是layui vue,动态配置如下:

一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

export const generator = (   routeMap: any[],   parentId: string | number,   routeItem?: any | [], ) => {   return routeMap     //.filter(item => item.menuKey === parentId)     .map(item => {        const { title, requireAuth, menuKey } = item || {};       const currentRouter: RouteRecordRaw = {         // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace         path: item.path,         // 路由名称,建议唯一         //name: `${item.id}`,         // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)         meta: {           title,           requireAuth,           menuKey         },         name: item.name,         children: [],         // 该路由对应页面的 组件 (动态加载 @/views/ 下面的路径文件)         component: item.component && defineRouteComponentKeys.includes(item.component)           ? defineRouteComponents[item.component]           : () => url.value,        };        // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠       if (!currentRouter.path.startsWith('http')) {         currentRouter.path = currentRouter.path.replace('//', '/');       }        // 重定向       item.redirect && (currentRouter.redirect = item.redirect);       if (item.children != null) {         // 子菜单,递归处理         currentRouter.children = generator(item.children, item.menuKey, currentRouter);       }       if (currentRouter.children === undefined || currentRouter.children.length <= 0) {         currentRouter.children;       }       return currentRouter;     })     .filter(item => item); };

View Code

  通过以上代码,获取动态路由,然后把它加入到你的路由菜单中,这样便实现了页面菜单动态加载。

  四、项目效果

一步一步搭建,功能最全的权限管理系统之动态路由菜单(一)

  五、说明

  以上便是实现vue+webapi实现动态路由的全部核心代码

  注:关注我,我们一起搭建完整的权限管理系统。

   1、预览地址:http://139.155.137.144:8012

   2、qq群:801913255