说说在 jBPM 工作流中如何实现【会签】功能

会签(会审),指的是在流程中某个业务需要经过多人表决,并且根据表决意见的汇总结果以及设定的规则,来决定流程的走向,它是审批流程中常见的需求。

会签可以分为两种:

  1. 单步 - 只使用一个活动来处理。
  2. 多步 - 由多个活动所组成的。

单步会签比较常见,也容易实现,主要的解决方案是在会签活动的主任务基础上,动态创建若干个子任务来实现,它的具体解决方案是:

  1. 编写专门用于会签活动的任务分配处理器(实现 AssignmentHandler),在这个处理器中,通过流程变量来获取参加会议的用户 id,并为这些用户动态地创建 “会签任务” 对象。
  2. 编写完成 “会签任务” 的命令,在命令中设定会签的业务逻辑。

会签的业务逻辑有以下 4 种情况:

情况 说明
一票否决制 参加会签的用户中,只要有一个人不同意,会签就结束,进入 “会签否决” 转移;如果所有人都同意,则进入 “会签通过” 转移。
一票通过制 逻辑与 “一票否决制”相反。
按比例否决制 全部参加会签的用户提交任务后,根据提交的意见,按比例来决定是否进入 “会签否决” 转移。
意见收集制 简单,全部会签用户通过任务表单提交任务后,收集这些意见,然后结束任务。

多步会签,相对较复杂,建议使用动态创建子流程的方式来实现。

至于更复杂的业务场景,比如将第三方业务系统接入会签,可以考虑使用 JMS 活动来发送消息,并监听第三方业务系统的应答模式,异步地实现会签需求。

会签流程定义

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process name="JointlySign" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start1" g="166,199,48,48">
      <transition to="会签"/>
   </start>
   <task name="会签" g="259,196,92,52">
      <!-- 定义任务分配处理器,它会根据参与者(participants)动态地创建出相应的子任务-->
      <assignment-handler class="net.deniro.jbpm.JointSignAssignment">
         <!-- 这里,也可以为 participants 设定为流程变量的值,这样就可以动态地决定参与会签的用户啦(通过上一步任务表单来选定会签的任务)-->
         <field name="participants">
            <list>
               <string value="Deniro"/>
               <string value="Jack"/>
               <string value="Lucy"/>
            </list>
         </field>
      </assignment-handler>
      <!-- 会签否决-->
      <transition name="to end" to="end1"/>
      <!-- 会签通过,则进入【执行】-->
      <transition name="to execute" to="执行"/>
   </task>
   <state name="执行" g="365,268,92,52">
      <transition to="end1"/>
   </state>
   <end name="end1" g="508,199,48,48"/>
</process>
  • assignment-handler 定义了任务分配处理器,它会根据参与者(participants)动态地创建出相应的子任务。
  • 可以在 participants 设定为流程变量的值,这样就可以动态地决定参与会签的用户啦(通过上一步任务表单来选定会签的任务)
  • 会签被否决,则流程结束。
  • 全体通过会签,则进入【执行】活动。

会签活动的任务处理器:

public class JointSignAssignment implements AssignmentHandler {

    //会签参与者 ID 列表(在流程定义中注入)
    private List<String> participants;

    //任务服务
    private final static TaskService taskService = Configuration.getProcessEngine()
            .getTaskService();

    @Override
    public void assign(Assignable assignable, OpenExecution execution) throws Exception {
        String instanceId = execution.getProcessInstance().getId();

        //获取会签活动任务对象
        Task task = taskService.createTaskQuery().processInstanceId(instanceId)
                .activityName(execution.getName()).uniqueResult();

        //创建会签子任务
        createSubTasks3(task);
    }

  ...
}

创建会签子任务有三种实现方法。

基于主任务:

private void createSubTasks(Task task) {
    if (participants == null) {
        return;
    }

    for (String participant : participants) {
        //基于主任务,创建会签子任务
        Task subTask = taskService.newTask(task.getId());

        //设置会签参与者为子任务的可处理者
        subTask.setAssignee(participant);
        taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
    }
}

这样做有问题,因为这个 taskService.newTask() 方法会立即持久化子任务及其历史,而此时的主任务还未提交。因此这样创建的子任务无法关联到主任务,会抛出持久化异常。

脱离主任务:

private void createSubTasks2(Task task) {
    if (participants == null) {
        return;
    }

    for (String participant : participants) {
        //创建独立的会签子任务
        Task subTask = taskService.newTask();

        //设置会签参与者为子任务的可处理者
        subTask.setAssignee(participant);
        taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
    }
}

这样也有问题,因为这样凭空创建的任务,虽然不会在持久化时出现异常,但它无法关联到主任务。这样创建的任务其实是孤立的,这在后续的会签操作、级联删除以及历史分析,都会出现很大的问题。

使用主任务的 Task 对象:

private void createSubTasks3(Task task) {
    if (participants == null) {
        return;
    }

    for (String participant : participants) {
        //使用主任务的 Task 对象的 createSubTask 方法(不会持久化)来创建会签子任务
        Task subTask = ((OpenTask)task).createSubTask();

        //设置会签参与者为子任务的可处理者
        subTask.setAssignee(participant);

        //关联会签任务到主任务
        taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
    }
}

这样做既可以关联到主任务以及流程实例,又可以随着主任务一同被持久化。就选这种方法啦O(∩_∩)O哈哈~

基于【一票否决制】的会签任务提交命令类设计如下:

public class SubmitJoinSignTaskCmd implements Command<Boolean> {

    //传递会签意见的任务变量
    public static final String VAR_SIGN = "sign";

    //会签通过时的转移路径名称(由构造函数传入)
    private String passTransitionName;

    //会签否决时的转移路径名称(由构造函数传入)
    private String noPassTransitionName;

    //主任务 ID(由构造函数传入)
    private String mainTaskId;

    //主任务对象
    private Task mainTask;

    //流程实例 ID
    private String instanceId;

    //会签任务对象(Setter 方法传入)
    private Task joinSignTask;

    public void setJoinSignTask(Task joinSignTask) {
        this.joinSignTask = joinSignTask;
    }

    public String getInstanceId() {
        return instanceId;
    }

    public SubmitJoinSignTaskCmd(String mainTaskId, String passTransitionName, String noPassTransitionName) {
        this.mainTaskId = mainTaskId;
        this.passTransitionName = passTransitionName;
        this.noPassTransitionName = noPassTransitionName;
    }

    @Override
    public Boolean execute(Environment environment) throws Exception {
        //获取任务服务
        TaskService taskService = environment.get(TaskService.class);

        //获取主任务与流程实例 ID
        mainTask = taskService.getTask(mainTaskId);
        instanceId = mainTask.getExecutionId();

        //获取当前会签任务
        String joinSignTaskId = joinSignTask.getId();

        //从当前会签任务的任务变量中获取 ”会签意见“
        String sign = (String) taskService.getVariable(joinSignTaskId, VAR_SIGN);

        //规则如下:如果意见为“不同意”,则表示否决
        if (sign != null && sign.equals("不同意")) {
            //存在否决意见,则会签活动结束(一票否决制)
            //结束会签任务
            taskService.completeTask(joinSignTaskId);

            //为主任务增加一条会签意见记录(注释)
            taskService.addTaskComment(mainTaskId, "用户:" + joinSignTask.getAssignee()
                    + ",会签意见:" + sign);

            //结束主任务,流向【否决】转移
            taskService.completeTask(mainTaskId, noPassTransitionName);

            //会签结束
            return true;
        }

        /**
         * 通过会签
         */
        //完成会签任务
        taskService.completeTask(joinSignTaskId);

        //为主任务增加一条会签意见
        taskService.addTaskComment(mainTaskId,"用户:"+joinSignTask.getAssignee()+
                ";会签意见为:"+sign);

        //判定是否还有会签子任务
        if(taskService.getSubTasks(mainTaskId).isEmpty()){//通过会签
            //结束主任务,流向会签通过转移
            taskService.completeTask(mainTaskId,passTransitionName);
            return true;
        }else{//会签活动还未结束
            return false;
        }
    }
}

单元测试:

//发起流程实例
ProcessInstance processInstance=executionService.startProcessInstanceByKey("JointlySign");
instanceId=processInstance.getId();//实例 ID

//获取会签主任务
Task task=taskService.createTaskQuery().processInstanceId(instanceId)
        .activityName(processInstance.findActiveActivityNames().iterator().next()
        ).uniqueResult();
//断言当前活动为会签
assertTrue(processInstance.isActive("会签"));

List<Task> subTasks=taskService.getSubTasks(task.getId());
//断言主任务产生了 3 条子任务
assertEquals(3, subTasks.size());


//获取主任务 ID
String taskId=task.getId();
//创建会签任务命令,指定会签通过转移以及会签否决转移
cmd=new SubmitJoinSignTaskCmd(taskId,"to execute","to end");

至此又分为两种情况:

否决会签:

//获取会签用户 Deniro 的任务
Task deniroTask=taskService.findPersonalTasks("Deniro").get(0);

//通过变量来模拟否决会签
Map<String,Object> vars=new HashMap<>();
vars.put(SubmitJoinSignTaskCmd.VAR_SIGN,"不同意");
taskService.setVariables(deniroTask.getId(), vars);
cmd.setJoinSignTask(deniroTask);

//提交会签任务(执行自定义命令)
boolean result= Configuration.getProcessEngine().execute(cmd);

//断言会签活动已完成
assertTrue(result);

//断言流程实例结束
assertProcessInstanceEnded(cmd.getInstanceId());

通过会签:

//获取会签用户 Deniro 的任务
Task deniroTask=taskService.findPersonalTasks("Deniro").get(0);
cmd.setJoinSignTask(deniroTask);//不设置否决意见,即通过

//提交会签任务(执行自定义命令)
boolean result= Configuration.getProcessEngine().execute(cmd);

//断言会签任务未完成
assertFalse(result);

//获取会签用户 Jack 的任务
Task jackTask=taskService.findPersonalTasks("Jack").get(0);
cmd.setJoinSignTask(jackTask);//不设置否决意见,即通过

//提交会签任务(执行自定义命令)
result= Configuration.getProcessEngine().execute(cmd);

//断言会签任务未完成
assertFalse(result);

//获取会签用户 Lucy 的任务
Task lucyTask=taskService.findPersonalTasks("Lucy").get(0);
cmd.setJoinSignTask(lucyTask);//不设置否决意见,即通过

//提交会签任务(执行自定义命令)
result= Configuration.getProcessEngine().execute(cmd);

//断言会签活动已完成
assertTrue(result);

//断言流程实例到达【执行】活动
ProcessInstance processInstance=executionService.findProcessInstanceById
        (instanceId);
assertTrue(processInstance.isActive("执行"));

//完成【执行】活动
String executionId=processInstance.findActiveExecutionIn("执行").getId();
executionService.signalExecutionById(executionId);

//断言流程实例结束
assertProcessInstanceEnded(instanceId);

现在清楚了吧O(∩_∩)O哈哈~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容