说说在 jBPM 工作流中如何实现【撤销】功能

撤销(Withdraw):针对当前用户已办理的任务(历史任务),重置回待办状态。

撤销一般发生在以下场景中:当用户办理完任务后,发现办理的任务存在业务错误或者觉得下一节点办理人需要重新选择,那么这时就需要撤销任务啦 O(∩_∩)O哈哈~

注意:如果下一节点办理人已完成任务或者流程可能已经流转了若干节点(甚至走了分支),那么这时执行【撤销】操作,就需要对业务进行很大的补偿。是否必要这么做,需要权衡。

假设,只能【撤销】到上一步,解决方案如下:

  1. 为【撤销】到上一步的活动定义一个【撤销监听器】,动态生成当前活动到上一步活动的转移路径。
  2. 定义【撤销命令】,在命令中结束当前活动的所有任务(清除当前活动的历史痕迹,如果需要的话),然后转移到上一步活动。之所以使用命令模式,是因为这里可能涉及多处的写数据库操作,所以利用命令模式来实现事务的控制。
  3. 如果【撤销】操作引发业务损失,那么需要在【撤销命令】中进行补偿;如果业务损失很严重(比如下一步活动已办理),那么可以拒绝【撤销】操作。

流程定义:

注意:虚线的【撤销】路径是动态生成的。

jPDL:

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

<process name="Withdraw" xmlns="http://jbpm.org/4.4/jpdl">
    <start name="start1" g="124,211,48,48">
        <transition to="申请"/>
    </start>
    <end name="end1" g="487,213,48,48"/>
    <task name="申请" g="213,207,92,52" assignee="Deniro">
        <transition to="审核">
            <!-- 设置撤销监听器-->
            <event-listener class="net.deniro.jbpm.WithdrawListener"/>
        </transition>
    </task>
    <task name="审核" g="343,209,92,52" assignee="Jack">
        <transition to="end1"/>
    </task>
</process>

在【审核】的转移路径上设置了撤销监听器,监听器定义如下:

public class WithdrawListener implements EventListener {

    /**
     * 动态创建【撤销】路径
     *
     * @param execution
     * @throws Exception
     */
    @Override
    public void notify(EventListenerExecution execution) throws Exception {
        TransitionImpl transition = ((ExecutionImpl) execution).getTransition();

        //【撤销】操作的源活动
        ActivityImpl from = transition.getDestination();

        //【撤销】操作的目标活动
        ActivityImpl to = transition.getSource();

        //合理性判断
        if (from == null || to == null) {
            throw new Exception("无法【撤销】");
        }

        //动态创建转移路径
        TransitionImpl withdrawTransition = from.createOutgoingTransition();
        withdrawTransition.setName(from.getName() + " 撤销到 " + to.getName());
        withdrawTransition.setDestination(to);
    }
}

通过构造函数,传入流程实例与撤销目标活动名称。

撤销命令:

public class WithdrawCommand implements Command<Void> {

    /**
     * 流程实例 ID
     */
    private String processId;

    /**
     * 目标活动名称
     */
    private String targetActivityName;

    /**
     * @param processId
     * @param targetActivityName
     */
    public WithdrawCommand(String processId, String targetActivityName) {
        this.processId = processId;
        this.targetActivityName = targetActivityName;
    }

    @Override
    public Void execute(Environment environment) throws Exception {

        /**
         * 获取当前活动名称
         */
        ExecutionService executionService = environment.get(ExecutionService.class);
        Execution execution = executionService.findExecutionById(processId);
        Set<String> activityNames = execution.findActiveActivityNames();
        //合理性判断
        if (activityNames == null || activityNames.isEmpty()) {
            throw new Exception("无法获取当前活动名称");
        }
        if (activityNames.size() > 1) {
            throw new Exception("存在多个当前活动");
        }
        String currentActivityName = activityNames.iterator().next();//当前活动名称

        /**
         * 执行撤销任务
         */
        String transitionName = currentActivityName + " 撤销到 " + targetActivityName;//转移路径名称
        TaskService taskService = environment.get(TaskService.class);
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processId)
                .activityName(currentActivityName).list();//获取当前活动的任务
        for (Task task : tasks) {//撤销任务
            try {
                taskService.completeTask(task.getId(), transitionName);
            } catch (Exception e) {//说明当前活动已非撤销操作的目标活动咯,可能已经流转了多个节点
                throw new Exception("路径:" + transitionName + "不存在");
            }
        }

        /**
         * 需要的话,清除历史(删除历史活动实例和历史任务)
         */
        HistoryService historyService = environment.get(HistoryService.class);
        HistoryActivityInstanceImpl historyActivityInstance = (HistoryActivityInstanceImpl)
                historyService.createHistoryActivityInstanceQuery().activityName
                        (currentActivityName).executionId(processId).uniqueResult();
        //使用 Hibernate 的 Session 执行删除操作
        Session session = environment.get(Session.class);
        session.delete(historyActivityInstance);
        return null;
    }
}

该命令执行以下步骤:

  1. 获取当前活动名称。
  2. 执行撤销任务。
  3. 清除历史。

单元测试:

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

//完成【申请】任务
Task applyTask=taskService.findPersonalTasks("Deniro").get(0);
taskService.completeTask(applyTask.getId());

//断言到【审核】任务
processInstance=executionService.findProcessInstanceById(instanceId);
assertTrue(processInstance.isActive("审核"));

//执行【撤销】操作
Configuration.getProcessEngine().execute(new WithdrawCommand(instanceId,"申请"));

//断言已清除历史
List<HistoryTask> historyTasks=historyService.createHistoryTaskQuery().assignee
        ("Jack").executionId(instanceId).list();
assertEquals(0,historyTasks.size());//办理人 Jack 已无历史任务
List<HistoryActivityInstance> historyActivityInstances=historyService
        .createHistoryActivityInstanceQuery().activityName("审核").list();
assertEquals(0,historyActivityInstances.size());//已无【审核】活动的历史信息

//断言到【申请】任务
processInstance=executionService.findProcessInstanceById(instanceId);
assertTrue(processInstance.isActive("申请"));

//断言【申请】任务被分配给了【Deniro】
List<Task> applyTasks=taskService.findPersonalTasks("Deniro");
assertEquals(1,applyTasks.size());

//完成【申请】任务
taskService.completeTask(applyTasks.get(0).getId());

//完成【审核】任务
Task auditTask=taskService.findPersonalTasks("Jack").get(0);
taskService.completeTask(auditTask.getId());

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

是不是很简单呀 O(∩_∩)O哈哈~

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

推荐阅读更多精彩内容

  • 回退,指的是用户主动回退到当前任务的上一流程节点(上一步骤)。 想象这样一种场景,当前用户接收任务后,发现这个任务...
    deniro阅读 1,515评论 2 1
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,040评论 8 265
  • 现在十九岁的我,很迷茫、慌张、害怕,是一名大二在校学生。 十八岁高中毕业,高三那段时期吸收了老师为我们编造...
    少侠866阅读 258评论 0 0
  • 昨天提到对孩子的鼓励需要慎重。 因为鼓励的话让孩子常常觉得就是一个标准。 比如,有次考试,我考了95分,老师说,“...
    田雪源阅读 135评论 0 0
  • 这里一只荧火虫, 那里一只荧火虫, 一只虫, 两只虫, 满天都是荧火虫。 这边一个好朋友, 那边一个好朋友, 这一...
    童心慢读阅读 349评论 0 5