flowable筆記 - 簡單的通用流程


簡介

通用流程可以用於一些基本的申請,例如請假、加班
大致過程是:

1. 創建申請
2. 分配給審批人(需要審批人列表,當前審批人) 
       -> 有下一個審批人 -> 3 
       -> 無 -> 4
3. 審批人審批 
       -> 同意 -> 2 
       -> 拒絕 -> 5
4. 存儲數據,發送通知
5. 結束

比較簡單,唯一的難點就是動態設置審批人或者審批組,下面開始代碼部分。

demo地址:https://github.com/xwbz2017/flowable-demo

bpmn20文件

    ...
    <!-- standardRequest用來開始流程,在flowable里稱為processDefinitionKey -->
    <process id="standardRequest" name="標准申請流程" isExecutable="true">
        <!-- 第一步:開始流程,創建申請 -->
        <startEvent id="startEvent" name="創建申請"/>
        <sequenceFlow sourceRef="startEvent" targetRef="assignToAuditor"/>
        <!-- 第二步:分配審批人 -->
        <!-- 這個AssignToAuditorDelegate類就是解決動態設置審批人的Java類 -->
        <!-- 審批人列表需要從外部傳入或者根據當前流程id來從數據庫獲取 -->
        <serviceTask id="assignToAuditor" name="分配審批人" flowable:class="me.xwbz.flowable.delegate.AssignToAuditorDelegate"/>
        <sequenceFlow sourceRef="assignToAuditor" targetRef="auditorExist"/>
        <!-- 唯一網關:類似於switch,只能通過一個序列流 -->
        <!-- 這里就是要么存在,要么不存在 -->
        <!-- 使用default屬性定義默認序列流,在其他序列流條件都不滿足的情況下使用  -->
        <exclusiveGateway id="auditorExist" name="審批人是否存在" default="auditorNotExistFlow"/>
        <sequenceFlow sourceRef="auditorExist" targetRef="approveTask">
            <!-- auditMethod是Spring里的一個bean,下面有提到 -->
            <!-- execution是flowable內部變量,類型是org.flowable.engine.delegate.DelegateExecution,也就是serviceTask里的代理方法拿到的 -->
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
                    ${auditMethod.existAuditor(execution)}
                ]]>
            </conditionExpression>
        </sequenceFlow>

        <sequenceFlow id="auditorNotExistFlow" sourceRef="auditorExist" targetRef="agreeDelegate" />
        <!-- 第三步:審批人審批 -->
        <userTask id="approveTask" name="等待審批"
                flowable:candidateGroups="${auditMethod.getCandidateGroups(execution)}"
                flowable:candidateUsers="${auditMethod.getCandidateUsers(execution)}"/>
        <sequenceFlow sourceRef="approveTask" targetRef="decision"/>
        <!-- 唯一網關:一個審批一個審批人 -->
        <exclusiveGateway id="decision" default="rejectFlow"/>
        <sequenceFlow sourceRef="decision" targetRef="assignToAuditor">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
                    ${auditMethod.isApproved(execution)}
                ]]>
            </conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="rejectFlow" sourceRef="decision" targetRef="rejectDelegate" />
        <!-- 第四步:同意后存儲數據,發送通知 -->
        <serviceTask id="agreeDelegate" name="數據存儲"
                     flowable:class="me.xwbz.flowable.delegate.StandardRequestAgreeDelegate"/>
        <sequenceFlow sourceRef="agreeDelegate" targetRef="approveEnd"/>

        <serviceTask id="rejectDelegate" name="回復拒絕消息"
                     flowable:class="me.xwbz.flowable.delegate.BaseRejectDelegate"/>
        <sequenceFlow sourceRef="rejectDelegate" targetRef="rejectEnd"/>
        <!-- 第五步:結束 -->
        <endEvent id="approveEnd" name="已同意"/>

        <endEvent id="rejectEnd" name="已駁回"/>

    </process>
    ...

常量部分

這次沒有另外存儲數據,所以變量都是直接存儲到flowable自帶的變量表里
強烈建議大家另外存儲,自帶的查詢起來非常麻煩!

  • 審批人列表:AUDITOR_LIST_KEY = "AUDITOR_LIST";
  • 當前審批人:AUDITOR_KEY = "AUDITOR";
  • 當前審批人下標:AUDITOR_IDX_KEY = "AUDITOR_IDX";
  • 是否已審批:APPROVED_KEY = "AUDIT_APPROVED";
  • 申請類型:AUDIT_TYPE_KEY = "AUDIT_TYPE";
  • 申請狀態:AUDIT_STATUS_KEY = "AUDIT_STATUS";
  • 其他參數:AUDIT_PARAMS_KEY = "AUDIT_PARAMS";
  • 申請狀態
public enum AuditStatus {
    /** 待審批 */
    WAIT_AUDIT,
    /** 已同意申請 */
    AGREE_AUDIT,
    /** 已拒絕申請 */
    REJECT_AUDIT,
    /** 已取消 */
    CANCEL
}
  • 申請人類型
public enum CandidateType{
    /** 候選人 */
    USER,

    /** 候選組 */
    GROUP
}

審批使用的方法定義

一個普通的Java類

package me.xwbz.flowable.method;

import com.alibaba.fastjson.JSONObject;
import org.flowable.engine.delegate.DelegateExecution;

/**
 * 審批相關的方法
 *
 * 用於flowable流程使用
 */
public class AuditMethod {

    /**
     * 是否存在審批者
     * <sequenceFlow sourceRef="decision" targetRef="assignToAuditor">
     *        <conditionExpression xsi:type="tFormalExpression">
     *                 <![CDATA[
     *                     ${auditMethod.existAuditor(execution)}
     *                 ]]>
     *    </conditionExpression>
     *  </sequenceFlow>
     */
    public boolean existAuditor(DelegateExecution execution){
        return execution.hasVariable(AUDITOR_KEY);
    }

    /**
     * 獲取當前審批者
     */
    public JSONObject getCurrentAuditor(DelegateExecution execution){
        return JSONObject.parseObject((String)execution.getVariable(AUDITOR_KEY));
    }
    /**
     * 獲取當前候選組
     */
    public String getCandidateGroups(DelegateExecution execution){
        JSONObject candidate = getCurrentAuditor(execution);
        if(candidate.getIntValue("type") == CandidateType.GROUP.ordinal()) {
            return candidate.getString("id");
        }
        return null;
    }

    public String getCandidateUsers(DelegateExecution execution){
        JSONObject candidate = getCurrentAuditor(execution);
        if(candidate.getIntValue("type") == CandidateType.USER.ordinal()) {
            return candidate.getString("id");
        }
        return null;
    }

    /**
     * 獲取當前審批者id
     * <userTask id="approveTask" name="等待審批" flowable:assignee="${auditMethod.getCurrentAuditorId(execution)}" />
     */
    public String getCurrentAuditorId(DelegateExecution execution){
        JSONObject auditor = getCurrentAuditor(execution);
        return JSONObject.toJavaObject(auditor, User.class).getId();
    }

    /**
     * 是否同意申請
     */
    public boolean isApproved(DelegateExecution execution){
        Boolean approved = execution.getVariable(APPROVED_KEY, Boolean.class);
        return approved != null && approved;
    }
}

流程結束處理

下面是同意處理,主要是更改狀態。“拒絕處理”同理

可以根據業務增加消息通知,保存數據等。

package me.xwbz.flowable.delegate;

import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

@Slf4j
public class BaseAgreeDelegate implements JavaDelegate {

    @Override
    public void execute(DelegateExecution execution) {
        log.info("{}已被同意", execution.getVariables());
        execution.setVariable(AUDIT_STATUS_KEY, AuditStatus.AGREE_AUDIT.toString());
    }
}

flowable結合Spring可以直接使用Spring里的bean。

像下面這樣定義,然后直接${auditMethod.isApproved(execution)}就可以調用auditMethod里的isApproved方法。

import me.xwbz.flowable.method.AuditMethod;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FlowableConfig {
    @Bean(name = "auditMethod")
    public AuditMethod auditMethod(){
        return new AuditMethod();
    }
}

動態設置審批人

這個是配置在serviceTask里的,所以需要實現JavaDelegate接口

package me.xwbz.flowable.delegate;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

/**
 * delegate - 分配審批人
 */
public class AssignToAuditorDelegate implements JavaDelegate {

    @Override
    public void execute(DelegateExecution execution) {
        // 初始化變量,清空臨時變量
        execution.removeVariable(APPROVED_KEY);
        execution.removeVariable(AUDITOR_KEY);
        execution.setVariable(AUDIT_STATUS_KEY, AuditStatus.WAIT_AUDIT.toString());

        // 拿到審批人列表
        JSONArray auditorList = JSON.parseArray(execution.getVariable(AUDITOR_LIST_KEY).toString());
        // 當前審批人在審批人列表的下標
        Integer auditorIdx = execution.getVariable(AUDITOR_IDX_KEY, Integer.class);
        if (auditorIdx == null) {
            // 第一次分配,初始化為第一個
            auditorIdx = 0;
        } else if (auditorIdx + 1 >= auditorList.size()) {
            //  所有審批人審批完成,結束分配
            return;
        } else {
            // 下一個
            auditorIdx++;
        }
        JSONObject auditor = auditorList.getJSONObject(auditorIdx);
        execution.setVariable(AUDITOR_KEY, auditor.toJSONString());
        execution.setVariable(AUDITOR_IDX_KEY, auditorIdx);
    }
}

開始流程

使用runtimeService#startProcessInstanceByKey開始這個流程,記得開始之前要使用identityService#setAuthenticatedUserId設置當前用戶編號,這個是綁定到線程的,單線程環境下設置一次就行了。

Map<String, Object> vars = new HashMap<>();
// 放入申請類型
vars.put(AUDIT_TYPE_KEY, param.getType());
// 放入審批人人列表
vars.put(AUDITOR_LIST_KEY, JSONObject.toJSONString(param.getAuditors()));
// 放入其他參數
vars.put(AUDIT_PARAMS_KEY, param.getParams());
// 放入審批狀態
vars.put(AUDIT_STATUS_KEY, AuditStatus.WAIT_AUDIT.toString());

logger.debug("當前用戶id: {} ", Authentication.getAuthenticatedUserId());
// 設置發起人
// identityService.setAuthenticatedUserId(user.getId());
ProcessInstance pro = runtimeService.startProcessInstanceByKey("standardRequest", 生成的編號, vars);
// 文件材料
if (param.getFiles() != null && !param.getFiles().isEmpty()) {
    // 上傳附件,可以直接上傳字節流(不建議)
    param.getFiles().forEach(file ->
            taskService.createAttachment("application/octet-stream", null,
                    pro.getId(), file.getName(), null, file.getId()));
}

查看待我審批的任務

taskService.createTaskQuery()只能查詢到正在進行的任務

要是想既能查詢到正在進行的,也要結束的可以使用下面的語句:

TaskInfoQueryWrapper taskInfoQueryWrapper = runtime ? new TaskInfoQueryWrapper(taskService.createTaskQuery()) : new TaskInfoQueryWrapper(historyService.createHistoricTaskInstanceQuery());

也就是說你要先確定是要那種。

TaskQuery query = taskService.createTaskQuery()
        // or() 和 endOr()就像是左括號和右括號,中間用or連接條件
        // 指定是我審批的任務或者所在的組別審批的任務
        // 實在太復雜的情況,建議不使用flowable的查詢
        .or()
        .taskAssignee(user.getId())
        .taskCandidateUser(user.getId())
        .taskCandidateGroup(user.getGroup())
        .endOr();
        // 查詢自定義字段
    if (StringUtils.isNotEmpty(auditType)) {
        query.processVariableValueEquals(AUDIT_TYPE_KEY, auditType);
    }
    if(auditStatus != null){
        query.processVariableValueEquals(AUDIT_STATUS_KEY, auditStatus.toString());
    }
    // 根據創建時間倒序
query.orderByTaskCreateTime().desc()
    // 分頁
    .listPage(0, 10)
    .stream().map(t -> {
    // 拿到這個任務的流程實例,用於顯示流程開始時間、結束時間、業務編號
    HistoricProcessInstance p = historyService.createHistoricProcessInstanceQuery()
            .processInstanceId(t.getProcessInstanceId())
            .singleResult();
    return new Process(p).withTask(t) // 拿到任務編號和任務名稱
            // 拿到創建時和中途加入的自定義參數
            .withVariables(taskService.getVariables(t.getId()))
            .withFiles(taskService.getProcessInstanceAttachments(p.getId()));
}).collect(Collectors.toList()

查看我已審批的任務

任務審批后就走下一個序列流,這里只能從歷史紀錄里獲取已審批的。

當前設置歷史紀錄(HistoryLevel)粒度為audit,這是默認的。

注意這里不能篩選自定義參數,所以要么自定義sql,要么另外存儲。

// 如果不需要篩選自定義參數
if(auditStatus == null && StringUtils.isEmpty(auditType)){
    return historyService.createHistoricActivityInstanceQuery()
            // 我審批的
            .taskAssignee(assignee)
            // 按照結束時間倒序
            .orderByHistoricActivityInstanceEndTime().desc()
            // 已結束的(其實就是判斷有沒有結束時間)
            .finished()
            // 分頁
            .listPage(firstIdx, pageSize);
}
// 否則需要自定義sql
// managementService.getTableName是用來獲取表名的(加上上一篇提到的liquibase,估計flowable作者對數據表命名很糾結)
// 這里從HistoricVariableInstance對應的表里找到自定義參數
// 篩選對象類型不支持二進制,存儲的時候盡量使用字符串、數字、布爾值、時間,用來比較的值也有很多限制,例如null不能用like比較。
String sql = "SELECT DISTINCT RES.* " +
            "FROM " + managementService.getTableName(HistoricActivityInstance.class) + " RES " +
            "INNER JOIN " + managementService.getTableName(HistoricVariableInstance.class) + " var " +
            "ON var.PROC_INST_ID_ = res.PROC_INST_ID_  " +
            "WHERE RES.ASSIGNEE_ = #{assignee} " +
            "AND RES.END_TIME_ IS NOT NULL ";
if(auditStatus != null && StringUtils.isNotEmpty(auditType)){
    sql += "AND ((var.name_ = #{typeKey} AND var.TEXT_ = #{typeValue}) OR  (var.name_ = #{statusKey} AND var.TEXT_ = #{statusValue}))";
} else if(auditStatus != null){
    sql += "AND var.name_ = #{statusKey} AND var.TEXT_ = #{statusValue}";
} else {
    sql += "AND var.name_ = #{typeKey} AND var.TEXT_ = #{typeValue}";
}
sql +=  " ORDER BY RES.END_TIME_ DESC";
    return historyService.createNativeHistoricActivityInstanceQuery().sql(sql)
    // 參數用#{assignee}占位后,再調用parameter("assignee", assignee)填入值
    // 參數值可以多出來沒用到的,比hibernate好多了
            .parameter("assignee", assignee)
            .parameter("typeKey", AUDIT_TYPE_KEY)
            .parameter("typeValue", auditType)
            .parameter("statusKey", AUDIT_STATUS_KEY)
            .parameter("statusValue", auditStatus == null ? null : auditStatus.toString())
            .listPage(firstIdx, pageSize);

后續獲取詳細和自定義參數

list.stream().map(a -> {
    // 同上面的拿到這個任務的流程實例
    HistoricProcessInstance p = historyService.createHistoricProcessInstanceQuery()
            .processInstanceId(a.getProcessInstanceId())
            .singleResult();
    // 因為任務已結束(我看到有提到刪除任務TaskHelper#completeTask),所以只能從歷史里獲取
    Map<String, Object> params = historyService.createHistoricVariableInstanceQuery()
            .processInstanceId(a.getProcessInstanceId()).list()
            // 拿到的是HistoricVariableInstance對象,需要轉成原來存儲的方式
            .stream().collect(Collectors.toMap(HistoricVariableInstance::getVariableName, HistoricVariableInstance::getValue));

    return new Process(p).withActivity(a).withVariables(params);
}).collect(Collectors.toList())

查看我創建的任務

這個比較方便拿到,但是當前最新的任務比較難拿到,有時還不准確

// startedBy:創建任務時設置的發起人
HistoricProcessInstanceQuery instanceQuery = historyService.createHistoricProcessInstanceQuery()
        .startedBy(user.getId());
// 自定義參數篩選
if (StringUtils.isNotEmpty(auditType)) {
    instanceQuery.variableValueEquals(AUDIT_TYPE_KEY, auditType);
}
if(auditStatus != null){
    instanceQuery.variableValueEquals(AUDIT_STATUS_KEY, auditStatus.toString());
}

instanceQuery
    .orderByProcessInstanceStartTime().desc()
    .listPage(firstIdx, pageSize).stream()
    //  獲取其中的詳細和自定義參數
    .map(this::convertHostoryProcess)
    .collect(Collectors.toList())

獲取其中的詳細和自定義參數

    private Process convertHostoryProcess(HistoricProcessInstance p) {
        // 不管流程是否結束,到歷史里查,最方便
        Map<String, Object> params = historyService.createHistoricVariableInstanceQuery().processInstanceId(p.getId()).list()
                .stream().collect(Collectors.toMap(HistoricVariableInstance::getVariableName, HistoricVariableInstance::getValue));
        // 獲取最新的一個userTask,也就是任務活動紀錄
        List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(p.getId())
                .orderByHistoricActivityInstanceStartTime().desc()
                .orderByHistoricActivityInstanceEndTime().asc().
                        listPage(0, 1);
        Process data = new Process(p);
        if (!activities.isEmpty()) {
            data.withActivity(activities.get(0));
        }
        return data.withVariables(params);
    }

撤銷流程實例(標記刪除)

撤銷后,流程直接中斷,除了用戶不能操作和多了結束時間、刪除理由外,其他停留在撤銷前的狀態。

ProcessInstance process = runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
if (process == null) {
    throw new RuntimeException("該流程不在運行狀態");
}
Task task = taskService.createTaskQuery().processInstanceId(id).singleResult();
runtimeService.setVariable(task.getExecutionId(), AUDIT_STATUS_KEY, AuditStatus.CANCEL.toString());
runtimeService.deleteProcessInstance(id, "用戶撤銷");

用戶操作(同意、拒絕)

注意:如果beforeAgreeOrRejecttaskService.complete在同一個事物里,且beforeAgreeOrReject里跟AssignToAuditorDelegate有更新數據庫數據,會導致事物異常。

是否有審批權限需要自己判斷。

操作后進入下一序列流,再次拿這個taskId會獲取不到這個Task,所以上傳審批意見和附件什么的要在操作前。

if(!isAssigneeOrCandidate(user, taskId)){
    throw new RuntimeException("無法操作");
}
// 同意前設置,上傳審批意見和附件
beforeAgreeOrReject(user, taskId, AuditStatus.AGREE_AUDIT, "同意", reason);
// 拒絕前設置,上傳審批意見和附件
// beforeAgreeOrReject(user, taskId, AuditStatus.REJECT_AUDIT, "拒絕", reason);
// 同意
taskService.complete(taskId, Collections.singletonMap(APPROVED_KEY, true));
// 拒絕
// taskService.complete(taskId, Collections.singletonMap(APPROVED_KEY, false));

判斷是否有權限

public boolean isAssigneeOrCandidate(User user, String taskId){
    long count = taskService.createTaskQuery()
            .taskId(taskId)
            .or()
            .taskAssignee(user.getId())
            .taskCandidateUser(user.getId())
            .taskCandidateGroup(user.getGroup())
            .endOr().count();
    return count > 0;
}

操作前設置,上傳審批意見和附件

附件一般都另外存儲,這里存一個id就行了
這里是存在ftp文件服務器里的

public void beforeAgreeOrReject(User user, String taskId, AuditStatus auditStatus, String operate, ReasonParam reason){
    // 組成員操作后方便查詢
    taskService.setAssignee(taskId, user.getId());
    if(StringUtils.isNotEmpty(reason.getText())){
        // 審批意見
        taskService.addComment(taskId, null, null, reason.getText());
    }
    if (reason.getFiles() != null && !reason.getFiles().isEmpty()) {
        files.forEach(file ->
            // 上傳附件,可以直接上傳字節流(不建議)
            taskService.createAttachment("application/octet-stream", taskId,
                    null, file.getName(), null, file.getId()));
    }
}

收工

剛寫博客,不太會用文字表達,歡迎大家提出意見。

以前沒有用文字記事的習慣,這一篇全是代碼的文章都寫了兩三個小時。。。

感謝閱讀,歡迎點贊up~

更新日志

2018-12-17

支持審批組,遇到的坑提醒

2019-01-05

代碼上傳到github,github地址
同時發現文中有很多地方代碼沒更新,得找時間更新,
感謝@久伴1101 提出的建議

2019-01-06

修改之前錯誤的地方,當然建議是看看上方的github代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM