springboot+activiti7+react实现模仿钉钉功能的审批流(五、实现类似钉钉的流程设计器)

开发自定义流程设计器的原因:
软件领域,一直都是没有最好的方案,只有最适合的方案,单说方案,没有任何意义,要结合应用场景,不然就不会有这么多的开发语言、技术框架,早就被最好的一统江湖了。
目前比较流行的设计器:
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代码吧:

新建流程进去之后的模板: '发起人' -> '审批人' -> '抄送' -> '结束'

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions
  xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
  xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
  xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:activiti="http://activiti.org/bpmn" targetNamespace="http://www.activiti.org/processdef">
  <bpmn2:process id="process" name="流程模版" isExecutable="true">
    <bpmn2:startEvent id="sid-50C14770-7956-4B49-BE8A-EAAC55C959BD">
      <bpmn2:outgoing>sid-DE4A60B5-3607-4501-BC30-C83C86223148</bpmn2:outgoing>
    </bpmn2:startEvent>
    <bpmn2:sequenceFlow id="sid-DE4A60B5-3607-4501-BC30-C83C86223148" sourceRef="sid-50C14770-7956-4B49-BE8A-EAAC55C959BD" targetRef="sid-1351A193-F51D-4030-A915-656FE09A44AF" />
    <bpmn2:userTask id="sid-1351A193-F51D-4030-A915-656FE09A44AF" name="审批人" activiti:assignee="${signer}">
      <bpmn2:extensionElements>
        <activiti:executionListener event="start" delegateExpression="${taskSequentialListenerCreate}">
          <activiti:field name="type">
            <activiti:string>0</activiti:string>
          </activiti:field>
          <activiti:field name="users">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="roles">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="grade">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="heightGrade">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="moreGradeType">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="gradeNext">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="defaultUserId">
            <activiti:string><![CDATA[]]></activiti:string>
          </activiti:field>
          <activiti:field name="collectionKey">
            <activiti:string>SAIUWVDZ</activiti:string>
          </activiti:field>
        </activiti:executionListener>
        <activiti:taskListener event="complete" delegateExpression="${taskSequentialListenerComplete}" />
      </bpmn2:extensionElements>
      <bpmn2:multiInstanceLoopCharacteristics activiti:collection="SAIUWVDZ" activiti:elementVariable="signer">
        <bpmn2:completionCondition>${nrOfCompletedInstances == nrOfInstances}</bpmn2:completionCondition>
      </bpmn2:multiInstanceLoopCharacteristics>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="sid-D9B182A6-214F-4341-A505-E2B9AD709AB3" sourceRef="sid-1351A193-F51D-4030-A915-656FE09A44AF" targetRef="sid-FD1F01B6-C756-4FC4-8483-7D724E1CEBFB" />
    <bpmn2:serviceTask id="sid-FD1F01B6-C756-4FC4-8483-7D724E1CEBFB" name="抄送" activiti:delegateExpression="${copyService}">
      <bpmn2:extensionElements>
        <activiti:field name="users">
          <activiti:string><![CDATA[]]></activiti:string>
        </activiti:field>
        <activiti:field name="roles">
          <activiti:string><![CDATA[]]></activiti:string>
        </activiti:field>
        <activiti:field name="grades">
          <activiti:string><![CDATA[]]></activiti:string>
        </activiti:field>
      </bpmn2:extensionElements>
    </bpmn2:serviceTask>
    <bpmn2:sequenceFlow id="sid-200FC431-2CD0-47CC-89D2-35AC17E9CFF4" sourceRef="sid-FD1F01B6-C756-4FC4-8483-7D724E1CEBFB" targetRef="sid-2F8049AB-BF8F-4C94-AF51-3638612FF364" />
    <bpmn2:endEvent id="sid-2F8049AB-BF8F-4C94-AF51-3638612FF364">
      <bpmn2:incoming>sid-46FA3418-A51D-45EB-9337-FB7A1D8CEDDC</bpmn2:incoming>
    </bpmn2:endEvent>
  </bpmn2:process>
</bpmn2:definitions>

解读:

  • taskSequentialListenerCreate,实现了ExecutionListener接口的类,在节点创建时动态指定审批人
  • activiti:field,bpmn-moddle可以塞的各种值比如用户ids、角色ids,然后在taskSequentialListenerCreate里面获取并做操作
  • copyService,抄送bean

taskSequentialListenerCreate:


/**
 * 会签任务监听-创建
 * 指定collection参数,会签人
 * <p>
 * <bpmn2:userTask id="sid-6C10AB05-75BA-48C5-B708-C839C3497EF0" name="审批" activiti:assignee="user001">
 * <extensionElements>
 * <activiti:taskListener event="create" delegateExpression="${taskSequentialListenerCreate}">
 * <activiti:field name="type">
 * <activiti:string><![CDATA[0]]></activiti:string>
 * </activiti:field>
 * <activiti:field name="users">
 * <activiti:string><![CDATA[1,5,6,1002]]></activiti:string>
 * </activiti:field>
 * </activiti:taskListener>
 * <activiti:taskListener event="complete" delegateExpression="${taskSequentialListenerComplete}">
 * <activiti:field name="name">
 * <activiti:string><![CDATA[hello]]></activiti:string>
 * </activiti:field>
 * </activiti:taskListener>
 * </extensionElements>
 * </bpmn2:userTask>
 */
@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<Integer> 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<Integer> userList = new ArrayList<>();
        List<SysUser> 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<Integer> 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<Integer> userRoleList = new HashSet<>();
                Object rolesObj = ObjectUtil.isNotEmpty(roles) ? roles.getValue(execution) : null;
                if (ObjectUtil.isNotEmpty(rolesObj)) {
                    List<Integer> 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<Integer> userSet = new LinkedHashSet<>();
        userSet.addAll(userList);
        log.info("userSet: {}", userSet);
        //离职人员不分配任务,需要指派
        Set<Integer> ids = sysUserService.listOffJob(userSet);
        log.info("ids: {}", ids);
//        //获取审批人列表
//        List<LinkedHashSet<Integer>> 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:

/**
 * 抄送任务-服务任务
 * 设计器上
 * <bpmn2:serviceTask id="sid-28D27128-3AD7-4CB6-AC95-5C07E1F42007" name="服务-抄送" activiti:delegateExpression="${copyService}">
 * <extensionElements>
 * <activiti:field name="users">
 * <activiti:string><![CDATA[1,2,6,7]]></activiti:string>
 * </activiti:field>
 * </extensionElements>
 * </bpmn2:serviceTask>
 *
 * @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<Integer> userList = new LinkedHashSet<>();
        //1.处理用户
        Object usersObj = ObjectUtil.isNotEmpty(users) ? users.getValue(execution) : null;
        if (ObjectUtil.isNotEmpty(usersObj)) {
            List<Integer> 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<Integer> rs = getList(rolesObj);
            log.info("抄送角色rs: {}", rs);
            if (ObjectUtil.isNotEmpty(rs)) {
                List<SysUser> 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<Integer> gs = getList(gradesObj);
            log.info("抄送主管层级gs: {}", gs);
            if (ObjectUtil.isNotEmpty(gs)) {
                List<SysUser> users = sysUserService.getUsersByGrades(startUserId, gs);
                for (SysUser user : users) {
                    userList.add(user.getId());
                }
            }
        }
        log.info("抄送用户userList: {}", userList);
        //执行抄送
        if (!userList.isEmpty()) {
            //离职人员ls
            Set<Integer> ls = sysUserService.listOffJobIds(userList);
            if (ls.isEmpty()) {
                copyToUser(userList, execution);
            } else {
                //抄送人里面排除掉离职人员
                Set<Integer> res = new HashSet<>();
                for (Integer id : userList) {
                    if (!ls.contains(id)) {
                        res.add(id);
                    }
                }
                copyToUser(res, execution);
            }
        }
        log.info("抄送服务....end");
    }

    /**
     * 返回list值
     *
     * @return
     */
    public List<Integer> getList(Object obj) {
        //用set去重
        List<Integer> 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<Integer> us, DelegateExecution execution) {
        if (us.isEmpty()) {
            return;
        }
        List<ActCopyTask> 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)存到表单类型表里面,这样每次发起流程都是最新的版本发布;此处结合表单类型管理查阅。

  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
### 回答1: 基于Spring Boot和Activiti,我们可以实现一个简单的流程审批系统。首先,需要搭建一个Spring Boot项目,并添加Activiti的相关依赖。然后,我们可以使用Activiti提供的API来定义流程、发起审批和处理审批。 在项目中,我们可以通过编写BPMN 2.0的定义文件来定义流程。在该文件中,我们可以添加活动(任务),并定义活动之间的流程转条件。然后,使用Activiti的API部署该流程定义到Activiti引擎中。 在发起审批时,可以通过调用Activiti的API来启动一个流程实例。在启动流程实例时,可以设置相关的参数,如申请人、审批金额等。Activiti会根据流程定义自动创建相应的流程实例和任务。 在处理审批时,可以根据当前用户查询待办任务列表,并选择某个任务进行处理。处理任务可以包括审批通过、驳回、转交等操作。在处理任务时,可以通过Activiti的API设置相关的参数,并将任务转到下一步或退回上一步。 此外,我们还可以添加监听来监听流程的各个状态,如流程启动、任务分配、任务完成等。通过监听,我们可以在流程转时执行一些自定义的逻辑,如发送邮件通知、记录审批历史等。 综上所述,基于Spring Boot和Activiti,我们可以实现一个简单的流程审批系统。通过配置流程定义、发起审批和处理审批,我们可以实现审批流程的自动化和可视化。同时,通过添加监听和自定义逻辑,我们可以满足不同业务场景下的特定需求。 ### 回答2: 利用Spring Boot和Activiti可以很方便地实现流程审批功能。下面我将详细介绍如何使用Spring Boot和Activiti实现流程审批。 首先,需要在Spring Boot项目的pom.xml文件中添加Activiti的依赖。可以在Maven仓库中找到最新的Activiti版本,并将其添加到pom.xml文件中。 接下来,需要创建Activiti的配置类,以便在Spring Boot中集成Activiti。这个配置类应该继承自Activiti的ProcessEngineConfigurationConfigurer接口,并实现其configure方法。在configure方法中,我们可以进行一些Activiti的配置,比如设置数据库相关的配置、添加流程监听等。 然后,需要创建流程定义文件。可以使用Activiti提供的图形化工具来创建流程定义文件,也可以通过编码的方式来创建。在流程定义文件中,需要定义流程的各个节点,以及节点之间的连线和流程变量。 接下来,需要编写与流程审批相关的业务逻辑代码。这些代码应该包括启动流程实例、查询待办任务、完成任务等功能。可以使用Activiti提供的API来完成这些操作。 最后,需要创建前端页面用于展示和处理审批任务。可以使用Thymeleaf等模板引擎来创建页面,并通过Ajax请求与后端进行数据交互。 综上所述,借助于Spring Boot和Activiti,我们可以快速便捷地实现流程审批功能。这种方式不仅可以提高开发效率,还有利于代码的维护和扩展。希望以上内容对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小绿豆

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值