开篇前的废话:工作流是我们在做互联网应用开发时经常需要用到的一种技术,复杂的工作流我们基本是借助一些开源的 工作流项目来做,比如 ccflow等,但是有时候,我们只需要实现一些简单的工作流流程,这时候用 ccflow等就显得杀鸡用牛刀了,这时候我们就得自己写一个简单的工作流的流程了,一个简单的工作流的实现,如果没有自己动手做过,单凭看别人的博客是很难理解的,我就曾在这个问题上掉进大坑。下面把我对简单工作流的实现简单的记录一下。
说明:因为工作流是涉及一个任务请求在多个人之间流转的业务流程,所以我们本篇博客的实现需要立足在一个至少有三个用户的项目基础上的,这里我不会从最基础的部分开始做,那样将耗费太多时间,过程也很麻烦。我将在我手上一个现成的项目上实现这个流程,所以你是个比我还菜的菜鸟,可能会看懵,如果我没让你看明白,请不要骂我,因为我也很菜。我会尽量把过程写的清楚明白一些,尽可能让看的人都能看明白。
-
业务描述
本篇我将写一个简单的工作流流程,用来实现一个公司员工的请假流程,简单来说,可以用下图来描述:
这是一个简单且常用的一个工作流程,需要三个用户,分别扮演三种角色,普通员工、部门经理和总经理。
-
数据库设计
工作流的数据库设计主要涉及 4 张表,其中一张是用来存储我们的请假申请的内容(比如:请假时长、请假原因、收假时间等),另外三张表则主要实现工作流程的核心;
如图所示:数据库的另外三张表分别为 流程实例表,流程节点表,流程流转记录表,
它们的作用分别是:
流程节点表:该表定义一个流程有几个节点,每个节点在流程中的位置如何,他的前一个节点是谁,后一个节点是谁,该节点由谁来操作等等;比如,一个流程从发起到完成审批共需要经手3个人,则就有3个节点。示例:
这个流程从发起到审批完成有3个人经手,所以在这里添加三个节点,注明每个节点之间的关系;后期,如果某个节点的人审批过了,则通过查找这张表,来寻找它的上一节点或下一节点,然后通过改变节点的值,使流程向下一个节点流转;
流程实例表:流程实例表是工作流的核心,在请假单提交时,同步新建一个 流程实例 ,这个流程实例表就相当于是每个节点之间的一个纽带,上一个人操作过后,就把当前节点的编号由当前节点改为下一节点,就这样,每个人操作过后,就改一个该表中的节点编号,这样就可以实现流程在每个节点之间传递、移动;
流程流转记录表:每个人对流程进行操作后,同步在该表中创建一个操作记录,记录是谁操作的,操作结果如何等等;
以下列出数据实体:
Request.cs
namespace Modules.Wflow { /// <summary> /// 请假申请 /// </summary> [Table("LeaveRequest")] public class LeaveRequest { [Key] public Guid Id { get; set; } /// <summary> /// 请假原因 /// </summary> [MaxLength(500)] public string Reason { get; set; } /// <summary> /// 请假时长 /// </summary> public string Duration { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreateTime { get; set; } } }
Node.cs
namespace Modules.Wflow { /// <summary> /// 节点表 /// </summary> [Table("WF_Node")] public class Node { public Node() { IsDelete = false; } [Key] public Guid Id { get; set; } /// <summary> /// 节点编号 /// </summary> [MaxLength(11)] [Required] public string NodeSN { get; set; } /// <summary> /// 节点名称 /// </summary> [MaxLength(50)] [Required] public string NodeName { get; set; } ///// <summary> ///// 流程id ///// </summary> //public Guid? FlowId { get; set; } //[MaxLength(100)] ///// <summary> ///// 流程名称 ///// </summary> //public string FlowName { get; set; } /// <summary> /// 执行人Id /// </summary> public Guid? OperatorId { get; set; } /// <summary> ///执行人名称 /// </summary> public string Operator { get; set; } /// <summary> /// 下一节点编号 /// </summary> public string NextNodeSN { get; set; } /// <summary> /// 上一节点编号(退回节点) /// </summary> public string LastNodeSN { get; set; } public DateTime CreateTime { get; set; } /// <summary> /// 是否删除 /// </summary> public bool IsDelete { get; set; } } }
FlowInstance .cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Modules.Wflow { /// <summary> /// 流程实例表 /// </summary> [Table("WF_FlowInstance")] public class FlowInstance { [Key] public Guid Id { get; set; } /// <summary> /// 当前节点 /// </summary> [MaxLength(30)] [Required] public string NodeSN { get; set; } /// <summary> /// 节点名称 /// </summary> [MaxLength(50)] public string NodeName { get; set; } /// <summary> /// 流状态 /// </summary> [MaxLength(30)] public string WFStatus { get; set; } /// <summary> /// 流程发起人userId /// </summary> public Guid? StarterId { get; set; } /// <summary> /// 流程发起人姓名 /// </summary> public string Starter { get; set; } /// <summary> /// 当前操作人userId /// </summary> public Guid? OperatorId { get; set; } /// <summary> /// 当前操作人姓名 /// </summary> public string Operator { get; set; } /// <summary> /// 待办人Id /// </summary> public Guid? ToDoerId { get; set; } /// <summary> /// 待办人名称 /// </summary> public string ToDoer { get; set; } /// <summary> /// 已操作人 /// </summary> [MaxLength(200)] public string Operated { get; set; } /// <summary> /// 流程创建时间 /// </summary> public DateTime CreateTime { get; set; } /// <summary> /// 流程更新时间 /// </summary> public DateTime UpdateTime { get; set; } /// <summary> /// 申请单Id /// </summary> public Guid? RequisitionId { get; set; } } }
FlowRecord.cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Modules.Wflow { /// <summary> /// 流程记录表 /// </summary> [Table("WF_FlowRecord")] public class FlowRecord { [Key] public Guid Id { get; set; } /// <summary> /// 流程实例Id /// </summary> public Guid? WorkId { get; set; } /// <summary> /// 当前节点编号 /// </summary> [MaxLength(20)] public string CurrentNodeSN { get; set; } /// <summary> /// 当前节点名称 /// </summary> [MaxLength(50)] public string CurrentNode { get; set; } /// <summary> /// 操作人Id /// </summary> public Guid? OperatorId { get; set; } /// <summary> /// 操作人名称 /// </summary> [MaxLength(50)] public string Operator { get; set; } /// <summary> /// 更新时间 /// </summary> public DateTime UpdateTime { get; set; } /// <summary> /// 是否读取 /// </summary> public bool IsRead { get; set; } /// <summary> /// 是否通过 /// </summary> public bool IsPass { get; set; } } }
-
工作流业务代码实现
业务代码主要以流程的起始节点和第二个节点为例进行说明。
首先,我在项目中添加了三个人员,分别是 张三(总经理)、李四(部门经理)、王五(普通员工)
然后,分别创建对应的控制器和前端的菜单等,如下图:
这里我主要针对新建申请和待办审批两个功能的实现进行编码:
(1)新建申请:
如图,这是我创建的一个请假单,当该请假提交后,会进行如下几项处理:
(1)保存请假单到数据库;
(2)创建工作流实例,给该实例的各项赋值;具体赋值为:
当前节点(NodeSN):赋值为该流程的第一个节点(101);
流程发起人(Starter):赋值为当前用户的用户名(王五);
当前操作人(Operator): 赋值为当前用户的用户名(王五);
待办人(ToDoer):赋值待办人(通过节点表查询待办人是谁)(此处应为李四)(下一个人会根据待办人的值来查找自己是否有待办事项);
请假单Id(RequisitionId):赋值为刚保存的请假单的id(刚提交的申请单ID);
(3)创建工作流操作记录,具体赋值为:
流程实例Id:赋值为(2)中创建的流程实例的Id;
当前处理人:同(2)中;
当前节点:同(2)中;
是否已读和通过:这个值在流程发起节点是不需要写的,或者写 true;
该部分代码如下:
/// <summary> /// 保存申请单并提交工作流 /// </summary> /// <param name="request"></param> /// <returns></returns> public ActionResult Save(LeaveRequest request) { try { request.CreateTime = DateTime.Now; //1.保存请假单 _context.LeaveRequests.AddOrUpdate(request); //2.创建工作流 var flow = new FlowInstance { Id = Guid.NewGuid()}; //当前登录人员信息 var userInfo = GetEmployeeInfo(CacheUtil.LoginUser.Id); //工作流当前节点 flow.NodeSN = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("发起申请")).NodeSN; flow.NodeName = "发起申请"; //申请处理状态 flow.WFStatus = "已申请"; //申请人(流程发起人) flow.StarterId = userInfo.Id; flow.Starter = userInfo.FullName; //当前操作者 flow.OperatorId = userInfo.Id; flow.Operator = userInfo.FullName; //下一个节点处理人 flow.ToDoerId = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("部门经理审批")).OperatorId; flow.ToDoer = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("部门经理审批")).Operator; //申请单ID flow.RequisitionId = request.Id; flow.UpdateTime = DateTime.Now; flow.CreateTime = DateTime.Now; //已操作过的人 flow.Operated = userInfo.Id.ToString(); _context.FlowInstances.AddOrUpdate(flow); //3.新建流操作记录 var flowRecord = new FlowRecord { Id = Guid.NewGuid() }; //流程实例Id flowRecord.WorkId = flow.Id; //当前处理人 flowRecord.Operator = userInfo.FullName; flowRecord.OperatorId = userInfo.Id; //当前节点 flowRecord.CurrentNodeSN = flow.NodeSN; flowRecord.CurrentNode = flow.NodeName; //是否已读 flowRecord.IsRead = true; //是否通过 flowRecord.IsPass = true; flowRecord.UpdateTime = DateTime.Now; _context.FlowRecords.Add(flowRecord); int saveResult = _context.SaveChanges(); if (saveResult > 0) { return Json(new { Result = true }); } else { throw new Exception("提交失败"); } } catch (Exception exception) { return Json(new { Result = false, exception.Message }); } }
(2)获取待办审批
若按照刚才的操作进行的话,那么则会新建一个工作流实例,该实例存储的数据如下:
同时,在流程记录表中会得到一条记录数据,如下:
接下来,我们来看一下获取待办审批的步骤,首先看下效果:
登录李四的账号:
然后,说明一下获取待办审批的步骤,以及向下一节点流转的步骤:
(1)获取待办审批:根据工作流实例中的 待办人Id 来进行获取,若待办人为当前登录的用户,则获取这个待办事项;
/// <summary> /// 获取待办审批 /// </summary> /// <returns></returns> public ActionResult GetList(int page) { try { // var userInfo = GetEmployeeInfo(CacheUtil.LoginUser.Id); var todo = _context.FlowInstances.Where(x => x.ToDoerId == UserInfo.Id).OrderByDescending(x => x.CreateTime); //此处获取待办人列表,根据待办人Id 等于 当前登录用户Id获取 int count = todo.Count(); var pagedList = todo.ToPage(page, count).ToList(); var todoList = pagedList.Select(x => new { x.Id, x.Starter,//申请人 x.Operator, //上一操作人 UpdateTime = x.UpdateTime.Format("yyyy年MM月dd日 hh:mm"), //更新时间, x.RequisitionId //对应申请单id }).ToList(); return Json(new { Data = todoList, Result = true, Count = count }); } catch (Exception exception) { return Json(new { Result = false, exception.Message }); } }
(2)若同意,则点击确定,执行相关操作,进行流程流转:
具体步骤为:
1>根据条件获取该工作流实例;
2>新增当前操作人记录:
依次记录 工作流实例Id、当前节点编号、当前操作人、是否通过等信息;
需要注意的是:先新增记录,然后判断记录是否保存成功,如果成功保存,才能执行 流实例 的状态转变操作;
3>改变流实例表的值:
当前操作人:赋值为当前用户;
节点:节点由当前节点变为下一节点;
待办人:根据节点表 获取 待办人 信息;
节点之间的流转其实主要涉及的就是待办人这个值的转换,根据下表,可以清楚看到这个转换:
以上三项最为重要,其他一些需要更新的值再次不列出。
然后看一下代码:
/// <summary> /// 同意审批 /// </summary> /// <param name="id"></param> /// <returns></returns> public ActionResult Agree(Guid id) { try { var flow = _context.FlowInstances.FirstOrDefault(x => x.RequisitionId == id);//根据申请单号获取flow实例 //记录当前人员操作记录 var flowRecord = new FlowRecord { Id = Guid.NewGuid() }; flowRecord.WorkId = flow.Id; flowRecord.CurrentNode = flow.NodeName; flowRecord.CurrentNodeSN = _context.Nodes.FirstOrDefault( x => x.NodeName.Equals(flow.NodeName)).NodeSN; flowRecord.Operator = UserInfo.FullName; flowRecord.OperatorId = UserInfo.Id; flowRecord.UpdateTime = DateTime.Now; flowRecord.IsRead = true; flowRecord.IsPass = true; _context.FlowRecords.Add(flowRecord); int saveResult = _context.SaveChanges(); if (saveResult > 0) {
//改变流实例的状态,使之流向下一节点
flow.OperatorId = UserInfo.Id;
flow.Operator = UserInfo.FullName;
flow.NodeSN = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(flow.NodeSN)).NextNodeSN; //当前操作节点的编号变为下一节点的编号
var nextNode = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(flow.NodeSN)).NextNodeSN;
flow.NodeName = _context.Nodes.FirstOrDefault( x => x.NodeSN.Equals(flow.NodeSN)).NodeName;
flow.WFStatus = "已同意";
flow.ToDoerId = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(nextNode)).OperatorId;
flow.ToDoer = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(nextNode)).Operator;
flow.UpdateTime = DateTime.Now;
flow.Operated = flow.Operated + '@' + UserInfo.Id.ToString();
_context.FlowInstances.AddOrUpdate(flow);
int i = _context.SaveChanges(); if (i > 0) { return Json(new { Result = true }); } else { throw new Exception("提交失败"); } } else { throw new Exception("提交失败"); } } catch (Exception exception) { return Json(new { Result = false, exception.Message }); } }
看一下数据库:
实例表:
记录表:
然后看一下现象:
仍登录李四账号,发现,李四的待办事项里已经没有数据了;
然后,登录张三的账号,查看其待办事项:
发现刚才的流程已经流转到张三的账号里了。
总结一下:工作流的实现思路,只是干巴巴的看的话,觉得挺复杂,挺难以捉摸的,但是实际操作一遍,发现也并不难,关键就在于上面三张表的设计以及流程流转时,各个值的状态应该如何转变,由此我也得到一点感悟,看一千遍不如动手做一遍,在学习的过程中,实践真的很重要,切忌纸上谈兵,脱离现实。
以上代码尚不完善,也没有经过测试,可能存在一些bug,重点看思路,我很菜,写的也很混乱,觉得写的不好的,请多多指出我的缺点,非常感谢!