一步一步搭建,功能最全的許可權管理系統之動態路由選單(一)

陈逸子风發表於2024-03-27

  一、前言

  這是一篇搭建許可權管理系統的系列文章。

  隨著網路的發展,資訊保安對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。

  說明:由於搭建一個全新的專案過於繁瑣,所有作者將挑選核心程式碼和核心思路進行分享。

  二、技術選擇

  三、開始設計

  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

相關文章