应用开发平台集成工作流系列之15——任务处理设计与实现

背景

依托Camunda工作流引擎,辅以集成工作,实现了流程建模、表单定制、我的待办、我的已办、我的申请等功能,接下来重点就是流程任务的处理了,今天总体来说说一个流程,从发起、环节处理到结束的过程。

任务处理方式

关于任务的处理的方式,有必要先提一下。
市面上很多工作流系统或平台,在任务处理的时候,填写审批意见后,点击提交按钮,提示操作成功后关闭页面。这种方式,对用户而言其实并不友好,在任务提交前,用户实际不清楚提交后会流转到哪个环节了。少数系统或平台做了进一步优化,在提交成功后显示下一审批环节。不过,这种方式只是省掉了用户主动查询的步骤,仍旧是事后通知。如发现与预期不符,往往需要执行撤回功能,甚至线下联系下一环节处理人执行驳回操作。
在我设计的平台中,工作流任务处理时,在任务提交时,会显示下一处理环节,这样就实现了事先得知,从而减少差错和避免不必要的沟通。

流程发起

用户访问业务导航功能菜单,系统分门别类显示当前用户有权限发起的所有流程模板。
应用开发平台集成工作流系列之15——任务处理设计与实现_第1张图片
点击对应流程模板按钮,进入流程发起界面
应用开发平台集成工作流系列之15——任务处理设计与实现_第2张图片
填写数据后,点击保存按钮,系统一方面保存表单对应的实体对象,另一方面调用Camunda的API,来发起流程,包括验证流程启动权限、设置流程启动处理人、设置流程变量、启动流程、更新流程表单的与工作流相关的属性(流程类型、流程实例标识、流程状态等)。

后端处理代码如下:

@Override
protected void afterAdd(Leave entity) {

    String userId=UserUtil.getId();
    String code= entity.getClass().getSimpleName();
    //验证流程启动权限
    flowTemplateService.checkProcessStartPermission(code);

    //设置流程启动处理人,缺失此句会导致act_hi_procinst表的START_USER_ID_字段,即流程启动人数据为空
    identityService.setAuthenticatedUserId(userId);

    // 首环节处理人默认为启动人
    Map<String,Object> instanceParams=new HashMap<>(5);
    instanceParams.put(WorkFlowConstant.INSTANCE_FIRST_STEP_HANDLER,userId);

    //设置请假天数
    instanceParams.put("total",entity.getTotal());


    WorkflowTemplate workflowTemplate=flowTemplateService.getByCode(code);
    //启动流程
    ProcessInstance processInstance =runtimeService.startProcessInstanceById(workflowTemplate.getProcessDefinitionId(),entity.getBillNo(),instanceParams);

    //更新流程相关字段
    //流程类型
    entity.setFlowTypeName(workflowTemplate.getName());
    //流程实例标识
    entity.setFlowInstanceId(processInstance.getProcessInstanceId());
    //流程状态
    entity.setFlowStatus(WorkflowInstanceStatusEnum.ACTIVE.name());
    //发起时间
    entity.setInitiateTime(LocalDateTime.now());

    //保存
    modify(entity);

}

流程发起环节一旦点击保存按钮,实际就发起了流程,生成了任务数据。用户访问待办任务菜单,可以查看当前明确分配给自己的任务,或未指派到具体人,但指派到角色的任务,然后进行后续处理。

提交

在发起环节,执行保存操作后,后端会发起流程,然后前端页面会出现“提交”按钮,点击后弹出任务提交界面,填写审批意见,显示下一环节选择,以及处理人(根据流程建模时的环节配置决定是否需要选择处理人)。
应用开发平台集成工作流系列之15——任务处理设计与实现_第3张图片

获取下一环节的核心逻辑

   public List<WorkflowNodeConfig> getForwardNodeList(String taskId) {

        // 结束环节标识
        String endNodeId="";

        //后续环节列表
        List<WorkflowNodeConfig> list = new ArrayList<>();
        //是否包含结束环节
        boolean containEndEvent = false;
        // 获取任务
        ProcessTask task = get(taskId);
        // 获取流程模型
        BpmnModelInstance modelInstance = repositoryService.getBpmnModelInstance(task.getProcessDefinitionId());

        // 获取当前任务在模型中对应节点定义
        UserTask userTask = (UserTask) modelInstance.getModelElementById(task.getTaskDefinitionKey());
        // 获取所有流出边
        Collection<SequenceFlow> outgoing = userTask.getOutgoing();
        // 获取顺序边,或者非回退标记的条件边指向的用户任务类型节点
        List<String> nodeIdList = outgoing.stream()
                .filter(x -> x.getTarget() instanceof UserTask &&
                        (x.getConditionExpression() == null) || (
                        x.getConditionExpression() != null &&
                                !x.getConditionExpression().getTextContent().equals(WorkFlowConstant.REJECT_FLAG_VALUE)))
                .map(x -> x.getTarget().getId())
                .collect(Collectors.toList());

        //判断是否包括网关环节
        //TODO:此处未考虑连续使用网关的复杂场景
        List<FlowNode> gatewayList = outgoing.stream().filter(x -> x.getTarget() instanceof Gateway)
                .map(x -> x.getTarget())
                .collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(gatewayList)) {
            FlowNode gatewayNode = gatewayList.get(0);
            if (gatewayNode instanceof ExclusiveGateway) {
                // 排他网关
                // 获取输出流条件边
                Collection<SequenceFlow> sequenceFlowList = gatewayNode.getOutgoing();
                for (SequenceFlow item : sequenceFlowList) {
                    //计算el表达式,如符合,则获取后续环节
                    boolean conditionResult = checkCondition(item.getConditionExpression().getTextContent(),
                            runtimeService.getVariables(task.getExecutionId()));
                    if (conditionResult) {
                        nodeIdList.add(item.getTarget().getId());
                        break;
                    }
                }
            } else {
                //并行网关或包容网关
                Collection<SequenceFlow> outSequenceFlowList = gatewayNode.getOutgoing();
                if (outSequenceFlowList.size() == 1) {
                    //流出边只有1条,认为是汇聚节点,继续往下找
                    FlowNode nextNode = outSequenceFlowList.stream().iterator().next().getTarget();
                    if (nextNode instanceof UserTask) {
                        // 用户任务加入后续环节列表中
                        nodeIdList.add(nextNode.getId());
                    } else if (nextNode instanceof EndEvent) {
                        // 结束环节做标记
                        endNodeId=nextNode.getId();
                        containEndEvent = true;
                    }
                } else if (outSequenceFlowList.size() > 1) {
                    //流出边多条,认为是分支节点
                    WorkflowNodeConfig gatewayConfig = new WorkflowNodeConfig();
                    gatewayConfig.setSetAssigneeFlag(YesOrNoEnum.NO.name());
                    gatewayConfig.setNodeId(gatewayNode.getId());
                    gatewayConfig.setName(gatewayNode.getName());
                    list.add(gatewayConfig);

                }
            }
        }

        // 查询环节配置信息
        if (CollectionUtils.isNotEmpty(nodeIdList)) {
            QueryWrapper<WorkflowNodeConfig> queryWrapper = new QueryWrapper<>();
            queryWrapper.lambda().eq(WorkflowNodeConfig::getProcessDefinitionId, task.getProcessDefinitionId())
                    .in(WorkflowNodeConfig::getNodeId, nodeIdList);
            list.addAll(workflowNodeConfigService.list(queryWrapper));
        }

        //判断是否包括结束环节
        List<String> endIdList = outgoing.stream().filter(x -> x.getTarget().getElementType().getTypeName()
                .equals(WorkFlowConstant.END_EVENT_TYPE_NAME))
                .map(x -> x.getTarget().getId())
                .collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(endIdList)) {
            endNodeId=endIdList.get(0);
            containEndEvent = true;
        }
        // 如包含,追加结束环节
        if (containEndEvent) {
            WorkflowNodeConfig endConfig = new WorkflowNodeConfig();
            endConfig.setSetAssigneeFlag(YesOrNoEnum.NO.name());
            endConfig.setNodeId(endNodeId);
            endConfig.setName("流程结束");
            list.add(endConfig);
        }
        return list;
    }

执行提交任务的操作

 @Override
    @Transactional(rollbackFor = Exception.class)
    public void commit(String taskId, String comment, String nextStepId,
                       List<String> assigneeList) {      
        // 设置当前处理人
        identityService.setAuthenticatedUserId(UserUtil.getId());
        // 获取任务
        ProcessTask task = get(taskId);
        // 记录处理意见
        workflowCommentService.addComment(task.getProcessInstanceId(),task.getTaskDefinitionKey(),task.getNodeName(), comment,
                CommitTypeEnum.COMMIT);
        //判断当前是否被委派任务
        String delegation = task.getDelegation();
        if (StringUtils.isNotBlank(delegation) && delegation.equals(WorkFlowConstant.DELEGATION_PENDING)) {
            //委派状态为等待时,调用resolveTask方法提交
            taskService.resolveTask(taskId);
            return;
        }
        // 处理任务
        handleTask(task.getProcessInstanceId(), taskId, nextStepId, assigneeList);
    }

    private void handleTask(String processInstanceId, String taskId, String nextStepId, List<String> assigneeList) {

        Map<String, Object> instanceVariable = generateInstanceVariable(taskId, nextStepId, assigneeList);
        taskService.setVariables(taskId, instanceVariable);
        taskService.complete(taskId);
    }

    /**
     * 生成实例变量
     *
     * @param taskId       任务标识
     * @param nextStepId   下一环节标识
     * @param assigneeList 处理人列表
     * @return {@link Map}<{@link String}, {@link Object}>
     */
    private  Map<String, Object> generateInstanceVariable(String taskId, String nextStepId, List<String> assigneeList) {
        // 设置下一环节及处理人实例变量
        Map<String, Object> instanceVariable = new HashMap<String, Object>(2);
        instanceVariable.put(WorkFlowConstant.INSTANCE_NEXT_STEP, nextStepId);
        //获取环节设置
        ProcessTask processTask = get(taskId);
        WorkflowNodeConfig nodeConfig = workflowNodeConfigService
                .getNodeConfig(processTask.getProcessDefinitionId(), nextStepId);
        if (nodeConfig != null) {
            if (nodeConfig.getMode().equals(NodeModeEnum.COUNTERSIGN.name())) {
                // 会签环节
                instanceVariable.put(WorkFlowConstant.INSTANCE_APPROVER_LIST, assigneeList);
            } else {
                // 普通环节
                if (nodeConfig.getSetAssigneeFlag().equals(YesOrNoEnum.YES.name())) {
                    // 指定处理人
                    instanceVariable.put("singleHandler", assigneeList.get(0));
                } else {
                    // 不指定处理人
                    instanceVariable.put("singleHandler", null);
                }

            }
        } else {
            // 用于并行网关后续环节处理,不设置为null流程会将任务派发到空处理人
            instanceVariable.put("singleHandler", null);
        }
        return instanceVariable;
    }

注意,如果当前任务是通过委派产生的,需要调用resolveTask方法,否则调用complete方法,对应着Camunda组件TaskService的API。

回退

回退是流程处理的常见操作。流程建模时,可以配置某一用户任务环节,可以回退的环节列表。点击驳回按钮时,弹出对话框,在预先配置限定的范围中选择。
应用开发平台集成工作流系列之15——任务处理设计与实现_第4张图片
获取可回退的环节列表数据的代码如下:

 @Override
    public List<WorkflowNodeConfig> getBackNodeList(String taskId) {
        // 获取任务
        ProcessTask task = get(taskId);

        // 从任务信息中获取对应的流程定义标识和环节标识
        String processDefinitionId=task.getProcessDefinitionId();
        String nodeId=task.getTaskDefinitionKey();

        // 从环节跳转配置中获取可跳转的目标节点列表
        List<String> targetNodeIdList=workflowBackNodeConfigService.getTargetNodeIdList(processDefinitionId,nodeId);

        // 根据目标节点列表获取环节配置信息
        List<WorkflowNodeConfig> entityList=new ArrayList<>();
        if(CollectionUtils.isNotEmpty(targetNodeIdList)){
            entityList = workflowNodeConfigService.getByIdList(processDefinitionId, targetNodeIdList);
        }

        // 发起环节处理
        if(targetNodeIdList.contains(ROOT_CODE)){
            WorkflowNodeConfig root=new WorkflowNodeConfig();
            root.setSetAssigneeFlag(YesOrNoEnum.YES.name());
            root.setNodeId(ROOT_CODE);
            root.setMode(NodeModeEnum.NORMAL.name());
            root.setName("填报");
            entityList.add(root);
        }
        return entityList;
    }

注意:发起环节比较特殊,是用户任务,但该环节的处理人并不是通过流程建模配置产生,因此信息也不是保存 回退环节列表的库中,需要单独处理。

执行回退操作代码如下:

 @Override
    @Transactional(rollbackFor = Exception.class)
    public void reject(String taskId, String comment, String nextStepId, List<String> assigneeList) {

        jumpTargetStep(taskId, comment, nextStepId, assigneeList);
    }

    private void jumpTargetStep(String taskId, String comment, String nextStepId, List<String> assigneeList) {

        // 设置当前处理人
        identityService.setAuthenticatedUserId(UserUtil.getId());

        // 获取任务
        ProcessTask processTask = get(taskId);

        // 记录处理意见
        workflowCommentService.addComment(processTask.getProcessInstanceId(), processTask.getProcessDefinitionKey(),processTask.getNodeName(),
                comment,
                CommitTypeEnum.REJECT);

        // 生成实例变量
        Map<String, Object> instanceVariable = generateInstanceVariable(taskId, nextStepId, assigneeList);


        // 跳转到指定环节
        ActivityInstance activityInstance = runtimeService.getActivityInstance(processTask.getProcessInstanceId());
        runtimeService.createProcessInstanceModification(processTask.getProcessInstanceId())
                // 作废当前任务
                .cancelActivityInstance(activityInstance.getId())
                // 启动目标活动节点
                .startBeforeActivity(nextStepId)
                // 流程的可变参数赋值
                .setVariables(instanceVariable).execute();
    }

其关键操作是调用Camunda的API,即runtimeService.createProcessInstanceModification来作废掉原任务,然后创建目标环节任务,并传入流程变量。

跳转

跳转与回退非常类型。流程建模时,可以配置某一用户任务环节,可以跳转的环节列表。点击跳转按钮时,弹出对话框,在预先配置限定的范围中选择。
应用开发平台集成工作流系列之15——任务处理设计与实现_第5张图片

获取可跳转的环节列表数据的代码如下:

  @Override
    public List<WorkflowNodeConfig> getJumpNodeList(String taskId) {
        // 获取任务
        ProcessTask task = get(taskId);

        // 从任务信息中获取对应的流程定义标识和环节标识
        String processDefinitionId=task.getProcessDefinitionId();
        String nodeId=task.getTaskDefinitionKey();

        // 从环节跳转配置中获取可跳转的目标节点列表
        List<String> targetNodeIdList=workflowJumpNodeConfigService.getTargetNodeIdList(processDefinitionId,nodeId);

        // 根据目标节点列表获取环节配置信息
        List<WorkflowNodeConfig> entityList=new ArrayList<>();
        if(CollectionUtils.isNotEmpty(targetNodeIdList)){
            entityList = workflowNodeConfigService.getByIdList(processDefinitionId, targetNodeIdList);
        }

        // 发起环节处理
        if(targetNodeIdList.contains(ROOT_CODE)){
            WorkflowNodeConfig root=new WorkflowNodeConfig();
            root.setSetAssigneeFlag(YesOrNoEnum.YES.name());
            root.setNodeId(ROOT_CODE);
            root.setMode(NodeModeEnum.NORMAL.name());
            root.setName("填报");
            entityList.add(root);
        }
        return entityList;
    }

注意:发起环节比较特殊,是用户任务,但该环节的处理人并不是通过流程建模配置产生,因此信息也不是保存 跳转环节列表的库中,需要单独处理。

跳转操作后端执行的处理,实际跟回退完全一样,对于Camunda而已,都是环节间跳跃式流转,回退与跳转,实际是平台设计层面的逻辑概念,使其更符合业务需要而已。

转办

转办是直接将当前任务转交给他人处理,他人处理完后会流转到后续环节,底层处理是直接修改办理人。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(String taskId, String assignee, String comment) {
      
        // 获取任务
        ProcessTask task = get(taskId);
        //任务转办
        taskService.setAssignee(taskId, assignee);
        //记录处理意见
        workflowCommentService.addComment(task.getProcessInstanceId(), task.getProcessDefinitionKey(), task.getNodeName(), comment,
                CommitTypeEnum.TRANSFER);

    }

委派

委派是将当前任务委托给他人处理,他人处理完后,再回到原办理人处,底层是通过调整任务所有者(owner)实现的。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delegate(String taskId, String assignee, String comment) {
      
        // 获取任务
        ProcessTask task = get(taskId);
        //任务委派
        taskService.delegateTask(taskId, assignee);
        //记录审批日志
        workflowCommentService.addComment(task.getProcessInstanceId(),  task.getProcessDefinitionKey(),task.getNodeName(), comment,
                CommitTypeEnum.DELEGATE);

    }

需要注意的是提交任务时,需判断当前任务是不是来源于委派,如是,则调用resolve方法来提交任务。

签收/撤销签收

该模式不指定具体处理人,而是将任务分配给角色,该角色下所有人员都可以看到,通常用于两种应用场景:
1.处理任务的角色对应的人员职责等同,对于流传过来的单据,谁都有能力处理,不再进一步区分,例如出纳,只是因为业务量大,需要设置多人同时来处理任务
2.处理任务的角色对应的人员职责不同,不同的人负责不同类型的单据,需要根据单据属性来最终决定应该由某个人或某几个人来处理,并且没有明确的业务分配规则(如有,则可以通过工作流和程序来实现自动派发),这种情况下相当于在一定人员范围内模糊派单,由各负责人自行查看领取任务并处理。

这里为什么要增加签收环节呢?
这是因为,某些任务办理需要一定时长来处理,如没有签收操作,意味着可能有多人进行重复的任务处理,为了避免出现该情况,增加签收操作,执行签收后,任务处理人由分配到角色,变更为直接分配到人,并且该角色下的其他人,在待办任务中也不会再看到该条任务了。

实现也很简单,直接调用API,setAssignee指定人就好了,这就是灵活运用Camunda提供的基本API来实现自己的业务需求和功能的范例。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void signIn(String taskId) {
      
        // 获取任务
        ProcessTask task = get(taskId);
        // 设置处理人为当前用户
        taskService.setAssignee(taskId, UserUtil.getId());
        //记录处理意见
        workflowCommentService.addComment(task.getProcessInstanceId(), task.getProcessDefinitionKey(), task.getNodeName(), "签收任务",
                CommitTypeEnum.SIGN_IN);

    }

撤销签收功能是为了避免误操作,签收了发现并不该自己处理,通过该操作撤销,通过调用方法setAssignee,将处理人置空即可,这样该环节配置的处理角色下所有人能再次在待办任务中看到并进行签收和处理。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelSignIn(String taskId) {

        // 获取任务
        ProcessTask task = get(taskId);
        // 取消签收
        taskService.setAssignee(taskId, null);
        //记录处理意见
        workflowCommentService.addComment(task.getProcessInstanceId(), task.getProcessDefinitionKey(), task.getNodeName(), "撤销签收",
                CommitTypeEnum.CANCEL_SIGN_IN);

    }

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
开源不易,欢迎收藏、点赞、评论。

你可能感兴趣的:(#,工作流集成,工作流引擎,camunda,activiti,工作流集成,应用开发平台)