流程审批系统设计思路及实现方法

背景

流程审批系统是一个很常见的系统,包括我们在日常权限申请,订单状态流转等很多场景都会接触。其核心的点有两个:1.状态流转 2.流程驱动

我们以我们常见的流程审批为例,比如我想向上街申请一台新电脑,解释整个流程。
状态流转:也就是我们从发起审批流程到上级审批再到最终通过或者驳回。
流程驱动:流程驱动既是我们点了申请之后,后续操作我们只需等着就好,一条审批会按照既定的过程完成审批,对于接入的开发理解就是调用了api之后我就可以等着回调了。

1. 流程审批分析

具体的审批API在每个公司内部都有提供,这里抽象处理。

流程审批的过程是申请人填写审批人向一个通用的审批API发起审批流程,审批API向负责人发送审批申请,负责人对申请进行处理,审批API根据审批结果进行回调,如图:
这里写图片描述

上述例子是单人审批,多人审批将重复上述流程,直到最终拿到审批结果。

流程审批可以抽象为pipeline,审批人是其中的节点(node),在普通的链式审批模式中,审批节点从头开始构成一条链表,而复杂的审批流程中,可以加入线段(line),将链表拓展成为有向图,其中node定义审批人信息,line控制审批流向,pipeline驱动流程。
链式模型图:
这里写图片描述
有向图模型图:
这里写图片描述
其本质可以抽象成pipeline,通过myoa事件进行驱动,最后根据事件结果结束pipeline。

2. 实现思路

整个流程审批系统分为三个部分:

  1. 核心模块-提供核心的流程控制以及单据流转。
  2. 持久化模块-提供单据持久化功能。
  3. 调度模块-失败重试,定时调度等任务。

2.1 核心模块

其中核心模块定义:

Flow:一条审批流程

Node:审批节点,审批节点可能是静态或者是动态的,静态节点是指申请时输入审批人,而动态节点则是拉取申请人上级或是上级审批人上级,不同的申请人审批人可能不同。同时一个审批节点可能包含多个审批人,因此需要设置审批通过阈值。

NodeHandler:审批节点的处理器,每一种类型Node都将省成不同的NodeHandler,NodeHandler负责处理当前Node的流转以及结果判断。

NodeResult:NodeHandler处理结果,分为:Continue,Abort,End,Aysnc,Ignore五类。其中Aysnc需要将更新后的FlowInstance和Node持久化。

FlowInstance:一条审批流程对应的框架实例,整个框架中驱动的是FlowInstance,FlowInstance中包含了完整的原始申请单据,以及每次审批意见。对开发者使用。

FlowEngine:流程驱动器,负责驱动所有的FlowInstance,每个FlowInstance中Node流转都在FlowEngine中处理。

关于pipeline的设计参考了Netty的ChannelPipeline,为每一条pipeline设置一条双向Node链表,其中head和tail是初始化时生成的。链表中的Node对应于Netty的ChannelHandlerContext,而NodeHandler则对应ChannelHandler。FlowEngine则对应于DefaultChannelPipeline。

核心流程代码:

public void process(FlowInstance instance) {
   FlowNode node = instance.getCurNode();
   while (node != null) {
      NodeHandler handler = map.get(node.getClass());
      if (null != handler) {
         instance.setCurNode(node);
         NodeResult r = handler.handle(instance);
         if (r==NodeResult.Aysnc){
            this.save(instance);
            return;
         }

         if (r == NodeResult.Abort || r == NodeResult.End) {
            instance.setResult(r);
            return;
         }

         if (r == NodeResult.Continue) {
            node = node.next;
         }
      }
   }
   instance.setResult(NodeResult.End);
}

核心流程反序列化代码:

public void resume(String flowInstance, MyOACallbackItem callbackItem) {
       FlowInstance instance = load(flowInstance);
       instance.setMyOACallbackItem(callbackItem);
       FlowNode node = instance.getCurNode();
   NodeResult r;
       if (node instanceof SimpleMyOANode) {
      r = ((SimpleMyOANode) node).judge(callbackItem);
      DBHelper.storeFlowNode((SimpleMyOANode)node);
      save(instance);

      if (r == NodeResult.Abort || r == NodeResult.End) {
         logger.info("flow end :" + flowInstance);
         instance.setResult(r);
         return;
      }

      if (r == NodeResult.Ignore) {

      }

      if (r == NodeResult.Continue) {
         instance.setCurNode(node.next);
         process(instance);
      }
   }
}

2.2 持久化模块

持久化分为两个部分:

  1. 流程中实例的持久化。
  2. 用户定义数据的持久化。

为什么需要持久化模块,因为上述任何一个类型都是内存中的一个实例,机器重启将会被销毁,因此需要将每个实例在变化之后立即持久化。持久化的选择可以多种多样,结合管理后台的使用场景,DB是非常合适的。head节点的任务就是将Node和FlowInstance持久化,并驱动流程至用户定义节点。

相比于框架层面的持久化,用户层面的持久化是指用户在创建流程时传入的回调类。回调类的持久化比较简单,但是从DB中如何反序列化是一个问题,newInstance对框架是一个黑盒,用户的回调类可能注入了很多bean,因此需要借助spring等容器来实现。

2.3调度模块

调度模块主要负责对状态异常的节点进行定时修复。对于回调请求丢失或者是提交单据失败的情况进行定时恢复。

3 具体实例

myoa是腾讯内部提供的一个流程审批http接口,在审批人审批结束后会通过预先设定的http回调,并且可以支持回调原参数。但是其只能支持单人审批,无法实现复杂的审批模型。即其职能完成单个节点的任务。

代码demo

1.提交审核

通过代码的方式主要是构造SubmitItem,其原理与通过页面配置相似

SubmitItem item = new SubmitItem("listViewTitle","listViewDetail","detailViewTitle",
        "detailViewDetail", BusCategory.TEST,handlers,callbackData,"title",rtx);
//函数定义如下
public static String submit(SubmitItem item, Class<? extends FlowInstanceResultListener> callbackClass)

SubmitItem提供了两个构造函数,分别用户处理两种场景。

  1. 单人审批节点,及A->B->C依次审核,依次通过即可完成最后的审核,中途拒绝将终止流程。
  2. 多人审批节点,A、B->C、D->E、F,多人审核,需要超过一个阈值才能进入下一步,例如A、B两人审核,只需其中一人通过即可。

单人审批节点构造器:

public SubmitItem(String listViewTitle, String listViewDetail,
                  String detailViewTitle, String detailViewDetail,
                  BusCategory category, List<String> handlers,
                  Map<String,List<String>> callbackData,
                  String title, String application)

具体参数含义请阅读myoa参数定义,参数名称与json字段一一对应。handlers是一个队列,对应着A,B,C。会先向A发起审核,A通过后向B发起,B拒接则审核关闭,不会再向C发送。

多人审批节点构造器:

public SubmitItem(String listViewTitle, String listViewDetail,
                  String detailViewTitle, String detailViewDetail,
                  List<List<String>> handlerList, BusCategory category,
                  List<Integer> handlerValve, Map<String,List<String>> callbackData,
                  String title,String application)

handlerList是一个二维数组,其中每个一维数组定义着一次审核的人数,如A、B,handlerValve定义着需要通过的阈值,如二人只需通过一个即可valve则为1。A、B节点审核结束后会向C、D发送审核。

2.接收回调函数

审核结束应该回调发起者,然后对审核结果进行判断。审核结果的数据封装在FlowInstance中的MyOACallbackItem,具体字段内容与myoa回调单据一致。

@Controller
public class MyoaDemoController extends MyoaBaseController{
    ...
    @Override
    public void onResult(FlowInstance ins) {
        try {
            List<MyOACallbackItem> list = ins.getMyOACallbackItem();
            MyOACallbackItem item = list.get(0);
            //回调数据,回调数据由提交时传递
            Map<String, List<String>> callbackData = item.getData();
            //审批结果
            NodeResult result = ins.getResult();
        } catch (Exception e) {
            logger.error("MyoaDemoController onresult err {}",e);
        }

    }
}

回调json示例,MyOACallbackItem将存储了整个回调json数据,字段名称一一对应。

{
    "id": "5b1f49880c64f159a4c39612",
    "business_key": "633C74A559724244A185EA88749C6B97:admin_oa_process:4b3cc81a-0fc7-410c-9929-a4c281f5fb43",
    "handler": "zoehzhang",
    "category": "633C74A559724244A185EA88749C6B97",
    "process_name": "admin_oa_process",
    "process_inst_id": "4b3cc81a-0fc7-410c-9929-a4c281f5fb43",
    "activity": "approve",
    "submit_source": "PC",
    "submit_action": "2",
    "submit_action_name": "驳回",
    "submit_opinion": " 【通过PC快速审批】",
    "submit_form": {},
    "data": [{
        "key": "id",
        "value": ["2001"]
    }],
    "create_time": "2018-06-12T12:18:16.479+08:00",
    "submit_time": "2018-06-12T12:19:06.047199031+08:00"
}

代码demo地址:

更复杂的可以参考阿里巴巴的开源项目 https://github.com/alibaba/bulbasaur

猜你喜欢

转载自blog.csdn.net/WSRspirit/article/details/81412344