资讯详情

ASP.NET MVC工作流管理系统源码解析:流程引擎与表单设计器实战

📅 2026/10/19 0:52:51 | 华诺云谱 👁 阅读
ASP.NET MVC工作流管理系统源码解析:流程引擎与表单设计器实战
简介这是一套基于ASP.NET MVC5开发的工作流管理系统源码内置可视化流程设计器与表单设计器适合正在从事OA办公、CRM客户关系或HR人事管理系统的开发者参考学习也可作为掌握MVC架构与工作流引擎原理的实战范例。压缩包共约2000个文件整体60.93MB涵盖201个cs源码、106个cshtml视图、206个js脚本、88个css样式及251个dll依赖库另含gif、png等界面素材与xml、config配置文件完整呈现前后端结构与Bootstrap前端布局。资源中流程节点配置、表单控件渲染、上传与删除等ashx处理逻辑均有对应实现便于读者梳理工作流引擎的调度机制与页面交互细节。目前已有1098人学习下载适合需要搭建审批流转、表单定制能力的初中级.NET开发者对照研读快速理解MVC分层设计与工作流落地思路。1. 从一张请假单说起这套 ASP.NET MVC 工作流系统到底能干什么很多团队做 OA 系统第一版都是硬编码审批流请假单写死三级审批报销单写死两级审批改一次流程就要改一次代码、发一次版。这套 ASP.NET MVC 工作流管理系统源码解决的正是这个痛点——它把「流程定义」和「业务表单」拆开流程走数据库配置表单走可视化设计器改流程不用重新编译。技术栈是 ASP.NET MVC Bootstrap jQuery后端 .NET Framework数据库 SQL Server前端表单设计器基于 JS 动态渲染。适合谁一是接 OA、审批类外包项目、想省掉从零搭流程引擎的开发者二是正在维护老 .NET 项目、需要给现有系统补一套审批能力的团队。它不是一个玩具 Demo而是把流程定义、表单设计、待办流转、节点权限这几块都串起来的完整工程下面我按「能干什么 → 怎么跑 → 怎么改」拆开讲。2. 架构与目录结构流程引擎、表单设计器、业务层怎么分层2.1 整体分层与请求链路这套源码的分层思路很典型属于 .NET MVC 时代 OA 系统的标准打法但把工作流这块单独抽了出来。整体分四层表现层MVC Controller View、工作流引擎层流程定义解析、节点流转、任务分配、业务服务层表单数据、业务实体、数据访问层EF 或原生 ADO.NET SQL Server。一个审批请求的完整链路是这样的用户在待办列表点开一条任务 → Controller 拿到 TaskId → 工作流引擎根据流程定义查出当前节点和下一步节点 → 渲染对应表单 → 用户提交 → 引擎写入审批记录、推进流程实例状态 → 生成下一节点的待办任务。关键点在于「流程实例」和「流程定义」是两张独立的表实例是定义的一次运行改定义不影响已跑起来的实例这是工作流系统能不能用的分水岭。常见做法是引擎层不直接碰业务表只维护WF_Process流程定义、WF_ProcessInstance流程实例、WF_Task任务/待办、WF_ApproveLog审批记录这几张核心表业务表单数据单独存自己的表通过BusinessKey关联。这样业务和流程解耦加一个新审批类型不用动引擎代码。2.2 目录结构逐个说源码目录大致长这样不同版本命名略有差异但结构一致/WorkflowSystem /Controllers WorkflowController.cs // 流程定义、发起、审批入口 FormDesignController.cs // 表单设计器后端接口 TaskController.cs // 待办、已办、我的申请 /Models WF_Process.cs WF_ProcessInstance.cs WF_Task.cs WF_ApproveLog.cs /Services WorkflowEngine.cs // 核心流转逻辑 TaskService.cs // 任务分配、认领 FormService.cs // 表单 schema 存取 /Views /Workflow /FormDesign /Scripts formdesigner.js // 前端表单设计器 workflow.js /Content bootstrap.css /App_Data workflow.sql // 建表脚本WorkflowEngine.cs是整个项目的心脏流程推进、节点判断、条件分支都在这里。formdesigner.js是前端最值钱的部分拖拽生成表单、导出 JSON schema、后端存库、渲染时再解析回 HTML。workflow.sql是初始化脚本跑起来之前必须先执行否则登录进去待办列表直接报错。2.3 核心表结构设计流程能不能灵活配置全看表设计。核心几张表我列一下关键字段表名关键字段作用WF_ProcessId, Name, Version, DefinitionJson, IsActive流程定义DefinitionJson 存节点和连线WF_ProcessInstanceId, ProcessId, BusinessKey, CurrentNodeId, Status流程实例一条业务数据对应一条WF_TaskId, InstanceId, NodeId, Assignee, Status, CreateTime待办任务Status 区分待处理/已处理WF_ApproveLogId, InstanceId, TaskId, Approver, Action, Comment审批记录留痕用DefinitionJson是设计的关键节点、审批人规则、条件表达式都序列化在这一个字段里。好处是加节点不用改表结构坏处是查询和统计不方便所以常见做法是再冗余一张WF_Node表做查询用两边同步。这套源码用的是前者简单直接中小项目够用。3. 跑起来环境准备、数据库初始化和第一个审批流3.1 环境与依赖跑这套源码之前环境得先对齐不然报错能让你怀疑人生。需要Visual Studio 2017 及以上.NET Framework 4.5 项目用 VS 打开 .sln 直接能编译SQL Server 2012 及以上或者 LocalDBIIS ExpressVS 自带F5 就能跑浏览器用 Chrome表单设计器对 IE 兼容一般依赖的 NuGet 包主要是 EntityFramework、Newtonsoft.Json、Bootstrap 前端包。打开项目后先右键解决方案「还原 NuGet 包」这一步不做编译时Newtonsoft.Json找不到引用报一堆红。3.2 数据库初始化数据库是第一个坑。App_Data/workflow.sql里是建表脚本但很多人直接 F5 就跑结果登录后待办列表 500。正确顺序是先在 SQL Server 里建一个空库比如WorkflowDB然后执行脚本-- 先建库 CREATE DATABASE WorkflowDB; GO USE WorkflowDB; GO -- 再执行 workflow.sql 里的建表语句 -- 核心几张表WF_Process / WF_ProcessInstance / WF_Task / WF_ApproveLog执行完检查一下表是不是都建出来了尤其是WF_Task漏了这张待办直接空。然后改Web.config里的连接串connectionStrings add nameWorkflowContext connectionStringData Source.;Initial CatalogWorkflowDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStringsData Source.表示本机默认实例用 LocalDB 的话改成(localdb)\MSSQLLocalDB。Integrated SecurityTrue是 Windows 身份验证用 SQL 账号密码就换成User Idsa;Passwordxxx。连接串改错是最常见的启动失败原因报错一般是「无法打开登录所请求的数据库」。3.3 发起第一个审批流环境通了之后先跑通一条最简单的流程验证引擎是活的。步骤登录系统默认账号密码在workflow.sql的Sys_User插入语句里一般是 admin/123456进「流程管理」→「流程定义」新建一个流程拖两个节点开始 → 审批 → 结束给审批节点配审批人选具体人或者按角色进「表单设计」给这个流程绑一张表单拖几个字段申请人、天数、事由进「我的申请」发起一条然后切到审批人账号看待办这条链路走通说明引擎、表单、任务三块都活了。走不通的话八成是流程定义的DefinitionJson没存对或者节点 ID 和表单绑定对不上。我一般会在WorkflowEngine.cs的推进方法里打断点看CurrentNodeId有没有正确流转到下一节点。3.4 表单设计器的数据流表单设计器是这套源码的亮点值得单独说清楚它的数据流。前端formdesigner.js拖拽生成的是 JSON schema结构大概这样// 表单 schema前端拖拽生成存到后端 FormSchema 字段 { fields: [ { type: text, name: applicant, label: 申请人, required: true }, { type: number, name: days, label: 请假天数, required: true }, { type: textarea, name: reason, label: 事由, required: false } ] }后端FormService.cs把这个 JSON 原样存库渲染表单时再取出来前端根据type动态生成对应控件。name字段是关键它对应业务表里的列名提交时按name组装数据。所以改表单字段名要小心改了之后历史数据的映射就对不上了。常见做法是字段名一旦上线就不改只加不改要改就新建版本。4. 核心模块拆解流程引擎、任务分配、表单渲染三段代码4.1 流程引擎的推进逻辑引擎的核心就一个方法给定当前任务算出下一步该谁审。这段逻辑我按源码结构还原一下// WorkflowEngine.cs 核心推进方法 public void Approve(int taskId, string approver, string action, string comment) { // 1. 取任务和流程实例 var task _taskRepo.Get(taskId); var instance _instanceRepo.Get(task.InstanceId); // 2. 写审批记录留痕 _logRepo.Add(new WF_ApproveLog { InstanceId instance.Id, TaskId taskId, Approver approver, Action action, // agree / reject Comment comment, ApproveTime DateTime.Now }); // 3. 驳回直接回退到发起人同意才往下走 if (action reject) { instance.CurrentNodeId instance.StartNodeId; instance.Status rejected; } else { // 4. 根据流程定义找下一节点 var definition _processRepo.GetDefinition(instance.ProcessId); var nextNode definition.GetNextNode(instance.CurrentNodeId, instance.BusinessData); instance.CurrentNodeId nextNode.Id; instance.Status nextNode.IsEnd ? finished : running; // 5. 非结束节点生成新待办 if (!nextNode.IsEnd) { _taskRepo.Add(new WF_Task { InstanceId instance.Id, NodeId nextNode.Id, Assignee ResolveAssignee(nextNode, instance), Status pending, CreateTime DateTime.Now }); } } // 6. 更新任务和实例状态 task.Status done; _taskRepo.Update(task); _instanceRepo.Update(instance); }逻辑说明第 4 步的GetNextNode是分支判断的核心它读DefinitionJson里的连线条件比如「天数 3 走总监否则走经理」。ResolveAssignee负责解析审批人支持指定人、角色、发起人上级三种规则。参数action只有 agree/reject 两种驳回逻辑简单粗暴直接回发起人实际项目里常见做法是支持驳回到任意历史节点这套源码没做二次开发可以补。4.2 任务分配与待办查询任务分配看着简单坑不少。ResolveAssignee的三种规则里角色解析最容易出问题——一个用户可能有多个角色一个角色可能有多个人到底生成一条待办还是多条这套源码的做法是指定人生成一条角色生成多条每个角色成员一条发起人上级生成一条。查询待办时按Assignee 当前用户 AND Status pending过滤// TaskService.cs 待办列表查询 public ListWF_Task GetPendingTasks(string userId) { return _db.WF_Task .Where(t t.Assignee userId t.Status pending) .OrderByDescending(t t.CreateTime) .ToList(); }这里有个性能点WF_Task表数据量大了之后Assignee Status一定要建联合索引否则待办列表越用越慢。我见过一个项目待办表几十万行没索引打开待办要等七八秒加个索引直接降到毫秒级。索引脚本在workflow.sql里其实有但很多人执行脚本时跳过了索引部分血泪经验。4.3 表单渲染与数据回填表单渲染分两步先按 schema 生成 HTML再把业务数据回填进去。前端workflow.js里大概是这样// 根据 schema 渲染表单并回填已有数据 function renderForm(schema, data) { var $form $(#dynamicForm).empty(); schema.fields.forEach(function (field) { var $group $(div classform-group/div); $group.append(label field.label /label); var $input; if (field.type textarea) { $input $(textarea classform-control/textarea); } else { $input $(input classform-control type field.type ); } $input.attr(name, field.name); // 回填有数据就填没有就空 if (data data[field.name] ! undefined) { $input.val(data[field.name]); } if (field.required) { $input.attr(required, true); } $group.append($input); $form.append($group); }); }逻辑说明field.name同时是表单控件的 name 和业务数据的 key提交时$form.serialize()出来的就是{applicant: xx, days: 3}后端直接存。data参数是回填数据审批人打开待办时要把申请人填的内容显示出来就是靠这个。注意required属性只是前端校验后端一定要再校验一遍前端校验能绕过这是安全底线。5. 避坑与排查启动报错、流程卡住、表单错位的真实记录5.1 启动就 500连接串和建表脚本背锅现象F5 启动登录页能开一登录就 500或者待办列表直接报错。原因九成是连接串没改或者workflow.sql没执行全。解决先确认Web.config里的WorkflowContext连接串指向的库存在再进 SQL Server 看WF_Task、WF_Process这几张表在不在。表不在就重新执行脚本注意脚本里可能有GO分批用 SSMS 整体执行别一段段复制。5.2 流程发起后卡在第一个节点不动现象申请提交了审批人待办里看不到。原因ResolveAssignee没解析出审批人或者节点 ID 和流程定义里的对不上。解决查WF_Task表看有没有生成记录没有就是分配逻辑问题有记录但Assignee是空就是审批人规则没配对。我一般先在流程定义里把审批人改成「指定人」测一遍排除角色解析的干扰。5.3 表单字段改了名历史数据全空现象把表单里「天数」的 name 从days改成leaveDays之前提交的单子打开全是空。原因业务数据是按name存的改名等于换了 key历史数据还在旧 key 上。解决字段名上线后不改要改就加新字段、做数据迁移或者表单 schema 加版本号按版本渲染。这是表单设计器类系统最典型的坑没有后悔药只能提前约定。5.4 待办列表越用越慢现象系统跑几个月待办列表打开要好几秒。原因WF_Task表没索引或者Status字段用了字符串导致索引失效。解决给(Assignee, Status)建联合索引Status如果数据量大可以改成 tinyint 枚举。另外已办任务要定期归档别让WF_Task无限膨胀常见做法是已完成的实例数据迁到历史表。5.5 并发审批导致流程状态错乱现象两个审批人同时点同意流程实例状态变成 finished 之后又被改了一次。原因Approve方法没有并发控制两个请求同时读到pending状态。解决更新任务时加乐观锁UPDATE WF_Task SET Statusdone WHERE Idid AND Statuspending看影响行数为 0 说明已被处理直接返回提示。这个坑在小项目里不常见一旦用户量上来必踩。6. 二次开发加一个「驳回到任意节点」和流程版本管理拿到源码之后最值得动手改的两个地方一个是驳回逻辑一个是流程版本管理。原生的驳回只能回发起人实际业务里经常要「驳回到上一节点」甚至「驳回到指定节点」这个改造我做过好几次思路分享一下。先看驳回改造。核心是给WF_ApproveLog加一个TargetNodeId字段驳回时记录目标节点然后Approve方法里判断if (action reject) { // 默认回发起人指定了目标节点就回目标节点 var targetNodeId string.IsNullOrEmpty(targetNodeId) ? instance.StartNodeId : targetNodeId; instance.CurrentNodeId targetNodeId; instance.Status running; // 注意驳回后实例还是运行中不是 rejected // 给目标节点重新生成待办 _taskRepo.Add(new WF_Task { InstanceId instance.Id, NodeId targetNodeId, Assignee ResolveAssigneeByNode(targetNodeId, instance), Status pending, CreateTime DateTime.Now }); }这里有个容易翻车的点驳回后实例状态如果设成rejected那目标节点的待办就查不出来了因为待办查询默认只查running的实例。所以驳回后状态要设回running只是当前节点变了。这个细节不注意驳回功能就是坏的。再看流程版本管理。原生的WF_Process有Version字段但没真正用起来改流程定义直接覆盖导致跑着的实例行为变了。正确做法是流程定义每次修改生成新版本WF_ProcessInstance记录发起时用的ProcessVersion引擎推进时按实例的版本取定义而不是取最新版。这样老实例按老流程走完新实例用新流程互不干扰。// 按实例记录的版本取定义而不是取最新 var definition _processRepo.GetDefinition(instance.ProcessId, instance.ProcessVersion);改造完之后怎么验证我的习惯是写一个最小回归用例发起一条三级审批中途改流程定义加一个节点然后继续审批老实例看它是不是还按老流程走完。如果老实例突然多出一个节点说明版本没隔离干净。这个验证我每次改引擎都强制走一遍吃过亏——有次上线后老流程全乱套就是因为引擎取了最新定义。最后说个表单设计器的扩展技巧。原生设计器只支持文本、数字、日期几种基础控件要加「附件上传」「人员选择」这类复杂控件扩展点在formdesigner.js的控件注册表里加一个 type 和对应的渲染函数就行后端FormService不用动因为 schema 是原样存的。但要注意附件这类控件的值不是简单字符串存的时候要序列化成 JSON回填时再解析不然提交上来是[object Object]。从那以后我每次加自定义控件都先在 schema 里把值的类型定清楚字符串就是字符串对象就明确序列化不给自己留坑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑