简介:这是一套基于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_Process | Id, Name, Version, DefinitionJson, IsActive | 流程定义,DefinitionJson 存节点和连线 |
| WF_ProcessInstance | Id, ProcessId, BusinessKey, CurrentNodeId, Status | 流程实例,一条业务数据对应一条 |
| WF_Task | Id, InstanceId, NodeId, Assignee, Status, CreateTime | 待办任务,Status 区分待处理/已处理 |
| WF_ApproveLog | Id, InstanceId, TaskId, Approver, Action, Comment | 审批记录,留痕用 |
DefinitionJson是设计的关键,节点、审批人规则、条件表达式都序列化在这一个字段里。好处是加节点不用改表结构,坏处是查询和统计不方便,所以常见做法是再冗余一张WF_Node表做查询用,两边同步。这套源码用的是前者,简单直接,中小项目够用。
3. 跑起来:环境准备、数据库初始化和第一个审批流
3.1 环境与依赖
跑这套源码之前,环境得先对齐,不然报错能让你怀疑人生。需要:
- Visual Studio 2017 及以上(.NET Framework 4.5+ 项目,用 VS 打开 .sln 直接能编译)
- SQL Server 2012 及以上,或者 LocalDB
- IIS Express(VS 自带,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 name="WorkflowContext" connectionString="Data Source=.;Initial Catalog=WorkflowDB;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings>Data Source=.表示本机默认实例,用 LocalDB 的话改成(localdb)\MSSQLLocalDB。Integrated Security=True是 Windows 身份验证,用 SQL 账号密码就换成User Id=sa;Password=xxx。连接串改错是最常见的启动失败原因,报错一般是「无法打开登录所请求的数据库」。
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 List<WF_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 class="form-group"></div>'); $group.append('<label>' + field.label + '</label>'); var $input; if (field.type === 'textarea') { $input = $('<textarea class="form-control"></textarea>'); } else { $input = $('<input class="form-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 Status='done' WHERE Id=@id AND Status='pending',看影响行数,为 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 里把值的类型定清楚,字符串就是字符串,对象就明确序列化,不给自己留坑。希望帮到你。
本文还有配套的精品资源,点击获取