一、前言
这是一篇搭建权限管理系统的系列文章。
随着网络的发展,信息安全对应任何企业来说都越发的重要,而本系列文章将和大家一起一步一步搭建一个全新的权限管理系统。
说明:由于搭建一个全新的项目过于繁琐,所有作者将挑选核心代码和核心思路进行分享。
二、技术选择
三、开始设计
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 Inse服务器托管网rt(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.QueryFirstint>(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 ", pageResultModel.tableField, pageResultModel.tableName, pageResultModel.selectWhere, orderBy, skip, pageResultModel.pageIndex * pageResultModel.pageSize); using var reader = connection.QueryMultiple(sb.ToString()); resultModel.total = reader.ReadFirstint>(); 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
上述代码解释:可以看见上述代码继承了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
以上便是后端实现动态菜单的核心代码,到这一节点,后端的工作基本完成。
在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
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
实现跨域请求,我们首先要先知道什么是跨域,紧接着便是跨域的方法,最后则是跨域时cookie的处理。 什么是跨域 1.什么是同源策略及其限制内容? 同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击…