开发自定义流程设计器的原因:
软件领域,一直都是没有最好的方案,只有最适合的方案,单说方案,没有任何意义,要结合应用场景,不然就不会有这么多的开发语言、技术框架,早就被最好的一统江湖了。
目前比较流行的设计器:
1.activiti-modeler:
优点:灵活,可以实现各种bpmn流程设计操作;
缺点:学习成本高,容易配置出错,配置流程只能由熟悉这套逻辑的人,甚至是只有开发人员才能配置,人工成本高;
适用场景:流程灵活复杂的场景;
2.bpmn.js:
优点:灵活度适中,可定制开发;
缺点:若扩展功能,需额外开发成本;学习成本稍低,但也需要熟悉bpmn规范,配置流程只能由熟悉这套逻辑的人配置,人工成本稍高;
适用场景:流程不复杂,只需要简易的bpmn流程功能的场景;
3.类似钉钉的流程设计器:
优点:流程配置简单,学习成本低,普通用户容易上手;
缺点:开发成本高,只能配置简易的流程;
适用场景:流程简单,需C端用户自己配置流程的场景;
这块是整个系统最复杂的部分,需要熟悉bpmn规范,算法,后端部署操作;
实现的核心是前端操作和生成bpmn的xml字符串给后端直接使用,后端不用做什么,只需要1.存xml 2.查询返回xml给前端操作 3.部署xml;所以工作量基本都在前端;
具体步骤:
新建流程:
1.前端直接从定义好的xml模板中初始化;
这里的模板其实就是提前在保存好的xml文件,钉钉也有类似的功能,进去之后左侧可以选择一些它设置好的流程。我这里预习放了一个模板: '发起人' -> '审批人' -> '抄送' -> '结束'。这里也是一个技巧,bpmn-moddle不需要创建对象在操作属性,可以直接从模板中创建对象再操作属性,会方便很多,类似写文章填空(审批人、分支条件、抄送人)总比重头写省不少事;
上几个模板的xml代码吧:
新建流程进去之后的模板: '发起人' -> '审批人' -> '抄送' -> '结束'
sid-DE4A60B5-3607-4501-BC30-C83C86223148
0
SAIUWVDZ
${nrOfCompletedInstances == nrOfInstances}
sid-46FA3418-A51D-45EB-9337-FB7A1D8CEDDC
解读:
taskSequentialListenerCreate:
/**
* 会签任务监听-创建
* 指定collection参数,会签人
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
@Data
@Slf4j
@Service
public class TaskSequentialListenerCreate implements ExecutionListener {
/**
* 会签类型(0:指定人员,1:角色,2:主管,3:多级主管,4:发起人自己)
*/
private Expression type;
/**
* 集合key,为了区分多个会签任务节点(集合(多实例)设置的字符串)
*/
private Expression collectionKey;
/**
* 用户ls
*/
private Expression users;
/**
* 角色ls(财务、人事、研发、行政...,钉钉这里只能是一个角色,此处做了升级优化,可以设置多个角色)
*/
private Expression roles;
/**
* 主管ls(发起人的直接主管、从下往上第0....19级主管)
*/
private Expression grade;
/**
* 通讯录中的第xx级主管,从上往下(第0....19级主管)
*/
private Expression heightGrade;
/**
* 多级主管类型 0:指定角色 1:通讯录中的heightGrade
*/
private Expression moreGradeType;
/**
* 找不到主管时,由上级主管代审批标记(默认true) true:找不到往上找,false:指点主管层级
*/
private Expression gradeNext;
/**
* 审批人为空时,指定的默认审批用户
*/
private Expression defaultUserId;
@Autowired
private TaskService taskService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private CopyService copyService;
@Autowired
private ISysUserService sysUserService;
@Override
public void notify(DelegateExecution execution) {
String key = String.valueOf(collectionKey.getValue(execution));
LinkedHashSet us = execution.getVariable(key, LinkedHashSet.class);
//不空的话就不用重复赋值
if (ObjectUtil.isNotNull(us)) {
return;
}
// log.info("type: {}, collectionKey: {}, users: {}, roles: {}, grade: {}, heightGrade: {}, moreGradeType: {}, gradeNext: {}, defaultUserId: {}, ",
// type.getValue(execution), collectionKey.getValue(execution), users.getValue(execution),
// roles.getValue(execution), grade.getValue(execution), heightGrade.getValue(execution),
// moreGradeType.getValue(execution), gradeNext.getValue(execution), defaultUserId.getValue(execution));
String typeStr = String.valueOf(type.getValue(execution));
//用户id ls,要保证有序
List userList = new ArrayList<>();
List userLs = new ArrayList<>();
// //处理用户
switch (typeStr) {
//指定成员
case "0": {
Object usersObj = ObjectUtil.isNotEmpty(users) ? users.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(usersObj)) {
//用户id ls
userList = copyService.getList(String.valueOf(usersObj));
}
break;
}
//角色
case "1": {
Object rolesObj = ObjectUtil.isNotEmpty(roles) ? roles.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(rolesObj)) {
List roleList = copyService.getList(String.valueOf(rolesObj));
if (ObjectUtil.isNotEmpty(roleList)) {
//角色获取用户
userLs = sysUserService.getUserListByActRoleList(roleList);
for (SysUser user : userLs) {
userList.add(user.getId());
}
}
}
break;
}
//主管
case "2": {
//默认0 直接主管
Integer gradeNum = 0;
Object gradeObj = ObjectUtil.isNotEmpty(grade) ? grade.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(gradeObj)) {
gradeNum = Integer.parseInt(String.valueOf(gradeObj));
}
//默认true 找不到则往上找
Boolean gradeNextFlag = true;
Object gradeNextObj = ObjectUtil.isNotEmpty(gradeNext) ? gradeNext.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(gradeNextObj)) {
gradeNextFlag = Boolean.parseBoolean(String.valueOf(gradeNextObj));
}
//获取主管
//发起人所在的部门。
Integer deptId = execution.getVariable("startUserDeptId", Integer.class);
if (ObjectUtil.isEmpty(deptId)) {
//部门id空的时候,使用启动用户所在部门id
//这里不能这样查,第一个节点的pi查出来是null的!
// ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(execution.getProcessInstanceId()).singleResult();
// Integer startUserId = Integer.parseInt(pi.getStartUserId());
//发起人用户id
Integer startUserId = execution.getVariable("startUserId", Integer.class);
deptId = sysUserService.getById(startUserId).getDeptId();
}
if (ObjectUtil.isNotEmpty(deptId)) {
userLs = sysUserService.getUsersByDeptId(deptId, gradeNum, gradeNextFlag);
for (SysUser user : userLs) {
userList.add(user.getId());
}
}
break;
}
//多级主管,实现上有个问题就是一个部门上有多个主管的,目前做法的随机的顺序签
case "3": {
//多级主管类型 0:指定角色 1:通讯录中的heightGrade
String moreGradeTypeStr = "0";
Object moreGradeTypeObj = ObjectUtil.isNotEmpty(moreGradeType) ? moreGradeType.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(moreGradeTypeObj)) {
moreGradeTypeStr = String.valueOf(moreGradeTypeObj);
}
//角色获取用户
Set userRoleList = new HashSet<>();
Object rolesObj = ObjectUtil.isNotEmpty(roles) ? roles.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(rolesObj)) {
List roleList = copyService.getList(String.valueOf(rolesObj));
if (ObjectUtil.isNotEmpty(roleList)) {
for (SysUser user : sysUserService.getUserListByActRoleList(roleList)) {
userRoleList.add(user.getId());
}
}
}
//向上层级,默认0
Integer gradeNum = 0;
Object gradeObj = ObjectUtil.isNotEmpty(grade) ? grade.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(gradeObj)) {
gradeNum = Integer.parseInt(String.valueOf(gradeObj));
}
//向下层级,默认0
Integer heightGradeNum = 0;
Object heightGradeObj = ObjectUtil.isNotEmpty(heightGrade) ? heightGrade.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(heightGradeObj)) {
heightGradeNum = Integer.parseInt(String.valueOf(heightGradeObj));
}
//启动用户所在部门
Integer deptId = execution.getVariable("startUserDeptId", Integer.class);
if (ObjectUtil.isEmpty(deptId)) {
//部门id空的时候,使用启动用户所在部门id
//这里不能这样查,第一个节点的pi查出来是null的!
// ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(execution.getProcessInstanceId()).singleResult();
// Integer startUserId = Integer.parseInt(pi.getStartUserId());
//发起人用户id
Integer startUserId = execution.getVariable("startUserId", Integer.class);
deptId = sysUserService.getById(startUserId).getDeptId();
}
if (ObjectUtil.isNotEmpty(deptId)) {
userLs = sysUserService.getUsersByDeptId(deptId, moreGradeTypeStr, userRoleList, gradeNum, heightGradeNum);
for (SysUser user : userLs) {
userList.add(user.getId());
}
}
break;
}
//发起人自己
case "4": {
//发起人用户id
Integer startUserId = execution.getVariable("startUserId", Integer.class);
userList.add(startUserId);
}
default:
}
if (ObjectUtil.isEmpty(userList)) {
//审批人为空时指定默认用户,defaultUserId默认为0,兜底处理,userList为空数组的话,流程里面这个节点就跳过了!!!
Integer defaultUser = 0;
Object defaultUserObj = ObjectUtil.isNotEmpty(defaultUserId) ? defaultUserId.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(defaultUserObj)) {
defaultUser = Integer.parseInt(String.valueOf(defaultUserObj));
}
log.info("defaultUserId: {}", defaultUser);
userList.add(defaultUser);
}
//去重(避免同一个节点同一个人多次审批)
LinkedHashSet userSet = new LinkedHashSet<>();
userSet.addAll(userList);
log.info("userSet: {}", userSet);
//离职人员不分配任务,需要指派
Set ids = sysUserService.listOffJob(userSet);
log.info("ids: {}", ids);
// //获取审批人列表
// List> arr = execution.getVariable("assigneeListHis", List.class);
// if(ObjectUtil.isEmpty(arr)) {
// arr = new ArrayList<>();
// }
// if(ObjectUtil.isNotEmpty(userSet)) {
// arr.add(userSet);
// //塞审批人
// execution.setVariable("assigneeListHis", arr);
// }
//塞一下审批人类型, 给自动通过审批使用
execution.setVariable("assigneeType", typeStr);
//设置会签用户
runtimeService.setVariable(execution.getId(), key, ids);
}
}
copyService:
/**
* 抄送任务-服务任务
* 设计器上
*
*
*
*
*
*
*
*
* @Author: ltx
* @Date: 2020/5/15 12:39
* @Description:
*/
@Service
@Slf4j
public class CopyService implements JavaDelegate {
@Autowired
private IActCopyTaskService actCopyTaskService;
@Autowired
private ISysUserService sysUserService;
@Autowired
@Lazy
private IActService actService;
@Autowired
private DingTalk dingTalk;
/**
* 用户ls
*/
private Expression users;
/**
* 角色ls
*/
private Expression roles;
/**
* 主管ls
*/
private Expression grades;
@Override
public void execute(DelegateExecution execution) {
log.info("抄送服务....start");
//使用LinkedHashSet,避免同一用户抄送两次
LinkedHashSet userList = new LinkedHashSet<>();
//1.处理用户
Object usersObj = ObjectUtil.isNotEmpty(users) ? users.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(usersObj)) {
List us = getList(usersObj);
log.info("抄送用户us: {}", us);
userList.addAll(us);
}
//2.处理角色
Object rolesObj = ObjectUtil.isNotEmpty(roles) ? roles.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(rolesObj)) {
List rs = getList(rolesObj);
log.info("抄送角色rs: {}", rs);
if (ObjectUtil.isNotEmpty(rs)) {
List users = sysUserService.getUserListByActRoleList(rs);
for (SysUser user : users) {
userList.add(user.getId());
}
}
}
//处理主管
//发起人用户id
Integer startUserId = execution.getVariable("startUserId", Integer.class);
Object gradesObj = ObjectUtil.isNotEmpty(grades) ? grades.getValue(execution) : null;
if (ObjectUtil.isNotEmpty(gradesObj)) {
List gs = getList(gradesObj);
log.info("抄送主管层级gs: {}", gs);
if (ObjectUtil.isNotEmpty(gs)) {
List users = sysUserService.getUsersByGrades(startUserId, gs);
for (SysUser user : users) {
userList.add(user.getId());
}
}
}
log.info("抄送用户userList: {}", userList);
//执行抄送
if (!userList.isEmpty()) {
//离职人员ls
Set ls = sysUserService.listOffJobIds(userList);
if (ls.isEmpty()) {
copyToUser(userList, execution);
} else {
//抄送人里面排除掉离职人员
Set res = new HashSet<>();
for (Integer id : userList) {
if (!ls.contains(id)) {
res.add(id);
}
}
copyToUser(res, execution);
}
}
log.info("抄送服务....end");
}
/**
* 返回list值
*
* @return
*/
public List getList(Object obj) {
//用set去重
List ls = new ArrayList<>();
if (ObjectUtil.isNotNull(obj)) {
String objStr = String.valueOf(obj).trim();
String[] strs = objStr.split(",");
for (String s : strs) {
if (StrUtil.isNotBlank(s)) {
Integer id = Integer.parseInt(s);
if (!id.equals(0)) {
ls.add(Integer.parseInt(s));
}
}
}
}
return ls;
}
/**
* 数据保存在抄送表
*
* @param us 抄送用户ids
* @return
*/
public void copyToUser(Collection us, DelegateExecution execution) {
if (us.isEmpty()) {
return;
}
List ls = new ArrayList<>();
for (Integer u : us) {
ActCopyTask act = new ActCopyTask();
act.setUserId(u);
act.setExecutionId(execution.getId());
act.setProcessInstanceId(execution.getProcessInstanceId());
act.setProcessDefinitionId(execution.getProcessDefinitionId());
act.setCreateTime(new Date());
ls.add(act);
//推送数据到websocket
actService.sendTaskByUserId(String.valueOf(u));
}
//批量保存
actCopyTaskService.saveBatch(ls);
}
}
2.使用bpmn-moddle对节点进行操作;
比如修改节点属性值,添加自定义属性等操作,这里就可以把审批人设置到节点上,比如配置审批人为'指定成员'、'主管'、'角色'、'连续多级主管'、'发起人自己',多人会签方式:'依次审批'、'会签(须所有审批人同意)'、'或签(一名审批人同意或拒绝即可)',抄送;
审批人节点用的是bpmn2:userTask节点,抄送用的是bpmn2:serviceTask节点;
3.bpmn-moddle将操作的对象转xml,发给后端存起来,或直接部署;
bpmn-moddle核心方法:
将xml转换成对象,这个对象类似一个json结构,主要由有点和线组成,前端跑一下它README的demo就清楚了;
用有向图的算法操作这个对象(节点新增和删除,节点可以是bpmn规范里面的任意节点),对象类似json结构,上面的节点也有很多function可以任意set设置节点属性(审批人、会签属性、分支条件....),最后把最终的data转成xml给后端即可;
编辑流程:
1.前端请求后端拿到xml字符串,转bpmn-moddle对象之后操作,完了之后就上上面的第2、3步;
样式和布局:
直接拿了钉钉的前端布局和样式,基本和它一致;当然需要拆解封装各种自己需要的组件,比如发起人节点组件、条件组件、审批人节点组件、抄送节点组件,代码复用组件式开发,数据驱动dom;
前端算法逻辑:
bpmn是一个有向图,只不过它的表现形式是点和线,得用邻接表数据结构表示和操作即可;就是xml->读取节点和线->邻接表->完成图节点的增删->还原点和线->xml。
没写条件分支之前我以为是一个链表,对链表进行节点的增删,觉得还是简单的,写到条件分支(网关)时候才改成有向图算法。
前端js有向图算法部分代码:
import {generateMixed} from '@/utils/tools';
import userTaskTemp from "@/pages/bpmn/edit/template/userTask.xml";
import copyTaskTemp from "@/pages/bpmn/edit/template/copyTask.xml";
import ExclusiveGateway from "@/pages/bpmn/edit/template/ExclusiveGateway.xml";
import sequenceFlow from "@/pages/bpmn/edit/template/sequenceFlow.xml";
import BpmnModdle from 'bpmn-moddle';
import {v4 as uuidv4} from 'uuid';
//所有的顶点, se为出度, te为入度
export let nodes = [
{id: '1', name: 'S', se: 0, te: 0, adj: [], type: 'start'},
{id: '6', name: 'G1', se: 0, te: 0, adj: [], type: 'exclusiveGateway'},
{id: '2', name: 'A', se: 0, te: 0, adj: [], type: 'userTask'},
{id: '3', name: 'B', se: 0, te: 0, adj: [], type: 'userTask'},
{id: '4', name: 'C', se: 0, te: 0, adj: [], type: 'userTask'},
{id: '5', name: 'E', se: 0, te: 0, adj: [], type: 'end'},
{id: '7', name: 'D', se: 0, te: 0, adj: [], type: 'exclusiveGateway'},
{id: '8', name: 'F', se: 0, te: 0, adj: [], type: 'userTask'},
{id: '9', name: 'G', se: 0, te: 0, adj: [], type: 'userTask'},
{id: '10', name: 'H', se: 0, te: 0, adj: [], type: 'userTask'},
];
//所有的边
export let edges = [
{sid: '1', tid: '6'},
{sid: '2', tid: '3'},
{sid: '3', tid: '4'},
{sid: '4', tid: '5'},
{sid: '6', tid: '2'},
{sid: '6', tid: '7'},
{sid: '7', tid: '8'},
{sid: '7', tid: '9'},
{sid: '8', tid: '10'},
{sid: '9', tid: '10'},
{sid: '10', tid: '5'},
];
export let jsonData = [
{id: '1', name: 'S', se: 0, te: 0, adj: ['6'], type: 'start'},
{
id: '6', name: 'G1', se: 0, te: 0, adj: ['2', '5'], type: 'exclusiveGateway',
conditions: [{
id: `sid-${uuidv4().toUpperCase()}`,
name: '条件1',
extension: {
des: '',
jsonStr: '',
},
el: '',
//对象
children: [
{id: '2', name: 'A', se: 0, te: 0, adj: ['3'], type: 'userTask'},
{id: '3', name: 'B', se: 0, te: 0, adj: ['4'], type: 'userTask'},
{id: '4', name: 'C', se: 0, te: 0, adj: ['5'], type: 'userTask'},
]
}, {
id: `sid-${uuidv4().toUpperCase()}`,
name: '条件2',
extension: {
des: '',
jsonStr: '',
},
el: '',
children: []
}]
},
{id: '5', name: 'E', se: 0, te: 0, adj: [], type: 'end'},
];
//使用模版生成bpmn对象,可以省很多事情,简直666
export const getBpmnFromXml = async (type) => {
let bpmnModdle = new BpmnModdle();
switch (type) {
case 'userTask': {
//设置下集合的key
let userTaskTempStr = userTaskTemp.replace(
/collectionKeyValue/g,
generateMixed(8),
);
const {rootElement: definitions} = await bpmnModdle.fromXML(
userTaskTempStr,
);
let userTask = definitions.rootElements[0].flowElements[0];
//设置下随机id
userTask.id = `sid-${uuidv4().toUpperCase()}`;
return userTask;
}
case 'copyTask': {
const {rootElement: definitions} = await bpmnModdle.fromXML(copyTaskTemp);
let copyTask = definitions.rootElements[0].flowElements[0];
//设置下随机id
copyTask.id = `sid-${uuidv4().toUpperCase()}`;
return copyTask;
}
case 'condition': {
const {rootElement: definitions} = await bpmnModdle.fromXML(ExclusiveGateway);
let condition = definitions.rootElements[0].flowElements[0];
//设置下随机id
condition.id = `sid-${uuidv4().toUpperCase()}`;
return condition;
}
case 'sequenceFlow': {
const {rootElement: definitions} = await bpmnModdle.fromXML(sequenceFlow);
let condition = definitions.rootElements[0].flowElements[0];
//设置下随机id
condition.id = `sid-${uuidv4().toUpperCase()}`;
return condition;
}
}
//打印bpmn结果
// const {rootElement: definitions} = await bpmnModdle.fromXML(userTaskTemp);
// console.log(definitions.rootElements[0].flowElements[0]);
};
//id查找节点
export const findNodeById = (id) => {
for (let node of nodes) {
if (id == node.id) {
return node;
}
}
};
//id查找出度节点
export const findTNodesById = (id) => {
let tNodes = [];
for (let node of nodes) {
if (id == node.id) {
node.adj.map((a) => {
tNodes.push(findNodeById(a));
});
}
}
return tNodes;
};
//id查找入度节点
export const findSNodesById = (id) => {
let sNodes = [];
for (let node of nodes) {
for (let a of node.adj) {
if (a == id) {
sNodes.push(node);
break;
}
}
}
return sNodes;
};
//查找起始节点(入度为0的则是起始节点)
export const findStartNode = () => {
for (let node of nodes) {
if (node.te == 0) {
return node;
}
}
};
//打印顺序的串
export const randerNode = (nodeId, ls) => {
// console.log(nodeId);
let node = findNodeById(nodeId);
ls.push(node);
if (node.adj.length) {
return node.adj.map((a) => {
return randerNode(a, ls);
});
}
};
//添加任务节点
export const addNode = async (index, node, parents, type) => {
// console.log(index, node, parents, type);
let newNode;
switch (type) {
case 'approver': {
//新节点
newNode = await getBpmnFromXml('userTask');
//激活
newNode.active = true;
newNode.adj = [];
newNode.se = 1;
newNode.te = 1;
break;
}
case 'notifier': {
//抄送
newNode = await getBpmnFromXml('copyTask');
//激活
newNode.active = true;
newNode.adj = [];
newNode.se = 1;
newNode.te = 1;
break;
}
case 'condition': {
let ls = [];
let i = index + 1;
let newp = [...parents];
newp.map((item, indexI) => {
if (indexI > index && item.$type != 'bpmn:EndEvent') {
ls.push(item);
parents.splice(i, 1);
}
});
newNode = await getBpmnFromXml('condition');
newNode.conditions = [{
id: `sid-${uuidv4().toUpperCase()}`,
name: '条件1',
extension: {
des: '',
jsonStr: '',
},
el: '',
children: ls
}, {
id: `sid-${uuidv4().toUpperCase()}`,
name: '条件2',
extension: {
des: '',
jsonStr: '',
},
el: '',
children: []
}];
break;
}
}
parents.splice(index + 1, 0, newNode);
// console.log(parents);
// console.log(jsonData);
jsonData = [...jsonData];
return newNode;
};
//删除元素
export const delNode = (index, node, parents) => {
parents.splice(index, 1);
// console.log(jsonData);
jsonData = [...jsonData];
};
//添加条件
export const addNodeCondition = (node) => {
let newNode = {
id: `sid-${uuidv4().toUpperCase()}`,
name: `条件${node.conditions.length + 1}`,
extension: {
des: '',
jsonStr: '',
},
el: '',
children: []
};
node.conditions.push(newNode);
// console.log(jsonData);
jsonData = [...jsonData];
};
//调整条件节点位置
export const sortNodeCondition = (conditions, index, type) => {
let temp = conditions[index];
if (type == 'left') {
conditions[index] = conditions[index - 1];
conditions[index - 1] = temp;
} else if (type == 'right') {
conditions[index] = conditions[index + 1];
conditions[index + 1] = temp;
}
// console.log(jsonData);
jsonData = [...jsonData];
};
//删除条件
export const delNodeCondition = (parents, index, gIndex, gParent) => {
if (parents.length > 2) {
parents.splice(index, 1);
} else if (parents.length == 2) {
//剩余的元素全部放外层
// console.log(parents[1 - index].children, gIndex, gParent);
let i = gIndex;
parents[1 - index].children.map((item) => {
gParent.splice(i, 0, item);
i++;
});
gParent.splice(i, 1);
}
// parents.splice(index, 1);
// console.log(jsonData);
jsonData = [...jsonData];
};
//初始化边
export const addEdge = (edge) => {
//源节点
let sNode = findNodeById(edge.sourceRef.id);
//目标节点
let tNode = findNodeById(edge.targetRef.id);
sNode.adj.push(tNode.id);
sNode.lines.push(edge);
sNode.se++;
tNode.te++;
};
/**
* 邻接表图数据结构转json
* @param item
* @param rs
*/
export const handerGtoJ = (item, rs) => {
rs.push(item);
if (item.adj.length) {
if (item.adj.length > 1) {
//有分支
item.conditions = [];
let pls = [];
// let lastNode;
item.adj.map((a, index) => {
let node = findNodeById(a);
let ls = [];
handerGtoJ(node, ls);
pls.push([...ls]);
// console.log(item);
//处理条件
let condition = {};
if (item?.lines?.length) {
if(item.lines[index]) {
let line = item.lines[index];
if (line?.extensionElements?.values?.length) {
let eles = line.extensionElements.values;
eles.map(ele => {
if (ele.$children?.length) {
ele.$children.map(v => {
if ('des' == ele.name) {
if(v.$body) {
condition.des = unescape(v.$body);
}
}
if ('jsonStr' == ele.name) {
//需要转义一下
if(v.$body) {
condition.jsonStr = unescape(v.$body);
}
}
});
}
});
}
}
}
item.conditions.push({
id: `sid-${uuidv4().toUpperCase()}`,
name: item.lines[index].name,
extension: condition,
el: item.lines[index]?.conditionExpression?.body ? item.lines[index].conditionExpression.body : '',
children: ls
});
});
//这里的算法:结尾相同的,合并了,然后放上一级
let f = [];
for (let pl of pls) {
pl.reverse();
}
let pln = pls[pls.length - 1];
for (let j = 0; j < pln.length; j++) {
f.push(true);
pls.map((ps) => {
if (ps[j] != pln[j]) {
f[j] = false;
}
});
}
f.reverse();
pln.reverse();
f.map((fe, index) => {
if (fe) {
rs.push(pln[index]);
item.conditions.map((ele) => {
ele.children.pop();
});
}
});
} else {
//无分支
item.adj.map((a) => {
let node = findNodeById(a);
handerGtoJ(node, rs);
});
}
}
};
/**
* json结构转邻接表
*/
export const handerJtoG = (node, rs) => {
rs.push(node);
if (node?.conditions?.length) {
node.conditions.map((condition) => {
if (condition?.children?.length) {
condition.children.map((item, index) => {
handerJtoG(item, rs);
});
}
});
}
};
export const handleChangeNodeData = (node, newData) => {
if (newData.id == node.id) {
node = newData;
}else {
node.active = false;
}
if (node?.conditions?.length) {
node.conditions.map((condition) => {
if (condition?.children?.length) {
condition.children.map((item, index) => {
handleChangeNodeData(item, newData);
});
}
});
}
};
/**
* 修改节点数据
* @param newData
*/
export const changeNodeData = (newData) => {
jsonData.map((node, index) => {
handleChangeNodeData(node, newData);
});
jsonData = [...jsonData];
// console.log(jsonData);
};
export const handleChangeConditionData = (node, newConditionData) => {
if (node?.conditions?.length) {
node.conditions.map((item) => {
if (newConditionData.id == item.id) {
item = newConditionData;
}
});
}
};
/**
* 修改条件数据
* @param newData
*/
export const changeConditionData = (newConditionData) => {
jsonData.map((node, index) => {
handleChangeConditionData(node, newConditionData);
});
jsonData = [...jsonData];
// console.log(jsonData);
};
/**
*json转邻接表
*/
export const JtoG = () => {
let rs = [];
jsonData.map((node, index) => {
handerJtoG(node, rs);
});
// console.log(rs);
return rs;
};
export const handleAdj = (node, nodeN, rs) => {
// rs.push(node.id);
// console.log(node, nodeN, rs);
if (node?.conditions?.length) {
// console.log(node);
node.conditions.map((condition) => {
if (condition?.children?.length) {
rs.push(condition.children[0].id);
condition.children.map((item, index) => {
if (index < condition.children.length - 1) {
handleAdj(item, condition.children[index + 1], []);
} else if (index == condition.children.length - 1) {
//只有一个子节点(网关特殊处理)
item.adj = [];
if(item.$type == 'bpmn:ExclusiveGateway') {
item.conditions.map(()=>{
item.adj.push(nodeN.id);
});
}else {
item.adj.push(nodeN.id);
}
}
});
} else {
rs.push(nodeN.id);
}
});
} else {
rs.push(nodeN.id);
}
node.adj = rs;
};
/**
* 各个节点修改新的adj
*/
export const resetAdj = () => {
// console.log(jsonData);
jsonData.map((item, index) => {
if (index < jsonData.length - 1) {
handleAdj(item, jsonData[index + 1], []);
}
});
// console.log(jsonData);
};
/**
* 点和线转邻接表图数据结构
* @param edges
*/
export const NtoG = () => {
//初始化邻接表
edges.map((edge) => {
addEdge(edge);
});
// console.log(nodes);
};
/**
* 邻接表转json
* @param edges
* @constructor
*/
export const GtoJ = () => {
let startNode = findStartNode();
let rs = [];
handerGtoJ(startNode, rs);
// console.log(rs);
jsonData = [...rs];
};
/**
* 获取邻接表结构
*/
export const handleG = async () => {
resetAdj();
nodes = JtoG();
//获取邻接表结构
let eles = [...nodes];
//连线
await Promise.all(nodes.map((node, index) => {
// console.log(node);
node.incoming = [];
node.outgoing = [];
//
// if (node?.conditions?.length > 1) {
//
// }
//创建连线
node.adj.map(async (a, index) => {
let newFlow = await getBpmnFromXml('sequenceFlow');
newFlow.sourceRef = node;
newFlow.targetRef = findNodeById(a);
if (node?.conditions?.length > 1) {
// console.log(node);
newFlow.conditionExpression.body = node.conditions[index].el;
newFlow.name = node.conditions[index].name;
//网关要设置出口连线
node.outgoing.push(newFlow);
if (node?.conditions[index]?.extension) {
if (newFlow?.extensionElements?.values?.length) {
let eles = newFlow.extensionElements.values;
eles.map(ele => {
if (ele.$children?.length) {
ele.$children.map(v => {
if ('des' == ele.name) {
if(node?.conditions[index]?.extension?.des) {
v.$body = escape(node.conditions[index].extension.des);
}
}
if ('jsonStr' == ele.name) {
if(node?.conditions[index]?.extension?.jsonStr) {
//需要转义一下
v.$body = escape(node.conditions[index].extension.jsonStr);
}
}
});
}
});
}
}
}
eles.push(newFlow);
// return;
});
// console.log(eles);
}));
// console.log(eles);
return eles;
};
export const getG = async () => {
resetAdj();
//获取邻接表结构
return JtoG();
};
/**
* 处理点和线
* @param ls
*/
export const getNodeLine = (ls) => {
let dots = [];
let lines = [];
ls.map((item) => {
if ('bpmn:SequenceFlow' == item.$type) {
lines.push(item);
} else {
item.adj = [];
item.lines = [];
item.se = 0;
item.te = 0;
dots.push(item);
}
});
nodes = dots;
edges = lines;
// console.log(nodes);
// console.log(edges);
};
//初始化
export const init = (data) => {
getNodeLine(data);
//点和线转邻接表图数据结构
NtoG();
//邻接表转json
GtoJ();
//json转邻接表
// JtoG();
};
细节:
发起节点:什么都不用管,就一个bpmn2:startEvent,钉钉上还做了权限控制,我项目还没实现,我的实现思路是建立部署id和用户id的关联关系,有权限的用户就可以"看到"这个流程,可以发起,没权限的就"看不到"这个流程,不能发起;
审批节点:上面也说了,它是一个xml节点,bpmn2:userTask节点,可以在上面放任何自定义属性和内部标签,后端可以拿到这些属性就可以操作了;
抄送节点:同审批节点,只不过它用的是bpmn2:serviceTask节点;
条件节点:bpmn2:exclusiveGateway 网关节点,可以在上面配置分支条件,bpmn规范里面网关多分支情况,从左侧开始判断,例如 条件分支1,条件分支2,条件分支3,判断顺序1->2->3,必须有条件出口,不然会报异常,条件分支3可以不配置任何条件,就是1和2都不符合的其他情况都会走分支3出口;
结束节点:什么都不用管,就一个bpmn2:endEvent;
动态分配审批人:
所有审批人节点,监听创建事件,实现ExecutionListener接口,动态指定assignee,这样就可以实现各种情况的审批人,'指定成员'、'主管'、'角色'、'连续多级主管'、'发起人自己'。
动态抄送:
serviceTask,bean实现JavaDelegate,在execute方法里面获取配置项,往抄送表(自己添加的表)里面写用户id和流程实例id;
条件分支:
前端直接操作节点,写条件属性;
流程发布:
发布之后把部署之后的部署id(deployment_id)存到表单类型表里面,这样每次发起流程都是最新的版本发布;此处结合表单类型管理查阅。