flowable筆記 - 簡單的通用流程
簡介
通用流程可以用於一些基本的申請,例如請假、加班。
大致過程是:
1. 創建申請
2. 分配給審批人(需要審批人列表,當前審批人)
-> 有下一個審批人 -> 3
-> 無 -> 4
3. 審批人審批
-> 同意 -> 2
-> 拒絕 -> 5
4. 存儲數據,發送通知
5. 結束
比較簡單,唯一的難點就是動態設置審批人或者審批組,下面開始代碼部分。
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:assignee="${auditMethod.getCurrentAuditorId(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 {
/** 待審批 */
waitAudit,
/** 已同意申請 */
agreeAudit,
/** 已拒絕申請 */
rejectAudit,
/** 已取消 */
cancel
}
審批使用的方法定義
一個普通的Java類
package me.xwbz.flowable.method; import com.alibaba.fastjson.JSONObject; import me.xwbz.flowable.config.FlowableConfig; import me.xwbz.flowable.vo.UserVO; 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(FlowableConfig.AUDITOR_KEY); } /** * 獲取當前審批者 */ public JSONObject getCurrentAuditor(DelegateExecution execution){ return JSONObject.parseObject((String)execution.getVariable(FlowableConfig.AUDITOR_KEY)); } /** * 獲取當前審批者id * <userTask id="approveTask" name="等待審批" flowable:assignee="${auditMethod.getCurrentAuditorId(execution)}" /> */ public String getCurrentAuditorId(DelegateExecution execution){ JSONObject auditor = getCurrentAuditor(execution); return JSONObject.toJavaObject(auditor, UserVO.class).getId(); } /** * 是否同意申請 */ public boolean isApproved(DelegateExecution execution){ Boolean approved = execution.getVariable(FlowableConfig.APPROVED_KEY, Boolean.class); return approved != null && approved; } }
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 me.xwbz.flowable.config.FlowableConfig;
import me.xwbz.flowable.vo.ProcessVO;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
/**
* delegate - 分配審批人
*/
public class AssignToAuditorDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
// 初始化變量,清空臨時變量
this.init(execution);
// 拿到審批人列表
JSONArray auditorList = JSON.parseArray(execution.getVariable(FlowableConfig.AUDITOR_LIST_KEY).toString());
// 當前審批人在審批人列表的下標
Integer auditorIdx = execution.getVariable(FlowableConfig.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(FlowableConfig.AUDITOR_KEY, auditor.toJSONString());
execution.setVariable(FlowableConfig.AUDITOR_IDX_KEY, auditorIdx);
}
private void init(DelegateExecution execution){
// 去掉“同意”標識
execution.removeVariable(FlowableConfig.APPROVED_KEY);
execution.removeVariable(FlowableConfig.AUDITOR_KEY);
execution.setVariable(FlowableConfig.AUDIT_STATUS_KEY, ProcessVO.AuditStatus.waitAudit.toString());
}
}
開始流程
使用runtimeService#startProcessInstanceByKey
開始這個流程,記得開始之前要使用identityService#setAuthenticatedUserId
設置當前用戶編號,這個是綁定到線程的,單線程環境下設置一次就行了。
Map<String, Object> vars = new HashMap<>();
// 放入申請類型
vars.put(FlowableConfig.AUDIT_TYPE_KEY, param.getType());
// 放入審批人人列表
vars.put(FlowableConfig.AUDITOR_LIST_KEY, JSONObject.toJSONString(param.getAuditors()));
// 放入其他參數
vars.put(FlowableConfig.AUDIT_PARAMS_KEY, param.getParams());
// 放入審批狀態
vars.put(FlowableConfig.AUDIT_STATUS_KEY, ProcessVO.AuditStatus.waitAudit.toString());
logger.debug("當前用戶id: {} ", Authentication.getAuthenticatedUserId());
// 設置發起人
// identityService.setAuthenticatedUserId(user.getId());
runtimeService.startProcessInstanceByKey("standardRequest", 生成的編號, vars);
查看待我審批的任務
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())
.taskCandidateGroup(user.getGroup())
.endOr();
// 查詢自定義字段
if (StringUtils.isNotEmpty(auditType)) {
query.taskVariableValueEquals(FlowableConfig.AUDIT_TYPE_KEY, auditType);
}
if(auditStatus != null){
query.taskVariableValueEquals(FlowableConfig.AUDIT_STATUS_KEY, auditStatus.toString());
}
// 根據創建時間倒序
query.orderByTaskCreateTime().desc()
// 分頁
.listPage(0, 10)
.stream().map(t -> {
// 拿到這個任務的流程實例,用於顯示流程開始時間、結束時間、業務編號
HistoricProcessInstance p = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(t.getProcessInstanceId())
.singleResult();
return new ProcessVO(p).withTask(t) // 拿到任務編號和任務名稱
// 拿到創建時和中途加入的自定義參數
.withVariables(taskService.getVariables(t.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", FlowableConfig.AUDIT_TYPE_KEY)
.parameter("typeValue", auditType)
.parameter("statusKey", FlowableConfig.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 ProcessVO(p).withActivity(a).withVariables(params);
}).collect(Collectors.toList())
查看我創建的任務
這個比較方便拿到,但是當前最新的任務比較難拿到,有時還不準確
// startedBy:創建任務時設置的發起人
HistoricProcessInstanceQuery instanceQuery = historyService.createHistoricProcessInstanceQuery()
.startedBy(user.getId());
// 自定義參數篩選
if (StringUtils.isNotEmpty(auditType)) {
instanceQuery.variableValueEquals(FlowableConfig.AUDIT_TYPE_KEY, auditType);
}
if(auditStatus != null){
instanceQuery.variableValueEquals(FlowableConfig.AUDIT_STATUS_KEY, auditStatus.toString());
}
instanceQuery
.orderByProcessInstanceStartTime().desc()
.listPage(getFirstIdx(pageNum), getPageSize()).stream()
// 獲取其中的詳細和自定義參數
.map(this::convertHostoryProcess)
.collect(Collectors.toList())
獲取其中的詳細和自定義參數
private ProcessVO 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);
ProcessVO data = new ProcessVO(p);
if (!activities.isEmpty()) {
data.withActivity(activities.get(0));
}
return (ProcessVO) data.withVariables(params);
}
撤銷流程實例(假的刪除)
撤銷後,流程直接中斷,除了用戶不能操作和多了結束時間、刪除理由外,其他停留在撤銷前的狀態。
Task task = taskService.createTaskQuery().processInstanceId(id).singleResult();
// TODO 可能需要限制只能在審批前刪除
// ProcessVO.AuditStatus auditStatus = ProcessVO.AuditStatus.valueOf((String)runtimeService.getVariable(task.getExecutionId(), FlowableConfig.AUDIT_STATUS_KEY));
runtimeService.setVariable(task.getExecutionId(), FlowableConfig.AUDIT_STATUS_KEY, ProcessVO.AuditStatus.cancel.toString());
runtimeService.deleteProcessInstance(id, "用戶撤銷");
用戶操作(同意、拒絕)
是否有審批權限需要自己判斷。
操作後進入下一序列流,再次拿這個taskId會獲取不到這個Task,所以上傳審批意見和附件什麽的要在操作前。
if(!userTaskService.isAssigneeOrInCandidateGroup(user, taskId)){
return new ApiResult(ApiResultCode.ILLEGAL_PARAMS, "無法操作");
}
// 同意前設置,上傳審批意見和附件
userTaskService.beforeAgreeOrReject(user, taskId, ProcessVO.AuditStatus.agreeAudit, "同意", reason);
// 拒絕前設置,上傳審批意見和附件
// userTaskService.beforeAgreeOrReject(user, taskId, ProcessVO.AuditStatus.rejectAudit, "拒絕", reason);
// 同意
taskService.complete(taskId, Collections.singletonMap(FlowableConfig.APPROVED_KEY, true));
// 拒絕
// taskService.complete(taskId, Collections.singletonMap(FlowableConfig.APPROVED_KEY, false));
判斷是否有權限
public boolean isAssigneeOrInCandidateGroup(UserVO user, String taskId){
long count = taskService.createTaskQuery()
.taskId(taskId)
.or()
.taskAssignee(user.getId())
.taskCandidateGroup(user.getGroup())
.endOr().count();
return count > 0;
}
操作前設置,上傳審批意見和附件
public void beforeAgreeOrReject(UserVO user, String taskId, ProcessVO.AuditStatus auditStatus, String operate, ReasonParam reason){
// 組成員操作後方便查詢
taskService.setAssignee(taskId, user.getId());
if(StringUtils.isNotEmpty(reason.getText())){
// 審批意見
taskService.addComment(taskId, null, operate, reason.getText());
}
if(StringUtils.isNotEmpty(reason.getImages())){
String[] imgs = reason.getImages().split(",");
for(String img : imgs){
// 上傳附件,可以直接上傳字節流(不建議)
taskService.createAttachment("image/*", taskId,
null, img, null, FTP_SERVER_DOWNLOAD_URL + img);
}
}
// 更新狀態,方便篩選
taskService.setVariable(taskId, FlowableConfig.AUDIT_STATUS_KEY, auditStatus.toString());
}
收工
剛寫博客,不太會用文字表達,歡迎大家提出意見。
以前沒有用文字記事的習慣,這一篇全是代碼的文章都寫了兩三個小時。。。
感謝閱讀,歡迎點贊up~
flowable筆記 - 簡單的通用流程