flowable 工作流

156 阅读8分钟

当涉及到系统有多种流程需要审批时,纯代码实现简直就是噩梦,不灵活、高耦合、难维护、难扩展等等。在公司旧 erp 系统我是深有体会,维护这个项目的人都离职了,偶尔有 bug 不得不啃这个屎山。幸好公司觉得维护这个项目成本太高,重构了该项目。于是工作流程被安排上了,其中好处就不多说了。下面是对 flowable 工作流的基础操作

flowable 主要学习技巧: 跟踪观察流程执行每一步 sql 及对应表的数据变化,流程相关表信息可以自己百度

设置处理人

指定处理人: 既在 bpmn xml 中指定具体任务的处理人,在集成自己系统体系时,一般都是使用系统唯一用户编号、角色编号、部门编号等

动态处理人: 既在 bpmn xml 中使用动态变量标识处理人,在运行流程的时候设置这些动态变量

指定用户

指定用户,如指定 zhangsan

<bpmn2:userTask id="task1" name="指定用户" flowable:assignee="zhangsan"></bpmn2:userTask>

动态用户,如启动流程时候设置动态变量 user="1001",即 1001 为任务处理人

<bpmn2:userTask id="task1" name="动态用户" flowable:assignee="${user}"></bpmn2:userTask>

// 在启动流程的时候设置变量
String key = "test";
Map<String, Object> variables = new HashMap<>();
variables.put("user", "1001");
runtimeService.startProcessInstanceByKey(key, variables);

候选用户

指定候选用户,如指定 zhangsan 和李四

<bpmn2:userTask id="test2" name="指定候选用户审批" flowable:candidateUsers="zhangsan,lisi">
    <bpmn2:incoming>Flow_0k1j90n</bpmn2:incoming>
    <bpmn2:outgoing>Flow_1mxru1o</bpmn2:outgoing>s
</bpmn2:userTask>

动态候选用户,如启动流程时候设置动态变量 users="u1001,u1002",即 u1001 和 u1002 都可以处理该任务

<bpmn2:userTask id="task1" name="动态候选用户" flowable:candidateUsers="${users}"></bpmn2:userTask>

// 在启动流程的时候设置变量
String key = "test";
Map<String, Object> variables = new HashMap<>();
variables.put("users", "u1001,u1002");
runtimeService.startProcessInstanceByKey(key,variables);

候选组

指定候选组,如指定部门、指定角色、指定用户组 、指定职务,如指定 dept_1 部门可以处理该任务

<bpmn2:userTask id="deptTask1" name="指定候选组" flowable:candidateGroups="dept_1"></bpmn2:userTask>

动态候选组,如启动流程时候设置动态变量 groups="g1001,g1002",即 g1001,g1002 都可以处理该任务

<bpmn2:userTask id="task1" name="动态候选组" flowable:candidateUsers="${groups}"></bpmn2:userTask>

// 在启动流程的时候设置变量
String key = "test";
Map<String, Object> variables = new HashMap<>();
variables.put("groups", "g1001,g1002");
runtimeService.startProcessInstanceByKey(key,variables);

Bpmn 操作

模型 ID 获取 bpmnBytes

repositoryService.getModelEditorSource(modelId)

bpmnBytes 转 BpmnModel

BpmnXMLConverter converter = new BpmnXMLConverter();
BpmnModel bpmnModel = converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), false, false);

BpmnModel 转 bpmnXml string

BpmnXMLConverter converter = new BpmnXMLConverter();
String bpmnXml = new String(converter.convertToXML(bpmnModel));

获取 BpmnModel 指定类型元素信息集合

/**
 * 获取BpmnModel指定类型的流程元素信息集合
 *
 * @param bpmnModel BpmnModel对象
 * @param clazz     类型 如{@link UserTask}、{@link Gateway} 等
 * @return 元素信息集合
 */
public <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel bpmnModel, Class<T> clazz) {
    List<T> result = new ArrayList<>();
    bpmnModel.getProcesses().forEach(process -> {
        process.getFlowElements().forEach(flowElement -> {
            if (flowElement.getClass().isAssignableFrom(clazz)) {
                result.add((T) flowElement);
            }
        });
    });
    return result;
}

获取 BpmnModel 指定流程元素 ID 元素信息

/**
 * 获取BpmnModel 指定流程元素ID的流程元素信息
 *
 * @param bpmnModel     BpmnModel对象
 * @param flowElementId 元素 ID
 * @return 元素信息
 */
public FlowElement getFlowElementById(BpmnModel model, String flowElementId) {
    Process process = model.getMainProcess();
    return process.getFlowElement(flowElementId);
}

流程操作

新增流程模型

// 保存流程定义 表act_re_model
Model model = repositoryService.newModel();
repositoryService.saveModel(model);
// 保存 BPMN XML 表act_ge_bytearray
repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));

部署流程模型

// 通过模型ID获取流程模型
Model model = repositoryService.getModel(modelId);
// 通过模型ID获取bpmnBytes
byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId());
// 部署流程,涉及主要表act_re_procdef、act_re_deployment、act_ge_bytearray
Deployment deploy = repositoryService.createDeployment()
                .key(model.getKey()).name(model.getName()).category(model.getCategory())
                .addBytes(model.getKey() + BpmnModelConstants.BPMN_FILE_SUFFIX, bpmnBytes)
                .disableSchemaValidation() // 禁用 XML Schema 验证
                .deploy();

查询流程模型

/**
 * 分页查询流程模型
 */
private void queryModelPage(String tenantId, String key, String name, String category, Integer pageNo, Integer pageSize) {
    ModelQuery modelQuery = repositoryService.createModelQuery();
    if (StrUtil.isNotBlank(tenantId)) {
        modelQuery.modelTenantId(tenantId);
    }
    if (StrUtil.isNotBlank(key)) {
        modelQuery.modelKey(key);
    }
    if (StrUtil.isNotBlank(name)) {
        modelQuery.modelNameLike("%" + name + "%"); // 模糊匹配
    }
    if (StrUtil.isNotBlank(category)) {
        modelQuery.modelCategory(category);
    }

    // 执行查询
    long count = modelQuery.count();
    if (count == 0) {
        log.info("分页查询流程模型:null");
        return;
    }
    List<Model> models = modelQuery
            .orderByCreateTime().desc()
            .listPage((pageNo - 1) * pageSize, pageSize);
    log.info("分页查询流程模型:{}", JSONUtil.toJsonStr(models));
}

发起流程

/**
 * 发起流程 通过key
 *
 * @param userId      发起人
 * @param key         流程key
 * @param businessKey 业务key 根据需要可不传
 * @param variables   流程变量
 */
private void startProcessByKey(String userId, String key, String businessKey, Map<String, Object> variables) {
    // 设置流程发起人上下文,内部执行的时候会把流程发起人存入流程实例表act_hi_procinst START_USER_ID_字段中
    // 可以web过滤器进行处理,而不用在这里手动处理
    Authentication.setAuthenticatedUserId(userId);
    ProcessInstance instance = runtimeService.startProcessInstanceByKey(key, businessKey, variables);
    log.info("流程实例ID:{},流程定义ID:{},业务标识:{}", instance.getId(), instance.getProcessDefinitionId(), instance.getBusinessKey());
}

查询流程实例

/**
 * 查询流程实例
 *
 * @param tenantId            租户ID
 * @param userId              发起人
 * @param processInstanceName 流程实例名称
 * @param processDefinitionId 流程定义ID
 * @param businessKey         业务key
 * @param category            分类
 * @param startTime           流程开始时间 start
 * @param endTime             流程开始时间  end
 * @param pageNo              第几页
 * @param pageSize            每页条数
 */
private void queryProcessInstance(String tenantId, String userId, String processInstanceName,
                                  String processDefinitionId, String businessKey, String category,
                                  Date startTime, Date endTime, Integer pageNo, Integer pageSize) {
    HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery()
            .includeProcessVariables()// 左关联ACT_HI_VARINST历史流程变量
            .orderByProcessInstanceStartTime().desc();
    if (StrUtil.isNotBlank(tenantId)) {
        processInstanceQuery.processInstanceTenantId(tenantId);
    }
    // 发起人
    if (StrUtil.isNotBlank(userId)) {
        processInstanceQuery.startedBy(userId);
    }
    if (StrUtil.isNotBlank(processInstanceName)) {
        processInstanceQuery.processInstanceNameLike("%" + processInstanceName + "%");
    }
    if (StrUtil.isNotBlank(processDefinitionId)) {
        processInstanceQuery.processDefinitionId("%" + processDefinitionId + "%");
    }
    if (StrUtil.isNotBlank(businessKey)) {
        processInstanceQuery.processInstanceBusinessKey(businessKey);
    }
    if (StrUtil.isNotBlank(category)) {
        processInstanceQuery.processDefinitionCategory(category);
    }
    if (ObjectUtil.isNotNull(startTime) && ObjectUtil.isNotNull(endTime)) {
        // yyyy-MM-dd HH:mm:ss
        processInstanceQuery.startedAfter(startTime);
        processInstanceQuery.startedBefore(endTime);
    }
    // 查询数量
    long processInstanceCount = processInstanceQuery.count();
    if (processInstanceCount == 0) {
        log.info("分页查询流程实例:null");
        return;
    }
    // 查询列表
    List<HistoricProcessInstance> processInstances = processInstanceQuery.listPage((pageNo - 1) * pageSize, pageSize);
    log.info("分页查询流程实例:{}", JSONUtil.toJsonStr(processInstances));

}

查询待办任务

/**
 * 查询我的待办任务列表
 *
 * @param userId      处理人
 * @param businessKey 业务key
 * @return 待办任务列表
 */
private List<Task> queryMyTodoTask(String userId, String deptId, String roleId, String positionId, String userGroupId, String businessKey) {
    // 查询待办事项列表
    TaskQuery taskQuery = taskService.createTaskQuery()
            .active() // 未挂起状态
            .includeProcessVariables()
            .orderByTaskCreateTime().desc();

    if (StrUtil.isNotBlank(userId)) {
        taskQuery.taskCandidateOrAssigned(userId);
    }
    //部门id dept:1、角色id role:1、职务id position:1 、用户组 userGroup:1
    List<String> candidateGroups = new ArrayList<>();
    if (StrUtil.isNotBlank(deptId)) {
        candidateGroups.add(deptId);
    }
    if (StrUtil.isNotBlank(roleId)) {
        candidateGroups.add(roleId);
    }
    if (StrUtil.isNotBlank(positionId)) {
        candidateGroups.add(positionId);
    }
    if (StrUtil.isNotBlank(userGroupId)) {
        candidateGroups.add(userGroupId);
    }
    if (candidateGroups.size() > 0) {
        taskQuery.taskCandidateGroupIn(candidateGroups);
    }
    // 业务key
    if (StrUtil.isNotBlank(businessKey)) {
        taskQuery.processInstanceBusinessKey(businessKey);
    }

    List<Task> list = taskQuery.list();
    log.info("我的待办任务列表:{}", JSONUtil.toJsonStr(list));
    return list;
}

查询已办任务

/**
 * 查询我的已办任务列表
 *
 * @param userId      处理人
 * @param businessKey 业务key
 * @return 已办任务列表
 */
private List<HistoricTaskInstance> queryMyFinishTask(String userId, String businessKey) {
    HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery()
            .finished() // 已完成
            .taskAssignee(userId) // 分配给自己
            .includeTaskLocalVariables()
            .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序


    // 业务key
    if (StrUtil.isNotBlank(businessKey)) {
        historicTaskInstanceQuery.processInstanceBusinessKey(businessKey);
    }
    List<HistoricTaskInstance> list = historicTaskInstanceQuery.list();
    log.info("我的已办任务列表:{}", JSONUtil.toJsonStr(list));
    return list;
}

查询审批记录

/**
 * 查询审批记录
 */
@Test
public void queryHistoryTaskByProcessInstanceId() {
    log.info("根据流程实例查询历史任务(其实就是审批进度)start");
    String processInstanceId = "4daf22f1-38ee-11ef-bcaa-00ff0e3fbc3b";
    List<HistoricTaskInstance> historyTasks = historyService.createHistoricTaskInstanceQuery()
            .includeTaskLocalVariables()// 查询出任务本地变量
            .processInstanceId(processInstanceId)
            .orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序
            .list();
    log.info("实例任务:{}", JSONUtil.toJsonStr(historyTasks));
    List<Comment> taskComments = taskService.getProcessInstanceComments(processInstanceId);
    log.info("实例评论:{}", JSONUtil.toJsonStr(taskComments));
    for (HistoricTaskInstance historyTask : historyTasks) {
        // 任务所匹配评论
        for (Comment taskComment : taskComments) {
            if (taskComment.getTaskId().equals(taskComment.getTaskId())) {
                log.info("任务所匹配评论{}", JSONUtil.toJsonStr(taskComment));
            }
        }
        //select * from ACT_HI_COMMENT where TASK_ID_ = ? and TYPE_ = 'comment' order by TIME_ desc
        // 默认类型为comment,我们插入指定了类型所以查询出来应该没有
        List<Comment> taskComments1 = taskService.getTaskComments(historyTask.getId());
        log.info("任务评论:{}", JSONUtil.toJsonStr(taskComments1));

    }
    log.info("根据流程实例查询历史任务(其实就是审批进度)end");
}

处理任务

审核通过任务

// 查询审核的任务
Task task = taskService.createTaskQuery().taskId(taskId).includeTaskLocalVariables().singleResult();
// 查询任务对应流程实例
ProcessInstance processInstance =  runtimeService.createProcessInstanceQuery()
        .processInstanceId(task.getProcessInstanceId())
        .includeProcessVariables()
        .singleResult();
// 添加审核评论
taskService.addComment(taskId, processInstance.getId(),
    BpmCommentTypeEnum.APPROVE.getType(), BpmCommentTypeEnum.APPROVE.formatComment(reason));
// 认领任务 (效果类似于setAssignee方法)
taskService.claim(taskId, userId);
// 去完成任务
if (CollUtil.isNotEmpty(variables)) { // 是否有变量
    taskService.complete(taskId, variables, true);
} else {
    taskService.complete(taskId);
}

审核不通过任务

// 查询审核的任务
Task task = taskService.createTaskQuery().taskId(taskId).includeTaskLocalVariables().singleResult();
// 查询任务对应流程实例
ProcessInstance processInstance =  runtimeService.createProcessInstanceQuery()
        .processInstanceId(task.getProcessInstanceId())
        .includeProcessVariables()
        .singleResult();
// 添加审核评论
taskService.addComment(taskId, processInstance.getId(),
    BpmCommentTypeEnum.REJECT.getType(), BpmCommentTypeEnum.REJECT.formatComment(reason));
// 认领任务 (效果类似于setAssignee方法)
taskService.claim(taskId, userId);
// 去完成任务
if (CollUtil.isNotEmpty(variables)) { // 是否有变量
    taskService.complete(taskId, variables, true);
} else {
    taskService.complete(taskId);
}
// 删除流程实例
runtimeService.deleteProcessInstance(processDefinitionId, reason);

委托任务

这个审批自己一时把握不了,先给上级处理审批,如果上级处理通过。在把任务还给自己,如果上级处理驳回,直接结束审批流程

/**
 * 委托任务
 *
 * @param userId         委托操作人
 * @param taskId         任务ID
 * @param delegateUserId 委托接收人
 * @param reason         委托原因
 */
public void delegateTask(String userId, String taskId, String delegateUserId, String reason) {
    // 查询审核的任务
    Task task = taskService.createTaskQuery().taskId(taskId).includeTaskLocalVariables().singleResult();;
    if (task == null) {
        throw new RuntimeException("任务不存在");
    }
    if (task.getAssignee() != null && !Objects.equals(task.getAssignee(), userId)) {
        throw new RuntimeException("该任务的审批人不是你");
    }
    // todo 1.2 校验目标用户存在

    // 添加委托意见
    taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(),
            BpmCommentTypeEnum.DELEGATE_START.formatComment(userId, delegateUserId, reason));
    // 认领任务(效果类似于setAssignee方法)
    taskService.claim(taskId, userId);
    // 设置任务所有者 (owner) 为原任务的处理人,即当前认领任务的人即发起委托的人
    //        taskService.setOwner(taskId, task.getAssignee());
    // 设置任务所有者 (owner) 为即当前认领任务的人即发起委托的人
    taskService.setOwner(taskId, userId);
    // 执行委派,将任务委派给 delegateUser
   /**
     * 底层调用
     * task.setDelegationState(DelegationState.PENDING);
     * TaskHelper.changeTaskAssignee(task, userId);
     * ACT_RU_TASK 表中  assignee = owner ,DELEGATION_ = PENDING
     */
    taskService.delegateTask(taskId, delegateUserId);
}

/**
 * 审核委托任务
 */

// 查询审核的任务
Task task = taskService.createTaskQuery().taskId(taskId).includeTaskLocalVariables().singleResult();
// 查询任务对应流程实例
ProcessInstance processInstance =  runtimeService.createProcessInstanceQuery()
        .processInstanceId(task.getProcessInstanceId())
        .includeProcessVariables()
        .singleResult();
 // 委托的任务状态为PENDING
 if (DelegationState.PENDING.equals(task.getDelegationState())) {
   // 任务所有者,发起委托的人
    String owner = task.getOwner();
    Assert.notNull(owner, "发起委托的人找不到,需要检查数据");
    // 添加审批意见
    taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(),
            BpmCommentTypeEnum.DELEGATE_END.formatComment(userId, task.getOwner(), reason));
     /**
     * 底层调用
     * task.setDelegationState(DelegationState.RESOLVED);
     * TaskHelper.changeTaskAssignee(task, task.getOwner())
     *
     * ACT_RU_TASK 表中  assignee = owner ,DELEGATION_ = RESOLVED
     */
    taskService.resolveTask(taskId);
 }

转办任务

将任务交给其他人处理

 /**
 * 转办任务
 *
 * @param userId         转办操作人
 * @param taskId         任务ID
 * @param assigneeUserId 转办接收人
 * @param reason         转办原因
 */
public void transferTask(String userId, String taskId, String assigneeUserId, String reason) {
    // 查询任务
    Task task = taskService.createTaskQuery().taskId(taskId).includeTaskLocalVariables().singleResult();
    if (task == null) {
        throw new RuntimeException("任务不存在");
    }
    if (task.getAssignee() != null && !Objects.equals(task.getAssignee(), userId)) {
        throw new RuntimeException("该任务的审批人不是你");
    }
    if ((task.getAssignee() != null && task.getAssignee().equals(assigneeUserId)) || (userId.equals(assigneeUserId))) {
        throw new RuntimeException("任务转办失败,转办人不能同一人");
    }

    // 添加转办评论
    taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(),
            BpmCommentTypeEnum.TRANSFER.formatComment(userId, assigneeUserId, reason));
    // 设置任务所有者
//        taskService.setOwner(taskId, task.getAssignee());
    taskService.setOwner(taskId, userId);
    // 将任务转派给 assigneeUserId
    taskService.setAssignee(taskId, assigneeUserId);
}