十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MVC5+EF6快速开发框架:权限、工作流与代码生成实践

MVC5+EF6快速开发框架:权限、工作流与代码生成实践 简介面向.NET后端开发者的Ymnets快速开发框架基于最新版ASP.NET MVC5与Entity Framework 6构建内置工作流引擎可快速搭建企业级后台管理系统。压缩包共3558个文件约113.59MB涵盖962个C#源码、413个Razor视图、284个JavaScript脚本及大量DLL程序集、样式与图片资源便于直接编译与二次开发。框架采用MVC分层与接口解耦设计集成IOC容器实现依赖注入配合EF6完成数据持久化前端选用EasyUI提供现成界面组件并附带部署文档、数据字典及任务状态流转示例能帮助开发者理解实际项目中的分层架构与工作流设计。目前已有1920人学习下载是理解ASP.NET MVC企业级架构和快速生成后台系统的实用参考。1. 从标题看这套架构在解决什么问题接手过内部后台系统的开发都应该有这样的体验需求方要的是“界面像模像样、权限别乱套、流程能走通”而代码仓库往往是 WebForms 时代留下来的 900 行 aspx.cs。MVC5 EF6 快速生成框架的组合本质上是把“后台管理系统”重新拉回可控节奏用 Area 隔离后台模块用 EF6 的 Code First 完成表和 C# 实体的双向同步再把重复度最高的列表、新增、编辑、删除做成模板化产物。标题里的 Ymnets 就是这类自有快速开发框架的代号它产出的不是玩具代码而是可以直接挂到 IIS 上的整套后台包括菜单权限和一条工作流审批链路。这篇文章按一个 .NET 老手的实现顺序把这套骨架拆开讲清楚。2. MVC5 与 EF6 的技术骨架Area 划分、DbContext 封装与迁移2.1 为什么不推荐把 MVC5 项目直接翻成 .NET Core先说结论如果现有系统已经用 MVC5 稳定跑了三五年业务数据和复杂的权限逻辑都沉淀在里面盲目迁移到 .NET Core 的成本远高于收益。.NET Core 的启动管道Startup.cs和内置 DI 与 MVC5 的 Global.asax、Ninject 时代完全是两套心智加上 WebForms 页面和第三方控件无法迁移强行翻新等于重写。这个背景下新模块用 MVC5 继续扩展老模块逐步收敛是更常见的企业方案。MVC5 的核心是路由到 Controller 再到 ActionView 用 Razor 渲染管线按生命周期事件执行。理解这一点后续做权限拦截和日志拦截会非常顺手只要在 Action 执行前后挂上自定义逻辑就能覆盖整个后台入口不需要每个页面写一遍。2.2 用 Area 把后台管理隔离成独立模块后台管理系统通常和前台门户共用同一个项目。如果不做隔离Controller 命名会乱路由也得手工维护。MVC5 的 Area 机制正好解决这个问题在 Areas 目录下建 Admin 区域区域内自带 AdminAreaRegistration.cs路由前缀固定为 Admin/{controller}/{action}/{id}。一个典型的 AdminAreaRegistration 长这样public class AdminAreaRegistration : AreaRegistration { public override string AreaName { get { return Admin; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( Admin_default, Admin/{controller}/{action}/{id}, new { controller Home, action Index, id UrlParameter.Optional }, new[] { Ymnets.Web.Areas.Admin.Controllers } ); } }注意最后那个字符串数组约束路由只扫描 Admin 区域下的 Controller避免与主项目的同名控制器冲突。后台的任何请求都带 /Admin/ 前缀IIS 的 URL 改写和反向代理也更容易匹配。2.3 Code First 建库DbContext 与全局软删除EF6 时代的多数后台框架都有两个约定主键叫 Id删除用软标记。这个设计下把“已删除数据从查询中排除”放到全局过滤而不是每个 Linq 查询写 Where(x !x.IsDeleted)能减少大量重复代码。定义一个基础 DbContextpublic class AdminDbContext : DbContext { public AdminDbContext() : base(nameAdminConn) { // 关闭懒加载配合仓储模式使用避免查询时意外访问导航属性触发二次 SQL Configuration.LazyLoadingEnabled false; } public DbSetSysUser Users { get; set; } public DbSetSysRole Roles { get; set; } public DbSetSysMenu Menus { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // 约定所有实体如果带 IsDeleted 字段一律走软删除过滤 modelBuilder.Filter(IsDeleted, (ISoftDelete d) d.IsDeleted, false); } }base(nameAdminConn) 表示从 Web.config 的连接字符串读取名字必须和配置一致。关闭 LazyLoading 是给性能保底后台列表页经常需要连表展示懒加载会让循环渲染时逐行发查询。代码里引用的 Filter 是第三方库 EntityFramework.Filters 提供的能力没有这个包时也可以在实体上写查询过滤器效果一样。2.4 迁移脚本的日常节奏Code First 建库之后表结构的变更依赖 Migration。这里最容易踩的坑是“多人开发时连接字符串指向各自本机数据库迁移历史文件冲突”。团队里通常约定所有模型变更在分支上先 Add-Migration合并主干后再统一 Update-Database。命令如下Enable-Migrations -ContextTypeName AdminDbContext -MigrationsDirectory Migrations/Admin Add-Migration -Name AddWorkflowTable -ConfigurationTypeName Configuration Update-Database -ConfigurationTypeName Configuration -Verbose第一条命令只执行一次用于生成 Migrations 目录。第二条在每次模型改完后执行生成时间戳命名的迁移类建议命名里带上本次改动的内容方便日后回溯。第三条才是真正改库-Verbose 会打印实际执行的 SQL出现权限或约束错误时能直接看到语句。写迁移类时还要注意某些列类型变更会触发表重建生产环境数据量大时最好手动写 Sql() 辅助脚本而不是依赖自动迁移。3. 权限模型菜单、按钮、操作权限的分层设计后台系统的权限如果只在登录时判断一次角色迟早会变成“点击就报错”。常见做法是把权限拆成三层菜单权限决定左边导航显示什么按钮权限决定页面里哪些操作可见接口权限决定请求能否执行。三者共用一套权限码只是作用位置不同。3.1 五张核心表的字段设计以 SQL Server 为例给出精简表结构。用 EF6 Code First 的实体类表示逻辑更直观public class SysUser { public int Id { get; set; } public string UserName { get; set; } public string PasswordHash { get; set; } public int RoleId { get; set; } public bool IsDeleted { get; set; } } public class SysRole { public int Id { get; set; } public string RoleName { get; set; } public string Description { get; set; } } public class SysMenu { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public string Icon { get; set; } public int ParentId { get; set; } public int Sort { get; set; } public string PermissionCode { get; set; } }角色和菜单的关联用一张中间表 SysRoleMenuPermissionCode 是整个系统的授权最小单位。比如订单列表的查询按钮PermissionCode 写 Order_View导出按钮写 Order_Export不同角色关联不同权限码就完成了按钮级控制。3.2 登录后的 Session 与权限缓存MVC5 时代没有内置的认证中间件多数框架用 Session 保存当前用户和权限码集合。登录成功后把角色对应的 PermissionCode 全部查出来放进 Session避免每次请求都连表查权限数据库[HttpPost] public ActionResult Login(string userName, string password) { var user _db.Users.Include(u u.Role).FirstOrDefault(u u.UserName userName); if (user null || !PasswordHelper.Verify(password, user.PasswordHash)) { return Json(new { code 0, msg 用户名或密码错误 }); } var codes _db.RoleMenus .Where(rm rm.RoleId user.RoleId) .Select(rm rm.Menu.PermissionCode) .ToList(); Session[User] user; Session[PermCodes] new HashSetstring(codes); return Json(new { code 1, msg ok }); }这里用 RoleMenus 中间表一次性取权限码集合存入 HashSet 。因为 Session 是进程内的对象集合类型的 Contains 速度是 O(1)比每次都 List.Any() 要快。3.3 AuthorizeAttribute 的二次开发MVC5 的 AuthorizeAttribute 只做“是否登录”的判断要实现权限码校验需要重写 AuthorizeCorepublic class PermissionAttribute : AuthorizeAttribute { public string Code { get; set; } protected override bool AuthorizeCore(HttpContextBase httpContext) { var codes httpContext.Session[PermCodes] as HashSetstring; if (codes null) return false; return codes.Contains(Code); } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { // Ajax 请求返回 JSON让前端统一弹提示而不是跳到登录页 filterContext.Result new JsonResult { Data new { code 403, msg 没有操作权限 }, JsonRequestBehavior JsonRequestBehavior.AllowGet }; } else { base.HandleUnauthorizedRequest(filterContext); } } }把 Code 作为特性参数传入页面和按钮的权限码统一起了作用[Permission(Code Order_Export)] public ActionResult Export() { // 导出逻辑 }HandleUnauthorizedRequest 区分了 Ajax 和普通请求。后台页面的按钮大多通过 jQuery Ajax 提交未授权弹一个 403 JSON 比跳转到登录页更合理。3.4 前端菜单的动态渲染菜单不用写死。进入布局页时从 Session 里的权限码过滤出有权的菜单传给视图渲染。这一步用 Html.Action 在布局页拉取一个局部视图比较常见Html.Action(Menu, Home, new { area Admin })对应 Action 里把菜单整理成树形结构返回 PartialView。前端只需遍历树节点渲染 ul/li权限调整后刷新页面即可生效不用发布新代码。4. 代码生成器的思路用元数据反射批量产出增删改查页面快速开发框架的价值在于“模板化的重复劳动交给代码跑”。对 MVC5 来说最稳定的基础元数据就是实体类反射反射拿到的属性名、特性标注、类型信息足够驱动一套标准页面的生成。4.1 为什么在原项目里做生成器而不是独立脚手架独立脚手架的问题在于它生成的代码和项目当前的分层、命名空间、请求包装格式不一致。与其迁就工具不如在框架内做一个轻量生成器读取已存在的实体类按约定生成 Controller、View 和菜单 SQL产出物的命名空间天然匹配当前项目。4.2 实体的约定与扫描入口实体必须携带特定特性生成器才知道哪些字段要展示、哪些要排除。约定如下// 实体上标 [Table(sys_order)]字段上标 [Display(Name订单号)] // 主键约定为 Id编辑页面自动排除该字段列表页默认显示除大字段外的所有字段 [Table(sys_order)] public class Order { [Key] public int Id { get; set; } [Display(Name 订单号)] [Required] public string OrderNo { get; set; } [Display(Name 金额)] public decimal Amount { get; set; } [Display(Name 备注)] [DataType(DataType.MultilineText)] public string Remark { get; set; } }生成器入口只做一件事通过反射拿到实体类型的 PropertyInfo 列表按 DisplayAttribute 判定字段展示名按 DataType 判定前端控件类型。4.3 生成列表页的核心方法列表页生成器的核心逻辑是拼接 HTML 与 C#。用 StringBuilder 拼字符串虽然不好看但胜在可控、无依赖public string BuildListPage(Type entity) { var props entity.GetProperties() .Where(p p.IsDefined(typeof(DisplayAttribute), false)) .Where(p p.Name ! Id) .ToList(); var sb new StringBuilder(); sb.AppendLine(table class\table table-bordered\); sb.AppendLine(theadtr); foreach (var p in props) { var name p.GetCustomAttributeDisplayAttribute().Name; sb.AppendLine($th{name}/th); } sb.AppendLine(th操作/th); sb.AppendLine(/tr/thead); // 行主体在运行时由页面模型填充这里生成的是 View 模板骨架 return sb.ToString(); }生成后输出的字符串写到 Areas/Admin/Views/Order/Index.cshtml再配合一个泛型控制器基类就能直接运行。查询条件区域也按相同套路生成字符串类型字段生成文本框数字字段生成区间输入框日期字段生成 date 控件。4.4 生成后必须手工检查的三处自动生成的代码只能保证跑通不能保证正确。常被忽略的点有三个一是外键字段Order 表里的 CustomerId 反射生成文本框用户根本无法录入需要在编辑页替换为下拉框二是日期范围查询数据库里存的是 datetime页面上要传两个字符串再统一解析三是审计字段创建人、创建时间这类字段虽然标记了 [Display]但生成时应直接排除否则用户可随意篡改。5. 在 MVC5 后台里集成轻量级工作流引擎的务实做法“带工作流”是这类框架介绍里最含糊的描述。有的框架只是给审批业务加了一个状态字段有的则引入独立服务端引擎。对 MVC5 项目而言工作流的落地策略应该是业务表增加状态字段流程规则用配置表达流转记录全部落库。5.1 工作流引擎的选型对比方案集成方式适合场景主要代价自研状态机引擎项目内普通类审批流固定、节点少并行节点、动态路由需要自己扩展Workflow CoreNuGet 包需要并行、会签、条件分支学习配置模型版本更新快Flowable / Camunda独立服务或容器跨语言、跨系统流程编排部署重与 MVC5 旧项目割裂常见误区是看到“工作流”就直接引入 Flowable。Flowable 本身是 Java 生态ASP.NET 项目接入需要在 Windows 上单独部署服务运维成本高。后台管理系统里 90% 的流程是“提交、部门审核、财务审核、结束”的链式审批自研状态机加上一张配置表就足够了。5.2 状态机的数据库设计用三张表承载流程数据。第一张是流程定义表存储流程名称、起始节点、节点 JSON 配置第二张是流程实例表记录当前实例在哪个节点、关联哪张业务表哪条记录第三张是审批记录表每一步的操作人和意见都写在这里。表结构用 Code First 类表示public class WorkflowNode { public int Id { get; set; } public string FlowCode { get; set; } // 流程编号如 LEAVE public string NodeCode { get; set; } // 当前节点如 APPLY / DEPT_APPROVE public string NextNode { get; set; } // 审批通过后进入的节点 public string RejectNode { get; set; } // 驳回后回到的节点 public string ActionName { get; set; } // 触发动作Submit / Agree / Reject public string RoleCode { get; set; } // 该节点允许操作的角色 }WorkflowNode 的每一行代表一条流转规则。NextNode 填 END 表示流程结束ActionName 区分提交和审核。规则存在数据库里意味着新增流程不需要改代码录入几条数据即可。5.3 核心引擎执行一次流转操作引擎类负责完成逻辑校验和状态变更。核心方法可以写成public class WorkflowEngine { private readonly AdminDbContext _db; public WorkflowEngine(AdminDbContext db) { _db db; } public WorkflowResult Execute(string flowCode, string businessId, string actionName, string userId, string remark) { var inst _db.WorkflowInstances.FirstOrDefault(w w.FlowCode flowCode w.BusinessId businessId w.State 1); if (inst null) return WorkflowResult.Fail(流程实例不存在或已结束); // 找到当前节点对应的规则 var rule _db.WorkflowNodes.FirstOrDefault(n n.FlowCode flowCode n.NodeCode inst.CurrentNode n.ActionName actionName); if (rule null) return WorkflowResult.Fail($节点 [{inst.CurrentNode}] 不支持操作 [{actionName}]); // 校验当前用户是否属于规则绑定角色 var user _db.Users.Include(u u.Role).FirstOrDefault(u u.Id userId); if (user null || user.Role.RoleCode ! rule.RoleCode) return WorkflowResult.Fail(当前角色无权执行此操作); // 写入审批记录 _db.WorkflowLogs.Add(new WorkflowLog { InstanceId inst.Id, UserId userId, ActionName actionName, Remark remark, CreateTime DateTime.Now }); // 状态推进 inst.CurrentNode actionName Reject ? rule.RejectNode : rule.NextNode; if (inst.CurrentNode END) { inst.State 0; // 0 表示流程结束 } _db.SaveChanges(); return WorkflowResult.Ok(inst.CurrentNode); } }围绕这个方法需要解释两点。其一角色校验放在引擎内部而不是过滤器里是因为同一个操作可能同时抛给财务和部门经理两种角色Action 上没法写死一个权限码必须依赖流程规则表。其二驳回节点用 RejectNode 而不是固定回到上一节点是为了处理“部门审核驳回给提交人但财务审核驳回给部门负责人”这种常见差异。5.4 业务表如何接入流程在业务表比如请假单 LeaveApply上加两个字段CurrentState 表示当前节点编码InstanceId 表示关联的流程实例。提交时调用 WorkflowEngine.StartFlow 创建实例审核时调用 Execute流程结束时触发一次状态回调把业务单标记为最终状态闭环就完成了。单独建实例表而不是把状态直接写在业务表里原因是一个流程可能经历多次提交、驳回、再提交单独存实例可以完整保留每一次流转历史业务表只保留最末状态。6. 部署前后留给自己的几个有效动作6.1 EF6 查询性能后台列表页最容易出现的两类慢查询一是循环中访问导航属性触发懒加载二是关联表没有索引。前者在 DbContext 里已经关了 LazyLoading查询时用 Include 显式连表后者用 MiniProfiler 逐条观察 SQL。开发时把 MiniProfiler 挂到全局可以看到每个请求执行了哪些 SQL、耗时多少、有没有重复查询。这个检查在接口自测阶段做一次比上线后看日志高效得多。6.2 会话状态与 IIS 部署后台系统默认走 Session部署在单个 IIS 服务器时用的是 InProc 模式重启应用池会把在线用户全部踢下线。如果对掉线容忍度低把 Session 状态换成 SQL Server 模式虽然性能上有开销但对后台系统的并发规模来说可以接受。6.3 数据库连接字符串与迁移环境开发、测试、生产三个环境的连接字符串都写在 Web.config 的 connectionStrings 节点里按 build 配置切换不要硬编码连接。EF6 的 MigrateDatabaseToLatestVersion 初始化器在调试环境下很省事生产环境别用一旦代码与数据库不同步应用启动直接报错。6.4 一个小技巧把权限校验日志化在 PermissionAttribute 的 AuthorizeCore 里加一个埋点记录被拒绝的用户名、权限码、URL 和时间写入数据库日志表。权限问题大多不是程序逻辑错了而是某角色的权限配多了或者配少了。有了日志排查权限问题只需要查一次数据库而不是逐个角色去试。本文还有配套的精品资源点击获取
返回列表