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


      一、前言

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

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

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

      二、技术选择

     

      三、开始设计

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

      这是我搭建的

     后端:   前端:

     

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

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

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

      2、搭建数据库

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

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

      这是我的菜单表结构

      

     

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

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

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

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

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

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

      然后实现这个仓储接口

    /// 
    /// 仓储基类
    /// 
    /// 实体类型
    /// 主键类型
    public abstract class Repository : IRepository where T : class, new()
    {
        /// 
        /// 删除
        /// 
        /// 删除sql
        /// 
        public int Delete(string key, string deleteSql)
        {
            using var connection = DataBaseConnectConfig.GetSqlConnection();
            return connection.Execute(deleteSql, new { Key = key });
        }
    
        /// 
        /// 根据主键获取模型
        /// 
        /// 主键
        /// 查询sql
        /// 
        public T GetByKey(string id, string selectSql)
        {
            using var connection = DataBaseConnectConfig.GetSqlConnection();
            return connection.QueryFirstOrDefault(selectSql, new { Key = id });
        }
    
        /// 
        /// 获取所有数据
        /// 
        /// 查询sql
        /// 
        public List GetAll(string selectAllSql)
        {
            using var connection = DataBaseConnectConfig.GetSqlConnection();
            return connection.Query(selectAllSql).ToList();
        }
    
        /// 
        /// 新增
        /// 
        /// 新增实体
        /// 新增sql
        /// 
        public int Insert(T entity, string innserSql)
        {
            using var connection = DataBaseConnectConfig.GetSqlConnection();
            return connection.Execute(innserSql, entity);
        }
    
        /// 
        /// 根据唯一主键验证数据是否存在
        /// 
        /// 主键
        /// 查询sql
        /// 返回true存在,false不存在
        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;
        }
    
        /// 
        /// 更新
        /// 
        /// 更新实体
        /// 更新sql
        /// 
        public int Update(T entity, string updateSql)
        {
            using var connection = DataBaseConnectConfig.GetSqlConnection();
            return connection.Execute(updateSql, entity);
        }
    
        /// 
        /// 分页方法
        /// 
        /// 泛型集合实体类
        /// 分页模型
        /// 
        public PageResultModel GetPageList(PageResultModel pageResultModel)
        {
            PageResultModel 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().ToList();
            return resultModel;
        }
    
    }
    View Code

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

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

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

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

      菜单仓储接口

     /// 
     /// 菜单仓储
     /// 
     public interface ISysMenuRepository : IRepository
     {}

      菜单仓储接口实现

    /// 
    /// 菜单仓储
    /// 
    public class SysMenuRepository : Repository, ISysMenuRepository
    {}

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

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

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

      领域服务接口

    复制代码
     /// 
     /// 菜单服务接口
     /// 
     public interface ISysMenuService
     {
          /// 
         /// 获取所有菜单--上下级关系
         /// 
         /// 
         List GetAllChildren();
    }
    复制代码

      领域接口实现

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

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

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

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

    复制代码
     public static class MenuCore
     {
         #region 用于菜单导航的树形结构
    
         /// 
         /// 递归获取菜单结构--呈现上下级关系
         /// 用于菜单的树形结构
         /// 
         /// 
         public static List GetMenuDao(List menuList)
         {
             List list = new();
             List 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;
         }
    
         /// 
         /// 实现递归
         /// 
         /// 
         /// 
         /// 
         private static List GetChildrenMenu(List moduleOutput, int id)
         {
             List 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

     

  • 相关阅读:
    由C# dynamic是否装箱引发的思考
    BoT-SORT与Strong-SORT论文对比及思考总结
    Android Studio(项目打包成APK)
    【HDLBits 刷题】Verilog Language -- Basics 部分
    【UI自动化测试】Jenkins配置
    Leetcode1545-找出第 N 个二进制字符串中的第 K 位
    高中数学:平面向量-常考题型汇总
    【rainbowzhou 面试9/101】技术提问--常见的大数据基准测试工具有哪些未命名文章
    R语言ggplot2可视化:使用ggplot2可视化散点图、使用labs参数自定义X轴的轴标签文本(customize X axis labels)
    ERINE系列论文解读
  • 原文地址:https://www.cnblogs.com/cyzf/p/18096545