通用流程可以用于一些基本的申请,例如请假、加班。
大致过程是:
1. 创建申请
2. 分配给审批人(需要审批人列表,当前审批人)
-> 有下一个审批人 -> 3
-> 无 -> 4
3. 审批人审批
-> 同意 -> 2
-> 拒绝 -> 5
4. 存储数据,发送通知
5. 结束
比较简单,唯一的难点就是动态设置审批人或者审批组,下面开始代码部分。
...
<!-- 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~
原文:https://www.cnblogs.com/xwbz/p/9886696.html