.NET Core微服务 权限系统+工作流(二)工作流系统
一、前言
接上一篇 .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文件,并写一些方便读取节点、连线的方法
/// <summary>
/// workflow context
/// </summary>
public class MsWorkFlowContext : WorkFlowContext
{
/// <summary>
/// 构造器传参
/// </summary>
/// <param name="dbworkflow"></param>
public MsWorkFlowContext(WorkFlow dbworkflow)
{
if (dbworkflow.FlowId == default(Guid))
{
throw new ArgumentNullException("FlowId", " input workflow flowid is null");
}
if (dbworkflow.FlowJSON.IsNullOrEmpty())
{
throw new ArgumentException("FlowJSON", "input workflow json is null");
}
if (dbworkflow.ActivityNodeId == null)
{
throw new ArgumentException("ActivityNodeId", "input workflow ActivityNodeId is null");
} this.WorkFlow = dbworkflow; dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
//获取节点
this.WorkFlow.Nodes = this.GetNodes(jsonobj.nodes);
//获取连线
this.WorkFlow.Lines = this.GetFromLines(jsonobj.lines); this.WorkFlow.ActivityNodeId = dbworkflow.ActivityNodeId == default(Guid) ? this.WorkFlow.StartNodeId : dbworkflow.ActivityNodeId; this.WorkFlow.ActivityNodeType = this.GetNodeType(this.WorkFlow.ActivityNodeId); //会签开始节点和流程结束节点没有下一步
if (this.WorkFlow.ActivityNodeType == WorkFlowInstanceNodeType.ChatNode || this.WorkFlow.ActivityNodeType == WorkFlowInstanceNodeType.EndRound)
{
this.WorkFlow.NextNodeId = default(Guid);//未找到节点
this.WorkFlow.NextNodeType = WorkFlowInstanceNodeType.NotRun;
}
else
{
var nodeids = this.GetNextNodeId(this.WorkFlow.ActivityNodeId);
if (nodeids.Count == )
{
this.WorkFlow.NextNodeId = nodeids[];
this.WorkFlow.NextNodeType = this.GetNodeType(this.WorkFlow.NextNodeId);
}
else
{
//多个下个节点情况
this.WorkFlow.NextNodeId = default(Guid);
this.WorkFlow.NextNodeType = WorkFlowInstanceNodeType.NotRun;
}
}
} /// <summary>
/// 下个节点是否是多个
/// </summary>
public bool IsMultipleNextNode { get; set; } /// <summary>
/// 获取节点集合
/// </summary>
/// <param name="nodesobj"></param>
/// <returns></returns>
private Dictionary<Guid, FlowNode> GetNodes(dynamic nodesobj)
{
Dictionary<Guid, FlowNode> nodes = new Dictionary<Guid, FlowNode>(); foreach (JObject item in nodesobj)
{
FlowNode node = item.ToObject<FlowNode>();
if (!nodes.ContainsKey(node.Id))
{
nodes.Add(node.Id, node);
}
if (node.Type == FlowNode.START)
{
this.WorkFlow.StartNodeId = node.Id;
}
}
return nodes;
} /// <summary>
/// 获取工作流节点及以节点为出发点的流程
/// </summary>
/// <param name="linesobj"></param>
/// <returns></returns>
private Dictionary<Guid, List<FlowLine>> GetFromLines(dynamic linesobj)
{
Dictionary<Guid, List<FlowLine>> lines = new Dictionary<Guid, List<FlowLine>>(); foreach (JObject item in linesobj)
{
FlowLine line = item.ToObject<FlowLine>(); if (!lines.ContainsKey(line.From))
{
lines.Add(line.From, new List<FlowLine> { line });
}
else
{
lines[line.From].Add(line);
}
} return lines;
} /// <summary>
/// 获取全部流程线
/// </summary>
/// <returns></returns>
public List<FlowLine> GetAllLines()
{
dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
List<FlowLine> lines = new List<FlowLine>();
foreach (JObject item in jsonobj.lines)
{
FlowLine line = item.ToObject<FlowLine>();
lines.Add(line);
}
return lines;
} /// <summary>
/// 根据节点ID获取From(流入的线条)
/// </summary>
/// <param name="nodeid"></param>
/// <returns></returns>
public List<FlowLine> GetLinesForFrom(Guid nodeid)
{
var lines = GetAllLines().Where(m => m.To == nodeid).ToList();
return lines;
} public List<FlowLine> GetLinesForTo(Guid nodeid)
{
var lines = GetAllLines().Where(m => m.From == nodeid).ToList();
return lines;
} /// <summary>
/// 获取全部节点
/// </summary>
/// <returns></returns>
public List<FlowNode> GetAllNodes()
{
dynamic jsonobj = JsonConvert.DeserializeObject(this.WorkFlow.FlowJSON);
List<FlowNode> nodes = new List<FlowNode>();
foreach (JObject item in jsonobj.nodes)
{
FlowNode node = item.ToObject<FlowNode>();
nodes.Add(node);
}
return nodes;
} /// <summary>
/// 根据节点ID获取节点类型
/// </summary>
/// <param name="nodeId"></param>
/// <returns></returns>
public WorkFlowInstanceNodeType GetNodeType(Guid nodeId)
{
var _thisnode = this.WorkFlow.Nodes[nodeId];
return _thisnode.NodeType();
} /// <summary>
/// 根据节点id获取下个节点id
/// </summary>
/// <param name="nodeId"></param>
/// <returns></returns>
public List<Guid> GetNextNodeId(Guid nodeId)
{
List<FlowLine> lines = this.WorkFlow.Lines[nodeId];
if (lines.Count > )
{
this.IsMultipleNextNode = true;
}
return lines.Select(m => m.To).ToList();
} /// <summary>
/// 节点驳回
/// </summary>
/// <param name="rejectType">驳回节点类型</param>
/// <param name="rejectNodeid">要驳回到的节点</param>
/// <returns></returns>
public Guid RejectNode(NodeRejectType rejectType, Guid? rejectNodeid)
{
switch (rejectType)
{
case NodeRejectType.PreviousStep:
return this.WorkFlow.PreviousId;
case NodeRejectType.FirstStep:
var startNextNodeId = this.GetNextNodeId(this.WorkFlow.StartNodeId).First();
return startNextNodeId;
case NodeRejectType.ForOneStep:
if (rejectNodeid == null || rejectNodeid == default(Guid))
{
throw new Exception("驳回节点没有值!");
}
var fornode = this.WorkFlow.Nodes[rejectNodeid.Value];
return fornode.Id;
case NodeRejectType.UnHandled:
default:
return this.WorkFlow.PreviousId;
}
} }
流程流转代码(主要部分):这段代码是处理流转核心功能,只完成了部分核心功能
/// <summary>
/// 流程过程流转处理
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public async Task<WorkFlowResult> ProcessTransitionFlowAsync(WorkFlowProcessTransition model)
{
WorkFlowResult result = new WorkFlowResult();
switch (model.MenuType)
{
case WorkFlowMenu.Submit:
break;
case WorkFlowMenu.ReSubmit:
result = await ProcessTransitionReSubmitAsync(model);
break;
case WorkFlowMenu.Agree:
result = await ProcessTransitionAgreeAsync(model);
break;
case WorkFlowMenu.Deprecate:
result = await ProcessTransitionDeprecateAsync(model);
break;
case WorkFlowMenu.Back:
result = await ProcessTransitionBackAsync(model);
break;
case WorkFlowMenu.Stop://刚开始提交,下一个节点未审批情况,流程发起人可以终止
result = await ProcessTransitionStopAsync(model);
break;
case WorkFlowMenu.Cancel:
break;
case WorkFlowMenu.Throgh:
break;
case WorkFlowMenu.Assign:
break;
case WorkFlowMenu.View:
break;
case WorkFlowMenu.FlowImage:
break;
case WorkFlowMenu.Approval:
break;
case WorkFlowMenu.CC:
break;
case WorkFlowMenu.Suspend:
break;
case WorkFlowMenu.Resume:
break;
case WorkFlowMenu.Save:
case WorkFlowMenu.Return:
default:
result = WorkFlowResult.Error("未找到匹配按钮!");
break;
}
return result;
}
如果以定制表单关联流程的方式开发,会遇到一个重要问题:流程状态如何与表单同步?因为工作流与业务流是区分开的,怎么办?
我的做法是(以请假为例):让实体先继承流程状态实体,通过CAP的方式推送和订阅,我以前的公司工作流是通过页面回调的方式实现,我感觉这个很不靠谱,实际上也是经常出问题
流程状态的判断:WfWorkflowInstance实体下的两个字段, 这块可能不太好理解,尤其是没有开发过的朋友,简单解释下:IsFinish 是表示流程运行的状态,Status表示用户操作流程的状态,我们判断这个流程是否结束不能单纯的判断根据IsFinish进行判断,
举个例子(请假):
我提交了一个请假申请==>下个节点审批不同意。你说这个流程有没有结束?当然结束了,只不过它没有审批通过而已。简而言之,IsFinish表示流程流转是否结束,即是否最终到了最后一个结束节点。
#region 结合起来判断流程是否结束
/* 流转状态判断 实际情况组合
* IsFinish=1 & Status=WorkFlowStatus.IsFinish 表示通过
* IsFinish==null & Status=WorkFlowStatus.UnSubmit 表示未提交
* IsFinish=0 & Status=WorkFlowStatus.Running 表示运行中
* IsFinish=0 & Status=WorkFlowStatus.Deprecate 表示不同意
* IsFinish=0 & Status=WorkFlowStatus.Back 表示流程被退回
* **/
/// <summary>
/// 流程节点是否结束
/// 注:此字段代表工作流流转过程中运行的状态判断
/// </summary>
public int? IsFinish { get; set; } /// <summary>
/// 用户操作状态<see cref="WorkFlowStatus"/>
/// 注:此字段代表用户操作流程的状态
/// </summary>
public int Status { get; set; } #endregion
至于页面审批按钮的展示,因为这个功能是公用的,我把它写在了组件里面,共两个菜单组件,一个是定制一个是系统生成,代码稍微有些不同,组件视图代码比较多,就不展示了。
下面走一个不同意的请假流程:
1、wms账号先选择要发起的流程
2、流程发起界面
3、流程提交之后的界面,注:终止:当用户提交表单之后,下个节点未进行审批的时候,流程发起人有权终止(取消流程)
4、wangwu账号登录
5、结果展示
6、审批意见查看
7、流程图查看,绿色节点表示流程当前节点。
8、也可以在OA员工请假看到结果
注:因为工作流引擎不涉及具体的业务逻辑,通常与OA系统进行表单绑定,所以我建了OA服务,并简单写了个请假流程方便测试。工作流依赖于之前的权限系统,如果登录人员显示没有权限,请先进行授权
四、结束
每个程序员刚毕业的时候都有一种我要独立写一个超级牛逼系统的冲动,我也是,都不记得多少年了,断断续续坚持到现在,虽然不算完善,更谈不上多么牛逼,写这两篇算是给自己一个交代吧。如果大家觉得有研究价值的话,我会继续升级迭代。
运行方式参考 上一篇 (末尾)
管理员登录账号wms,密码:所有账号密码都是123
代码地址:
https://github.com/wangmaosheng/MsSystem-BPM-ServiceAndWebApps
如果觉得有点作用的话,可以 start 下,后续会持续更新。
欢迎加微信讨论,共同进步(妹子更好哟@--@)
.NET Core微服务 权限系统+工作流(二)工作流系统的更多相关文章
- .NET Core微服务 权限系统+工作流(一)权限系统
一.前言 实际上权限系统老早之前我就在一直开发,大概在刚毕业没多久就想一个人写一个系统,断断续续一直坚持到现在,毕竟自己亲动手自写的系统才有收获,本篇仅介绍权限. 小小系统上不了台面,望各位大神勿喷. ...
- NET Core微服务之路:实战SkyWalking+Exceptionless体验生产环境下的追踪系统
前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的问题: 从APM上说,知道某个节点出现异常,或延迟过过高,却不能及时知道日志反馈情况,总不可能去相应的节点 ...
- NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统
原文:NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统 前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的 ...
- 【NET CORE微服务一条龙应用】第三章 认证授权与动态权限配置
介绍 系列目录:[NET CORE微服务一条龙应用]开始篇与目录 在微服务的应用中,统一的认证授权是必不可少的组件,本文将介绍微服务中网关和子服务如何使用统一的权限认证 主要介绍内容为: 1.子服务如 ...
- .NET Core微服务二:Ocelot API网关
.NET Core微服务一:Consul服务中心 .NET Core微服务二:Ocelot API网关 .NET Core微服务三:polly熔断与降级 本文的项目代码,在文章结尾处可以下载. 本文使 ...
- .Net Core微服务入门全纪录(二)——Consul-服务注册与发现(上)
前言 上一篇[.Net Core微服务入门全纪录(一)--项目搭建]讲到要做到服务的灵活伸缩,那么需要有一种机制来实现它,这个机制就是服务注册与发现.当然这也并不是必要的,如果你的服务实例很少,并且很 ...
- .NET Core 微服务—API网关(Ocelot) 教程 [二]
上篇文章(.NET Core 微服务—API网关(Ocelot) 教程 [一])介绍了Ocelot 的相关介绍. 接下来就一起来看如何使用,让它运行起来. 环境准备 为了验证Ocelot 网关效果,我 ...
- .NET Core微服务系列基础文章索引(目录导航Final版)
一.为啥要总结和收集这个系列? 今年从原来的Team里面被抽出来加入了新的Team,开始做Java微服务的开发工作,接触了Spring Boot, Spring Cloud等技术栈,对微服务这种架构有 ...
- .NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构总览 这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为 ...
随机推荐
- JAVA 1.7并发之Fork/Join框架
在之前的博文里有说过executor框架,其实Fork/Join就是继承executor的升级版啦 executor用于创建一个线程池,但是需要手动的添加任务,如果需要将大型任务分治,显然比较麻烦 而 ...
- wpf 可视化树的注意点
将控件的visibility 设置为collapse时,控件的可视化树并未生成,此时并不能查找到控件内的子控件. 若某控件想对用户不可见,且可以生成可视化树,则需将控件的visibility 设置为h ...
- Python命令模块argparse学习笔记(四)
默认参数 ArgumentParser.set_defaults(**kwargs) set_defaults()可以设置一些参数的默认值 >>> parser = argparse ...
- linux日常管理-查看系统负载
查看系统的负载常用命令w 16:32::15是系统时间 up 16 min 是开机使用时间 1 user 是登录的用户数 重要 load average:0.00 0.00 0.00 负载分别表示1分 ...
- javadoc 工具生成开发API文档
=====================先来一点成就感===================== package com.springMybatis.dao; import com.springMy ...
- Tomcat 服务器详解
工具/原料 1.JDK:版本为jdk-7-windows-i586.exe 下载地址 http://www.oracle.com/technetwork/java/javase/download ...
- 菜鸟攻城狮3(Holle World)
1.创建一个HolleWorld.java文本文件 2.代码:public class HolleWorld { public static void main(String[] args) { Sy ...
- 从Github远程库安装Node.JS
3)从Github远程库安装Node.JS在这个方法中我们需要一些步骤来把Node.js的从Github上的远程的仓库克隆到本地仓库目录 在开始克隆(克隆)包到本地并且配制之前,我们要先安装以下依赖包 ...
- sklearn解决过拟合的例子
Learning curve 检视过拟合 sklearn.learning_curve 中的 learning curve 可以很直观的看出我们的 model 学习的进度, 对比发现有没有 overf ...
- BSGS(大小步)算法
BSGS算法主要用于求解形如ax≡b(mod p)的式子中x的值. 在这里我们不妨设 x=k1*n-k2 这时我们就可以将式子转化为 ak1*n≡b*ak2(mod p) 这里的n我们设为√p,所以我 ...