開篇前的廢話:工作流是我們在做互聯網應用開發時經常需要用到的一種技術,復雜的工作流我們基本是借助一些開源的 工作流項目來做,比如 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,重點看思路,我很菜,寫的也很混亂,覺得寫的不好的,請多多指出我的缺點,非常感謝!
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan