Activiti 7 学习整合 BPMN.js(二)

动态表当读取历史数据接口

  • 我们在上面的渲染动态表单接口 (formDataShow)方法中进行扩充,大致思路是在渲染表单之前会根据 taskId 查出流程 id,并且根据流程实例 id 查出所有表单填写的值,将这些值构建成一个 hashMap 字典,这样就是一个 key-value 的形式,那么接下来如果我们的表单默认值如果不是一个字符串而是一个以 FormProperty 开头的其他表单的名称,我们就从这个字典里面通过 key 将之前存的值读取出来并且渲染出来,这就是我们的大致思路
ActivitiMapper 中编写查询表单历史数据的方法
    /**
     * 读取表单数据
     * @param PROC_INST_ID   流程实例 id
     * @return
     */
    @Select("select Control_ID_,Control_VALUE_ from formdata where PROC_INST_ID_ = #{PROC_INST_ID}")
    public List> selectFormData(@Param("PROC_INST_ID")String PROC_INST_ID);
formDataShow 方法中读取表单历史数据,并保存在数据字典中
/*
                构建表单控件历史数据字典
                key 是控件的 id
                value 是控件的值
                这里是我们根据 taskId 查询出来的所有流程的表单数据
            */
            Map controlListMap = new HashMap();
            // 读取数据库本流程实例的所有表单数据
            List> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
            // 将查询出来的控件 id 和值保存在数据字典中
            for(Map map : tempControlList){
                controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
            }
之前返回给前端的默认值是写死的现在需要,如果默认值保存的是之前任意环节的控件 id 时,需要读取出之前控件的值
// 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
                if(split[3].startsWith("FormProperty_")){
                    // 是表单数据  从之前的保存的所有控件历史数据字典中读取
                    // 在这里  split[3] 拿到的就是历史表单的 key
                    if(controlListMap.containsKey(split[3])){
                        form.put("controlDefValue", controlListMap.get(split[3]));
                    }else{
                        // 如果字典中不存在给出错误提示
                        form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
                    }
                }else{
                    // 不是之前表单数据
                    form.put("controlDefValue", split[3]);
                }
画 bpmn js 测试,我们需要在 A 环节填写的数据在 B 环节读取出来
image.png

image.png
  • 张三表单的数据
    FormProperty_26555eg-_-string-_-姓名-_-请输入姓名-_-f
    FormProperty_0c4etf8-_-string-_-姓别-_-男或女-_-f


    image.png

    image.png
  • 李四的表单数据
    李四自己的表单
    FormProperty_0gvpb9n-_-string-_-姓名-_-我是张三-_-f
    读取张三表单填写的内容,FormProperty_0c4etf8 这个保存的就是张三表单的key 通过它就可以读取到张三填写的值
    FormProperty_11muvfh-_-string-_-姓别-_-FormProperty_0c4etf8-_-f
bpmn js 创建好之后,上传,部署,略过......
查询流程
  • 张三填写数据的任务


    image.png
  • 模拟张三提交表单
  • 张三表单数据 这个 “女” 就是李四需要读取出来的
    FormProperty_26555eg-_-我是张三-_-f!!FormProperty_0c4etf8-_-女--s
    image.png
  • 切换用户查看渲染李四的表单数据,可以看到张三表单中填写的数据被读取出来了


    image.png
渲染表单 formDataShow 完整代码
    /**
     * 渲染动态表单
     * @return
     */
    @GetMapping("/formDataShow")
    public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);

            /*
                构建表单控件历史数据字典
                key 是控件的 id
                value 是控件的值
                这里是我们根据 taskId 查询出来的所有流程的表单数据
            */
            Map controlListMap = new HashMap();
            // 读取数据库本流程实例的所有表单数据
            List> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
            // 将查询出来的控件 id 和值保存在数据字典中
            for(Map map : tempControlList){
                controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
            }

            /**
             * 关键代码,这里在强调一下,在 activiti6 和 5 的时候实际上是有 form 这个类的
             * 但是在 activiti7 中去掉的为了轻量化,但是我们在 7 中还是有方法可以通过流程
             * 定义的 id 和任务的 id 拿到一个叫 userTask 的类,在 userTask 类中可以拿到表单属性
             */
            UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
                    /**
                     * 获取流程元素,这里是要传什么呢?这里实际上是要传任务的key的,在 task 里并没有任务的 key 的
                     * 在 activiti 6 中是有任务的 key 的,不过我们可以用另外一个方案,我们可以用表单的 key ,这里
                     * 我们可以表表单的 key 和任务的 key 启成一模一样的名字,这样你拿表单的 key 就相当于拿任务的 key
                     * 了
                     */
                    .getFlowElement(task.getFormKey());
            // 说明该环节是不需要表单的
            if(userTask == null){
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.SUCCESS.getCode(),
                        GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                        "无表单"
                );
            }

            List formProperties = userTask.getFormProperties();
            // 保存分割后的格式返回给前端
            List> listMap = new ArrayList>();
            for (FormProperty formProperty : formProperties) {
                // 分割表单数据
                String[] split = formProperty.getId().split("-_-");
                Map form = new HashMap();
                form.put("id", split[0]);
                form.put("controlType", split[1]);
                form.put("controlLabel", split[2]);

                // 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
                if(split[3].startsWith("FormProperty_")){
                    // 是表单数据  从之前的保存的所有控件历史数据字典中读取
                    // 在这里  split[3] 拿到的就是历史表单的 key
                    if(controlListMap.containsKey(split[3])){
                        form.put("controlDefValue", controlListMap.get(split[3]));
                    }else{
                        // 如果字典中不存在给出错误提示
                        form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
                    }
                }else{
                    // 不是之前表单数据
                    form.put("controlDefValue", split[3]);
                }

                form.put("controlParam", split[4]);
                listMap.add(form);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }

高亮历史流程渲染

效果

image.png

先画流程图,上传,部署,启动和之前的一样,这里略过

image.png

获取需要高亮的连线 id 编号方法

// 获取一条流程实例历史
            HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(instanceId)
                    .singleResult();
            // 根据流程定义 key 获取 BMPN
            BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
            // 获取流程
            Process process = bpmnModel.getProcesses().get(0);
            // 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
            Collection flowElements = process.getFlowElements();

            /**
             * 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
             * value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
             * 就可以获取到需要高亮的连线
             */
            Map map = new HashMap();
            for (FlowElement flowElement : flowElements) {
                // 判断是否是线条
                if(flowElement instanceof SequenceFlow){
                    SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
                    String ref = sequenceFlow.getSourceRef();
                    String targetRef = sequenceFlow.getTargetRef();
                    /**
                     * 保存开始节点和结束节点,与它们之间连线的对应关系
                     * key: 开始节点 编号 + 结束节点 编号
                     * value: 连线编号
                     */
                    map.put(ref+targetRef,sequenceFlow.getId());
                }
            }

            /**
             * 获取已经完成的全部流程历史节点
             */
            List list = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .list();
            /**
             * 将各个历史节的开始节点和结束几点的编号两两对应起来,
             * 就可以从上面的 map 中获取到需要高亮的连线
             */
            Set keyList = new HashSet();
            for (HistoricActivityInstance i : list) {
                for (HistoricActivityInstance j : list) {
                    if(i != j){
                        keyList.add(i.getActivityId()+j.getActivityId());
                    }
                }
            }

            // 获取高亮连线 id
            Set highLine = new HashSet();
            // 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
            keyList.forEach(s -> highLine.add(map.get(s)));
image.png
最终获取到的连线,因为 任务1 我已经执行过了所以获取到的是两根连线编号
image.png

获取需要高亮已经完成的任务节点编号方法

            // 获取已经完成的节点
            List listFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 已经完成的节点高亮
            Set highPoint = new HashSet<>();
            // 保存已经完成的流程节点编号
            listFinished.forEach(s -> highPoint.add(s.getActivityId()));
这时获取到的是 开始节点 和 任务1 节点,因为任务一我已经执行过了
image.png

获取需要高亮下一步我要代办执行的任务节点编号方法

            // 获取代办节点
            List listUnFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .unfinished()
                    .list();

            // 代办的节点高亮
            Set waitingToDo = new HashSet<>();
            // 保存需要代办的节点编号
            listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));
这时获取到的就是 任务2 节点编号,因为 任务1 执行完之后就流转到 任务2 了
image.png

获取当前用户已经完成的任务节点编号

            // 获取当前用户完成的任务
            List taskInstanceList = historyService.createHistoricTaskInstanceQuery()
                    .taskAssignee(userInfoBean.getUsername())
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 当前用户完成的高亮
            Set iDo = new HashSet();
            // 保存用户完成的节点编号
            taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));
当前用户完成了 任务1
image.png
最终得到的数据
{
    "status": 0,
    "msg": "成功",
    "obj": {
        "waitingToDo": [
            "Activity_2"
        ],
        "highPoint": [
            "StartEvent_1",
            "Activity_1"
        ],
        "iDo": [
            "Activity_1"
        ],
        "highLine": [
            null,
            "Flow_2",
            "Flow_1"
        ]
    }
}

完整方法

    /**
     * 高亮显示路程历史
     * @param instanceId     流程实例id
     * @param userInfoBean   用户信息
     * @return
     */
    @GetMapping("/getHighlight")
    public AjaxResponse getHighlight(@RequestParam("instanceId")String instanceId,
                                     @AuthenticationPrincipal UserInfoBean userInfoBean){
        try {
            // 获取一条流程实例历史
            HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(instanceId)
                    .singleResult();
            // 根据流程定义 key 获取 BMPN
            BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
            // 获取流程
            Process process = bpmnModel.getProcesses().get(0);
            // 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
            Collection flowElements = process.getFlowElements();

            /**
             * 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
             * value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
             * 就可以获取到需要高亮的连线
             */
            Map map = new HashMap();
            for (FlowElement flowElement : flowElements) {
                // 判断是否是线条
                if(flowElement instanceof SequenceFlow){
                    SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
                    String ref = sequenceFlow.getSourceRef();
                    String targetRef = sequenceFlow.getTargetRef();
                    /**
                     * 保存开始节点和结束节点,与它们之间连线的对应关系
                     * key: 开始节点 编号 + 结束节点 编号
                     * value: 连线编号
                     */
                    map.put(ref+targetRef,sequenceFlow.getId());
                }
            }

            /**
             * 获取已经完成的全部流程历史节点
             */
            List list = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .list();
            /**
             * 将各个历史节的开始节点和结束几点的编号两两对应起来,
             * 就可以从上面的 map 中获取到需要高亮的连线
             */
            Set keyList = new HashSet();
            for (HistoricActivityInstance i : list) {
                for (HistoricActivityInstance j : list) {
                    if(i != j){
                        keyList.add(i.getActivityId()+j.getActivityId());
                    }
                }
            }

            // 获取高亮连线 id
            Set highLine = new HashSet();
            // 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
            keyList.forEach(s -> highLine.add(map.get(s)));


            // 获取已经完成的节点
            List listFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 已经完成的节点高亮
            Set highPoint = new HashSet<>();
            // 保存已经完成的流程节点编号
            listFinished.forEach(s -> highPoint.add(s.getActivityId()));

            // 获取代办节点
            List listUnFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .unfinished()
                    .list();

            // 代办的节点高亮
            Set waitingToDo = new HashSet<>();
            // 保存需要代办的节点编号
            listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));


            // 获取当前用户完成的任务
            List taskInstanceList = historyService.createHistoricTaskInstanceQuery()
                    .taskAssignee(userInfoBean.getUsername())
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 当前用户完成的高亮
            Set iDo = new HashSet();
            // 保存用户完成的节点编号
            taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));

            Map reMap = new HashMap();
            // 高亮已经完成的节点
            reMap.put("highPoint",highPoint);
            // 高亮连线节点编号
            reMap.put("highLine", highLine);
            // 高亮代办节点编号
            reMap.put("waitingToDo", waitingToDo);
            // 高亮当前用户完成的节点编号
            reMap.put("iDo",iDo);

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    reMap
            );
        } catch (Exception e) {
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "高亮历史任务失败",
                    e.toString()
            );
        }
    }

bpmn js 扩展下载

在 bpmnjs 目录下 app 目录下的 index.html 中添加导出按钮

    
  • 导出
  • 创建 tools.js

    image.png
    import $ from 'jquery'
    const proHost = window.location.protocol + "//" + window.location.host;
    const href = window.location.href.split("bpmnjs")[0];
    const key = href.split(window.location.host)[1];
    const publicurl = proHost + key;
    const tools = {
        /**
         * 下载方法
         * @param bpmnModeler
         */
        download(bpmnModeler){
            var downloadLink = $("#downloadBPNM");
            bpmnModeler.saveXML({format:true},function(err,xml){
                if(err){
                    return console.error("could not save bpmn",err);
                }
                tools.setEncoded(downloadLink,"digaram.bpmn",xml);
            });
        },
        /**
         *
         * @param link  下载的按钮
         * @param name  下载的名字
         * @param data  下载的数据
         */
        setEncoded(link, name, data) {
         var encodedData = encodeURIComponent(data);
    
            if (data) {
                link.addClass('active').attr({
                    'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
                    'download': name
                });
            } else {
                link.removeClass('active');
            }
        }
    }
    
    export default tools
    

    在 index.html 同级目录的 index.js 中引入创建的 tools.js

    image.png

    在 index.html 同级目录的 index.js 添加导出按钮的点击事件,bpmnModeler 就是 bpmn 的模型

      $("#downloadBPNM").on('click',function(){
        tools.download(bpmnModeler);
      });
    
    image.png

    测试 正常导出

    image.png

    在线部署 bpmn

    index.html 中添加部署按钮

        
  • 部署
  • index.js 中添加部署的点击事件

    // 部署 bpmn
      $("#saveBPNM").on('click',function(){
        tools.saveBPMN(bpmnModeler);
      });
    

    tools.js 中添加在线部署的方法

    /**
         * 部署方法
         * @param bpmnModeler
         */
        saveBPMN(bpmnModeler){
            var downloadLink = $("#downloadBPNM");
            bpmnModeler.saveXML({format:true},function(err,xml){
                if(err){
                    return console.error("could not save bpmn",err);
                }
                console.info(xml)
                // 参数就是 bpmn xml 字符串
                var param = {
                    "xmlBPMN": xml
                };
                // 调用后台接口上传bpmn
                $.ajax({
                    url: publicurl + "processDefinition/addDeploymentByString",
                    type: "post",
                    dataType: "json",
                    data: param,
                    success: function(res){
                        if(res.status == 0){
                            alert("部署成功");
                        }else{
                            alert("部署失败");
                        }
                    },
                    error: function(err){
                        console.info(err);
                    }
                });
            });
        }
    

    上传 BPMN 并展示

    index.html 添加导入按钮

        
        
  • index.js 添加导入按钮变事件

      // 上传 bpmn
      $("#uploadFile").on("change",function(i){
        tools.uploadBPMN(bpmnModeler);
      });
    

    tools.js 中编写 uploadBPMN 上传 bpmn 方法

        /**
         * 上传 bpmn
         */
        uploadBPMN(bpmnModeler){
            // 获取文件
            var fileUpload = document.myForm.uploadFile.files[0];
            // 创建 FormData 对象
            var fm = new FormData();
            fm.append("processFile",fileUpload)
            $.ajax({
                url: publicurl + "processDefinition/uploadBPMN",
                type: "post",
                data: fm,
                async: false,
                contentType: false,
                processData: false,
                success: function(res){
                    if(res.status == 0){
                        var url = publicurl + "bpmn/" + res.obj;
                        // 打开上传的 bpmn
                        tools.openBPMN_URL(bpmnModeler,url);
                    }else{
                        alert(res.msg);
                    }
                }
            });
        },
        /**
         * 打开上传的 bpmn
         * @param url
         */
        openBPMN_URL(bpmnModeler,url){
            $.ajax(url,{dataType: "text"}).done(
                // 返回 xml 文件
                async function (xml) {
                    try {
                        // 导入 xml
                        await bpmnModeler.importXML(xml);
    
                    }catch (e) {
                        console.error(e);
                    }
                });
        }
    

    编写后台上传接口,uploadBpmnPath 是上传的文件目录在 yml 中配置的换成自己的就可以了

        /**
         * 添加流程定义通过在线提交 BPMN 的 xml
         * @param processFile
         * @return
         */
        @PostMapping("/uploadBPMN")
        public AjaxResponse addDeploymentByString(HttpServletRequest request,
                                                  @RequestParam("processFile") MultipartFile processFile
                                                  ){
            try{
                if(processFile.isEmpty()){
                    return AjaxResponse.AjaxData(
                            GlobalConfig.ResponseCode.ERROR.getCode(),
                            GlobalConfig.ResponseCode.ERROR.getDesc(),
                            "BPMN 不能为空"
                    );
                }
                // 获取原始文件名
                String originFileName = processFile.getOriginalFilename();
                // 获取文件后缀
                String suffixName = originFileName.substring(originFileName.lastIndexOf("."));
                // 新的文件名
                String fileName = UUID.randomUUID() + suffixName;
                // 上传文件的路径
                File filePath = new File(uploadBpmnPath+fileName);
                if(!filePath.getParentFile().exists()){
                    filePath.getParentFile().mkdirs();
                }
                // 上传
                processFile.transferTo(filePath);
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.SUCCESS.getCode(),
                        GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                        fileName
                );
            }catch (Exception e){
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.ERROR.getCode(),
                        "上传 BPMN 失败",
                        e.getMessage()
                );
            }
        }
    }
    

    uploadBpmnPath 上传的目录,yml 中 通过 @Value("${uploadBpmnPath}") 引用

    # bpmn 文件上传路径
    upload:
      bpmn:
        path: G:/gu-pao/activiti7_workflow/src/main/resources/resources/bpmn/
    

    上传的文件目录映射

    @Configuration
    public class PathMapping implements WebMvcConfigurer {
    
        @Value("${upload.bpmn.path}")
        private String uploadBpmnPath;
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            // 添加默认映射
            registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/");
            // 添加 bpmn 路径映射
            registry.addResourceHandler("/bpmn/**")
                    .addResourceLocations("file:"+uploadBpmnPath.replace("/","\\"));
        }
    }
    

    测试导入本地 bpmn,成功上传并回显

    image.png

    源码地址

    https://gitee.com/jitashou18089237297/activiti-integrates-bpmnjs.git

    你可能感兴趣的:(Activiti 7 学习整合 BPMN.js(二))