org.activiti
activiti-spring-boot-starter-basic
6.0.0
另外,我们这里有个场景(下面会讲到)会使用到 mvel 来计算el表达式,所以
org.mvel
mvel2
2.2.1.Final
然后则需要在这个文件夹下建立一个流程图
/resources/processes/
随着项目启动,可以看到,这两个表就已经有数据了
ACT_GE_BYTEARRAY
ACT_RE_PROCDEF
基本的整合就算完成了
3. 用户整合
其实就是把activiti的用户与系统中的整合。
ACTIVITI的认证信息其实在这四个表里,流程中配置的处理人/组 也都是依赖以下四个表。
例如上面BPMN文件中的 activiti:candidateGroups=“电费稽核员” 就表示 当前环节由【电费稽核员】这个用户组的成员进行审批。
ACT_ID_GROUP
ACT_ID_INFO
ACT_ID_MEMBERSHIP
ACT_ID_USER
方案一:弃用activiti自带的认证表
方案二:将自己系统内的用户表同步至activiti
我们这里使用了方案二,由于系统内并没有用户组所以就直接使用了ACT_ID_MEMBERSHIP 作为用户组。
而用户在增删改时,同步至ACT_ID_USER。这里代码很简单 ,只展示出来一些关键的代码
用户整合:
其中 com.account.project.system.user.domain.User 是系统的用户
@Transactional
public void syncSysUser2ActUser(String loginName) {
/* 判断当前登录名对应的流程用户是否存在
* 存在则更新之 否则新增
* */
if (StringUtils.isEmpty(loginName)) {
throw new RuntimeException("loginName不可为空!");
}
com.account.project.system.user.domain.User u = userMapper.selectUserByLoginName(loginName);//根据登录名查询出系统用户
if (u == null) {
throw new RuntimeException("无此用户" + loginName);
}
User user = turnSysUser2ActUser(u);//转化为activiti用户
identityService.saveUser(user);
/* 更新其他附属信息 */
fillUserRefInfo(u);
}
/**
* @Description: 将系统用户转换为流程用户
* @Author: fg
* @Date: 2019/8/8
*/
User turnSysUser2ActUser(com.account.project.system.user.domain.User sysUser) {
User resUser = selectActUserByLoginName(sysUser.getUserAccount());
if (resUser == null) {
resUser = identityService.newUser(sysUser.getUserAccount());
}
resUser.setEmail(sysUser.getMailAddress());
resUser.setPassword(sysUser.getPassword());
return resUser;
}
用户组新增
/**
* @Description: 添加用户组
* @Author: fg
* @Date: 2019/8/8
*/
public void addGroup(String groupId, String groupName) {
if (StringUtils.isEmpty(groupId)) {
throw new BusinessException("ID不可为空");
}
if (StringUtils.isEmpty(groupName)) {
throw new BusinessException("组名不可为空");
}
Group group = identityService.newGroup(groupId);
group.setName(groupName);
identityService.saveGroup(group);
}
添加用户至用户组:
/**
* @Description: 添加组织下的用户
* @Author: fg
* @Date: 2019/8/9
*/
public void addGroupUser(String groupId, String userAccount) {
if (StringUtils.isEmpty(groupId)) {
throw new BusinessException("groupId不可为空");
}
if (StringUtils.isEmpty(userAccount)) {
throw new BusinessException("用户登录名不可为空");
}
User u = identityService.createUserQuery()
.userId(userAccount)
.memberOfGroup(groupId)
.singleResult();
if (u != null) {
throw new BusinessException("当前用户已经在当前组中");
}
/* 关联用户和用户组 */
identityService.createMembership(userAccount, groupId);
}
4. 如何退回
我们这里使用了互斥网关来解决“中国式”的退回
参考上面的流程图
.....
关键点在于这个地方
执行退回
Map map = new HashMap<>();
map.put("action","reject");
taskService.complete(task.getId(), map);
这样就会根据网关中的条件自动走 驳回的线路。
5. 如何转办
转办就很简单了
taskService.setAssignee(taskId,userId);
6. 审批权限校验
大致想要的效果是 在执行每个节点时,会去校验当前用户是否有当前节点的处理权限。
从处理人那里其实分为两类:
一类是用户组,要根据用户组去查询用户
一类是办理人,直接匹配用户。
确定了思路,下面就简单了
/**
* @Description: 校验当前用户是否有权限去审批当前节点
* @Author: fg
* @Date: 2019/8/14
*/
private void validateAuthority(String userAccount, Task task) {
if (StringUtils.isNotEmpty(task.getAssignee())) {//如果是指定了审批人去审批
if (!userAccount.equals(task.getAssignee())) {//当前节点的审批人和即将要执行审批的审批人不一致
throw new BusinessException("当前节点审批人" + userAccount + "无权限审批![" + task.getAssignee() + "]");
}
} else {
List list = getThisNodeByInsId(task.getProcessInstanceId());
for (FlowElement f : list) {//遍历所有节点
List listGroup = ((UserTask) f).getCandidateGroups();
if (listGroup.size() == 0) {
return;
}
for (String group : listGroup) {//遍历所有组
List listUser = identityService.createUserQuery().memberOfGroup(group).list();
for (User u : listUser) {
if (u.getId().equals(userAccount)) {//匹配上了
return;
}
}
}
}
throw new BusinessException("无权限审批");
}
}
然后在执行任务下一步/退回/转办时,执行下这个方法即可(注意控制好事务,BusinessException是我自定义的异常,可自行修改)
7. 提交时选环节选人
这个就比较麻烦了,由于ACTIVITI原生是不支持的,所以就需要自己实现。
为了验证改造测试,我们更改了流程文件的模型。
对应的XML如下
思路如下:
于是代码就有了:
首先自定义一个用于返回前台审批环节和审批人的对象
/**
* @author fg
* @description: 用于流程中选择审批环节和人的vo
* @date 2019/8/1411:45
*/
public class ProcessChooser {
private List users;
private String processKey;
private String processName;
public List getUsers() {
return users;
}
public void setUsers(List users) {
this.users = users;
}
public String getProcessKey() {
return processKey;
}
public void setProcessKey(String processKey) {
this.processKey = processKey;
}
public String getProcessName() {
return processName;
}
public void setProcessName(String processName) {
this.processName = processName;
}
}
然后构造一个根据流程实例ID 去获取下个流程节点的方法:
/**
* @Description: 根据insId 获取下一个可能出现的节点
* @Author: fg
* @Date: 2019/8/14
*/
public List getNextNodeByInsId(String insId,String taskKey) {
List list = new ArrayList<>();
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(insId)
.singleResult();
Task task = taskService.createTaskQuery()
.processInstanceId(insId)
.singleResult();//获取到执行中的task
Map params = taskService.getVariables(task.getId());//获取当前task中的参数
//当前活动节点
List currentActs = runtimeService.getActiveActivityIds(processInstance.getId());
if (currentActs.size() != 1) {
throw new BusinessException("当前节点不支持选人!");
}
String activitiId = currentActs.get(0);//默认第一个节点 除了会签 应该不会出现这种情况
currentActs.forEach(n -> {
System.out.println("当前活动节点是【" + n + "】");
});
//pmmnModel 遍历节点需要它
BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
List processList = bpmnModel.getProcesses();
//循环多个物理流程
for (Process process : processList) {//返回该流程的所有任务,事件
Collection cColl = process.getFlowElements();
//遍历节点
for (FlowElement f : cColl) {//如果该节点是当前节点 输出该节点的下一个节点
if (!f.getId().equals(activitiId)) {//表名是当前的节点
continue;
}
//通过反射来判断是哪种类型
if (f instanceof org.activiti.bpmn.model.UserTask) {
List sequenceFlowList = ((org.activiti.bpmn.model.UserTask) f).getOutgoingFlows();
for (SequenceFlow sf : sequenceFlowList) {
String targetRef = sf.getTargetRef();
FlowElement ref = process.getFlowElement(targetRef);
if (ref instanceof org.activiti.bpmn.model.ExclusiveGateway) {//如果是网关 则查询网关后的节点
ExclusiveGateway gateway = ((org.activiti.bpmn.model.ExclusiveGateway) ref);
List tmpList = gateway.getOutgoingFlows();//网关后面的连线
for (SequenceFlow sf2 : tmpList) {//遍历这些连线 并查询到相关的userTask
if(StringUtils.isEmpty(sf2.getConditionExpression())){//SequenceFlow上如果没有conditionExpression 就不去关注
continue;
}
if(sf2.getConditionExpression().contains("taskKey") && StringUtils.isEmpty(taskKey)){//当前节点是选择的节点 不做处理
String targetRef2 = sf2.getTargetRef();
FlowElement ref2 = process.getFlowElement(targetRef2);
list.add(ref2);
}else if(!StringUtils.isEmpty(taskKey)){//根据输入条件的
Map tmpMap = new HashMap<>();
tmpMap.put("taskKey",taskKey);
Serializable compiled =MVEL.compileExpression(sf2.getConditionExpression().replace("${","").replace("}",""));
Boolean result = MVEL.executeExpression(compiled, tmpMap, Boolean.class);//判断是否会走当前节点
if(result){//符合条件
String targetRef2 = sf2.getTargetRef();
FlowElement ref2 = process.getFlowElement(targetRef2);
list.add(ref2);
}
}else{
Serializable compiled = MVEL.compileExpression(sf2.getConditionExpression().replace("${","").replace("}",""));
Boolean result = MVEL.executeExpression(compiled, params, Boolean.class);//判断是否会走当前节点
if(result){//符合条件
String targetRef2 = sf2.getTargetRef();
FlowElement ref2 = process.getFlowElement(targetRef2);
list.add(ref2);
}
}
}
} else {//货真价实的 userTask
list.add(ref);
}
}
} else if (f instanceof org.activiti.bpmn.model.SequenceFlow) {//SequenceFlow --暂时用不到
} else if (f instanceof org.activiti.bpmn.model.EndEvent) {//结束节点不做处理 --暂时用不到
} else if (f instanceof org.activiti.bpmn.model.ExclusiveGateway) {//路由节点 --暂时用不到
} else if (f instanceof org.activiti.bpmn.model.StartEvent) {//开始节点 --暂时用不到
}
break;
}
}
return list;
}
/**
* @Description: 根据insId获取下一个节点的审批人
* @Author: fg
* @Date: 2019/8/14
*/
public List getNextTaskUserByInsId(String insId,String taskKey) {
List list = new ArrayList<>();
List fList = getNextNodeByInsId(insId,taskKey);
for (FlowElement u : fList) {
List groups = ((org.activiti.bpmn.model.UserTask) u).getCandidateGroups();
for (String n : groups) {
List listUser = identityService.createUserQuery().memberOfGroup(n).list();
for (User m : listUser) {
list.add(m.getId());
}
}
}
return list;
}
这里有几点需要解释:
1,思路是通过instanceId 去获取到当前执行的usertask,然后找到后面的连线(SequenceFlow),再根据连线找到后面的元素X,这里的元素X如果是个usertask 就直接返回,如果是个网关,就把当前流程的参数带入EL表达式,来判断满足条件。把不符合条件的给过滤掉,再查询符合条件的SequenceFlow所连接的userTask,最后根据userTask的参数来确定可选的用户(组)。
2,这个方法不适用与会签,如果有需要,则自行更改
3,仔细阅读下1,可能还需要增加逻辑。
最后封装出用于返回给前台的数据(这里的headId 不用管 实际上就是我业务数据的ID和instanceId是一对一的关系)
public List queryProcessChooserByHeadId(Integer headId) {
List resList = new ArrayList<>();
List fowList = queryNextElementByHeadId(headId,null);
for(FlowElement ele:fowList){
ProcessChooser chooser = new ProcessChooser();
chooser.setProcessKey(ele.getId());
chooser.setProcessName(ele.getName());
chooser.setUsers(userService.selectListUserByList(queryApproverByHeadId(headId,ele.getId())));
resList.add(chooser);
}
return resList;
}
提交时就好处理了
/* 放置参数 */
Map map = new HashMap<>();
map.put("action",actProcessActionEnum.getCode());//执行操作 是下一步还是退回
if(StringUtils.isNotEmpty(taskKey)){
map.put("taskKey",taskKey);//选择下个环节
}
if(StringUtils.isNotEmpty(userAccount)){//userAccount 指定由谁来审批
map.put("userAccount",userAccount);//选择下一环节由谁来审批
}
然后把这个参数放到流程里就可以了
最后的最后 指定一个listener
/**
* @author fg
* @description: 用于选人的任务选择器
* @date 2019/8/1414:47
*/
public class ApproverChosserTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
Map map = delegateTask.getVariables();
if(map.get("userAccount") !=null && StringUtils.isNotEmpty(map.get("userAccount").toString())){
delegateTask.setAssignee(map.get("userAccount").toString());
}
}
}
这样就实现了选人,有什么疑问的可以加我的QQ 116475939一起探讨。