一、前言

  接上一篇 .NET Core微服务 权限系统+工作流(一)权限系统 ,再来一发

  工作流,我在接触这块开发的时候一直好奇它的实现方式,翻看各种工作流引擎代码,探究其实现方式,个人总结出来一个核心要点:

    实际上工作流引擎处理流转的核心要义是如何解析流转XML或者JSON或者其它持久化方式,工作流通过解析XML或者JSON判断当前节点的状态和下个节点的信息并做出一些处理。感觉等于没说?直白一点,就是通过解析JSON文件得到下一步是谁处理。

  工作流的流转线路实际上是固定死的,排列组合即可知道所有可能的线路,并没有想象中的那么难以理解。理解好这点,那么接下来开发就很简单了,垒代码而已(手动微笑.ing)。本系统着重分析工作流具体的实现方式,不阐述具体的实现步骤,详细代码请看GitHub地址。

二、系统介绍

深入研究过工作流的朋友可能会知道,流程表单它分为两种:

1、定制表单。更加贴近业务,但会累死开发人员。以前的公司都是这种方式开发,这个和具体的业务逻辑有关系,比较复杂的建议使用定制表单方式,即开发人员把业务功能开发完了,与流程关联即可。

2、代码生成的表单。不需要编写代码,系统可自动生成,方便,但是功能扩展性较差。

当然各有好处。本系统两种方式都已经实现,着重阐述定制流程。本系统人为规定:一个流程只能绑定一个表单,一个表单只能绑定一个流程。即一对一,这是一切的前提。至于为什么这么做?

通常情况下一个流程的走向是跟表单逻辑是相挂钩的,基本上不存在多个的可能性,而且容易造成组织错乱,有的话,那就在再画一个流程一个表单。@_^_@

三、工作流实现

还是以面向数据库的方法来开发,先看表:

wf_workflow : 工作流表,存放工作流基本信息

wf_workflow_category : 流程分类表

wf_workflow_form : 流程表单表,分为两种类型,系统生成表单和系统定制表单,系统定制表单只存放URL地址

wf_workflow_instance : 流程实例表,核心

wf_workflow_instance_form : 流程实例表单关联表

wf_workflow_line : 流程连线表。目前之存放两种相反的形式(同意、不同意),后期会添加自定义SQL判断业务逻辑流转节点

wf_workflow_operation_history : 流程操作历史表。用于获取审批意见等

wf_workflow_transition_history : 流程流转记录。用于获取 退回某一步获取节点等。

目前工作流实现了这几个功能:保存、提交、同意、不同意、退回、终止、流程图、审批意见,后期会继续升级迭代,如添加会签、挂起、通知等等,目前这几个功能应该能应付一般业务需求了,像会签这种功能99%用不到,但是确是比较复杂的功能,涉及并行、串行计算方式,80%时间都花在这些用不到的功能上来,所谓的二八法则吧。

全部功能较多,不一一列举了:目前只有流程分类功能没实现,后续再写吧,但是不影响功能使用,只是用于筛选而已

流程设计界面:采用GooFlow插件,并对其代码做出一些修改,界面确实比较难看,设计比较简陋,毕竟本人不会平面设计,如果觉得不丑,就当我没说。

核心代码:实际上就是解析JSON文件,并写一些方便读取节点、连线的方法

  1 /// <summary>
  2     /// workflow context
  3     /// </summary>
  4     public class MsWorkFlowContext : WorkFlowContext
  5     {
  6         /// <summary>
  7         /// 构造器传参
  8         /// </summary>
  9         /// <param name="dbworkflow"></param>
 10         public MsWorkFlowContext(WorkFlow dbworkflow)
 11         {
 12             if (dbworkflow.FlowId == default(Guid))
 13             {
 14                 throw new ArgumentNullException("FlowId", " input workflow flowid is null");
 15             }
 16             if (dbworkflow.FlowJSON.IsNullOrEmpty())
 17             {
 18                 throw new ArgumentException("FlowJSON", "input workflow json is null");
 19             }
 20             if (dbworkflow.ActivityNodeId == null)
 21             {
 22                 throw new ArgumentException("ActivityNodeId", "input workflow ActivityNodeId is null");
 23             }
 24 
 25             this.WorkFlow = dbworkflow;
 26 
 27             dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
 28             //获取节点
 29             this.WorkFlow.Nodes = this.GetNodes(jsonobj.nodes);
 30             //获取连线
 31             this.WorkFlow.Lines = this.GetFromLines(jsonobj.lines);
 32 
 33             this.WorkFlow.ActivityNodeId = dbworkflow.ActivityNodeId == default(Guid) ? this.WorkFlow.StartNodeId : dbworkflow.ActivityNodeId;
 34 
 35             this.WorkFlow.ActivityNodeType = this.GetNodeType(this.WorkFlow.ActivityNodeId);
 36 
 37             //会签开始节点和流程结束节点没有下一步
 38             if (this.WorkFlow.ActivityNodeType == WorkFlowInstanceNodeType.ChatNode || this.WorkFlow.ActivityNodeType == WorkFlowInstanceNodeType.EndRound)
 39             {
 40                 this.WorkFlow.NextNodeId = default(Guid);//未找到节点
 41                 this.WorkFlow.NextNodeType = WorkFlowInstanceNodeType.NotRun;
 42             }
 43             else
 44             {
 45                 var nodeids = this.GetNextNodeId(this.WorkFlow.ActivityNodeId);
 46                 if (nodeids.Count == 1)
 47                 {
 48                     this.WorkFlow.NextNodeId = nodeids[0];
 49                     this.WorkFlow.NextNodeType = this.GetNodeType(this.WorkFlow.NextNodeId);
 50                 }
 51                 else
 52                 {
 53                     //多个下个节点情况
 54                     this.WorkFlow.NextNodeId = default(Guid);
 55                     this.WorkFlow.NextNodeType = WorkFlowInstanceNodeType.NotRun;
 56                 }
 57             }
 58         }
 59 
 60         /// <summary>
 61         /// 下个节点是否是多个
 62         /// </summary>
 63         public bool IsMultipleNextNode { get; set; }
 64 
 65         /// <summary>
 66         /// 获取节点集合
 67         /// </summary>
 68         /// <param name="nodesobj"></param>
 69         /// <returns></returns>
 70         private Dictionary<Guid, FlowNode> GetNodes(dynamic nodesobj)
 71         {
 72             Dictionary<Guid, FlowNode> nodes = new Dictionary<Guid, FlowNode>();
 73 
 74             foreach (JObject item in nodesobj)
 75             {
 76                 FlowNode node = item.ToObject<FlowNode>();
 77                 if (!nodes.ContainsKey(node.Id))
 78                 {
 79                     nodes.Add(node.Id, node);
 80                 }
 81                 if (node.Type == FlowNode.START)
 82                 {
 83                     this.WorkFlow.StartNodeId = node.Id;
 84                 }
 85             }
 86             return nodes;
 87         }
 88 
 89         /// <summary>
 90         /// 获取工作流节点及以节点为出发点的流程
 91         /// </summary>
 92         /// <param name="linesobj"></param>
 93         /// <returns></returns>
 94         private Dictionary<Guid, List<FlowLine>> GetFromLines(dynamic linesobj)
 95         {
 96             Dictionary<Guid, List<FlowLine>> lines = new Dictionary<Guid, List<FlowLine>>();
 97 
 98             foreach (JObject item in linesobj)
 99             {
100                 FlowLine line = item.ToObject<FlowLine>();
101 
102                 if (!lines.ContainsKey(line.From))
103                 {
104                     lines.Add(line.From, new List<FlowLine> { line });
105                 }
106                 else
107                 {
108                     lines[line.From].Add(line);
109                 }
110             }
111 
112             return lines;
113         }
114 
115         /// <summary>
116         /// 获取全部流程线
117         /// </summary>
118         /// <returns></returns>
119         public List<FlowLine> GetAllLines()
120         {
121             dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
122             List<FlowLine> lines = new List<FlowLine>();
123             foreach (JObject item in jsonobj.lines)
124             {
125                 FlowLine line = item.ToObject<FlowLine>();
126                 lines.Add(line);
127             }
128             return lines;
129         }
130 
131         /// <summary>
132         /// 根据节点ID获取From(流入的线条)
133         /// </summary>
134         /// <param name="nodeid"></param>
135         /// <returns></returns>
136         public List<FlowLine> GetLinesForFrom(Guid nodeid)
137         {
138             var lines = GetAllLines().Where(m => m.To == nodeid).ToList();
139             return lines;
140         }
141 
142         public List<FlowLine> GetLinesForTo(Guid nodeid)
143         {
144             var lines = GetAllLines().Where(m => m.From == nodeid).ToList();
145             return lines;
146         }
147 
148         /// <summary>
149         /// 获取全部节点
150         /// </summary>
151         /// <returns></returns>
152         public List<FlowNode> GetAllNodes()
153         {
154             dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
155             List<FlowNode> nodes = new List<FlowNode>();
156             foreach (JObject item in jsonobj.nodes)
157             {
158                 FlowNode node = item.ToObject<FlowNode>();
159                 nodes.Add(node);
160             }
161             return nodes;
162         }
163 
164         /// <summary>
165         /// 根据节点ID获取节点类型
166         /// </summary>
167         /// <param name="nodeId"></param>
168         /// <returns></returns>
169         public WorkFlowInstanceNodeType GetNodeType(Guid nodeId)
170         {
171             var _thisnode = this.WorkFlow.Nodes[nodeId];
172             return _thisnode.NodeType();
173         }
174 
175         /// <summary>
176         /// 根据节点id获取下个节点id
177         /// </summary>
178         /// <param name="nodeId"></param>
179         /// <returns></returns>
180         public List<Guid> GetNextNodeId(Guid nodeId)
181         {
182             List<FlowLine> lines = this.WorkFlow.Lines[nodeId];
183             if (lines.Count > 1)
184             {
185                 this.IsMultipleNextNode = true;
186             }
187             return lines.Select(m => m.To).ToList();
188         }
189 
190         /// <summary>
191         /// 节点驳回
192         /// </summary>
193         /// <param name="rejectType">驳回节点类型</param>
194         /// <param name="rejectNodeid">要驳回到的节点</param>
195         /// <returns></returns>
196         public Guid RejectNode(NodeRejectType rejectType, Guid? rejectNodeid)
197         {
198             switch (rejectType)
199             {
200                 case NodeRejectType.PreviousStep:
201                     return this.WorkFlow.PreviousId;
202                 case NodeRejectType.FirstStep:
203                     var startNextNodeId = this.GetNextNodeId(this.WorkFlow.StartNodeId).First();
204                     return startNextNodeId;
205                 case NodeRejectType.ForOneStep:
206                     if (rejectNodeid == null || rejectNodeid == default(Guid))
207                     {
208                         throw new Exception("驳回节点没有值!");
209                     }
210                     var fornode = this.WorkFlow.Nodes[rejectNodeid.Value];
211                     return fornode.Id;
212                 case NodeRejectType.UnHandled:
213                 default:
214                     return this.WorkFlow.PreviousId;
215             }
216         }
217 
218     }

流程流转代码(主要部分):这段代码是处理流转核心功能,只完成了部分核心功能

 1         /// <summary>
 2         /// 流程过程流转处理
 3         /// </summary>
 4         /// <param name="model"></param>
 5         /// <returns></returns>
 6         public async Task<WorkFlowResult> ProcessTransitionFlowAsync(WorkFlowProcessTransition model)
 7         {
 8             WorkFlowResult result = new WorkFlowResult();
 9             switch (model.MenuType)
10             {
11                 case WorkFlowMenu.Submit:
12                     break;
13                 case WorkFlowMenu.ReSubmit:
14                     result = await ProcessTransitionReSubmitAsync(model);
15                     break;
16                 case WorkFlowMenu.Agree:
17                     result = await ProcessTransitionAgreeAsync(model);
18                     break;
19                 case WorkFlowMenu.Deprecate:
20                     result = await ProcessTransitionDeprecateAsync(model);
21                     break;
22                 case WorkFlowMenu.Back:
23                     result = await ProcessTransitionBackAsync(model);
24                     break;
25                 case WorkFlowMenu.Stop://刚开始提交,下一个节点未审批情况,流程发起人可以终止
26                     result = await ProcessTransitionStopAsync(model);
27                     break;
28                 case WorkFlowMenu.Cancel:
29                     break;
30                 case WorkFlowMenu.Throgh:
31                     break;
32                 case WorkFlowMenu.Assign:
33                     break;
34                 case WorkFlowMenu.View:
35                     break;
36                 case WorkFlowMenu.FlowImage:
37                     break;
38                 case WorkFlowMenu.Approval:
39                     break;
40                 case WorkFlowMenu.CC:
41                     break;
42                 case WorkFlowMenu.Suspend:
43                     break;
44                 case WorkFlowMenu.Resume:
45                     break;
46                 case WorkFlowMenu.Save:
47                 case WorkFlowMenu.Return:
48                 default:
49                     result = WorkFlowResult.Error("未找到匹配按钮!");
50                     break;
51             }
52             return result;
53         }

 

如果以定制表单关联流程的方式开发,会遇到一个重要问题:流程状态如何与表单同步?因为工作流与业务流是区分开的,怎么办?

  我的做法是(以请假为例):让实体先继承流程状态实体,通过CAP的方式推送和订阅,我以前的公司工作流是通过页面回调的方式实现,我感觉这个很不靠谱,实际上也是经常出问题

流程状态的判断:WfWorkflowInstance实体下的两个字段, 这块可能不太好理解,尤其是没有开发过的朋友,简单解释下:IsFinish 是表示流程运行的状态,Status表示用户操作流程的状态,我们判断这个流程是否结束不能单纯的判断根据IsFinish进行判断,

举个例子(请假):

  我提交了一个请假申请==>下个节点审批不同意。你说这个流程有没有结束?当然结束了,只不过它没有审批通过而已。简而言之,IsFinish表示流程流转是否结束,即是否最终到了最后一个结束节点。

 1         #region 结合起来判断流程是否结束
 2         /*              流转状态判断 实际情况组合
 3          * IsFinish=1 & Status=WorkFlowStatus.IsFinish      表示通过
 4          * IsFinish==null & Status=WorkFlowStatus.UnSubmit  表示未提交
 5          * IsFinish=0 & Status=WorkFlowStatus.Running       表示运行中
 6          * IsFinish=0 & Status=WorkFlowStatus.Deprecate     表示不同意
 7          * IsFinish=0 & Status=WorkFlowStatus.Back          表示流程被退回
 8          * **/
 9         /// <summary>
10         /// 流程节点是否结束
11         /// 注:此字段代表工作流流转过程中运行的状态判断
12         /// </summary>
13         public int? IsFinish { get; set; }
14 
15         /// <summary>
16         /// 用户操作状态<see cref="WorkFlowStatus"/>
17         /// 注:此字段代表用户操作流程的状态
18         /// </summary>
19         public int Status { get; set; }
20 
21         #endregion

 

至于页面审批按钮的展示,因为这个功能是公用的,我把它写在了组件里面,共两个菜单组件,一个是定制一个是系统生成,代码稍微有些不同,组件视图代码比较多,就不展示了。

下面走一个不同意的请假流程:

1、wms账号先选择要发起的流程

2、流程发起界面

3、流程提交之后的界面,注:终止:当用户提交表单之后,下个节点未进行审批的时候,流程发起人有权终止(取消流程)

4、wangwu账号登录

5、结果展示

6、审批意见查看

7、流程图查看,绿色节点表示流程当前节点。

8、也可以在OA员工请假看到结果

注:因为工作流引擎不涉及具体的业务逻辑,通常与OA系统进行表单绑定,所以我建了OA服务,并简单写了个请假流程方便测试。工作流依赖于之前的权限系统,如果登录人员显示没有权限,请先进行授权

四、结束

  每个程序员刚毕业的时候都有一种我要独立写一个超级牛逼系统的冲动,我也是,都不记得多少年了,断断续续坚持到现在,虽然不算完善,更谈不上多么牛逼,写这两篇算是给自己一个交代吧。如果大家觉得有研究价值的话,我会继续升级迭代。

运行方式参考 上一篇 (末尾)

管理员登录账号wms,密码:所有账号密码都是123

代码地址:

https://github.com/wangmaosheng/MsSystem-BPM-ServiceAndWebApps

如果觉得有点作用的话,可以 start 下,后续会持续更新。

欢迎加微信讨论,共同进步(妹子更好哟@--@

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!