Flowable教程

news2024/11/19 10:32:17

文章目录

    • 一、Flowable介绍
    • 1.简介
    • 2.Activiti、Flowable、Camunda
    • 二、Flowable实战(集成Flowable Modeler)
    • 三、流程的创建和使用
    • 1.BPMN基本概念介绍
    • 2.业务模型流程创建
    • 3.表单创建及使用
    • 4.流程的使用
    • 5.核心表介绍
    • 四、常见报错解决

一、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实战(集成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/,进入下面页面则算启动成功了在这里插入图片描述

三、流程的创建和使用

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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/124889.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3.JMeter基本组成部分

文章目录2.3 JMeter基本组成部分2.3 .1线程组2.3.1.1 添加线程组2.3.1.2 线程组的特点2.3.1.3 线程组分类2.3.1.4 线程组属性2.3.2 取样器 HTTP请求2.3.3 查看结果树2.3 JMeter基本组成部分 2.3 .1线程组 线程组是控制JMeter将用于执行测试的线程组数&#xff0c;也可以把一个…

代理模式

“接口隔离”模式 在组件构建过程中&#xff0c;某些接口之间直接的依赖常常会带来很多问题、甚至根本无法实现。采用添加一层间接&#xff08;稳定&#xff09;接口&#xff0c;来隔离本来互相紧密关联的接口是一种常见的解决方案。 典型模式 ①门面模式 ②代理模式 ③适配…

[oeasy]python0033_回车_carriage_return_figlet_字体变大

回到开头 回忆上次内容 进程前后台切换 ctrl z 把当前进程切换到后台并暂停jobs 查看所有作业 用 fg 可以把后台进程再切回前台 fg %1 可以把指定的任务切回前台用 bg 可以让进程在后台运行 进程查询 ps -elf 查看所有进程信息ps -lf 查看本终端相关进程信息kill -9 PID 给进…

部署项目到Nginx

目录 1、将vue脚手架项目打包 2、将服务端项目打为jar包后上传到linux 3、 使用nginx解决跨域问题 5、 proxy_pass配置问题 1、将vue脚手架项目打包 运行:npm run build命令将vue cli项目打包。 路径在终端会显示 在虚拟机上将此文件上传入nginx中 然后打开nginx,显示页面…

linux下以rpm包安装mysql

目录 下载 上传解压 安装 编辑my.cnf配置文件 启动数据库 修改初始密码,授权远程登录 常见问题 下载 访问以下地址下载对应操作系统下所需的版本 https://downloads.mysql.com/archives/community/ 上传解压 使用ftp或者rz命令上传压缩包到服务器并执行 tar -xvf 压…

JavaScript手写响应式原理(详解)

响应式原理 首先我们有一个对象 const obj {name: zlk,age: 18}这个对象可能在别处被用到 比如是这样的 function foo() {const newValue obj.nameconsole.log(hello world);console.log(obj.name);}我们来改变obj对象中的name的值 obj.name zlk这时候foo()应该被重新执…

Android设计模式详解之代理模式

前言 代理模式也称为委托模式&#xff0c;是一种结构型设计模式&#xff1b; 定义&#xff1a;为其他对象提供一种代理以控制对这个对象的访问&#xff1b; 使用场景&#xff1a;当无法或不想直接访问某个对象或访问某个对象存在困难时&#xff0c;可以通过一个代理对象来间…

css实现圆环、渐变色圆环的多种方式

css实现圆环、渐变色圆环的多种方式一、实现圆环方法具体如下&#xff1a;1. 两个div标签的叠加2.使用伪元素&#xff0c;before&#xff0f;after3. 使用border4. 使用border-shadow5. 使用radial-gradient二、实现渐变色圆环方法具体如下&#xff1a;1.background:linear-gra…

详细记录拉链表的实现过程

面试中被问到了&#xff0c;想了会儿思路混乱没答好&#xff0c;还是理解的不够深刻&#xff0c;重新好好理解记录一下~ 拉链表的用途&#xff0c;主要是用来在数仓中记录业务库数据的全部历史信息和当前最新信息&#xff0c;也就是用来实现对渐变维的记录。数仓中对渐变维的记…

.NET和JavaScript控件丨Infragistics功能简介

使用Infragistics Ultimate UI/UX工具包简化开发&#xff0c;提供综合的企业级 UI控件库和使用Indigo.Design的 UX设计-开发协作工具 -一个完整的设计到代码系统- 集成原型、设计系统、用户测试、应用程序构建和代码生成。 终极开发者工具包 为任何平台上的任何设备设计、现代…

minikube start

因为要安装的中间件需要运行在k8s的环境里。官方推荐用minikube 运行minikube start时遇到问题。 容器下载速度为0 &#xff08;没有截屏&#xff09;kubectl初始化超时 initial timeout of 40s passed: 解决问题1&#xff0c;需要加上–image-mirror-countrycn’参数。如果…

长短期记忆网络(LSTM)

长短期记忆网络有三种类型的门&#xff1a;输入门、遗忘门和输出门。 长短期记忆网络的隐藏层输出包括“隐状态”和“记忆元”。只有隐状态会传递到输出层&#xff0c;而记忆元完全属于内部信息。 长短期记忆网络可以缓解梯度消失和梯度爆炸。 由于序列的长距离依赖性&#…

27移除元素--双指针(快慢指针)

27移除元素–双指针&#xff08;快慢指针&#xff09; 移除元素这道题看起来很简单&#xff0c;但其蕴含的快慢指针的思想十分重要。 双for循环&#xff08;暴力法&#xff09;-- O(n2n^2n2) 使用第1个for循环 i 遍历数组所有元素 使用第2个for循环从 i 开始进行数组元素的前移…

骨传导耳机伤耳朵吗、骨传导耳机适合适用的人群有哪些?

事实上&#xff0c;骨传导耳机是对耳朵最健康的一种耳机了&#xff0c;下面就来详细说说这种耳机。 骨传导耳机是以人的骨骼为介质&#xff0c;不经过外耳道和耳膜&#xff0c;将声音传递给听觉器官的耳机。他对人的耳朵损害相比起传统的耳机损害更小&#xff0c;因为听力受损…

JavaSE笔记——Lambda表达式

文章目录前言一、第一个Lambda表达式二、如何辨别Lambda表达式三、引用值&#xff0c;而不是变量四、函数接口五、类型推断总结前言 Java 8 的最大变化是引入了 Lambda 表达式——一种紧凑的、传递行为的方式。 一、第一个Lambda表达式 Swing 是一个与平台无关的 Java 类库&a…

redhat7.6+grid 11.2.0.4部署遇到各种问题

一、add cluster node时&#xff0c;卡住 两个节点时间不同步&#xff0c;设置时间同步即可 二、部署Redhat7.6oracle11g部署中的bug Oracle 11.2.0.4 部署rac过程中&#xff0c;需要运行root.sh脚本报错。提示&#xff1a; ohasd集群无法启动。该补丁修改ohasd无法启动的问题…

红外成像系统测试

通常人们把红外辐射称为红外光、红外线。实际上其波段是指其波长约在0.75μm到1000μm的电磁波。人们将其划分为近、中、远红外三部分。近红外指波长为0.75-3.0μm;中红外指波长为3.0-20μm;远红外则指波长为20-1000μm。由于大气对红外辐射的吸收,只留下三个重要的“窗口”…

一把巴枪,和被改变的菜鸟驿站站长们

成立9年的菜鸟物流一直在答题。如果说之前这张答卷更多的标签是面向物流前端的配送和分拣等&#xff0c;那么如今&#xff0c;它的更多答案已经不单纯是前端的流通和连接&#xff0c;更有最末端基于科技对人的温度和赋能。 作者|丰兰 出品|产业家 数字化&#xff0c;正在…

少儿Python每日一题(6):角谷猜想

原题解答 本次的题目如下所示&#xff08;原题出处&#xff1a;NOC&#xff09;&#xff1a; 角谷猜想&#xff1a;以一个正整数n为例&#xff0c;如果n为偶数&#xff0c;就将它变为n/2&#xff1b;如果除后变成奇数&#xff0c;则将它乘3加1&#xff08;即3n1&#xff09;。…

latex常用语法速查

本文针对overleaf在线使用latex的情况编写。 文章目录文档结构要点导入图片使用表格添加引用参考资料文档结构 文档类型设置 \documentclass[12pt,article]{book} % []中设置文档格式&#xff0c;文档字体大小默认为10pt&#xff0c;article指定文档用纸类型&#xff0c;其他…