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

当前 Activiti 版本

        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>7.1.0.M4</version>
        </dependency>
        <dependency>
            <groupId>org.activiti.dependencies</groupId>
            <artifactId>activiti-dependencies</artifactId>
            <version>7.1.0.M4</version>
            <type>pom</type>
        </dependency>

这个版本自动创建的数据库 ACT_RE_DEPLOYMENT 缺少两个字段自己添加一下

PROJECT_RELEASE_VERSION_
VERSION_

deployment: 添加资源文件,获取部署信息,部署时间等

    @Autowired
    private RepositoryService repositoryService;

    /**
     * 流程部署测试 1
     *  影响的表
     *      act_re_procdef       流程定义表
     *      act_re_deployment    流程部署表
     *      act_ge_bytearray     通用的流程定义和流程资源
     *
     *      ge 代表  general     通用的
     *      re 代表  repository
     */
    @Test
    public void initDeploymentBPMN(){
        String fileName= "BPMN/part1_deployment.bpmn";
        String imageName = "BPMN/part1_deployment.png";
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource(fileName)
                .addClasspathResource(imageName)
                .name("部署流程测试_v1")
                .deploy();
        System.out.println(deployment.getName());
    }

    /**
     * 流程部署测试 zip
     */
    @Test
    public void initDeploymentZIP(){
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("BPMN/part1_deployment.zip");
        ZipInputStream zip = new ZipInputStream(resourceAsStream);
        Deployment deployment = repositoryService.createDeployment()
                .addZipInputStream(zip)
                .name("流程部署测试_zip")
                .deploy();
        System.out.println(deployment.getName());
    }

    /**
     * 查询部署流程列表
     */
    @Test
    public void getDeployments(){
        List<Deployment> list = repositoryService.createDeploymentQuery().list();
        list.forEach(deployment -> {
            System.out.println("id: "+ deployment.getId());
            System.out.println("name: " + deployment.getName());
            System.out.println("deploymentTime: " + deployment.getDeploymentTime());
            System.out.println("key: " + deployment.getKey());
        });
    }

processDefinition 获取版本号,key,资源名称,部署 id 等

    @Autowired
    private RepositoryService repositoryService;

    // 查询流程定义
    @Test
    public void getDefinition(){
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().list();
        for (ProcessDefinition pd : list) {
            System.out.println("name :" + pd.getName());
            System.out.println("key :" + pd.getKey());
            System.out.println("name :" + pd.getResourceName());
            System.out.println("deploymentId :" + pd.getDeploymentId());
            System.out.println("version :" + pd.getVersion());
        }
    }

    /**
     * 删除流程定义
     */
    @Test
    public void delDefinition(){
        String pid = "f434aaf6-28eb-11eb-8d4d-408d5c97c1d5";
        // 参数一: 流程定义id
        // 参数二: true 表示将删除该路程下所有的流程任务及历史
        //          false 不会删除任务及历史
        repositoryService.deleteDeployment(pid, false);
    }

流程实例 ProcessInstance

ProcessDefinition 与 ProcessInstance 是一对多的关系
理解为:行动计划于具体行动的关系,流程实例是流程定义的具体实现

用户任务的属性面板

image.png

创建流程

image.png

Task 任务

    @Autowired
    private TaskService taskService;

    /**
     * 查询所有任务
     */
    @Test
    public void getTasks(){
        List<Task> list = taskService.createTaskQuery().list();
        for (Task task : list) {
            System.out.println("任务名称:" + task.getName());
            System.out.println("任务执行人:" + task.getAssignee());
            System.out.println("流程实例id:" + task.getProcessInstanceId());
        }
    }

    /**
     * 查询我的待办任务
     */
    @Test
    public void getTaskByAssignee(){
        List<Task> zhangsanTask = taskService.createTaskQuery()
                .taskAssignee("张三")
                .list();
        for (Task task : zhangsanTask) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
            System.out.println("任务执行人:" + task.getAssignee());
            System.out.println("流程实例id:" + task.getProcessInstanceId());
        }
    }

    /**
     * 执行任务
     * 影响的表   act_run_task   act_hi_taskinst
     */
    @Test
    public void completeTask(){
        taskService.complete("f5a6ac5d-2a85-11eb-be00-408d5c97c1d5");
        System.out.println("执行任务");
    }

    //拾取任务
    @Test
    public void claimTask(){
        Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
        taskService.claim("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","bajie");
    }

    //归还与交办任务
    @Test
    public void setTaskAssignee(){
        Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
        taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","null");//归还候选任务
        taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","wukong");//交办
    }

查询历史记录

    @Autowired
    private HistoryService historyService;

    /**
     * 根据用户名查询历史记录
     */
    @Test
    public void historyTaskInstanceByUser(){
        List<HistoricTaskInstance> history = historyService.createHistoricTaskInstanceQuery()
                .orderByHistoricTaskInstanceEndTime()
                .asc()
                .taskAssignee("张三")
                .list();
        for (HistoricTaskInstance hi : history){
            System.out.println("执行人:" + hi.getAssignee());
            System.out.println("name:" + hi.getName());
            System.out.println("流程实例id:" + hi.getProcessInstanceId());
        }
    }

    /**
     *  根据流程实例查询任务
     */
    @Test
    public void historyTaskInstanceByProcessInstanceId(){
        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
                .orderByHistoricTaskInstanceEndTime().asc()
                .processInstanceId("f5a091d9-2a85-11eb-be00-408d5c97c1d5")
                .list();
        for (HistoricTaskInstance hi : list){
            System.out.println("执行人:" + hi.getAssignee());
            System.out.println("name:" + hi.getName());
            System.out.println("流程实例id:" + hi.getProcessInstanceId());
        }
    }

UEL 统一表达式语言(Unified Expression Language)

  • 表达式以 "{" 开始,以 "}" 结束,例如{day > 5}
  • 支持逻辑运算 ${userName == 'zhangsan' and pwd == '999'}
  • 支持变量与实例类赋值
  • 对应的数据表
    • act_ru_variable 运行时参数表
    • act_hi_varinst 历史参数表(参数实例的缩写)
      流程图


      image.png

      启动流程实例

    @Autowired
    private RepositoryService repositoryService;
    
    @Autowired
    private RuntimeService runtimeService;
    
    @Autowired
    private TaskService taskService;

    /**
     * 部署流程实例
     */
    @Test
    public void deplyProcessInstance(){
        Deployment deploy = repositoryService
                .createDeployment()
                .disableSchemaValidation()
                .addClasspathResource("BPMN/Part6_UEL.bpmn")
                .deploy();
        System.out.println("部署id:" + deploy.getId());
        System.out.println("部署名称:" + deploy.getName());
        System.out.println("部署版本:" + deploy.getVersion());
    }

    /**
     * 启动流程实例带参数
     */
    @Test
    public void initProcessInstanceWithArgs(){
        // 流程变量
        Map<String,Object> variables = new HashMap<String,Object>();
        variables.put("assignee","张一山");
        ProcessInstance processInstance = runtimeService
                .startProcessInstanceByKey("myProcess_UEL", variables);
        System.out.println("流程实例id:" + processInstance.getProcessDefinitionId());
    }

act_run_task 表


image.png

费用报销单,小于,等于 100 经理审批,大于 100 主管审批


image.png

image.png

image.png

部署,启动略过......

    /**
     * UEL 测试
     *  费用报销单,小于,等于 100 经理审批,大于 100 主管审批
     */
    @Test
    public void completeTaskWithArgs(){
        Map<String,Object> variables = new HashMap<String,Object>();
        variables.put("money", 101);
        taskService.complete("7ec08949-2c13-11eb-b4b1-408d5c97c1d5",variables);
        System.out.println("大于 100 应该是主管审批");
    }
image.png

UEL 执行人候选人测试

image.png

image.png

image.png

实体类

/**
 * 注意要实现 serializable 接口
 * 变量名要小写
 */
public class User implements Serializable {

    // 执行人
    private String assign;
    // 候选人
    private String candidate;
    // get set 略过 ......
}

部署流程略过......

启动流程实例

    /**
     * 启动流程实例,对象参数
     */
    @Test
    public void initProcessInstanceWithClassArgs(){
        User user = new User();
        // 设置执行人
        user.setAssign("zhangsan");
        // 设置候选人
        user.setCandidate("lisi,wangwu");
        Map<String,Object> variables = new HashMap<String,Object>();
        variables.put("user",user);
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess_11", variables);
        System.out.println("流程实例 id" + processInstance.getProcessDefinitionId());
    }

执行人是张三


image.png

张三执行完后,下一步是候选人执行但是,有两个候选人,需要先拾取任务,此时执行是空的


image.png

张三拾取组任务
    /**
     * 拾取任务
     */
    @Test
    public void claimTask(){
        taskService.claim("95152f81-2c19-11eb-b715-408d5c97c1d5","zhangsan");
        System.out.println("拾取任务成功");
    }
image.png
  • 设置流程变量
    /**
     * 直接指定全局流程变量
     */
    @Test
    public void setVariables(){
        // 通过 id 设置流程变量
        // 参数1:流程实例id
        // 参数2:流程变量名
        // 参数3:流程变量所对应的值

        // 设置单个变量
        runtimeService.setVariable("2501","name","zhangsan");

        Map<String,Object> variables = new HashMap<String,Object>();
        variables.put("name","zhangsan");
        variables.put("age","15");
        // 设置多个变量
        runtimeService.setVariables("2501",variables);

        // 通过任务节点 id 设置流程变量   同上
//        taskService.setVariables();
//        taskService.setVariable();
    }

    /**
     * 设置局部变量,只在当前环节有用
     */
    @Test
    public void setLocalVariable(){
        /*runtimeService.setVariableLocal();
        runtimeService.setVariablesLocal();
        taskService.setVariableLocal();
        taskService.setVariablesLocal();*/
    }

网关

排他网关

  • ExclusiveGateway:当时用排他网关,流程执行到网关时,按照顺序与计算条件进行处理,当计算条件为 true 时,继续执行下一个环节,没有满足的条件则抛出异常,如过多个条件都满足,排他网关会根据 bpmn 顺序靠前的条件继续执行,也就是说排他网关只会执行一个条件


    image.png
  • 排他网关需要指定执行条件,如果没有执行会执行其中的一个,具体执行某一个,先连接那根线会先执行哪一个会根据 BPMN xml 的顺序去执行

  • 当 zhangsan 填写请假单之后,请假大于 3 天应该又 wangwu 审核,小于等于 3 天 由 lisi 审核

    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;

    /**
     * 部署流程
     */
    @Test
    public void deployProcess(){
        Deployment deploy = repositoryService.createDeployment()
                .disableSchemaValidation()
                .addClasspathResource("BPMN/part8_exclusive.bpmn")
                .deploy();
        System.out.println("流程部署 id = " + deploy.getId());
    }

    /**
     * 启动流程实例
     */
    @Test
    public void startProcessInstance(){
        ProcessInstance pi = runtimeService.startProcessInstanceByKey("myProcess_exclusive");
        System.out.println("流程实例 id = " + pi.getId());
        System.out.println("流程实例名称:" + pi.getName());
    }
  • 可以看到张三有一个任务


    image.png
  • 请假 10 天 大于三天
    /**
     * zhangsan 执行请假任务  请假10 天  大于 3 天 应该是 wangwu 审核
     */
    @Test
    public void execTask(){
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("day",10);
        taskService.complete("d6eac7a1-2ed3-11eb-8d1b-9c5c8e79c1e8", map);
        System.out.println("zhansan 执行请假任务");
    }
  • wangwu 得到审批任务


    image.png
  • 排他网常用于下面这种流程,就是审批不同意的驳回,如果同意继续往下执行,可以加一些边界时间进行通知,不同意打回


    image.png

并行网关

  • ParallelGeteway:把任务拆分成多路,并把多路任务合并成一路,比较常用,多用于重要环节,需要多人审批的场景


    image.png
  • 当 zhangsan 填写完请假单后,lisi 和 wangwu 都会收到一条任务

  • 部署启动流程实例

    @Autowired
    RepositoryService repositoryService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private RuntimeService runtimeService;

    /**
     * 部署流程
     */
    @Test
    public void deployProcess(){
        Deployment deploy = repositoryService.createDeployment()
                .disableSchemaValidation()
                .addClasspathResource("BPMN/part7_parallelism.bpmn")
                .deploy();
        System.out.println("部署 id = " + deploy.getId());
    }

    /**
     * 启动流程实例
     */
    @Test
    public void startProcessInstance(){
        ProcessInstance pi = runtimeService.startProcessInstanceByKey("myProcess_holiday");
        System.out.println("流程实例 id = " + pi.getId());
        System.out.println("流程名称 = " + pi.getName());
    }
  • 部署启动之后张三会收到一个任务


    image.png
  • zhangsan 执行任务,zhangsan 执行完任务,lisi 和 wangwu 都应该都已一个审核任务
    /**
     * zhangsan 执行任务
     */
    @Test
    public void execTask(){
        taskService.complete("adc03b8a-2ca3-11eb-b95d-005056c00008");
        System.out.println("zhangsan 填写请假单");
    }
  • 果然 zhangsan 执行完成之后,lisi 和 wangwu 都收到了任务


    image.png

包容网关

  • InclusiveGateway:包容网关可以理解为能够添加条件的并行网关,并行网关是不能添加条件的,连出去几根线,就需要进行几个审核流程,而包容网关,可以在每条线路上设置条件,并且可以执行多条线路。包容网关与并行网关比是可以设置条件的,与排他网关比,排他网关只会有一个结束环节被启动,而包容网关可以有多个环节被启动


    image.png
  • 包容网关是并行网关的延伸版,所以也是成对出现的,上面的流程 zhangsan 填写请假单,请假 1 天,此时 wangwu 和 zhaoliu 都应当受到一条请假任务
  • 流程部署 zhangsan 会收到一个任务
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;

    /**
     * 流程部署
     */
    @Test
    public void deployProcess(){
        Deployment deploy = repositoryService.createDeployment()
                .disableSchemaValidation()
                .addClasspathResource("BPMN/part9_inclusive.bpmn")
                .deploy();
        System.out.println("流程部署 id = " + deploy.getId());
    }

    /**
     * 启动流程
     */
    @Test
    public void startProcess(){
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess_inclusive");
        System.out.println("流程实例 id = " + processInstance.getId());
    }
image.png
  • zhangsan 执行任务,按照预想 zhangsan 请假 1 天,此时 wangwu 和 zhaoliu 都应当受到一条请假任务
    /**
     * zhangsan 执行任务 请假一天
     */
    @Test
    public void execTask(){
        // 流程变量
        Map<String,Object> map = new HashMap<String,Object>();
        // 请假一天
        map.put("day", 1);
        taskService.complete("3c1c7f0a-2ee9-11eb-94e6-9c5c8e79c1e8",map);
        System.out.println("张三填写请假单");
    }
  • wangwu 和 zhaoliu 收到任务


    image.png
  • 王五赵六审批完成,流程结束
    /**
     * 王五赵六审批任务
     */
    @Test
    public void auditTask(){
        taskService.complete("8c78e4ba-2eea-11eb-bd03-9c5c8e79c1e8");
        taskService.complete("8c790bcc-2eea-11eb-bd03-9c5c8e79c1e8");
        System.out.println("wangwu zhaoliu 审批");
    }

事件网关

  • EventGateway:事件网关执行连接到事件,并且连接的事件必须大于等于两个

activiti 7 新特性

ProcessRuntime

  • 为了与 ProcessRuntime API 交互,当前登录用户必须具有 “ACTIVITI_USER” 角色,就是如果你要使用 ProcessRuntime API 进行交互当前登录角色必须是 “ACTIVITI_USER”,“ACTIVITI_USER” 是什么角色呢?,要执行流转在任务执行的环节把执行人写死就行了,这个执行人在不我的数据库里并没有关系,这是以前的做法,现在的做法不是了,你的用户必须在 springSecurity 中有才能去进行操作

github 找到 activiti 项目下载到本地,拷贝一下两个文件到项目

image.png
  • SecurityUtil 是一个登陆辅助类,在 activiti 7 中所有的方法都需要使用该类中的 logInAs 方法进行登陆,改类会去调用 springSecurity 自带的方法 loadUserByUserName 去查询当前的用户是否存在于 springSecurity 框架中,如果存在返回 UserDetails 类,通过该类可以获取到用户的信息等......,反之会抛出异常
@Component
public class SecurityUtil {

    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    @Autowired
    private UserDetailsService userDetailsService;

    public void logInAs(String username) {

        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }

            @Override
            public Object getCredentials() {
                return user.getPassword();
            }

            @Override
            public Object getDetails() {
                return user;
            }

            @Override
            public Object getPrincipal() {
                return user;
            }

            @Override
            public boolean isAuthenticated() {
                return true;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {

            }

            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}
  • DemoApplicaitonConfiguration 该类会运行中在内存中创建许多用户,实际项目中回去从数据库中读取用户信息,这里暂时这么使用
//@Configuration
public class DemoApplicationConfiguration {

    private Logger logger = LoggerFactory.getLogger(DemoApplicationConfiguration.class);

   // @Bean
    public UserDetailsService myUserDetailsService() {

        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();

        // 创建三个用户 zhangsan,lisi,wangwu ,拥有 ACTIVITI_USER 角色
        String[][] usersGroupsAndRoles = {
                {"zhangsan", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"lisi", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"wangwu", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {"admin", "password", "ROLE_ACTIVITI_ADMIN"},
        };

        for (String[] user : usersGroupsAndRoles) {
            List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
            logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
            inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
                    authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
        }
        return inMemoryUserDetailsManager;
    }

  //  @Bean  密码编码
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

ProcessRuntime API 测试,要使用 ProcessRuntime 就需要使用 springSecurity 安全框架去做,如果项目没有使用 springSecurity 安全框架,不使用新特性也是可以的

    @Autowired
    private ProcessRuntime processRuntime;
    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private RepositoryService repositoryService;

    // 获取流程实例
    @Test
    public void getProcessInstance(){
        // 登录 这个 securityUtil 是我们复制下来的,不是 Apache 下的
        securityUtil.logInAs("zhangsan");
        // 查询
        Page<ProcessInstance> pdp = processRuntime
                .processInstances(Pageable.of(0, 100));
        System.out.println("流程实例总数量:"+pdp.getTotalItems());
        // 查询流程实例
        List<ProcessInstance> pis = pdp.getContent();
        pis.forEach(p -> {
            System.out.println("流程id = "+p.getId());
            System.out.println("流程名称 = "+p.getName());
            System.out.println("开始日期 = "+p.getStartDate());
            System.out.println("状态 = "+p.getStartDate());
            System.out.println("流程定义id = "+p.getProcessDefinitionId());
            System.out.println("流程定义key = "+p.getProcessDefinitionKey());
        });
    }

    /**
     * 部署流程还是用以前的方式,新的 api 中没有封装
     */
    @Test
    public void deployProcessInstance(){
        Deployment deploy = repositoryService.createDeployment()
                .disableSchemaValidation()
                .addClasspathResource("BPMN/part10_processRuntime.bpmn")
                .deploy();
        System.out.println("流程部署 id = " + deploy.getId());
    }

    // 启动流程实例
    @Test
    public void startProcessInstance(){
        // 登录
        securityUtil.logInAs("zhangsan");
        processRuntime.start(ProcessPayloadBuilder
        .start()
        .withProcessDefinitionKey("myProcess_processRuntime")  // 根据流程 key 启动
//        .withVariable("aaa","bb" )
//        .withVariables("map")
        .withBusinessKey("可以自定义业务 key")
        .build()
        );
    }

    // 删除流程实例
    @Test
    public void delProcessInstance(){
        // 登录
        securityUtil.logInAs("zhangsan");
        processRuntime.delete(ProcessPayloadBuilder
        .delete()
        .withProcessInstanceId("edb14933-2f06-11eb-b202-9c5c8e79c1e8")
        .build()
        );
    }

    // 挂起流程
    @Test
    public void suspendProcessInstance(){
        // 登录
        securityUtil.logInAs("zhangsan");
        processRuntime.suspend(ProcessPayloadBuilder
        .suspend()
        .withProcessInstanceId("流程实例 id")
        .build()
        );
    }

    // 激活流程实例
    @Test
    public void resumeProcessInstance(){
        // 登录
        securityUtil.logInAs("zhangsan");
        processRuntime.resume(ProcessPayloadBuilder
        .resume()
        .withProcessInstanceId("流程实例 id")
        .build()
        );
    }

    // 流程实例参数
    @Test
    public void getVariables(){
        // 登录
        securityUtil.logInAs("zhangsan");
        List<VariableInstance> variables = processRuntime.variables(ProcessPayloadBuilder
                .variables()
                .withProcessInstanceId("d6e3277d-2ed3-11eb-8d1b-9c5c8e79c1e8")
                .build()
        );
        variables.forEach(var -> {
            System.out.println("变量名称="+var.getName());
            System.out.println("变量值="+var.getValue());
            System.out.println("任务id="+var.getTaskId());
            System.out.println("流程id="+var.getProcessInstanceId());
        });
    }

TaskRuntime

  • TaskRuntime 是 activiti 7 新出现的 api ,它是对 task 任务进行的封装,有task 的查询,创建,拾取,完成等等,taskRuntime 的封装还是做的挺好了,以前查询代办任务是一个方法,查询候选人任务也是一个方法,而 taskRuntime 封装成一个方法了,这个是比较符合实际的,并且 taskRuntime 还做了一些异常判断
    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private TaskRuntime taskRuntime;

    /**
     * 获取当前登录用户任务
     */
    @Test
    public void getTasks(){
        securityUtil.logInAs("zhangsan");
        // 查询 0 - 100 条
        Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
        List<Task> list = tasks.getContent();
        // taskRuntime 其实是将 执行人和候选人都查出来了
        list.forEach(t -> {
            System.out.println("任务id="+t.getId());
            System.out.println("任务名称="+t.getName());
            System.out.println("任务状态="+t.getStatus());
            System.out.println("创建时间="+t.getCreatedDate());
            // taskRuntime 其实是将 执行人和候选人都查出来了
            // 我们可以在这里做一个判断
            // 如果 assignee 执行人能查出来,但是如果等于 null ,说明某一个任务的候选人有他,需要当前用户去拾取任务
            if(t.getAssignee() == null){
                // 说明该任务的候选人有他,需要拾取
                System.out.println("assignee: 这是一条待拾取的任务");
            }else{
                System.out.println("执行人就是当前用户 assignee: "+t.getAssignee());
            }
        });
    }

    /**
     * 完成任务
     */
    @Test
    public void completeTask(){
        // 登录
        securityUtil.logInAs("zhangsan");
        // 执行任务
        Task task = taskRuntime.task("任务 id");
        // 如果执行人为 null 说明当前执行任务是候选人,需要先拾取任务
        if(task.getAssignee() == null){
            // 拾取任务
            taskRuntime.claim(TaskPayloadBuilder
            .claim()
            .withTaskId(task.getId())
            .build()
            );
        }
        // 执行任务
        taskRuntime.complete(TaskPayloadBuilder
        .complete()
        .withTaskId(task.getId())
        .build()
        );
        System.out.println("任务执行完成");
    }

SpringSecurity 的主要功能

认证

鉴权/授权

用户的三种来源

  • application.properties 配置用户
    这种是写死在配置文件中的
  • 代码中配置内存用户
    上面 SecurityUtis 类就是内存用户
  • 从数据库中加载用户

activiti 7 中默认已经集成了 SpirngSecurity 框架,我们拉测试一下

先注释掉之前的我们测试用的内容用户,新建一个测试类 controller

@RestController
public class HelloController {
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello() {
        return "Activiti7 欢迎你";
    }
}

启动 springboot 工程,访问 url http://localhost:8080/hello 我们创建的控制器,此时会跳转到 SpringSecuriy 内置的登录页

image.png

  • 默认的用户是 user,密码是在项目启动的时候回打印到控制台


    image.png
  • 我们现在使用内存用户登录,放开上面注释的内存用户类,此时不会再控制打印出用户密码了,可以使用内存用户登录了,登录也是没有任何问题的


    image.png
  • 我们最终想要的不是在内存中构建用户的,而是要在数据库中构建用户,所以我们需要自己去编码实现

编写测试代码

  • 创建一个 MyUserDetailsSevice 类,实现 SpringSecurity 的 UserDetailsService
@Component
@Configuration
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 登陆密码  需要进行加密
        String password = passwordEncoder().encode("666");
        return new User(username,   // 用户名,现在没有查数据库所以前端可以随便写
                         password,   // 密码必须为  666
                         // activiti 7 需要用户必须拥有 ROLE_ACTIVITI_USER 角色
                         AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ACTIVITI_USER"));
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
  • 此时我们启动项目用户名随便输,密码为 666 就可以访问了

从数据库中读取用户

  • 新建实体类 UserInfoBean 实现 UserDetails 接口,为什么要实现 UserDetails 接口呢?因为登录校验的时候返回的就是 UserDetails 类,SpringSecurity 会根据返回的用户去进行密码的比对
/**
 * 用户类
 */
public class UserInfoBean implements UserDetails {

    private Long id;             // 编号
    private String name;         // 名字
    private String address;     // 地址
    private String username;    // 用户名
    private String password;    // 密码
    private String roles;       // 角色

    public String getAddress() {
        return address;
    }

    // 这里返回用户的角色列表,为角色赋值
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.
                stream(roles.split(",")).    // 使用 , 号分割角色字符串,引用数据库里是这么存的
                map(r -> new SimpleGrantedAuthority(r))
                .collect(Collectors.toList());
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    // 下面暂时都先返回 true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 编写一个 mapper 类,这里使用的是 mabatis
@Mapper
public interface UserInfoBeanMapper {

    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    @Select("select * from user where username = #{username}")
    public UserInfoBean selectByUsername(@Param("username")String username);
}
  • 改造上面写的 MyUserDetailsService 类
@Component
@Configuration
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserInfoBeanMapper userInfoBeanMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 登陆密码  需要进行加密
        /*String password = passwordEncoder().encode("666");
        return new User(username,   // 用户名,现在没有查数据库所以前端可以随便写
                         password,   // 密码必须为  666
                         // activiti 7 需要用户必须拥有 ROLE_ACTIVITI_USER 角色
                         AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ACTIVITI_USER"));*/

        /**
         * 需要做的操作
         *      读取数据库判断用户
         *      如果用户是 null 抛出异常
         *      返回用户信息
         */
        UserInfoBean userInfoBean = userInfoBeanMapper.selectByUsername(username);
        if(userInfoBean == null){
            throw new UsernameNotFoundException("用户不存在!");
        }
        // 返回用户信息,密码比对不只是在这里进行比对的
        // 我们将查询到的用户信息返回给框架,然后安全框架内部根据用户输入的密码和查询的密码进行比对
        return userInfoBean;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
  • 下面是我创建的用户表,此时启动项目,就可以使用这里的用户名面来进行登陆了


    image.png

SpringSecurity 配置文件处理

  • 新建配置类 ActivitiSecurityConfig 继承 WebSecurityConfigurerAdapter 类
/**
 * SpringSecurity 配置类
 */
@Configuration
public class ActivitiSercurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .formLogin() // 这里我们就要使用 http 进行登录了,不能使用登录页登录了
                .loginPage("/login")  // 登录方法
                .loginProcessingUrl("/login")  // 配置没有访问权限的 url 的处理链接,需要 loginPage 一样
                .successHandler(loginSuccessHandler) // 登录成功处理类
                .failureHandler(loginFailureHandler) // 失败处理
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll() // 配置没有访问权限的 url 不需要登录访问
                .anyRequest().authenticated()  // 所有的 url 都需要登录才能访问
                .and()
                .logout().permitAll()
                .and()
                .csrf().disable()
                .headers().frameOptions().disable(); // frame 框架校验关闭,否则前端使用的 frame 框架也会报错
    }
}
  • 登陆成功处理类
/**
 * 登录成功处理类
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    // 这个是处理 ajax
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

    }

    // 登录成功输出一句话
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write("登录成功,用户名:"+authentication.getName());
    }
}
  • 登陆失败处理类
/**
 * 登录失败的处理
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 设置失败状态码
        httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write("登录失败:原因是"+e.getMessage());
    }
}
  • 没有开放的 url,并且是没有登陆的时候访问的处理 url
/**
 * 未登录用户处理
 */
@RestController
public class ActivitiSecurityController {

    @RequestMapping("/login")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response){
        return new String("需要登录才能访问!!!");
    }
}

测试

BPMN-JS

官网 bpmn.io

  • 进入官网下载实例


    image.png
  • 使用 git 下载或者直接下载 zip 都可以


    image.png

Activiti7整合BPMN-JS说明

安装Node.js

BPMN-JS地址

在resources文件夹下再创建一个resources文件夹

  • 实际路径为resources/resources/

把从github下载的bpmn-js-examples-master.zip压缩包解压

拷贝properties-panel到resources/resources/并改文件夹名字为bpmnjs

  • 实际路径为resources/resources/bpmnjs

使用命 terminal 令行工具打开resources/resources/bpmnjs/并执行命令

安装所需依赖

npm install     

启动

npm run dev

启动会在控制台输出访问连接


image.png
image.png

image.png

拷贝 bpmnjs 初始化文件夹下的内容到 resources/resources/bpmnjs 目录下

image.png

打开resources/resources/bpmnjs/app/index.js,注释以下内容(bpmn.js 默认是一个叫 camunda 的工作流,不是 activiti)

import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';

注释掉初始化部分

var bpmnModeler = new BpmnModeler({
  container: canvas,
  propertiesPanel: {
    parent: '#js-properties-panel'
  },
  additionalModules: [
    propertiesPanelModule,
    propertiesProviderModule
  ],
  moddleExtensions: {
    camunda: camundaModdleDescriptor
  }
});

添加以下内容,主要是汉化的部分和 activiti 部分

import propertiesProviderModule from '../resources/properties-panel/provider/activiti';
import activitiModdleDescriptor from '../resources/activiti.json';
import customTranslate from '../resources/customTranslate/customTranslate';
import customControlsModule from '../resources/customControls';

添加翻译组件

// 添加翻译组件
var customTranslateModule = {
   translate: ['value', customTranslate]
};


var bpmnModeler = new BpmnModeler({
   container: canvas,
   propertiesPanel: {
       parent: '#js-properties-panel'
   },
   additionalModules: [
       propertiesPanelModule,
       propertiesProviderModule,
       customControlsModule,
       customTranslateModule
   ],
   moddleExtensions: {
       activiti:activitiModdleDescriptor
   }
});
  • 这时重新刷新之前打开的 bpmn 页面,就会看到已经是汉化的了,并且生成的 bpmn 就是 activiti 需要的文件了,可以正常部署了


    image.png
  • 这里有一个小问题,就是我们在部署的时候,在页面上有一个 “可执行文件” 必须勾上才可以部署,但是默认其实不是勾选的,还是比较麻烦的,我们现在让他默认勾选上


    image.png
  • 其实我们默认打开的 bpmn 页面就是下面这个文件


    image.png
  • 用编辑器打开,将默认的 bpmn 文件将 isExeclutable 改为 true 就可以了


    image.png
  • npm run dev 启动就可以看见已经默认勾上了


    image.png
  • 我们可以直接访问项目目录下的 bpmn.js 画图页面进行画图下载 bmpn xml 文件进行测试上面的接口 http://localhost:8080/bpmnjs/dist/index.html ,但是可能会有乱码
    乱码解决,在下面俩个文件中加入 <meat charset="UTF-8"/> 即可
    image.png

    image.png

编码实现

创建全局枚举配置类

/**
 * 全局枚举配置类
 */
public class GlobalConfig {

    // 是否是测试环节
    public static final boolean Test = true;

    public enum ResponseCode{
        SUCCESS(0,"成功"),
        ERROR(1,"失败");
        private final int code;
        private final String desc;
        ResponseCode(int code,String desc){
            this.code = code;
            this.desc = desc;
        }
        public int getCode() {
            return code;
        }
        public String getDesc() {
            return desc;
        }
    }
}

创建 ajax 返回类,用户给前端返回同一类型

/**
 * ajax 返回类
 */
public class AjaxResponse {

    private int status;   // 返回状态码
    private String msg;    // 成功或错误提示
    private Object obj;    // 返回内容

    private AjaxResponse(int status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }
    public static AjaxResponse AjaxData(int status,String msg,Object obj){
        return new AjaxResponse(status,msg,obj);
    }
    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
}

改造之前的登录成功和失败处理类

  • LoginFailureHandler
/**
 * 登录成功处理类
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    // 这个是处理 ajax
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

    }

    // 返回成功处理
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(
                objectMapper.writeValueAsString(
                        AjaxResponse.AjaxData(
                                GlobalConfig.ResponseCode.SUCCESS.getCode(),
                                GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                                // 返回用户名
                                authentication.getName()))
                         );
    }
}
  • LoginFailureHandler
/**
 * 登录失败的处理
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    // 登录失败处理
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");

        httpServletResponse.getWriter().write(
                objectMapper.writeValueAsString(
                        AjaxResponse.AjaxData(
                                GlobalConfig.ResponseCode.ERROR.getCode(),
                                GlobalConfig.ResponseCode.ERROR.getDesc(),
                                "登录失败:原因是"+e.getMessage()
                        )
                ));
    }
}
  • 测试,这是返回的就是我们定义的 AjaxResponse 类了
    登录成功


    image.png

    登录失败


    image.png

流程定义接口

/**
 * 流程定义 控制类
 */
@RestController
@RequestMapping("/processDefinition")
public class ProcessDefinitionController {

    @Autowired
    private RepositoryService repositoryService;

    /**
     * 添加流程定义,通过上传 bpmn
     * @param multipartFile  流程定文件
     * @param deployName    部署名称
     * @return
     */
    @PostMapping("/uploadStreamAndDeployment")
    public AjaxResponse uploadStreamAndDeployment(
            @RequestParam("bpmnFile")MultipartFile multipartFile,
            @RequestParam("deployName")String deployName
            ){
        try {
            // 获取上传的文件名
            String fileName = multipartFile.getOriginalFilename();
            // 获取文件名扩展名
            String extension = FilenameUtils.getExtension(fileName);
            // 获取文件字节流对象
            InputStream fileInputStream = multipartFile.getInputStream();

            Deployment deployment = null;
            // 如果后缀名 zip 压缩包
            if(extension.equals("zip")){
                // 流程部署
                ZipInputStream zip = new ZipInputStream(fileInputStream);
                deployment = repositoryService.createDeployment()
                        .addZipInputStream(zip)
                        .deploy();
            }else {
                // bpmn 文件部署
                deployment = repositoryService.createDeployment()
                        .addInputStream(fileName,fileInputStream)
                        .name(deployName)
                        .deploy();
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    deployment.getId()+"|"+fileName
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "添加流程定义失败",
                    e.getMessage()
            );
        }
    }

    /**
     * 添加流程定义通过在线提交 BPMN 的 xml
     * @param xmlBPMN
     * @param deployName
     * @return
     */
    @PostMapping("/addDeploymentByString")
    public AjaxResponse addDeploymentByString(@RequestParam("xmlBPMN")String xmlBPMN,
                                              @RequestParam("deployName")String deployName){
        try{
            Deployment deployment = repositoryService.createDeployment()
                    // 这里由于是 xml 的字符串没有资源名称,这里我们先写死一个
                    .addString("createWithBPMNJS.bpmn", xmlBPMN)
                    .name(deployName)
                    .deploy();
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    deployment.getId()
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "添加流程定义失败",
                    e.getMessage()
            );
        }
    }

    // 获取流程定义列表
    @GetMapping("/getDefinitions")
    public AjaxResponse getDefinitions(){
        try{
            // 保存流程定义列表
            List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
            // 查询流程列表
            List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
                    .list();
            list.forEach(pd -> {
                Map<String,Object> map = new HashMap<String,Object>();
                // 流程定义名称
                map.put("name",pd.getName());
                // 流程定义 key
                map.put("key",pd.getKey());
                // 资源名称
                map.put("resourceName",pd.getResourceName());
                // 部署 id
                map.put("deploymentId",pd.getDeploymentId());
                // 版本
                map.put("version",pd.getVersion());
                listMap.add(map);
            });
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取流程定义列表失败",
                    e.toString()
            );
        }
    }
    
    // 获取流程定义的 xml 
    // 这个方法的作用是,在流程定的列表中当你想查看上传的 BPMN 是什么样子的时候,在 Activiti 7 以前,
    // 你只能在上传 BPMN 的同时在上传一张图片,这里去查看图片,那么在 activiti 7 中就不需要这么麻烦了,
    // 我们只需要将数据库中的二进制文件读出来并返回给前端 xml 即可
    /**
     * 
     * @param response
     * @param deploymentId    流程部署 id
     * @param resourceName    资源名称
     */
    @GetMapping("/getDefinitionXML")
    public void getDefinitionXML(HttpServletResponse response,
                                 @RequestParam("deploymentId")String deploymentId,
                                 @RequestParam("resourceName")String resourceName){
        // 声明返回的是一个 xml 类型
        response.setContentType("text/xml");
        try(
                // 读取流程部署文件流
                InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
                ) {
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = bufferedInputStream.read(bytes)) != -1){
                outputStream.write(bytes, 0, len);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 获取流程部署列表
     * @return
     */
    @GetMapping("/getDeployments")
    public AjaxResponse getDeployments(){
        try {
            List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
            // 流程部署列表
            List<Deployment> list = repositoryService.createDeploymentQuery()
                    .list();
            list.forEach(p -> {
                Map<String,Object> map = new HashMap<String,Object>();
                // 部署id
                map.put("id", p.getId());
                // 部署名称
                map.put("name",p.getName());
                // 部署时间
                map.put("deploymentTime", p.getDeploymentTime());
                listMap.add(map);
            });
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取流程部署列表失败",
                    e.toString()
            );
        }
    }

    /**
     * 删除流程定义
     * @param depId
     * @return
     */
    @DeleteMapping("/delDefinition")
    public AjaxResponse delDefinition(@RequestParam("depId")String depId){
        try {
            repositoryService.deleteDeployment(depId, true);
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    null
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "删除流程定义失败",
                    e.toString()
            );
        }
    }
}

流程实例接口

/**
 * 流程实例 控制类
 */
@RestController
@RequestMapping("/processInstance")
public class ProcessInstanceController {

    // 自己定义的 SecurityUtil 使用 activity 新特性需要登录
    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private ProcessRuntime processRuntime;
    @Autowired
    private RepositoryService repositoryService;
    /**
     * 获取流程实例列表
     * @AuthenticationPrincipal 注解可以获取用于登录信息
     * @param userInfoBean
     * @return
     */
    @GetMapping("/getInstances")
    public AjaxResponse getInstance(@AuthenticationPrincipal UserInfoBean userInfoBean){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 获取流程实例
            Page<ProcessInstance> processInstancePage = processRuntime.processInstances(Pageable.of(0, 100));
            // 将流程实例转换为 list
            List<ProcessInstance> lists = processInstancePage.getContent();
            // 对 list 进行排序  按照启动日期排序
            lists.sort((x,y) -> y.getStartDate().toString().compareTo(x.getStartDate().toString()));
            // 创建 map 保存流程实例列表
            List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
            for(ProcessInstance pi : lists){
                // 因为 ProcessInstance 中没有历史高亮需要的 deploymentID 和 资源 resourceName ,所以需要查询
                ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                        .processDefinitionId(pi.getProcessDefinitionId())
                        .singleResult();

                listMap.add(new CreateMap.Build()
                        .setAttribute("id",pi.getId())
                        .setAttribute("name",pi.getName())
                        .setAttribute("status",pi.getStatus())
                        .setAttribute("processDefinitionId",pi.getProcessDefinitionId())
                        .setAttribute("processDefinitionKey",pi.getProcessDefinitionKey())
                        .setAttribute("startDate",pi.getStartDate())
                        .setAttribute("processDefinitionVersion", pi.getProcessDefinitionVersion())
                        .setAttribute("deploymentId", processDefinition.getDeploymentId())
                        .setAttribute("resourceName",processDefinition.getResourceName())
                        .build());
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取流程实例列表失败",
                    e.toString()
            );
        }
    }


    /**
     * 启动流程实例
     * @param processDefinitionKey   流程定义 key
     * @param instanceName            流程实例名称
     * @return
     */
    @GetMapping("/startProcess")
    public AjaxResponse startProcess(@RequestParam("processDefinitionKey")String processDefinitionKey,
                                     @RequestParam("instanceName")String instanceName){
        try{
            // 测试环境使用内容用户登录
            if(GlobalConfig.Test){
                securityUtil.logInAs("zhangsan");
            }else{
                 // 这样也可以获取到登录人
                // securityUtil.logInAs(SecurityContextHolder.getContext().getAuthentication().getName());
            }
            ProcessInstance processInstance = processRuntime.start(
                    ProcessPayloadBuilder
                            .start()
                            .withProcessDefinitionKey(processDefinitionKey)
                            .withName(instanceName)
                            .withBusinessKey("自定义业务key")
                           // .withVariable("参数name", "参数值")  启动一般不加参数,业务生成是才加参数
                            .build()
            );
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.SUCCESS.getCode(),
                        GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                        null
                );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "启动流程实例失败",
                    e.toString()
            );
        }
    }

    // 挂起流程实例
    // 一般使用场景是:流程实例运行过程中,用户觉得流程需要暂停,但是又不想删除它,需要将历史保留起来,
    // 这个时候需要使用挂起了

    /**
     * 挂起流程实例
     * @param instanceId  实例 id
     * @return
     */
    @GetMapping("/suspendInstance")
    public AjaxResponse suspendInstance(@RequestParam("instanceId")String instanceId){
        try{
            // 测试环境使用内容用户登录
            if(GlobalConfig.Test){
                securityUtil.logInAs("zhangsan");
            }
            ProcessInstance processInstance = processRuntime.suspend(
                    ProcessPayloadBuilder
                            .suspend()
                            .withProcessInstanceId(instanceId)
                            .build()
            );

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    processInstance.getName()  // 返回实例名称
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "挂起暂停流程实例失败",
                    e.toString()
            );
        }
    }

    /**
     * 恢复流程实例
     * @param instanceId  实例 id
     * @return
     */
    @GetMapping("/resumeInstance")
    public AjaxResponse resumeInstance(@RequestParam("instanceId")String instanceId){
        try{
            // 测试环境使用内容用户登录
            if(GlobalConfig.Test){
                securityUtil.logInAs("zhangsan");
            }
            ProcessInstance processInstance = processRuntime.resume(
                    ProcessPayloadBuilder
                            .resume()
                            .withProcessInstanceId(instanceId)
                            .build()
            );

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    processInstance.getName()  // 返回实例名称
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "恢复流程实例失败",
                    e.toString()
            );
        }
    }

    /**
     * 删除流程实例
     * @param instanceId  实例 id
     * @return
     */
    @GetMapping("/deleteInstance")
    public AjaxResponse deleteInstance(@RequestParam("instanceId")String instanceId){
        try{
            // 测试环境使用内容用户登录
            if(GlobalConfig.Test){
                securityUtil.logInAs("zhangsan");
            }
            ProcessInstance processInstance = processRuntime.delete(
                    ProcessPayloadBuilder
                            .delete()
                            .withProcessInstanceId(instanceId)
                            .build()
            );

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    processInstance.getName()  // 返回实例名称
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "删除流程实例失败",
                    e.toString()
            );
        }
    }

    /**
     * 查询流程实例参数
     * @param instanceId  实例 id
     * @return
     */
    @GetMapping("/variables")
    public AjaxResponse variables(@RequestParam("instanceId")String instanceId){
        try{
            // 测试环境使用内容用户登录
            if(GlobalConfig.Test){
                securityUtil.logInAs("zhangsan");
            }
            List<VariableInstance> variables = processRuntime.variables(
                    ProcessPayloadBuilder
                            .variables()
                            .withProcessInstanceId(instanceId)
                            .build()
            );

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    variables  // 返回流程参数列表
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "查询流程实例参数失败",
                    e.toString()
            );
        }
    }
}

任务接口

/**
 * 任务 控制类
 */
@RestController
@RequestMapping("/task")
public class TaskController {

    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private TaskRuntime taskRuntime;
    @Autowired
    private ProcessRuntime processRuntime;

    /**
     * 获取我的待办任务
     * @return
     */
    @GetMapping("/getTasks")
    public AjaxResponse getTasks(){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 获取任务列表
            Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 100));
            List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
            for (Task task : tasks.getContent()){
                Map<String,Object> map = new HashMap<String, Object>();
                map.put("id", task.getId());
                map.put("name",task.getName());
                map.put("status",task.getStatus());
                map.put("createDate",task.getCreatedDate());
                /**
                 * 执行人这里有一个重要的细节
                 *      这个方法查询的是当前登录人所有的任务,task.getAssignee() 查询的就是实际登录人的名称
                 *      实际上这个方法不光是执行是当前登录人的所有任务,他还包括候选人是当前登录人的所有任务,
                 *      只不过如果候选人是当前登录用户 task.getAssignee() 返回的是 null,这里如果 task.getAssignee()
                 *      返回的确实是当前登录人,那么 task.getAssignee() 就是当前登录人,否则我们和给返回一个带拾取任务
                 *      这样前端用户就很清楚的知道是分配给他的任务,还是说是一条带拾取的任务
                 */
                // 我们判断一下
                if(task.getAssignee() == null){
                    map.put("assignee","带拾取任务"); // 这里先写死了
                }else{
                    map.put("assignee",task.getAssignee());  // 当前登录人
                }
                // 由于这里获取不到流程实例名称,我们需要查询一下
                ProcessInstance processInstance = processRuntime.processInstance(task.getProcessInstanceId());
                // 保存流程实例名称
                map.put("instanceName",processInstance.getName());
                listMap.add(map);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取我的任务列表失败",
                    e.toString()
            );
        }
    }


    /**
     * 完成任务
     * @return
     */
    @GetMapping("/completeTask")
    public AjaxResponse completeTask(@RequestParam("taskId")String taskId){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);
            // task.getAssignee() == null 说明这是一条带拾取的任务,我们先拾取在完成
            if(task.getAssignee() == null){
                taskRuntime.claim(TaskPayloadBuilder
                .claim()
                .withTaskId(taskId)
                .build()
                );
            }
            // 完成任务  注意:挂起的任务需要先激活在完成
            taskRuntime.complete(
                TaskPayloadBuilder
                .complete()
                .withTaskId(taskId)
                //.withVariable("xxx","xx") 完成任务也是可以设置实例参数的
                .build()
            );
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    null
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "完成任务 "+taskId+" 失败",
                    e.toString()
            );
        }
    }
}

历史任务接口

/**
 * 查询历史任务
 */
@RestController
@RequestMapping("/activitiHistory")
public class ActivitiHistoryController {

    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private HistoryService historyService;

    /**
     * 查询用户历史任务
     *
     * @return
     * @AuthenticationPrincipal 注解可以获取用于登录信息
     */
    @GetMapping("getInstanceByUserName")
    public AjaxResponse getInstanceByUserName(@AuthenticationPrincipal UserInfoBean userInfoBean) {
        try {
            // 获取用于历史任务列表
            List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery()
                    .orderByHistoricTaskInstanceEndTime().desc()
                    .taskAssignee(userInfoBean.getUsername())
                    .list();
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    historicTaskInstances
            );
        } catch (Exception e) {
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取用户历史任务列表失败",
                    e.toString()
            );
        }
    }

    /**
     * 根据流程实例id查询任务
     *
     * @param processInstanceId // 流程实例id
     * @return
     */
    @GetMapping("getInstanceByProcessInstanceId")
    public AjaxResponse getInstanceByProcessInstanceId(@RequestParam("processInstanceId") String processInstanceId) {
        try {
            // 获取用于历史任务列表
            List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery()
                    .orderByHistoricTaskInstanceEndTime().desc()
                    .processInstanceId(processInstanceId)
                    .list();
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    historicTaskInstances
            );
        } catch (Exception e) {
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取历史任务列表失败",
                    e.toString()
            );
        }
    }
}

Activiti 中的表单

动态表单

动态表单是程序员最喜欢实现的表单方式,因为他的页面会根据数据的内容来描述出来

普通表单

普通表单就是由前端一个一个的去做静态的表单页面

  • 在 activiti7 以前 6 或者 5 它是有 form 这个属性的,我们可以通过 form 属性来存和读响应的表单字段,但是在 activiti7 按照官网的说法为了轻量化去掉了者个内容,那么表单字段要如何渲染,表单字段不光是说我定义了一个 string 的字段,那么我就在前端显示一个输入框这么简单,他还有两个非常重要的使命,第一,这个字段是有默认值的,这个默认值可能是一个固定值,也有可能是之前的某一个任务环节天的值,在当前任务环节要显示,这个功能要这么做,这也是我们动态表单的一个重要功能,还有一个是什么呢,我们经常在页面填写的时候,填写的内容要做为 UEL 表达式的参数,比如说我要请三天假和请十天假是不同的人审批,我在页面上填写的内容是要作为参数去做流程判断的,或者说下一个环节是有谁审批,是由这一个环节的操作人去选取的,那么选取的值也要做为 UEL 表达式的参数,那么这一部分工作要怎么做,也是动态表单中的一个重要功能
    第一个我们要做的功能是什么呢,在 BPMN js 中绘制表单字段,然后使用接口去获取表单字段,我们先来看能拿到什么内容

渲染表单接口(也是在 taskController 中)

     * 渲染动态表单
     * @return
     */
    @GetMapping("/formDataShow")
    public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){

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

            /**
             * 关键代码,这里在强调一下,在 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());

            List<FormProperty> formProperties = userTask.getFormProperties();
            for (FormProperty formProperty : formProperties) {


            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    null
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "完成任务 "+taskId+" 失败",
                    e.toString()
            );
        }
    }
创建表单流程
image.png

image.png

image.png

image.png

下载到本地然后通过我们之前写的接口添加流程定义,通过 bpmn

image.png

通过接口启动流程

image.png

调用渲染表单接口打断点观察

image.png

通过断点可以看出在 activiti7 M4 版本中只能拿到表单的编号,和类型

image.png

activiti 中的表单

  • 动态表单:使用拼字符串的方式描述表单信息
  • 普通表单:需要设置 businessKey 以及任务与表单一对一关系

Activiti 表单字段的约定内容

  • 表单控件命名约束:FormProperty_0ueitp2-_-类型-_-名称-_-默认值-_-是否是参数

说明:

  • FormProperty_0ueitp2:bpmn js 自动生成的一个id
  • -_- 是我们自定义的一个表单字段分割符
  • 类型:例如 string,int ,date 等...前端可以根据类型渲染不同的控件
  • 名称:控件左边的 label 例如:姓名:,年龄:
  • 默认值:
    • 无:那么前端什么都不要写,就是空的
    • 字符常量:例如:请输入姓名,请输入年龄,到时候输入框里就会显示这些常量
    • FormProperty_开头定义过的控件ID:以 FormProperty_ 开头表示需要显示之前这个控件的值,显示到控件中
  • 是否是参数:f 为不是参数,s 是字符,t 是时间(不需要 int ,因为 int 等价于 string)
例子

FormProperty_0lwer-_-姓名-_-请输入姓名-_-f
FormProperty_fdfer-_-年龄-_-请输入年龄-_-s

  • 注意:表单 key 必须和任务编号一模一样,因为参数需要任务 key ,但是无法获取,只能获取表单 key “task.getFormKey()” 当做任务 key

实例

创建动态表单 bpmn
image.png

image.png

image.png

image.png
表单的内容是我们自定义的格式
  • 表单一内容:FormProperty_3s5ucaq-_-string-_-姓名-_-请输入姓名-_-f
  • 表单二内容:FormProperty_279b9u1-_-int-_-年龄-_-无-_-s
下载 bpmn 文件,通过我们编写的接口进行上传,部署流程,启动流程,这里略过......
渲染动态表单
    /**
     * 渲染动态表单
     * @return
     */
    @GetMapping("/formDataShow")
    public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){

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

            /**
             * 关键代码,这里在强调一下,在 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<FormProperty> formProperties = userTask.getFormProperties();
            // 保存分割后的格式返回给前端
            List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
            for (FormProperty formProperty : formProperties) {
                // 分割表单数据
                String[] split = formProperty.getId().split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                form.put("id", split[0]);
                form.put("controlType", split[1]);
                form.put("controlLabel", split[2]);
                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(),
                    "完成任务 "+taskId+" 失败",
                    e.toString()
            );
        }
    }
通过查询渲染表单接口最终返回给前端的数据
image.png

activiti 动态表单的提交

解析数据提交

activiti 动态表单提交参数的约定内容
  • 动态表单提交内容约定
    • formData:控件id-_-控件值-_-是否是参数!_!控件id-_-控件值-_-是否是参数
  • 列子:
  • FormProperty_0gkhjf-_-不是参数-_-f!_!FormProperty_1gkmgj-_-我是参数-_-s
  • !_! 时用来分割多个控件
  • 是否是参数:f 为不是参数,s 是字符,t 是时间(不需要 int ,因为 int 等价于 string)
保存动态表单测试,使用上面例子的数据
    /**
     * 保存动态表单
     * @param taskId      任务 id
     * @param formData    控件组字符串
     * @return
     */
    @PostMapping("/formDataSave")
    public AjaxResponse formDataSave(@RequestParam("taskId")String taskId,
                                     @RequestParam("formData")String formData){

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

            // 前端传来的字符串拆分为控件组
            String[] formDataList = formData.split("!_!");
            // 保存解析后的数据
            List<Map<String,Object>> controlItems = new ArrayList<Map<String, Object>>();
            for (String control : formDataList) {
                // 分割表单数据
                String[] split = control.split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                // 流程定义 key
                form.put("PROC_DEF_ID_", task.getProcessDefinitionId());
                // 流程实例 key
                form.put("PROC_INST_ID_", task.getProcessInstanceId());
                // 表单 key
                form.put("FORM_KEY_", task.getFormKey());
                // 控件 key
                form.put("Control_ID_", split[0]);
                // 控件值
                form.put("Control_VALUE_", split[1]);
                // 是否是流程参数
                form.put("Control_PARAM", split[2]);
                controlItems.add(form);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    controlItems
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }

最终解析

image.png

提交表单内容写入输入库

数据库表结构
image.png
编写插入数据库的 mapper
@Mapper
@Component
public interface ActivitiMapper {

    @Insert("<script>" +
            "insert into formdata(PROC_DEF_ID_,PROC_INST_ID_,FORM_KEY_,Control_ID_,Control_VALUE_)" +
            "values" +
            "<foreach collection=\"maps\" item=\"formdata\" index=\"index\" separator=\",\">" +
            "(" +
            "#{formdata.PROC_DEF_ID_,jdbcType=VARCHAR}," +
            "#{formdata.PROC_INST_ID_,jdbcType=VARCHAR}," +
            "#{formdata.FORM_KEY_,jdbcType=VARCHAR}," +
            "#{formdata.Control_ID_,jdbcType=VARCHAR}," +
            "#{formdata.Control_VALUE_,jdbcType=VARCHAR}" +
            ")" +
            "</foreach>" +
            "</script>")
    public int insertFormData(@Param("maps") List<Map<String,Object>> maps);
}
在保存动态表单方法中调用
    /**
     * 保存动态表单
     * @param taskId      任务 id
     * @param formData    控件组字符串
     * @return
     */
    @PostMapping("/formDataSave")
    public AjaxResponse formDataSave(@RequestParam("taskId")String taskId,
                                     @RequestParam("formData")String formData){

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

            // 前端传来的字符串拆分为控件组
            String[] formDataList = formData.split("!_!");
            // 保存解析后的数据
            List<Map<String,Object>> controlItems = new ArrayList<Map<String, Object>>();
            for (String control : formDataList) {
                // 分割表单数据
                String[] split = control.split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                // 流程定义 key
                form.put("PROC_DEF_ID_", task.getProcessDefinitionId());
                // 流程实例 key
                form.put("PROC_INST_ID_", task.getProcessInstanceId());
                // 表单 key
                form.put("FORM_KEY_", task.getFormKey());
                // 控件 key
                form.put("Control_ID_", split[0]);
                // 控件值
                form.put("Control_VALUE_", split[1]);
                // 是否是流程参数
                form.put("Control_PARAM", split[2]);
                controlItems.add(form);
            }
            // 将表单写入数据库
            activitiMapper.insertFormData(controlItems);
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    controlItems
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }
可以看到动态表单插入成功
image.png

参数作为 UEL 表达式参数

画 张三动态表单 bpmn 图
image.png

image.png

image.png

image.png
模拟表单一内容
  • FormProperty_2kou82p-_-string-_-姓名-_-请输入姓名-_-f
模拟表单二内容
  • FormProperty_11muvfh-_-long-_-年龄-_-请输入年龄-_-s


    image.png

    image.png
下载 bpmn js ,上传,部署,渲染表单略过,参考之前的操作......
模拟用户从前台填写表单
image.png
postman 调用保存动态表单接口测试
image.png
数据库插入成功,正常应该走的是大于 3 天流程,应该是主管审批
image.png
确实走的数主管的流程
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容