一、Flowable介绍

1.简介

Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义, 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。

BPMN:Business Process Modeling Notation,即业务流程建模符号,是一种流程建模的通用和标准语言,用来绘制业务流程图,以便更好地让各部门之间理解业务流程和相互关系。

2.Activiti、Flowable、Camunda

(1)为什么选这三者比较?
三者都是开源免费、社区活跃度比较高的;
三者都是同一个团队的分支,camunda基于activiti5,flowable基于activiti6,activiti5则是基于更早的jbpm4;

(2)优缺点
①功能比较
在这里插入图片描述

②Activiti7以后,对于流程引擎本身及相关引擎功能关注度并不高,核心很大精力放在构建其云生态环境(适配Docker、kubernates,适配Jenkins等devops工具);而Flowable分离出去做了很多引擎相关的完善。

③网上资料数对比,仅以GIT为例:
git上activiti的项目(16,618)是flowable(59629)两三倍,flowable是camunda(4077)十来倍

综上所述,Activiti7最大的优势是网上资料多,缺点是功能最少、易用性比较差。camunda最大的优势就是性能比较高,缺点是三者的资料是最少的。flowable是一个比较均衡的方案。

二、Flowable实战1:集成Flowable Modeler

你也可以选择不集成modeler编辑器,那样的话只需要引入flowable的starter和添加ProcessEngineConfig配置,在后面的流程的创建和使用中也能运行

1.源码下载,后面会用到
地址:https://github.com/flowable/flowable-engine/releases/tag/flowable-6.4.1/

2.引入依赖,我这里用的版本是6.4.1,替换下面的参数即可

       <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.1.3.RELEASE</version>
            <scope>test</scope>
        </dependency>

        <!-- Flowable spring-boot 版套餐 -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter-basic</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <!-- flowable 集成依赖 rest,logic,conf -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-rest</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-logic</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-conf</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <!-- flowable 集成依赖 engine -->
        <!--<dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-engine</artifactId>
            <version>${flowable.version}</version>
        </dependency>-->
        <!-- Flowable 内部日志采用 SLF4J -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
        <!-- 配置文件处理器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>3.0.1</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.24</version>
            <scope>compile</scope>
        </dependency>

        <!--数据库连接-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>
    </dependencies>

3.后端代码集成
需要这些文件,其中Security的包名不能变,否则不能生效,无法免登录
在这里插入图片描述
AppDispatcherServletConfiguration.java

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.flowable.config;

import org.flowable.ui.modeler.rest.app.EditorGroupsResource;
import org.flowable.ui.modeler.rest.app.EditorUsersResource;
import org.flowable.ui.modeler.rest.app.StencilSetResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

@Configuration
@ComponentScan(value = {
        "org.flowable.ui.modeler.rest.app",
        // 不加载 rest,因为 getAccount 接口需要我们自己实现
// "org.flowable.ui.common.rest"
},excludeFilters = {

        // 移除 EditorUsersResource 与 EditorGroupsResource,因为不使用 IDM 部分
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorUsersResource.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorGroupsResource.class),
        // 配置文件用自己的
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StencilSetResource.class),
}
)
@EnableAsync
public class AppDispatcherServletConfiguration implements WebMvcRegistrations {

    private static final Logger LOGGER = LoggerFactory.getLogger(AppDispatcherServletConfiguration.class);

    @Bean
    public SessionLocaleResolver localeResolver() {
        return new SessionLocaleResolver();
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LOGGER.debug("Configuring localeChangeInterceptor");
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        LOGGER.debug("Creating requestMappingHandlerMapping");
        RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping();
        requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
        requestMappingHandlerMapping.setRemoveSemicolonContent(false);
        Object[] interceptors = { localeChangeInterceptor() };
        requestMappingHandlerMapping.setInterceptors(interceptors);
        return requestMappingHandlerMapping;
    }
}

ApplicationConfiguration.java

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.flowable.config;

import org.flowable.ui.common.service.idm.RemoteIdmService;
import org.flowable.ui.modeler.properties.FlowableModelerAppProperties;
import org.flowable.ui.modeler.servlet.ApiDispatcherServletConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@EnableConfigurationProperties(FlowableModelerAppProperties.class)
@ComponentScan(basePackages = {

// "org.flowable.ui.modeler.conf", // 不引入 conf
        "org.flowable.ui.modeler.repository",
        "org.flowable.ui.modeler.service",
// "org.flowable.ui.modeler.security", //授权方面的都不需要
// "org.flowable.ui.common.conf", // flowable 开发环境内置的数据库连接
// "org.flowable.ui.common.filter", // IDM 方面的过滤器
        "org.flowable.ui.common.service",
        "org.flowable.ui.common.repository",
        //
// "org.flowable.ui.common.security",//授权方面的都不需要
        "org.flowable.ui.common.tenant" },excludeFilters = {

        // 移除 RemoteIdmService
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = RemoteIdmService.class)
}
)
public class ApplicationConfiguration {


    @Bean
    public ServletRegistrationBean modelerApiServlet(ApplicationContext applicationContext) {

        AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
        dispatcherServletConfiguration.setParent(applicationContext);
        dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class);
        DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration);
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/api/*");
        registrationBean.setName("Flowable Modeler App API Servlet");
        registrationBean.setLoadOnStartup(1);
        registrationBean.setAsyncSupported(true);
        return registrationBean;
    }
}

FlowableStencilSetResource.java

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.flowable.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.flowable.ui.common.service.exception.InternalServerErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/app")
public class FlowableStencilSetResource {

    private static final Logger LOGGER = LoggerFactory.getLogger(FlowableStencilSetResource.class);

    @Autowired
    protected ObjectMapper objectMapper;

    @RequestMapping(value = "/rest/stencil-sets/editor", method = RequestMethod.GET, produces = "application/json")
    public JsonNode getStencilSetForEditor() {
        try {
            JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream("stencilset/stencilset_bpmn.json"));
            return stencilNode;
        } catch (Exception e) {
            LOGGER.error("Error reading bpmn stencil set json", e);
            throw new InternalServerErrorException("Error reading bpmn stencil set json");
        }
    }

    @RequestMapping(value = "/rest/stencil-sets/cmmneditor", method = RequestMethod.GET, produces = "application/json")
    public JsonNode getCmmnStencilSetForEditor() {
        try {
            JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream("stencilset/stencilset_cmmn.json"));
            return stencilNode;
        } catch (Exception e) {
            LOGGER.error("Error reading bpmn stencil set json", e);
            throw new InternalServerErrorException("Error reading bpmn stencil set json");
        }
    }
}

ProcessEngineConfig.java

package com.example.flowable.config;

import lombok.Data;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/** * 流程引擎配置文件 * @author: jijh * @create: 2022-12-12 16:49 **/
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
@Data
public class ProcessEngineConfig {


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

    private String url;

    private String driverClassName;

    private String username;

    private String password;

    private String publicKey;

    /** * 初始化流程引擎 * @return */
    @Primary
    @Bean(name = "processEngine")
    public ProcessEngine initProcessEngine() {

        logger.info("=============================ProcessEngineBegin=============================");

        // 流程引擎配置
        ProcessEngineConfiguration cfg = null;

        try {

            cfg = new StandaloneProcessEngineConfiguration()
                    .setJdbcUrl(url)
                    .setJdbcUsername(username)
                    //.setJdbcPassword(ConfigTools.decrypt(publicKey, password))
                    .setJdbcPassword(password)
                    .setJdbcDriver(driverClassName)
                    // 初始化基础表,不需要的可以改为 DB_SCHEMA_UPDATE_FALSE
                    .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE)
                    // 默认邮箱配置
                    // 发邮件的主机地址,先用 QQ 邮箱
                    //.setMailServerHost("smtp.qq.com")
                    // POP3/SMTP服务的授权码
                    //.setMailServerPassword("xxxxxxx")
                    // 默认发件人
                    //.setMailServerDefaultFrom("836369078@qq.com")
                    // 设置发件人用户名
                    //.setMailServerUsername("管理员")
                    // 解决流程图乱码
                    .setActivityFontName("宋体")
                    .setLabelFontName("宋体")
                    .setAnnotationFontName("宋体");
        } catch (Exception e) {

            e.printStackTrace();
        }
        // 初始化流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        logger.info("=============================ProcessEngineEnd=============================");
        return processEngine;
    }
}

FlowableApplication.java

package com.example.flowable;

import com.example.flowable.config.AppDispatcherServletConfiguration;
import com.example.flowable.config.ApplicationConfiguration;
import org.flowable.ui.modeler.conf.DatabaseConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Import;

//启用全局异常拦截器
@Import(value={

        // 引入修改的配置
        ApplicationConfiguration.class,
        AppDispatcherServletConfiguration.class,
        // 引入 DatabaseConfiguration 表更新转换
        DatabaseConfiguration.class})
// Eureka 客户端
@EnableDiscoveryClient
@MapperScan("com.example.*.dao")
// 移除 Security 自动配置
// Spring Cloud 为 Finchley 版本
// @SpringBootApplication(exclude={SecurityAutoConfiguration.class})
// Spring Cloud 为 Greenwich 版本
@SpringBootApplication(exclude={
        SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class})

public class FlowableApplication {

    public static void main(String[] args) {
        SpringApplication.run(FlowableApplication.class, args);
    }

}

SecurityUtils.java

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flowable.ui.common.security;

import org.flowable.idm.api.User;
import org.flowable.ui.common.model.RemoteUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.ArrayList;
import java.util.List;

/**
 * Utility class for Spring Security.
 */
public class SecurityUtils {

    private static User assumeUser;

    private SecurityUtils() {
    }

    /**
     * Get the login of the current user.
     */
    public static String getCurrentUserId() {
        User user = getCurrentUserObject();
        if (user != null) {
            return user.getId();
        }
        return null;
    }

    /**
     * @return the {@link User} object associated with the current logged in user.
     */
    public static User getCurrentUserObject() {
        if (assumeUser != null) {

            return assumeUser;
        }

        RemoteUser user = new RemoteUser();
        user.setId("admin");
        user.setDisplayName("Administrator");
        user.setFirstName("Administrator");
        user.setLastName("Administrator");
        user.setEmail("admin@flowable.com");
        user.setPassword("123456");
        List<String> pris = new ArrayList<>();
        pris.add(DefaultPrivileges.ACCESS_MODELER);
        pris.add(DefaultPrivileges.ACCESS_IDM);
        pris.add(DefaultPrivileges.ACCESS_ADMIN);
        pris.add(DefaultPrivileges.ACCESS_TASK);
        pris.add(DefaultPrivileges.ACCESS_REST_API);
        user.setPrivileges(pris);
        return user;
    }

    public static FlowableAppUser getCurrentFlowableAppUser() {
        FlowableAppUser user = null;
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null && securityContext.getAuthentication() != null) {
            Object principal = securityContext.getAuthentication().getPrincipal();
            if (principal instanceof FlowableAppUser) {
                user = (FlowableAppUser) principal;
            }
        }
        return user;
    }

    public static boolean currentUserHasCapability(String capability) {
        FlowableAppUser user = getCurrentFlowableAppUser();
        for (GrantedAuthority grantedAuthority : user.getAuthorities()) {
            if (capability.equals(grantedAuthority.getAuthority())) {
                return true;
            }
        }
        return false;
    }

    public static void assumeUser(User user) {
        assumeUser = user;
    }

    public static void clearAssumeUser() {
        assumeUser = null;
    }

}

4.前端代码集成
目录结构如下:
在这里插入图片描述

static下的代码来自源码包的flowable-engine-flowable-6.4.1\modules\flowable-ui-modeler\flowable-ui-modeler-app\src\main\resources\static下面

resource\static\scripts\configuration\url-conf.js需要修改:

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var FLOWABLE = FLOWABLE || {};

/*
 * Contains methods to retrieve the (mostly) base urls of the different end points.
 * Two of the methods #getImageUrl and #getModelThumbnailUrl are exposed in the $rootScope for usage in the HTML views.
 */
FLOWABLE.APP_URL = {

    /* ACCOUNT URLS */

    getAccountUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/login/rest/account';
    },

    getLogoutUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/logout';
    },

    /* MODEL URLS */

    getModelsUrl: function (query) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models' + (query || "");
    },

    getModelUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId;
    },

    getModelModelJsonUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/model-json';
    },

    getModelBpmn20ExportUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/bpmn20?version=' + Date.now();
    },

    getCloneModelsUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/clone';
    },

    getModelHistoriesUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/history';
    },

    getModelHistoryUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/history/' + modelHistoryId;
    },

    getModelHistoryModelJsonUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/history/' + modelHistoryId + '/model-json';
    },

    getModelHistoryBpmn20ExportUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/history/' + modelHistoryId + '/bpmn20?version=' + Date.now();
    },

    getCmmnModelDownloadUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + (modelHistoryId ? '/history/' + modelHistoryId : '') + '/cmmn?version=' + Date.now();
    },

    getModelParentRelationsUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/parent-relations';
    },

    /* APP DEFINITION URLS  */

    getAppDefinitionImportUrl: function (renewIdmIds) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/import?renewIdmEntries=' + renewIdmIds;
    },

    getAppDefinitionTextImportUrl: function (renewIdmIds) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/text/import?renewIdmEntries=' + renewIdmIds;
    },

    getAppDefinitionUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId;
    },

    getAppDefinitionModelImportUrl: function (modelId, renewIdmIds) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/import?renewIdmEntries=' + renewIdmIds;
    },

    getAppDefinitionModelTextImportUrl: function (modelId, renewIdmIds) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/text/import?renewIdmEntries=' + renewIdmIds;
    },

    getAppDefinitionPublishUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/publish';
    },

    getAppDefinitionExportUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/export?version=' + Date.now();
    },

    getAppDefinitionBarExportUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/export-bar?version=' + Date.now();
    },

    getAppDefinitionHistoryUrl: function (modelId, historyModelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/app-definitions/' + modelId + '/history/' + historyModelId;
    },

    getModelsForAppDefinitionUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models-for-app-definition';
    },

    getCmmnModelsForAppDefinitionUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/cmmn-models-for-app-definition';
    },

    /* PROCESS INSTANCE URLS */

    getProcessInstanceModelJsonUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/process-instances/' + modelId + '/model-json';
    },

    getProcessInstanceModelJsonHistoryUrl: function (historyModelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/process-instances/history/' + historyModelId + '/model-json';
    },

    /* PROCESS DEFINITION URLS */

    getProcessDefinitionModelJsonUrl: function (processDefinitionId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/process-definitions/' + processDefinitionId + '/model-json';
    },

    /* PROCESS MODEL URLS */

    getImportProcessModelUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/import-process-model';
    },

    getImportProcessModelTextUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/import-process-model/text';
    },

    /* DECISION TABLE URLS */

    getDecisionTableModelsUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models';
    },

    getDecisionTableImportUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/import-decision-table';
    },

    getDecisionTableTextImportUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/import-decision-table-text';
    },

    getDecisionTableModelUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/' + modelId;
    },

    getDecisionTableModelValuesUrl: function (query) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/values?' + query;
    },

    getDecisionTableModelsHistoryUrl: function (modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/history/' + modelHistoryId;
    },

    getDecisionTableModelHistoryUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/decision-table-models/' + modelId + '/history/' + modelHistoryId;
    },

    /* FORM MODEL URLS */

    getFormModelsUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/form-models';
    },

    getFormModelValuesUrl: function (query) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/form-models/values?' + query;
    },

    getFormModelUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/form-models/' + modelId;
    },

    getFormModelHistoryUrl: function (modelId, modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/form-models/' + modelId + '/history/' + modelHistoryId;
    },

    /* CASE MODEL URLS */

    getCaseModelsUrl: function (query) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/case-models' + (query || "");
    },

    getCaseModelImportUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/import-case-model';
    },

    getCaseModelTextImportUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/import-case-model/text';
    },

    getCaseInstancesHistoryModelJsonUrl: function (modelHistoryId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/case-instances/history/' + modelHistoryId + '/model-json';
    },

    getCaseInstancesModelJsonUrl: function (modelId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/case-instances/' + modelId + '/model-json';
    },

    getCaseDefinitionModelJsonUrl: function (caseDefinitionId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/case-definitions/' + caseDefinitionId + '/model-json';
    },

    /* IMAGE URLS (exposed in rootscope in app.js */

    getImageUrl: function (imageId) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/image/' + imageId;
    },

    getModelThumbnailUrl: function (modelId, version) {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/models/' + modelId + '/thumbnail' + (version ? "?version=" + version : "");
    },

    /* OTHER URLS */

    getEditorUsersUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/editor-users';
    },

    getEditorGroupsUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/editor-groups';
    },

    getAboutInfoUrl: function () {
        return FLOWABLE.CONFIG.contextRoot + '/app/rest/about-info';
    }

};

stencilset下面的文件是汉化文件
https://download.csdn.net/download/tttalk/87347577
5.其余配置
application.yml

spring:
  application:
    name: flowable-service
  main:
    allow-circular-references: true
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/flowable2?serverTimezone=Asia/Shanghai&useUnicode=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource


# flowable 配置
flowable:
  # 关闭异步,不关闭历史数据的插入就是异步的,会在同一个事物里面,无法回滚
  # 开发可开启会提高些效率,上线需要关闭
  async-executor-activate: false


log4j.properties

log4j.rootLogger=DEBUG, CA
log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{ 
hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n

6.启动项目
进入http://localhost:8080/,进入下面页面则算启动成功了在这里插入图片描述

三、Flowable实战2:api调用

1.引入pom依赖

<!--flowable engine-->
 <dependency>
     <groupId>org.flowable</groupId>
     <artifactId>flowable-engine</artifactId>
     <version>6.3.0</version>
 </dependency>

2.配置数据库,推荐新建一个数据库“项目名称_flowable”
①flowable数据库配置:其中setDatabaseSchemaUpdate配置会帮助你自动创建或更新表结构

package com.fms.api.config.flowable;

import org.flowable.engine.*;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class FlowableConfiguration {

    @Value("${flowable.datasource.url}")
    private String dbUrl;
    @Value("${flowable.datasource.username}")
    private String username;
    @Value("${flowable.datasource.password}")
    private String password;
    @Value("${flowable.datasource.driver-class-name}")
    private String driverClassName;

    @Bean
    @Primary
    public ProcessEngine processEngine(){
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl(dbUrl)
                .setJdbcUsername(username)
                .setJdbcPassword(password)
                .setJdbcDriver(driverClassName)
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE)
                .setDatabaseType("mysql");
        ProcessEngine processEngine = cfg.buildProcessEngine();
        return processEngine;
    }

    @Bean
    public RepositoryService repositoryService(ProcessEngine processEngine){
        RepositoryService repositoryService = processEngine.getRepositoryService();
        return repositoryService;
    }

    @Bean
    public RuntimeService runtimeService(ProcessEngine processEngine){
        RuntimeService runtimeService = processEngine.getRuntimeService();
        return runtimeService;
    }

    @Bean
    public TaskService taskService(ProcessEngine processEngine){
        TaskService taskService = processEngine.getTaskService();
        return taskService;
    }

    @Bean
    public HistoryService historyService(ProcessEngine processEngine){
        HistoryService historyService = processEngine.getHistoryService();
        return historyService;
    }

    @Bean
    public IdentityService identityService(ProcessEngine processEngine){
        IdentityService identityService = processEngine.getIdentityService();
        return identityService;
    }
}

yml配置:

flowable:
  datasource:
    url: jdbc:mysql://xxx/项目名称_flowable?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: xxx
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver

③添加自定义表结构(可选)

CREATE TABLE `t_flow_apply` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `version` int NOT NULL COMMENT '版本号,从1开始递增,步长为1',
  `enabled` varchar(32) NOT NULL COMMENT '是否有效(字符串类型:1-有效,其他值-无效)',
  `create_by` bigint NOT NULL COMMENT '创建人标识',
  `create_time` varchar(32) NOT NULL COMMENT '创建时间',
  `update_by` bigint NOT NULL COMMENT '更新人标识',
  `update_time` varchar(32) NOT NULL COMMENT '更新时间',
  `process_id` varchar(50) NOT NULL COMMENT '流程实例id',
  `process_definition_key` varchar(30) NOT NULL COMMENT '流程的模板key',
  `user_id` bigint NOT NULL COMMENT '发起人id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='流程表';

3.添加api相关使用方法,其中诸如withdrawProcessLast(回退到上一节点)、getAllProcessInstances(获取某流程所有实例)是我根据实际使用情况添加的。
FlowService.java:

package com.fms.api.service.admin.flowable;

import com.fms.api.web.admin.flowable.vo.TaskEntity;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.task.api.Task;

import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface FlowService {

/**
     * 部署流程
     * @param processName 流程定义名
     * @param resourcePath 如flowable/process.bpmn
     * @return
     */

    public void createProcess(String processName, String resourcePath);


/**
     * 发起流程
     * @param processName 流程定义名
     * @param map 参数
     * @return 流程实例ID
     */

    public String applyProcess(String processName,Map<String,Object> map);


/**
     * 查询某用户/群体/角色待办列表
     * @return
     */

    public List<Task> todoList(String user);


/**
     * 查询所有任务节点
     * @param processId
     * @return
     */

    public List<TaskEntity> taskNodeList(String processId);


/**
     * 生成流程图
     * @param processId 任务ID
     */

    public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception;


/**
     * 完成任务
     * @param taskId 任务id
     * @param user 用户/角色id
     * @param map 流程变量
     */

    public void approveProcess(String taskId,String user,Map<String,Object> map);


/**
     * 将节点移动到任意节点上
     * @param taskId 任务id
     * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好
     * @return
     */

    public void withdrawProcess(String taskId,String taskDefinitionKey);


/**
     * 获取流程的历史节点列表
     * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点"userTask"的信息
     * @param processId 流程ID
     * @return
     */

    public List<HistoricActivityInstance> historyList(String processId);


/**
     * 获取流程模板
     * @return
     */

    public Set<String> getDeployProcess();


/**
     * 删除流程实例
     * @param processId 流程实例ID
     * @return
     */

    public void deleteProcess(String processId);


/**
     * 获取参数
     * @param processId
     * @param name
     */

    public Object getVariable(String processId,String name);


/**
     * 设置参数
     * @param processId
     * @param name
     * @param value
     * @return
     */

    public void setVariable(String processId,String name,Object value);

/**
     * 申领任务
     * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息,
     * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息,
     * 因为已经被这个用户领取了
     * @param taskId
     * @param user
     * @return
     */

    public void claim(String taskId,String user);


/**
     * 取消申领任务
     * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到,
     * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务
     * @param taskId
     * @return
     */

    public void unClaim(String taskId);

    public void withdrawProcessLast(String processId);

    public List<Map<String, Object>> getAllProcessInstances(String processDefinitionKey);
}

FlowServiceImpl.java

package com.fms.api.service.admin.flowable.impl;

import com.fms.api.common.exception.BizException;
import com.fms.api.common.exception.FmsErrorCode;
import com.fms.api.service.admin.flowable.FlowService;
import com.fms.api.web.admin.flowable.vo.TaskEntity;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricActivityInstanceQuery;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.identitylink.api.IdentityLink;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.*;

@Slf4j
@Service
public class FlowServiceImpl implements FlowService {

    @Resource
    private RepositoryService repositoryService;

    @Resource
    private RuntimeService runtimeService;

    @Resource
    private TaskService taskService;

    @Resource
    private ProcessEngine processEngine;

    @Resource
    private HistoryService historyService;

    @Resource
    private IdentityService identityService;

/**
     * 部署流程
     * @param processName 流程定义名
     * @param resourcePath 如flowable/process.bpmn
     * @return*/


    @Override
    public void createProcess(String processName, String resourcePath){
        Deployment deployment = repositoryService.createDeployment().name(processName).addClasspathResource(resourcePath).deploy();

    }

/**
     * 发起流程
     * @return*/


    @Override
    public String applyProcess(String processName,Map<String,Object>map){
        //指定发起人
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processName, map);
        log.info("流程实例ID:"+processInstance.getProcessInstanceId());
        return processInstance.getProcessInstanceId();
    }

/**
     * 查询某用户/群体/角色待办列表
     * @return*/


    @Override
    public List<Task> todoList(String user){
        List<Task> tasks = taskService.createTaskQuery().taskCandidateOrAssigned(user).orderByTaskCreateTime().desc().list();

        return tasks;
    }

    @Override
    public List<TaskEntity> taskNodeList(String processId){
        List<TaskEntity>resList = Lists.newArrayList();
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
        //流程走完的无法展示
        if (pi == null) {
            return Lists.newArrayList();
        }
        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        Iterator var12 = bpmnModel.getProcesses().iterator();
        Process process;
        Iterator var21;
        while(var12.hasNext()) {
            process = (Process)var12.next();
            var21 = process.findFlowElementsOfType(FlowNode.class).iterator();

            while(var21.hasNext()) {
                FlowNode flowNode = (FlowNode)var21.next();
                if(flowNode instanceof UserTask){
                    TaskEntity taskEntity = new TaskEntity();
                    taskEntity.setNodeId(flowNode.getId());
                    taskEntity.setAssignee(((UserTask) flowNode).getAssignee());
                    taskEntity.setName(flowNode.getName());
                    taskEntity.setCandidateUsers(((UserTask) flowNode).getCandidateUsers());
                    resList.add(taskEntity);
                }
                //Task task = taskService.createTaskQuery().taskDefinitionKey(flowNode.getId()).singleResult();
            }
        }
        return resList;
    }
/*
*
     * 生成流程图
     * @param processId 任务ID

*/

    @Override
    public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception {
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();

        //流程走完的不显示图
        if (pi == null) {
            return null;
        }

        //使用流程实例ID,查询正在执行的执行对象表,返回流程实例对象
        List<Execution> executions = runtimeService
                .createExecutionQuery()
                .processInstanceId(processId)
                .list();


        //得到正在执行的Activity的Id
        List<String> activityIds = new ArrayList<>();
        List<String> flows = new ArrayList<>();
        for (Execution exe : executions) {
            List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
            activityIds.addAll(ids);
        }

        //获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();


        //BMP	无压缩	极大	原始图像存储、专业图像处理
        //PNG	无损压缩	较大	透明背景、高质量网络图片
        //JPEG	有损压缩	小	照片、网页图片(牺牲部分画质)
        //GIF	无损压缩	较小	简单动画、低颜色数图像
        //SVG	矢量图形	极小	图标、可无限缩放的图形
        InputStream in = diagramGenerator.generateDiagram(bpmnModel,"BMP", activityIds,flows,"宋体","宋体","宋体",null,1.0);

        try (ByteArrayOutputStream finalOut = new ByteArrayOutputStream()) {
            // 读取生成的流程图图片
            BufferedImage originalImage = ImageIO.read(in);

            // 创建固定画布(例如 1200x900)
            int canvasWidth = 1200;
            int canvasHeight = 900;
            BufferedImage fixedImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = fixedImage.createGraphics();

            // 填充背景色(例如白色)
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, canvasWidth, canvasHeight);

            // 计算居中位置并绘制原图
            int x = (canvasWidth - originalImage.getWidth()) / 2;
            int y = (canvasHeight - originalImage.getHeight()) / 2;
            g.drawImage(originalImage, x, y, null);
            g.dispose();

            // 直接将图像写入内存流
            ImageIO.write(fixedImage, "png", finalOut);

            // 返回字节数组输出流(可根据需要转换为InputStream)
            return finalOut;  // 返回类型为ByteArrayOutputStream
        } finally {
            // 自动关闭资源(try-with-resources语法)
            if (in != null) {
                in.close();
            }
        }
    }

/**
     * 完成任务
     * @param taskId 任务id
     * @param user 用户/角色id
     * @param map 流程变量
     */

    @Override
    public void approveProcess(String taskId,String user,Map<String,Object> map){
        //先申领任务,相当于用户将这个流程任务占用,其他在这个用户组里的用户不能看到该流程任务
        taskService.claim(taskId,user);
        //再流转下一个节点
        taskService.complete(taskId, map);
    }


/**
     * 将节点移动到任意节点上
     * @param taskId 任务id
     * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好
     * @return*/


    @Override
    public void withdrawProcess(String taskId,String taskDefinitionKey){
        //获取当前任务,让其移动到目标节点位置
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        if(task == null) {
            log.info("工作流不存在", taskId);
            throw new BizException(FmsErrorCode.NO_TASK);
        }
        //将节点移动到目标节点
        runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId())
                .moveActivityIdTo(task.getTaskDefinitionKey(), taskDefinitionKey).changeState();
    }

/**
     * 获取流程的历史节点列表
     * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点"userTask"的信息
     * @param processId 流程ID
     * @return*/


    @Override
    public List<HistoricActivityInstance> historyList(String processId){
        // 查询历史活动实例
        HistoricActivityInstanceQuery historyQuery = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processId)
                .finished()
                .orderByHistoricActivityInstanceEndTime().desc();

        List<HistoricActivityInstance> activities = historyQuery.list();
        return activities;
    }

/**
     * 获取流程模板
     * @return*/


    @Override
    public Set<String> getDeployProcess(){
        List<Deployment>list = repositoryService.createDeploymentQuery().orderByDeploymenTime().desc().list();
        Map<String,String>map = Maps.newHashMap();
        for(Deployment deployment : list){
            if(!map.containsKey(deployment.getName())){
                map.put(deployment.getName(),"");
            }
        }
        return map.keySet();
    }

/**
     * 删除流程实例
     * @param processId 流程实例ID
     * @return*/


    @Override
    public void deleteProcess(String processId){
        runtimeService.deleteProcessInstance(processId, "");
    }

    @Override
    public Object getVariable(String processId,String name){
        return runtimeService.getVariable(processId,name);
    }

    @Override
    public void setVariable(String processId,String name,Object value){
        runtimeService.setVariable(processId,name,value);
    }

/**
     * 申领任务
     * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息,
     * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息,
     * 因为已经被这个用户领取了
     * @param taskId
     * @param user
     * @return*/


    @Override
    public void claim(String taskId,String user){
        taskService.claim(taskId,user);
    }

/**
     * 取消申领任务
     * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到,
     * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务
     * @param taskId
     * @return*/


    @Override
    public void unClaim(String taskId){

        taskService.unclaim(taskId);
    }

    @Override
    public void withdrawProcessLast(String taskId) {
        // 获取当前任务
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

        // 查询历史活动实例
        HistoricActivityInstanceQuery historyQuery = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(task.getProcessInstanceId())
                .finished()
                .orderByHistoricActivityInstanceEndTime().desc();

        List<HistoricActivityInstance> activities = historyQuery.list();
        String previousActivityId = activities.get(0).getActivityId(); // 获取最新的历史节点

        // 执行跳转
        runtimeService.createChangeActivityStateBuilder()
                .processInstanceId(task.getProcessInstanceId())
                .moveActivityIdTo(task.getTaskDefinitionKey(), previousActivityId)
                .changeState();
    }


    /**
     * 查询某个流程定义的所有流程实例(包括运行中和已结束的)
     * @param processDefinitionKey 流程定义Key(如"project_approval")
     * @return 流程实例列表
     */
    public List<Map<String, Object>> getAllProcessInstances(String processDefinitionKey) {
        // 查询历史流程实例(包含所有实例,无论是否结束)
        List<HistoricProcessInstance> historicInstances = historyService
                .createHistoricProcessInstanceQuery()
                .processDefinitionKey(processDefinitionKey)
                .orderByProcessInstanceStartTime().desc() // 按启动时间倒序
                .list();

        List<Map<String, Object>> result = new ArrayList<>();
        for (HistoricProcessInstance instance : historicInstances) {
            Map<String, Object> item = new HashMap<>();
            // 基础信息
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            item.put("processInstanceId", instance.getId());
            item.put("businessKey", instance.getBusinessKey());
            item.put("startTime", sdf.format(instance.getStartTime()));
            item.put("endTime", instance.getEndTime());

            // 获取流程变量中的业务状态(如"status")
            HistoricVariableInstance statusVar = historyService
                    .createHistoricVariableInstanceQuery()
                    .processInstanceId(instance.getId())
                    .variableName("status")
                    .singleResult();
            item.put("status", (statusVar != null) ? statusVar.getValue() : "未知");

            // 查询当前可操作人(仅当流程未结束时)
            List<String> operators = new ArrayList<>();
            if (instance.getEndTime() == null) {
                // 查询当前所有活动任务
                List<Task> activeTasks = taskService.createTaskQuery()
                        .processInstanceId(instance.getId())
                        .list();

                for (Task task : activeTasks) {
                    // 处理直接分配的用户
                    if (task.getAssignee() != null) {
                        operators.add("角色:" + task.getAssignee());
                    } else {
                        // 处理候选用户组和候选用户
                        List<IdentityLink> links = taskService.getIdentityLinksForTask(task.getId());
                        for (IdentityLink link : links) {
                            if (link.getGroupId() != null) {
                                operators.add("角色组:" + link.getGroupId());
                            }
                            if (link.getUserId() != null) {
                                operators.add("角色:" + link.getUserId());
                            }
                        }
                    }
                }
            }
            item.put("operators", operators);

            result.add(item);
        }
        return result;
    }

}

ISysFlowService.java

package com.fms.api.service.admin.flowable;


import com.fms.api.common.bean.PageBean;
import com.fms.api.dal.mysql.entity.FlowApplyDO;

import java.util.Map;

public interface ISysFlowService {

    /**
     * 发起流程
     * @param userId 发起人id
     * @param processDefinitionKey
     * @param map 参数
     */
    Integer apply(Long userId,String processDefinitionKey,Map<String,Object> map);

    /**
     * 获取我发起的所有流程
     * @param userId
     * @return
     */
    PageBean<FlowApplyDO> getFlowApplyDOList(Long userId, int pageNum, int pageSize);
}

ISysFlowServiceImpl.java:


package com.fms.api.service.admin.flowable.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fms.api.common.bean.PageBean;
import com.fms.api.service.admin.flowable.FlowService;
import com.fms.api.service.admin.flowable.ISysFlowService;
import com.fms.api.dal.mysql.entity.FlowApplyDO;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class ISysFlowServiceImpl implements ISysFlowService {

    @Resource
    private FlowService flowService;

    @Resource
    private com.fms.api.dal.mysql.mapper.generated.FlowApplyMapper FlowApplyMapper;

    @Override
    public Integer apply(Long userId, String processDefinitionKey, Map<String, Object> map) {
        String processId = flowService.applyProcess(processDefinitionKey,map);
        FlowApplyDO FlowApplyDO = new FlowApplyDO();
        FlowApplyDO.setUserId(userId);
        FlowApplyDO.setProcessId(processId);
        FlowApplyDO.setProcessDefinitionKey(processDefinitionKey);
        Integer id = FlowApplyMapper.insert(FlowApplyDO);
        return id;
    }


    @Override
    public PageBean<FlowApplyDO> getFlowApplyDOList(Long userId, int pageNum, int pageSize) {
        Page page = PageHelper.startPage(pageNum,pageSize);
        List<FlowApplyDO> list = FlowApplyMapper.selectList(new LambdaQueryWrapper<FlowApplyDO>().eq(FlowApplyDO::getUserId, userId));
        PageBean pageBean = new PageBean(list);
        BeanUtil.copyProperties(page,pageBean);
        return pageBean;
    }
}


4.controller和请求结构体
FlowableController.java:

package com.fms.api.web.admin.flowable;

import com.alibaba.fastjson.JSON;
import com.fms.api.common.bean.PageBean;
import com.fms.api.dal.mysql.entity.FlowApplyDO;
import com.fms.api.service.admin.flowable.FlowService;
import com.fms.api.service.admin.flowable.ISysFlowService;
import com.fms.api.service.admin.sys.dto.SysUserDTO;
import com.fms.api.util.CommUtil;
import com.fms.api.util.FileUtils;
import com.fms.api.util.PageUtils;
import com.fms.api.util.thread.NamedInheritableThreadLocalUtils;
import com.fms.api.web.admin.flowable.vo.FlowableReq;
import com.fms.api.web.admin.flowable.vo.TaskEntity;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.task.api.Task;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 工作流使用说明:
 * 0.在建模网站生成工作流xml,放到resources/processes下。网址为http://www.bpmnmodeler.com/flowable-modeler-online
 * 注:①不会画的可以先导入示例流程,然后修改后下载
 *    ②需要删除重复的标签xmlns:flowable="http://flowable.org/bpmn"
 *    ③修改已部署的工作流xml后(最好去网站上重新生成一下,否则流程图BPMNDiagram部分有可能长得不一样),需要项目重启后重新部署流程,否则无法生效
 * 1.部署流程createProcess:传入processName(文件名)、processDefinitionKey(流程id)。
 * 如示例流程.bpmn20.xml传入参数:{
 *   "processName": "示例流程",
 *   "processDefinitionKey": "example_flow"
 * }
 * 可通过获取流程模板getDeployProcess查看是否部署成功,对应bpmn文件中的startEvent步骤
 * 2.发起流程apply:传入processDefinitionKey
 * 3.获取发起的流程getFlowApplyList:获取processId
 * 4.查看获取流程图getPng(可选):传入processId
 * 5.查询流程节点taskNodeList(可选):传入processId
 * 6.获取待办列表flowList:传入processId,获取到list中的id为taskId。其中用户角色对应bpmn文件中的assignee标签
 * 7.审批通过approveProcess或者退回withdrawProcess,需要传入taskId和流程的审批结果如示例流程的checkResult
 * 如示例流程审批接口/project/example/approve的入参为{
 *   "taskId": "187503",
 *   "checkSuggestion": "yes",
 *   "checkResult":"1"
 * }
 * 8.查询某流程全部流程实例flowAllList:传入processDefinitionKey,可用于列表查询
 * 9.查询历史historyList(可选)
 *
 *
 */
@Slf4j
@RestController
@RequestMapping("/flowable")
public class FlowableController  {

    @Resource
    private FlowService flowService;

    @Resource
    private ISysFlowService iSysFlowService;

    //部署流程

    @PostMapping(value = "/createProcess")
    public Object createProcess(@RequestBody FlowableReq req) {
        log.info("流程部署{}", JSON.toJSONString(req));
        String path = "processes/"+req.getProcessName()+".bpmn20.xml";
        flowService.createProcess(req.getProcessDefinitionKey(),path);
        log.info("流程部署成功{}", JSON.toJSONString(req));
        return null;
    }

    //发起流程
    @PostMapping(value = "/apply")
    public Object apply(@RequestBody FlowableReq req) {
        log.info("流程发起{}", JSON.toJSONString(req));
        Map<String,Object> map = new HashMap<>();
        //对应bpmn中的startEvent
        //这里的userId应该从登录信息中获取
        iSysFlowService.apply("userId",req.getProcessDefinitionKey(),map);
        Map<String,Object> resultMap = new HashMap<>();
        return resultMap;

    }

    //获取我发起的所有流程
    @PostMapping(value = "/getFlowApplyList")
    public Object getFlowApplyList(@RequestBody FlowableReq req){
        //这里的userId应该从登录信息中获取
        PageBean<FlowApplyDO> pageBean = iSysFlowService.getFlowApplyDOList("userId",
                req.getPageNum(),req.getPageSize());
        return pageBean;
    }



    //生成流程图,标红的框是当前流程走到的地方
    @PostMapping(value = "/getPng")
    public ByteArrayOutputStream getPng(@RequestBody FlowableReq req, HttpServletResponse response) throws Exception {
        log.info("生成流程图{}", JSON.toJSONString(req));
        ByteArrayOutputStream out = flowService.genProcessDiagram(req.getProcessId());
        FileUtils.outPutImage(out,response);
        log.info("生成流程图成功{}", JSON.toJSONString(req));
        return out;
    }

    //查询待办流程列表

    @PostMapping(value = "/flowList")
    public Object flowList(@RequestBody FlowableReq req) {
        log.info("查询待办流程{}", JSON.toJSONString(req));
        //这里的userId应该从登录信息中获取
        List<Task> list = flowService.todoList("userId");
        List<Map<String, Object>> listMap = new ArrayList<>();
        String[] ps = {"id","name","processInstanceId"};
        list.forEach(task -> {
            //解决懒加载的对象Task无法在controller中返回的问题
            listMap.add(CommUtil.obj2map(task,ps));
        });
        PageInfo pageBean = PageUtils.listToPage(req.getPageNum(),req.getPageSize(), listMap.size(), listMap);
        log.info("查询待办流程成功{}", list.toString());
        return pageBean;
    }

    //流程处理,进行审批通过或退回操作
    @PostMapping(value = "/approveProcess")
    public Object approveProcess(@RequestBody FlowableReq req){
        
        log.info("流程处理{}", JSON.toJSONString(req));
        Map<String,Object> map = (Map<String,Object>)req.getData();
        //这里的userId应该从登录信息中获取
        flowService.approveProcess(req.getTaskId(),"userId",map);
        log.info("流程处理成功{}", JSON.toJSONString(req));
        return null;
    }


    //流程退回某一结点
    @PostMapping(value = "/withdrawProcess")
    public Object withdrawProcess(@RequestBody FlowableReq req)  {
        log.info("流程退回{}", JSON.toJSONString(req));
        flowService.withdrawProcess(req.getTaskId(),req.getNodeId());
        log.info("流程退回成功{}", JSON.toJSONString(req));
        return null;
    }

    //流程退回上一结点
    @PostMapping(value = "/withdrawProcessLast")
    public Object withdrawProcessLast(@RequestBody FlowableReq req)  {
        log.info("流程退回{}", JSON.toJSONString(req));
        flowService.withdrawProcessLast(req.getTaskId());
        log.info("流程退回成功{}", JSON.toJSONString(req));
        return null;
    }


    //查询历史
    @PostMapping(value = "/historyList")
    public Object historyList(@RequestBody FlowableReq req){
        log.info("查询历史信息{}", JSON.toJSONString(req));
        List<HistoricActivityInstance> list = flowService.historyList(req.getProcessId());
        PageInfo pageBean = PageUtils.listToPage(req.getPageNum(),req.getPageSize(), list.size(), list);
        log.info("查询历史信息成功{}", list.toString());
        return pageBean;
    }

    //获取流程模板
    @PostMapping(value = "/getDeployProcess")
    public Object getDeployProcess(){
        return flowService.getDeployProcess();
    }

    //查询所有任务节点
    @PostMapping(value = "/taskNodeList")
    public Object taskNodeList(@RequestBody FlowableReq req){
        log.info("查询所有任务节点processId:{}", req.getProcessId());
        List<TaskEntity> list = flowService.taskNodeList(req.getProcessId());
        log.info("查询所有任务节点成功{}", list.toString());
        return list;
    }


}

ProjectController.java:

package com.fms.api.web.admin.flowable;

import com.alibaba.fastjson.JSON;
import com.fms.api.dal.mysql.entity.SysRoleDO;
import com.fms.api.dal.mysql.mapper.custom.SysRoleCusMapper;
import com.fms.api.service.admin.flowable.FlowService;
import com.fms.api.service.admin.flowable.ISysFlowService;
import com.fms.api.service.admin.sys.dto.SysUserDTO;
import com.fms.api.util.CommUtil;
import com.fms.api.util.PageUtils;
import com.fms.api.util.thread.NamedInheritableThreadLocalUtils;
import com.fms.api.web.admin.flowable.vo.FlowableReq;
import com.fms.api.web.admin.flowable.vo.StartInitiationProcessReq;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.flowable.task.api.Task;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/project")
public class ProjectController {

    @Resource
    private FlowService flowService;

    @Resource
    private ISysFlowService iSysFlowService;
    @Resource
    private SysRoleCusMapper sysRoleCusMapper;



    // 启动示例流程
    @PostMapping("/example/start")
    public Object startInitiationProcess(@RequestBody StartInitiationProcessReq req) {
        //将请求对象转化为map<String, Object>,map的key为流程变量的名称,value为流程变量的值
        Map<String, Object> variables = JSON.parseObject(JSON.toJSONString(req), Map.class);
        //这里的userId应该从登录信息中获取
        Integer id = iSysFlowService.apply("userId","example_flow",variables);
        return id;
    }

    //查询待办流程列表
    @PostMapping(value = "/example/flowList")
    public Object flowList(@RequestBody FlowableReq req) {
        log.info("查询待办流程{}", JSON.toJSONString(req));
        //通过角色获取待办列表--flowable:assignee对应用户的角色,下面的roleList查询正常应该从数据库查询
        List<SysRoleDO> roleList = new ArrayList();
        List<Task> list = new ArrayList<>();
        for(SysRoleDO sysRoleDO : roleList){
            List<Task> list0 = flowService.todoList(sysRoleDO.getRoleName());
            list.addAll(list0);
        }
        List<Map<String, Object>> listMap = new ArrayList<>();
        String[] ps = {"id","name","processInstanceId"};
        list.forEach(task -> {
            //解决懒加载的对象Task无法在controller中返回的问题
            listMap.add(CommUtil.obj2map(task,ps));
        });
        PageInfo pageBean = PageUtils.listToPage(req.getPageNum(),req.getPageSize(), listMap.size(), listMap);
        log.info("查询待办流程成功{}", list.toString());
        return pageBean;
    }

    //查询该流程全部列表
    @PostMapping(value = "/example/flowAllList")
    public Object flowAllList(@RequestBody FlowableReq req) {
        log.info("查询该流程全部列表{}", JSON.toJSONString(req));
        //flowService.getAllProcessInstances(req.getProcessDefinitionKey());
        List<Map<String, Object>> list = flowService.getAllProcessInstances("example_flow");
        //todo:分页,并将结果封装为RESVO
        log.info("查询该流程全部列表成功{}", list.toString());
        return list;
    }



    // 项目立项流程审批
    @PostMapping("/example/approve")
    public Object completeTask(@RequestBody StartInitiationProcessReq req) {
        Map<String, Object> variables = JSON.parseObject(JSON.toJSONString(req), Map.class);
        //通过角色获取待办列表--flowable:assignee对应用户的角色,下面的roleList查询正常应该从数据库查询
        List<SysRoleDO> roleList = new ArrayList();
        List<Task> list = new ArrayList<>();
        for(SysRoleDO sysRoleDO : roleList) {
            //通过
            if("1".equals(req.getCheckResult())){
                flowService.approveProcess(req.getTaskId(), sysRoleDO.getRoleName(), variables);
            }else{
                //退回
                flowService.withdrawProcessLast(req.getTaskId());
            }
        }
        return null;
    }



}

FlowableReq.java:

package com.fms.api.web.admin.flowable.vo;

import lombok.Data;

@Data
public class FlowableReq {
    /*模型key*/
    private String processDefinitionKey;

    /*模型名称*/
    private String processName;

    /*流程id*/
    private String processId;

    /*用户编号*/
    private String userId;

    /*任务id*/
    private String taskId;

    /*流程节点id*/
    private String nodeId;

    /*参数传递*/
    private Object data;

    /*当前页数*/
    private int pageNum;

    /*每页数据量*/
    private int pageSize;
}

StartInitiationProcessReq.java:

package com.fms.api.web.admin.flowable.vo;

import lombok.Data;

@Data
public class StartInitiationProcessReq extends FlowableReq{
    private String projectName;
    private String projectDesc;
    private String budget;


    //审批意见
    private String checkSuggestion;
    //审批结果 0:退回 1:通过
    private String checkResult;
}

TaskEntity.java:

package com.fms.api.web.admin.flowable.vo;

import lombok.Data;

import java.util.List;

@Data
public class TaskEntity {
    private String nodeId;
    private String assignee;//处理人
    private String name;
    private List<String> candidateUsers;//候选人
}

BaseEntity.java:

package com.fms.api.mp;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//


import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.Version;
import java.io.Serializable;
import java.time.LocalDateTime;

public class BaseEntity implements Serializable {
    private static final long serialVersionUID = 4629294428070857674L;
    @TableId(
            value = "id",
            type = IdType.ASSIGN_ID
    )
    private Long id;
    @TableField(
            value = "version",
            fill = FieldFill.INSERT_UPDATE
    )
    @Version
    private Integer version;
    @TableLogic(
            value = "1",
            delval = "id"
    )
    @TableField(
            fill = FieldFill.INSERT
    )
    private Long enabled;
    @TableField(
            fill = FieldFill.INSERT
    )
    private Long createBy;
    @TableField(
            fill = FieldFill.INSERT
    )
    private LocalDateTime createTime;
    @TableField(
            fill = FieldFill.INSERT_UPDATE
    )
    private Long updateBy;
    @TableField(
            fill = FieldFill.INSERT_UPDATE
    )
    private LocalDateTime updateTime;

    public BaseEntity() {
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Integer getVersion() {
        return this.version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public Long getEnabled() {
        return this.enabled;
    }

    public void setEnabled(Long enabled) {
        this.enabled = enabled;
    }

    public Long getCreateBy() {
        return this.createBy;
    }

    public void setCreateBy(Long createBy) {
        this.createBy = createBy;
    }

    public LocalDateTime getCreateTime() {
        return this.createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public Long getUpdateBy() {
        return this.updateBy;
    }

    public void setUpdateBy(Long updateBy) {
        this.updateBy = updateBy;
    }

    public LocalDateTime getUpdateTime() {
        return this.updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
}

SysRoleDO.java:

package com.fms.api.dal.mysql.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fms.api.mp.BaseEntity;

/**
 * <p>
 * 角色信息表
 * </p>
 *
 * @author phzx
 * @since 2025-04-29
 */
@TableName("t_sys_role")
public class SysRoleDO extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 角色名称
     */
    @TableField("role_name")
    private String roleName;

    /**
     * 角色权限字符串
     */
    @TableField("role_key")
    private String roleKey;

    /**
     * 显示顺序
     */
    @TableField("role_sort")
    private Integer roleSort;

    /**
     * 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)
     */
    @TableField("data_scope")
    private String dataScope;

    /**
     * 角色状态(0正常 1停用)
     */
    @TableField("status")
    private String status;

    /**
     * 备注
     */
    @TableField("remark")
    private String remark;

    /**
     * 角色描述
     */
    @TableField("role_description")
    private String roleDescription;

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }
    public String getRoleKey() {
        return roleKey;
    }

    public void setRoleKey(String roleKey) {
        this.roleKey = roleKey;
    }
    public Integer getRoleSort() {
        return roleSort;
    }

    public void setRoleSort(Integer roleSort) {
        this.roleSort = roleSort;
    }
    public String getDataScope() {
        return dataScope;
    }

    public void setDataScope(String dataScope) {
        this.dataScope = dataScope;
    }
    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
    public String getRoleDescription() {
        return roleDescription;
    }

    public void setRoleDescription(String roleDescription) {
        this.roleDescription = roleDescription;
    }

    @Override
    public String toString() {
        return "SysRoleDO{" +
            "roleName=" + roleName +
            ", roleKey=" + roleKey +
            ", roleSort=" + roleSort +
            ", dataScope=" + dataScope +
            ", status=" + status +
            ", remark=" + remark +
            ", roleDescription=" + roleDescription +
        "}";
    }
}

FlowApplyDO.java:

package com.fms.api.dal.mysql.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fms.api.mp.BaseEntity;

/**
 * <p>
 * 流程表
 * </p>
 *
 * @author phzx
 * @since 2025-04-29
 */
@TableName("t_flow_apply")
public class FlowApplyDO extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 流程实例id
     */
    @TableField("process_id")
    private String processId;

    /**
     * 流程的模板key
     */
    @TableField("process_definition_key")
    private String processDefinitionKey;

    /**
     * 发起人id
     */
    @TableField("user_id")
    private Long userId;

    public String getProcessId() {
        return processId;
    }

    public void setProcessId(String processId) {
        this.processId = processId;
    }
    public String getProcessDefinitionKey() {
        return processDefinitionKey;
    }

    public void setProcessDefinitionKey(String processDefinitionKey) {
        this.processDefinitionKey = processDefinitionKey;
    }
    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "FlowApplyDO{" +
            "processId=" + processId +
            ", processDefinitionKey=" + processDefinitionKey +
            ", userId=" + userId +
        "}";
    }
}

5.添加工作流bpmn文件

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI"  targetNamespace="http://www.flowable.org/processdef">
  <process id="example_flow" name="示例流程" isExecutable="true">
    <startEvent id="start" name="示例流程开始">
      <extensionElements>
        <!-- 流程启动时设置初始状态 -->
        <flowable:executionListener
                event="start"
                expression="${execution.setVariable('status', '未立项')}"/>
      </extensionElements>
    </startEvent>
    <userTask id="pm_submit" name="项目经理提交" flowable:assignee="项目经理">
      <extensionElements>
        <!-- 任务结束时更新状态 -->
        <flowable:executionListener
                event="end"
                expression="${execution.setVariable('status', '立项中')}"/>
        <flowable:formProperty id="submitOpinion" name="提交意见" type="string" required="true" />
      </extensionElements>
    </userTask>
    <userTask id="compliance_approval" name="合规风控审批" flowable:assignee="合规风控">
      <extensionElements>
        <!-- 节点结束时更新状态 -->
        <flowable:executionListener
                event="end"
                expression="${execution.setVariable('status', '已立项')}"/>
        <flowable:formProperty id="checkSuggestion" name="审批意见" type="String"></flowable:formProperty>
        <flowable:formProperty id="checkResult" name="审批结果" type="enum">
          <flowable:value id="1" name="通过" />
          <flowable:value id="0" name="拒绝" />
        </flowable:formProperty>
      </extensionElements>
    </userTask>
    <userTask id="chairman_approval" name="董事长审批" flowable:assignee="总经理">
      <extensionElements>
        <flowable:executionListener
                event="end"
                expression="${execution.setVariable('status', '签署中')}"/>
        <flowable:formProperty id="checkSuggestion" name="审批意见" type="String"></flowable:formProperty>
        <flowable:formProperty id="checkResult" name="审批结果" type="enum">
          <flowable:value id="1" name="通过" />
          <flowable:value id="0" name="拒绝" />
        </flowable:formProperty>
      </extensionElements>
    </userTask>
    <endEvent id="end" name="示例流程结束" >
      <extensionElements>
        <flowable:executionListener
                event="end"
                expression="${execution.setVariable('status', '已签署')}"/>
      </extensionElements>
    </endEvent>

    <!--流程连线-->
    <sequenceFlow id="flow1" sourceRef="start" targetRef="pm_submit" />
    <sequenceFlow id="flow2" sourceRef="pm_submit" targetRef="compliance_approval" />
    <sequenceFlow id="flow3" sourceRef="compliance_approval" targetRef="chairman_approval">
      <conditionExpression xsi:type="tFormalExpression">${checkResult == '1'}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow4" sourceRef="chairman_approval" targetRef="end">
      <conditionExpression xsi:type="tFormalExpression">${checkResult == '1'}</conditionExpression>
    </sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="Diagram_project_initiation">
    <bpmndi:BPMNPlane id="Plane_project_initiation" bpmnElement="example_flow">
      <bpmndi:BPMNShape id="Shape_start" bpmnElement="start">
        <dc:Bounds x="332" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="319" y="142" width="66" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Shape_pm_submit" bpmnElement="pm_submit">
        <dc:Bounds x="470" y="80" width="100" height="80" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="470" y="165" width="100" height="20" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Shape_compliance_approval" bpmnElement="compliance_approval">
        <dc:Bounds x="670" y="80" width="100" height="80" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="670" y="165" width="100" height="20" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Shape_chairman_approval" bpmnElement="chairman_approval">
        <dc:Bounds x="880" y="80" width="100" height="80" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="880" y="165" width="100" height="20" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Shape_end" bpmnElement="end">
        <dc:Bounds x="1072" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="1059" y="142" width="66" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Edge_flow1" bpmnElement="flow1">
        <di:waypoint x="368" y="120" />
        <di:waypoint x="470" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Edge_flow2" bpmnElement="flow2">
        <di:waypoint x="570" y="120" />
        <di:waypoint x="670" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Edge_flow3" bpmnElement="flow3">
        <di:waypoint x="770" y="120" />
        <di:waypoint x="880" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Edge_flow4" bpmnElement="flow4">
        <di:waypoint x="980" y="120" />
        <di:waypoint x="1072" y="120" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

6.api调用顺序教程
(1)在建模网站生成工作流xml,放到resources/processes下。网址为http://www.bpmnmodeler.com/flowable-modeler-online
注:①不会画的可以先导入示例流程,然后修改后下载
②需要删除重复的标签xmlns:flowable=“http://flowable.org/bpmn”
③修改已部署的工作流xml后(最好去网站上重新生成一下,否则流程图BPMNDiagram部分有可能长得不一样),需要项目重启后重新部署流程,否则无法生效
(2)部署流程createProcess:传入processName(文件名)、processDefinitionKey(流程id)。
如示例流程.bpmn20.xml传入参数:{
“processName”: “示例流程”,
“processDefinitionKey”: “example_flow”
}
可通过获取流程模板getDeployProcess查看是否部署成功,对应bpmn文件中的startEvent步骤
(3)发起流程apply:传入processDefinitionKey
(4)获取发起的流程getFlowApplyList:获取processId
(5)查看获取流程图getPng(可选):传入processId
(6)查询流程节点taskNodeList(可选):传入processId
(7)获取待办列表flowList:传入processId,获取到list中的id为taskId。其中用户角色对应bpmn文件中的assignee标签
(8)审批通过approveProcess或者退回withdrawProcess,需要传入taskId和流程的审批结果如示例流程的checkResult
如示例流程审批接口/project/example/approve的入参为{
“taskId”: “187503”,
“checkSuggestion”: “yes”,
“checkResult”:“1”
}
(9)查询某流程全部流程实例flowAllList:传入processDefinitionKey,可用于列表查询
(10)查询历史historyList(可选)

四、流程的创建和使用

1.BPMN基本概念介绍

可以去BPMN官网学习相关知识 https://www.bpmn.org/
(1)流对象(Flow Objects):是定义业务流程的主要图形元素,包括三种:事件、活动、网关

事件(Events):指的是在业务流程的运行过程中发生的事情,分为:
开始:表示一个流程的开始
中间:发生的开始和结束事件之间,影响处理的流程
结束:表示该过程结束

活动(Activities):包括任务和子流程两类。子流程在图形的下方中间外加一个小加号(+)来区分。

网关(Gateways):用于表示流程的分支与合并。

排他网关:只有一条路径会被选择
并行网关:所有路径会被同时选择
包容网关:可以同时执行多条线路,也可以在网关上设置条件
事件网关:专门为中间捕获事件设置的,允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程处于等待状态,需要等待抛出事件才能将等待状态转换为活动状态。

(2)数据(Data):数据主要通过四种元素表示

数据对象(Data Objects)
数据输入(Data Inputs)
数据输出(Data Outputs)
数据存储(Data Stores)

(3)连接对象(Connecting Objects):流对象彼此互相连接或者连接到其他信息的方法主要有三种

顺序流:用一个带实心箭头的实心线表示,用于指定活动执行的顺序

信息流:用一条带箭头的虚线表示,用于描述两个独立的业务参与者(业务实体/业务角色)之间发送和接受的消息流动

关联:用一根带有线箭头的点线表示,用于将相关的数据、文本和其他人工信息与流对象联系起来。用于展示活动的输入和输出

(4)泳道(Swimlanes):通过泳道对主要的建模元素进行分组,将活动划分到不同的可视化类别中来描述由不同的参与者的责任与职责。

2.业务模型流程创建

我这里自己创建了一个流程,如果自己嫌麻烦可以直接使用我的(右上角导入)BPMN的XML文件即可,但是form不会生效

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
  <process id="a999" name="报销审批流程" isExecutable="true">
    <documentation>报销审批</documentation>
    <startEvent id="startEvent1"></startEvent>
    <userTask id="sid-5946EBF9-CCA9-41D5-A1B9-812886784183" name="用户申请 " flowable:candidateUsers="userid1,userid2" flowable:formKey="form999"></userTask>
    <sequenceFlow id="sid-62D0C9DD-539D-45C2-A46F-8F382C931ED6" sourceRef="startEvent1" targetRef="sid-5946EBF9-CCA9-41D5-A1B9-812886784183"></sequenceFlow>
    <userTask id="sid-8353A778-A852-48DC-A39F-EDB79EE618CF" name="部门领导审核" flowable:candidateUsers="leader1,leader2"></userTask>
    <userTask id="sid-197A3224-407C-44E4-959E-2EF7C098AD1D" name="人事部门审核"></userTask>
    <endEvent id="sid-D3E07881-6D59-44CC-9E0D-D8D4CC34868E"></endEvent>
    <exclusiveGateway id="sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C"></exclusiveGateway>
    <exclusiveGateway id="sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83"></exclusiveGateway>
    <sequenceFlow id="sid-81BBF9CE-1366-4DAF-AA94-EB8C6C200BF1" sourceRef="sid-197A3224-407C-44E4-959E-2EF7C098AD1D" targetRef="sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83"></sequenceFlow>
    <sequenceFlow id="sid-E4554206-B35E-404B-98DA-05E47430E5EF" sourceRef="sid-5946EBF9-CCA9-41D5-A1B9-812886784183" targetRef="sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C"></sequenceFlow>
    <exclusiveGateway id="sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B"></exclusiveGateway>
    <sequenceFlow id="sid-CD4D2389-39C4-4C8D-AFCD-CBFA528B3D0C" sourceRef="sid-8353A778-A852-48DC-A39F-EDB79EE618CF" targetRef="sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B"></sequenceFlow>
    <sequenceFlow id="sid-F18F027C-B121-4672-BB0E-B93D39F0F09E" name="通过" sourceRef="sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C" targetRef="sid-8353A778-A852-48DC-A39F-EDB79EE618CF">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '1'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-A18D3D02-1C7D-4B3D-A57F-D3CB1B29E6E8" name="退回" sourceRef="sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C" targetRef="sid-5946EBF9-CCA9-41D5-A1B9-812886784183">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '0'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-BF3D19F8-EFC7-47D0-BB96-87A26734B2B4" name="通过" sourceRef="sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B" targetRef="sid-197A3224-407C-44E4-959E-2EF7C098AD1D">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '1'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-BA470DA5-7754-40AC-9823-0A4E901BF2B1" name="退回" sourceRef="sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B" targetRef="sid-8353A778-A852-48DC-A39F-EDB79EE618CF">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '0'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-8406E503-FF81-47A9-9B08-73BB6CB37926" name="通过" sourceRef="sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83" targetRef="sid-D3E07881-6D59-44CC-9E0D-D8D4CC34868E">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '1'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="sid-9C84DFB1-9ECA-4202-ADEE-7176A12050BC" name="退回初始状态  " sourceRef="sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83" targetRef="sid-5946EBF9-CCA9-41D5-A1B9-812886784183">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approval == '0'}]]></conditionExpression>
    </sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_a999">
    <bpmndi:BPMNPlane bpmnElement="a999" id="BPMNPlane_a999">
      <bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
        <omgdc:Bounds height="30.0" width="30.0" x="100.0" y="163.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-5946EBF9-CCA9-41D5-A1B9-812886784183" id="BPMNShape_sid-5946EBF9-CCA9-41D5-A1B9-812886784183">
        <omgdc:Bounds height="80.0" width="100.0" x="165.0" y="139.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-8353A778-A852-48DC-A39F-EDB79EE618CF" id="BPMNShape_sid-8353A778-A852-48DC-A39F-EDB79EE618CF">
        <omgdc:Bounds height="80.0" width="100.0" x="435.0" y="139.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-197A3224-407C-44E4-959E-2EF7C098AD1D" id="BPMNShape_sid-197A3224-407C-44E4-959E-2EF7C098AD1D">
        <omgdc:Bounds height="80.0" width="100.0" x="705.0" y="139.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-D3E07881-6D59-44CC-9E0D-D8D4CC34868E" id="BPMNShape_sid-D3E07881-6D59-44CC-9E0D-D8D4CC34868E">
        <omgdc:Bounds height="28.0" width="28.0" x="1050.0" y="165.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C" id="BPMNShape_sid-15A21F4D-DBEC-4D57-B62D-F34B9388209C">
        <omgdc:Bounds height="40.0" width="40.0" x="330.0" y="158.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83" id="BPMNShape_sid-DE78AEF5-DFA7-44CA-9938-6DFB6A9FFF83">
        <omgdc:Bounds height="40.0" width="40.0" x="900.0" y="159.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B" id="BPMNShape_sid-3CC1F0CA-3862-4840-BD63-F3F34068BE7B">
        <omgdc:Bounds height="40.0" width="40.0" x="585.0" y="159.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="sid-E4554206-B35E-404B-98DA-05E47430E5EF" id="BPMNEdge_sid-E4554206-B35E-404B-98DA-05E47430E5EF">
        <omgdi:waypoint x="264.94999999999675" y="178.62962962962962"></omgdi:waypoint>
        <omgdi:waypoint x="330.14705882352825" y="178.1466911764706"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-A18D3D02-1C7D-4B3D-A57F-D3CB1B29E6E8" id="BPMNEdge_sid-A18D3D02-1C7D-4B3D-A57F-D3CB1B29E6E8">
        <omgdi:waypoint x="350.0" y="197.93754681647943"></omgdi:waypoint>
        <omgdi:waypoint x="350.0" y="258.0"></omgdi:waypoint>
        <omgdi:waypoint x="215.0" y="258.0"></omgdi:waypoint>
        <omgdi:waypoint x="215.0" y="218.95000000000002"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-9C84DFB1-9ECA-4202-ADEE-7176A12050BC" id="BPMNEdge_sid-9C84DFB1-9ECA-4202-ADEE-7176A12050BC">
        <omgdi:waypoint x="920.0" y="159.0"></omgdi:waypoint>
        <omgdi:waypoint x="920.0" y="56.0"></omgdi:waypoint>
        <omgdi:waypoint x="215.0" y="56.0"></omgdi:waypoint>
        <omgdi:waypoint x="215.0" y="139.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-62D0C9DD-539D-45C2-A46F-8F382C931ED6" id="BPMNEdge_sid-62D0C9DD-539D-45C2-A46F-8F382C931ED6">
        <omgdi:waypoint x="129.94919380537883" y="178.14949271315584"></omgdi:waypoint>
        <omgdi:waypoint x="164.99999999999716" y="178.49999999999997"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-81BBF9CE-1366-4DAF-AA94-EB8C6C200BF1" id="BPMNEdge_sid-81BBF9CE-1366-4DAF-AA94-EB8C6C200BF1">
        <omgdi:waypoint x="804.9499999999836" y="179.0"></omgdi:waypoint>
        <omgdi:waypoint x="900.0" y="179.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-BA470DA5-7754-40AC-9823-0A4E901BF2B1" id="BPMNEdge_sid-BA470DA5-7754-40AC-9823-0A4E901BF2B1">
        <omgdi:waypoint x="605.0" y="198.93754681647943"></omgdi:waypoint>
        <omgdi:waypoint x="605.0" y="259.0"></omgdi:waypoint>
        <omgdi:waypoint x="485.0" y="259.0"></omgdi:waypoint>
        <omgdi:waypoint x="485.0" y="218.95000000000002"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-8406E503-FF81-47A9-9B08-73BB6CB37926" id="BPMNEdge_sid-8406E503-FF81-47A9-9B08-73BB6CB37926">
        <omgdi:waypoint x="939.9430777238028" y="179.0"></omgdi:waypoint>
        <omgdi:waypoint x="1050.0" y="179.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-CD4D2389-39C4-4C8D-AFCD-CBFA528B3D0C" id="BPMNEdge_sid-CD4D2389-39C4-4C8D-AFCD-CBFA528B3D0C">
        <omgdi:waypoint x="534.95" y="179.0"></omgdi:waypoint>
        <omgdi:waypoint x="585.0" y="179.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-F18F027C-B121-4672-BB0E-B93D39F0F09E" id="BPMNEdge_sid-F18F027C-B121-4672-BB0E-B93D39F0F09E">
        <omgdi:waypoint x="369.79608743570697" y="178.14669117647063"></omgdi:waypoint>
        <omgdi:waypoint x="435.0" y="178.62962962962965"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-BF3D19F8-EFC7-47D0-BB96-87A26734B2B4" id="BPMNEdge_sid-BF3D19F8-EFC7-47D0-BB96-87A26734B2B4">
        <omgdi:waypoint x="624.943354430356" y="179.0"></omgdi:waypoint>
        <omgdi:waypoint x="705.0" y="179.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

在这里插入图片描述
折线绘制需要用到这个:在这里插入图片描述
每一个节点都需要分配用户
在这里插入图片描述
我这里id直接写死了,这个在代码里可以自己指派
在这里插入图片描述

分配完去左上角验证,没有什么警告之类的就可以
在这里插入图片描述

3.表单创建及使用

这个功能可能使用的比较少,一般在前端系统开发自己的表单
进入http://localhost:8080/#/forms

在这里插入图片描述
在流程中使用,只需要填表单key即可,记得分配用户
在这里插入图片描述

4.流程的使用

流程和表单等保存在act_de_model,可以去数据库里查看
(1)代码实现
在这里插入图片描述
在recource下的新建process放入我们刚刚画好的流程图
在这里插入图片描述

FlowableController .java

package com.example.flowable.controller;

import com.example.flowable.service.FlowService;
import com.google.common.collect.Maps;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.task.api.Task;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/flowable")
public class FlowableController {

    @Resource
    private FlowService flowService;


    //部署流程
    @RequestMapping(value = "/createProcess/{processDefinitionKey}", method = {RequestMethod.GET})
    public String createProcess(@PathVariable("processDefinitionKey") String processDefinitionKey) {
        //已知processDefinitionKey,可以通过act_de_model获取processName,我这里偷懒写死了
        String processName = "报销审批流程";
        String path = "process/"+processName+".bpmn20.xml";
        flowService.createProcess(processDefinitionKey,path);
        return "流程部署成功";
    }

    //发起流程
    @RequestMapping(value = "/apply/{processDefinitionKey}", method = {RequestMethod.GET})
    public String apply(@PathVariable("processDefinitionKey") String processDefinitionKey) throws Exception {
        Map<String,Object> map = new HashMap<>();
        String processId = flowService.applyProcess(processDefinitionKey,map);
        System.out.println(processId);//17501
        return "流程发起成功,流程id为"+processId;

    }

    //生成流程图,标红的框是当前流程走到的地方
    @RequestMapping(value = "/getPng/{processId}", method = {RequestMethod.GET})
    public String getPng(@PathVariable("processId") String processId) throws Exception {
        ByteArrayOutputStream out = flowService.genProcessDiagram(processId);
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\process" + processId + ".png");
        fileOutputStream.write(out.toByteArray());
        return "流程图生成成功";
    }

    //查询待办流程列表
    @RequestMapping(value = "/flowList/{userId}", method = {RequestMethod.GET})
    public Object flowList(@PathVariable("userId") String userId) {
        List<Task> list = flowService.todoList(userId);
        System.out.println(list.toString());
        return list.toString();
    }

    //流程审批通过或退回
    @RequestMapping(value = "/approveProcess/{userId}/{taskId}", method = {RequestMethod.GET})
    public void approveProcess(@PathVariable("taskId") String taskId,@PathVariable("userId") String userId){
        Map<String,Object> map = Maps.newHashMap();
        //这个map可以放在表单里传过来
        map.put("approval","1");
        //map.put("approval","0");
        flowService.approveProcess(taskId,userId,map);
    }


    //流程退回某一结点
    @RequestMapping(value = "/withdrawProcess/{taskId}/{nodeId}", method = {RequestMethod.GET})
    public void withdrawProcess(@PathVariable("taskId") String taskId,@PathVariable("nodeId") String nodeId) throws Exception {
        flowService.withdrawProcess(taskId,nodeId);
    }


    //查询历史
    @RequestMapping(value = "/historyList/{processId}", method = {RequestMethod.GET})
    public Object historyList(@PathVariable("processId") String processId){
        List<HistoricActivityInstance>list = flowService.historyList(processId);
        return list.toString();
    }



}

FlowService .java

package com.example.flowable.service;

import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.task.api.Task;

import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;

public interface FlowService {
    /**
     * 部署流程
     * @param processName 流程定义名
     * @param resourcePath 如flowable/process.bpmn
     * @return
     */
    public void createProcess(String processName, String resourcePath);

    /**
     * 发起流程
     * @param processName 流程定义名
     * @param map 参数
     * @return 流程实例ID
     */
    public String applyProcess(String processName,Map<String,Object> map);

    /**
     * 查询某用户/群体/角色待办列表
     * @return
     */
    public List<Task> todoList(String user);

    /**
     * 生成流程图
     * @param processId 任务ID
     */
    public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception;

    /**
     * 完成任务
     * @param taskId 任务id
     * @param user 用户/角色id
     * @param map 流程变量
     */
    public void approveProcess(String taskId,String user,Map<String,Object> map);

    /**
     * 将节点移动到任意节点上
     * @param taskId 任务id
     * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好
     * @return
     */
    public void withdrawProcess(String taskId,String taskDefinitionKey) throws Exception;

    /**
     * 获取流程的历史节点列表
     * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点"userTask"的信息
     * @param processId 流程ID
     * @return
     */
    public List<HistoricActivityInstance> historyList(String processId);

    /**
     * 删除流程实例
     * @param processId 流程实例ID
     * @return
     */
    public void deleteProcess(String processId);

    /**
     * 申领任务
     * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息,
     * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息,
     * 因为已经被这个用户领取了
     * @param taskId
     * @param user
     * @return
     */
    public void claim(String taskId,String user);

    /**
     * 取消申领任务
     * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到,
     * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务
     * @param taskId
     * @return
     */
    public void unClaim(String taskId);
}

FlowServiceImpl .java

package com.example.flowable.service.impl;

import com.example.flowable.service.FlowService;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class FlowServiceImpl implements FlowService {

    @Resource
    private RepositoryService repositoryService;

    @Resource
    private RuntimeService runtimeService;

    @Resource
    private TaskService taskService;

    @Resource
    private ProcessEngine processEngine;

    @Resource
    private HistoryService historyService;


    /**
     * 部署流程
     * @param processName 流程定义名
     * @param resourcePath 如flowable/process.bpmn
     * @return
     */
    @Override
    public void createProcess(String processName, String resourcePath){
        Deployment deployment = repositoryService.createDeployment().name(processName).addClasspathResource(resourcePath).deploy();

    }

    /**
     * 发起流程
     * @return
     */
    @Override
    public String applyProcess(String processName,Map<String,Object>map){
        //指定发起人
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processName, map);
        log.info("流程实例ID:"+processInstance.getProcessInstanceId());
        return processInstance.getProcessInstanceId();
    }

    /**
     * 查询某用户/群体/角色待办列表
     * @return
     */
    @Override
    public List<Task> todoList(String user){
        List<Task> tasks = taskService.createTaskQuery().taskCandidateOrAssigned(user).orderByTaskCreateTime().desc().list();

        return tasks;
    }

    /**
     * 生成流程图
     * @param processId 任务ID
     */
    @Override
    public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception {
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();

        //流程走完的不显示图
        if (pi == null) {
            return null;
        }
        Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
        //使用流程实例ID,查询正在执行的执行对象表,返回流程实例对象
        String InstanceId = task.getProcessInstanceId();
        List<Execution> executions = runtimeService
                .createExecutionQuery()
                .processInstanceId(InstanceId)
                .list();


        //得到正在执行的Activity的Id
        List<String> activityIds = new ArrayList<>();
        List<String> flows = new ArrayList<>();
        for (Execution exe : executions) {
            List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
            activityIds.addAll(ids);
        }

        //获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();

        InputStream in = diagramGenerator.generateDiagram(bpmnModel,"bmp", activityIds,flows,"宋体","宋体","宋体",null,1.0,false);
        ByteArrayOutputStream out = null;
        byte[] buf = new byte[1024];
        int legth = 0;
        try {
            out = new ByteArrayOutputStream();
            while ((legth = in.read(buf)) != -1) {
                out.write(buf, 0, legth);
            }
            return out;
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }

    /**
     * 完成任务
     * @param taskId 任务id
     * @param user 用户/角色id
     * @param map 流程变量
     */
    @Override
    public void approveProcess(String taskId,String user,Map<String,Object> map){
        //先申领任务,相当于用户将这个流程任务占用,其他在这个用户组里的用户不能看到该流程任务
        taskService.claim(taskId,user);
        //再流转下一个节点
        taskService.complete(taskId, map);
    }


    /**
     * 将节点移动到任意节点上
     * @param taskId 任务id
     * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好
     * @return
     */
    @Override
    public void withdrawProcess(String taskId,String taskDefinitionKey) throws Exception {
        //获取当前任务,让其移动到目标节点位置
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        if(task == null) {
            throw new Exception("任务不存在");
        }
        //将节点移动到目标节点
        runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId())
                .moveActivityIdTo(task.getTaskDefinitionKey(), taskDefinitionKey).changeState();
    }

    /**
     * 获取流程的历史节点列表
     * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点"userTask"的信息
     * @param processId 流程ID
     * @return
     */
    @Override
    public List<HistoricActivityInstance> historyList(String processId){
        List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processId).activityType("userTask").finished()
                .orderByHistoricActivityInstanceEndTime().desc().list();
        return activities;
    }


    /**
     * 删除流程实例
     * @param processId 流程实例ID
     * @return
     */
    @Override
    public void deleteProcess(String processId){
        runtimeService.deleteProcessInstance(processId, "");
    }

    /**
     * 申领任务
     * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息,
     * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息,
     * 因为已经被这个用户领取了
     * @param taskId
     * @param user
     * @return
     */
    @Override
    public void claim(String taskId,String user){
        taskService.claim(taskId,user);
    }

    /**
     * 取消申领任务
     * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到,
     * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务
     * @param taskId
     * @return
     */
    @Override
    public void unClaim(String taskId){
        taskService.unclaim(taskId);
    }

}

(2)测试
①首先是流程部署
进入http://localhost:8080/flowable/createProcess/a999
可以看到流程部署成功,此时act_re_deployment和act_ge_bytearray会看到数据记录
②发起流程
进入http://localhost:8080/flowable/apply/a999
发起成功后act_ru_execution会有记录,并且能看到流程id,这个id后面会使用到
在这里插入图片描述

③生成流程图
http://localhost:8080/flowable/getPng/35005
可以看到当前流程的进度在哪,我这里把生成的流程图放在D盘下面了
在这里插入图片描述

④查看当前用户的待办
进入http://localhost:8080/flowable/flowList/userid2,注意userid2是我设置的用户申请里的分配用户,在流程图里是candidateUsers,可以看到task_id,后面会用到
在这里插入图片描述
⑤用户进行审批或退回,我这里默认写死通过
进入http://localhost:8080/flowable/approveProcess/userid2/35010
⑥再次查看流程图
http://localhost:8080/flowable/getPng/35005
可以看到流程走到下一个节点了

在这里插入图片描述
再次进入http://localhost:8080/flowable/flowList/userid2,发现task_id变化了
在这里插入图片描述
进入http://localhost:8080/flowable/flowList/leader1,发现下一节点的用户也有任务了,这个id后面会用到
在这里插入图片描述

⑦退回某一特定节点,其中32503是task_id,sid-5946EBF9-CCA9-41D5-A1B9-812886784183是节点id
进入http://localhost:8080/flowable/withdrawProcess/35022/sid-5946EBF9-CCA9-41D5-A1B9-812886784183
在这里插入图片描述
然后再次查看流程图,发现流程变化了
http://localhost:8080/flowable/getPng/35005
在这里插入图片描述

5.核心表介绍

(1)表名分类
ACT_RE_* repository-静态信息数据。如流程定义、流程的资源(图片,规则等)。
ACT_RU_* runtime-运行数据。存储着流程变量,用户任务,变量,职责(job)等运行时的数据。flowable只存储实例执行期间的运行时数据,当流程实例结束时,将删除这些记录。这就保证了这些运行时的表小且快。
ACT_ID_* identity-组织机构数据。包含标识的信息,如用户,用户组,等等。
ACT_HI_* history-历史数据。包括流程实例,变量,任务,等等。
ACT_GE_* general-通用数据。各种情况都使用的数据。

(2)核心表
部署内容表:act_ge_bytearray 此表和ACT_RE_DEPLOYMENT是多对一的关系
部署ID表:act_re_deployment
流程表:act_re_procdef
历史节点表:act_hi_actinst
历史任务流程实例信息 :act_hi_taskinst
流程变量数据表:act_ru_variable
历史变量表:act_hi_varinst
流程实例历史:act_hi_procinst
历史流程人员表:act_hi_identitylink
运行时流程人员表:act_ru_identitylink
运行时任务节点表:act_ru_task

(3)流程启动到结束数据库变化
部署完毕后,act_re_deployment表中会有一条部署记录,记录这次部署的基本信息,然后是act_ge_bytearray表中有两条记录,记录的是本次上传的bpmn文件和对应的图片文件,每条记录都有act_re_deployment表的外键关联,然后是act_re_procdef表中有一条记录,记录的是该bpmn文件包含的基本信息,包含act_re_deployment表外键。

流程启动,首先向act_ru_execution表中插入一条记录,记录的是这个流程定义的执行实例,其中id和proc_inst_id相同都是流程执行实例id,也就是本次执行这个流程定义的id,包含流程定义的id外键。

然后向act_ru_task插入一条记录,记录的是第一个任务的信息,也就是开始执行第一个任务。包括act_ru_execution表中的execution_id外键和proc_inst_id外键,也就是本次执行实例id。

然后向act_hi_procinst表和act_hi_taskinst表中各插入一条记录,记录的是本次执行实例和任务的历史记录:

任务提交后,首先向act_ru_variable表中插入变量信息,包含本次流程执行实例的两个id外键,但不包括任务的id,因为setVariable方法设置的是全局变量,也就是整个流程都会有效的变量:

当流程中的一个节点任务完成后,进入下一个节点任务,act_ru_task表中这个节点任务的记录被删除,插入新的节点任务的记录。

同时act_ru_execution表中的记录并没有删除,而是将正在执行的任务变成新的节点任务。

同时向act_hi_var_inst和act_hi_taskinst插入历史记录。

整个流程执行完毕,act_ru_task,act_ru_execution和act_ru_variable表相关记录全被清空。

全程有一个表一直在记录所有动作,就是act_hi_actinst表

五、常见报错解决

1.自动建表提示 表已存在 Table ‘act_id_property’ already exists

mysql连接地址后面加上&nullCatalogMeansCurrent=true,如果还是不行,可以新建一个库再试试

2.集成SpringBoot项目报错 “SLF4J: Class path contains multiple SLF4J bindings.”

flowable 集成依赖 rest,logic,conf 的三个jar包加上下面的片段

<exclusions>
     <exclusion>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-api</artifactId>
     </exclusion>
 </exclusions>

3.集成SpringBoot的项目报错"NoClassDefFoundError: org/springframework/core/ErrorCoded"

将Flowable中Spring的相关包剔除

<exclusions>
   <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
    </exclusion>
    <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
    </exclusion>
    <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </exclusion>
</exclusions>

4.返回task的List对象时报错:Could not write content: lazy loading outside command context

原代码:

@PostMapping(value = "/flowList")
    public Object flowList(@RequestBody FlowableReq req) {
        log.info("查询待办流程{}", JSON.toJSONString(req));
        List<Task> list = flowService.todoList(req.getUserId());
        log.info("查询待办流程成功{}", list.toString());
        return buildJsonWrapSuccess(list );
    }

懒加载只能在session打开的状况下才会正常执行,而session在service层就已经关闭了。所以在controller中返回会报错,
我们知道实体类的set类底层是一个map集合(利用Map的Key不能重复, 来实现Set的值不重复),所以转成map就可以了。
解决方法:转成map之后便可以加载

@PostMapping(value = "/flowList")
    public Object flowList(@RequestBody FlowableReq req) {
        log.info("查询待办流程{}", JSON.toJSONString(req));
        List<Task> list = flowService.todoList(req.getUserId());
        List<Map<String, Object>> listMap = new ArrayList<>();
        String[] ps = {"id","name"};
        list.forEach(task -> {
            //解决懒加载的对象无法在controller中返回的问题
            listMap.add(CommUtil.obj2map(task,ps));
        });
        log.info("查询待办流程成功{}", listMap.toString());
        return buildJsonWrapSuccess(listMap);
    }

工具类:CommUtil

package org.jxzx.baseframe.utils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeanUtils;

public class CommUtil {

	/**
	 * 把指定的复杂对象属性,按照指定的内容,封装到新的map中
	 * @param source 目标对象
	 * @param ps     需要封装到map中的属性
	 * @return
	 */
	public static Map<String, Object> obj2map(Object source, String[] ps) {
		Map<String, Object> map = new HashMap<>();
		if (source == null)
			return null;
		if (ps == null || ps.length < 1) {
			return null;
		}
		for (String p : ps) {
			PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(
					source.getClass(), p);
			if (sourcePd != null && sourcePd.getReadMethod() != null) {
				try {
					Method readMethod = sourcePd.getReadMethod();
					if (!Modifier.isPublic(readMethod.getDeclaringClass()
							.getModifiers())) {
						readMethod.setAccessible(true);
					}
					Object value = readMethod.invoke(source, new Object[0]);
					map.put(p, value);
				} catch (Exception ex) {
					throw new RuntimeException(
							"Could not copy properties from source to target",
							ex);
				}
			}
		}
		return map;
	}
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐