前后端分离架构(Flowable在线流程图绘制:从入门到实战)

前后端分离架构(Flowable在线流程图绘制:从入门到实战)
Flowable在线流程图绘制:从入门到实战

Flowable在线流程图绘制:从入门到实战

前言

在企业级应用开发中,工作流引擎是不可或缺的组件。Flowable作为一个轻量级、开源的工作流和业务流程管理(BPM)平台,被广泛应用于各类业务系统中。然而,传统的流程设计方式需要借助专门的建模工具(如Flowable Modeler、Camunda Modeler等),操作复杂且不够直观。

本文将介绍如何基于 Flowable 6.8.0bpmn-js 构建一个在线流程图绘制系统,实现:

  • 在线绘制 BPMN 2.0 流程图
  • 流程定义保存到 MySQL 数据库
  • 一键部署流程到 Flowable 引擎
  • 完整的前后端分离架构

一、为什么选择 bpmn-js?

1.1 bpmn-js 简介

bpmn-js 是一个由 Camunda 开发的 BPMN 2.0 建模工具库,纯 JavaScript 实现,可以直接在浏览器中运行。它提供了:

  • 完整的 BPMN 2.0 支持:支持所有 BPMN 2.0 元素和属性
  • 轻量级:无需安装任何插件或软件
  • 可定制:支持自定义样式和行为
  • 开源免费:采用开源协议,可商用

1.2 bpmn-js vs 传统建模工具

特性 bpmn-js Flowable Modeler Eclipse Plugin 安装方式 无需安装 需要部署 需要安装 使用环境 浏览器 独立应用 Eclipse IDE 协作支持 易于集成 困难 困难 定制能力 高 中 低 学习曲线 平缓 陡峭 陡峭


二、系统架构设计

2.1 整体架构

本系统采用前后端分离的架构设计,主要包括以下层次:

整体架构

表现层(Presentation Layer)

  • Vue.js 3.x 前端框架
  • bpmn-js 流程设计器
  • Element Plus UI 组件库

业务服务层(Business Service Layer)

  • Spring Boot 2.7 应用框架
  • 流程设计服务、部署服务、实例服务
  • RESTful API 接口层

流程引擎层(Process Engine Layer)

  • Flowable 6.8.0 流程引擎
  • 流程定义引擎、任务引擎、历史引擎

数据存储层(Data Storage Layer)

  • MySQL 8.0 存储流程定义、实例、任务数据
  • Redis 6.x 缓存、会话管理
  • MinIO 对象存储(可选)

2.2 核心交互流程

核心交互流程


  1. 用户在前端界面点击"新建流程"
  2. 前端请求后端 API 创建流程定义
  3. 后端调用 Flowable 的 RepositoryService
  4. Flowable 引擎将流程定义保存到数据库
  5. 返回流程定义 ID 给前端
  6. 用户使用 bpmn-js 绘制流程图
  7. 保存时将 BPMN XML 发送到后端
  8. 后端更新数据库中的流程定义

三、环境准备

3.1 开发环境要求

软件 版本 说明 JDK 1.8+ Java 开发环境 Maven 3.6+ 项目构建工具 MySQL 5.7+ 或 8.0+ 关系数据库 Node.js 14+ 前端开发(可选)

3.2 技术栈版本

技术栈架构

后端技术栈

  • Spring Boot 2.7.18
  • Flowable 6.8.0
  • MyBatis Plus 3.5.2
  • MySQL Connector 8.0.28

前端技术栈

  • bpmn-js 14.0.0
  • jQuery 3.6.0
  • Bootstrap 5(可选)

四、项目初始化

4.1 创建 Maven 项目

使用 Spring Initializr 创建项目,或手动创建 pom.xml:

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>2.7.18</version></parent><dependencies>    <!-- Flowable Starter -->    <dependency>        <groupId>org.flowable</groupId>        <artifactId>flowable-spring-boot-starter</artifactId>        <version>6.8.0</version>    </dependency>    <!-- MyBatis Plus -->    <dependency>        <groupId>com.baomidou</groupId>        <artifactId>mybatis-plus-boot-starter</artifactId>        <version>3.5.2</version>    </dependency>    <!-- MySQL Driver -->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>8.0.28</version>    </dependency></dependencies>

4.2 数据库初始化

创建数据库和流程定义表:

CREATE DATABASE flowable_online CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE flowable_online;CREATE TABLE flow_definition (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    process_key VARCHAR(255) NOT NULL,    process_name VARCHAR(255) NOT NULL,    version INT NOT NULL DEFAULT 1,    bpmn_xml LONGTEXT NOT NULL,    description TEXT,    category VARCHAR(255),    created_by VARCHAR(100),    deployed TINYINT DEFAULT 0,    deployment_id VARCHAR(64),    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,    deleted TINYINT DEFAULT 0,    UNIQUE KEY uk_process_key_version (process_key, version, deleted));

五、后端实现

5.1 实体类设计

创建流程定义实体类:

@Data@TableName("flow_definition")public class FlowDefinition {    @TableId(type = IdType.AUTO)    private Long id;    private String processKey;    private String processName;    private Integer version;    private String bpmnXml;    private String description;    private String category;    private String createdBy;    private Integer deployed;    private String deploymentId;    private LocalDateTime createTime;    private LocalDateTime updateTime;    @TableLogic    private Integer deleted;}

5.2 数据访问层

使用 MyBatis Plus 的 Mapper 接口:

@Mapperpublic interface FlowDefinitionMapper extends BaseMapper<FlowDefinition> {    List<FlowDefinition> selectByProcessKey(@Param("processKey") String processKey);    List<FlowDefinition> selectByCreatedBy(@Param("createdBy") String createdBy);}

5.3 服务层实现

流程设计核心服务:

package com.example.flowable.service;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.example.flowable.entity.FlowDefinition;import com.example.flowable.entity.Result;import com.example.flowable.exception.FlowableException;import com.example.flowable.repository.FlowDefinitionMapper;import lombok.extern.slf4j.Slf4j;import org.flowable.engine.RepositoryService;import org.flowable.engine.repository.Deployment;import org.flowable.engine.repository.ProcessDefinition;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.StringUtils;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;import java.nio.charset.StandardCharsets;import java.time.LocalDateTime;import java.util.List;import java.util.zip.ZipInputStream;/** * 流程设计服务实现类 * 提供流程定义的保存、查询、删除、部署等功能 * @version 1.0.0 */@Slf4j@Servicepublic class FlowDesignService extends ServiceImpl<FlowDefinitionMapper, FlowDefinition> {    @Autowired    private FlowDefinitionMapper flowDefinitionMapper;    @Autowired    private RepositoryService repositoryService;    /**     * 保存流程定义     *     * @param flowDefinition 流程定义实体     * @return 保存结果     */    @Transactional(rollbackFor = Exception.class)    public Result<FlowDefinition> saveFlowDefinition(FlowDefinition flowDefinition) {        try {            // 验证必填字段            if (!StringUtils.hasText(flowDefinition.getProcessKey())) {                return Result.fail("流程Key不能为空");            }            if (!StringUtils.hasText(flowDefinition.getProcessName())) {                return Result.fail("流程名称不能为空");            }            if (!StringUtils.hasText(flowDefinition.getBpmnXml())) {                return Result.fail("BPMN XML内容不能为空");            }            // 检查流程Key是否已存在(同版本)            QueryWrapper<FlowDefinition> queryWrapper = new QueryWrapper<>();            queryWrapper.eq("process_key", flowDefinition.getProcessKey())                    .eq("version", flowDefinition.getVersion() != null ? flowDefinition.getVersion() : 1)                    .eq("deleted", 0);            Long count = flowDefinitionMapper.selectCount(queryWrapper);            if (count > 0) {                return Result.fail("流程Key和版本号已存在,请修改版本号");            }            // 设置默认值            if (flowDefinition.getVersion() == null) {                flowDefinition.setVersion(1);            }            if (flowDefinition.getCreateTime() == null) {                flowDefinition.setCreateTime(LocalDateTime.now());            }            flowDefinition.setUpdateTime(LocalDateTime.now());            flowDefinition.setDeleted(0);            flowDefinition.setDeployed(0);            // 保存到数据库            flowDefinitionMapper.insert(flowDefinition);            log.info("保存流程定义成功: id={}, key={}, name={}",                    flowDefinition.getId(), flowDefinition.getProcessKey(), flowDefinition.getProcessName());            return Result.success("流程定义保存成功", flowDefinition);        } catch (Exception e) {            log.error("保存流程定义失败: {}", e.getMessage(), e);            return Result.fail("保存流程定义失败: " + e.getMessage());        }    }    /**     * 更新流程定义     *     * @param flowDefinition 流程定义实体     * @return 更新结果     */    @Transactional(rollbackFor = Exception.class)    public Result<FlowDefinition> updateFlowDefinition(FlowDefinition flowDefinition) {        try {            if (flowDefinition.getId() == null) {                return Result.fail("流程定义ID不能为空");            }            FlowDefinition existing = flowDefinitionMapper.selectById(flowDefinition.getId());            if (existing == null) {                return Result.fail("流程定义不存在");            }            // 已部署的流程不能修改            if (existing.getDeployed() == 1) {                return Result.fail("已部署的流程不能修改,请新建版本");            }            flowDefinition.setUpdateTime(LocalDateTime.now());            int rows = flowDefinitionMapper.updateById(flowDefinition);            if (rows > 0) {                log.info("更新流程定义成功: id={}", flowDefinition.getId());                return Result.success("流程定义更新成功", flowDefinition);            } else {                return Result.fail("更新流程定义失败");            }        } catch (Exception e) {            log.error("更新流程定义失败: {}", e.getMessage(), e);            return Result.fail("更新流程定义失败: " + e.getMessage());        }    }    /**     * 删除流程定义     *     * @param id 流程定义ID     * @return 删除结果     */    @Transactional(rollbackFor = Exception.class)    public Result<Void> deleteFlowDefinition(Long id) {        try {            FlowDefinition flowDefinition = flowDefinitionMapper.selectById(id);            if (flowDefinition == null) {                return Result.fail("流程定义不存在");            }            // 逻辑删除            flowDefinition.setDeleted(1);            flowDefinition.setUpdateTime(LocalDateTime.now());            flowDefinitionMapper.updateById(flowDefinition);            log.info("删除流程定义成功: id={}", id);            return Result.success("流程定义删除成功");        } catch (Exception e) {            log.error("删除流程定义失败: {}", e.getMessage(), e);            return Result.fail("删除流程定义失败: " + e.getMessage());        }    }    /**     * 根据ID查询流程定义     *     * @param id 流程定义ID     * @return 查询结果     */    public Result<FlowDefinition> getFlowDefinitionById(Long id) {        try {            FlowDefinition flowDefinition = flowDefinitionMapper.selectById(id);            if (flowDefinition == null) {                return Result.fail("流程定义不存在");            }            return Result.success(flowDefinition);        } catch (Exception e) {            log.error("查询流程定义失败: {}", e.getMessage(), e);            return Result.fail("查询流程定义失败: " + e.getMessage());        }    }    /**     * 查询所有流程定义列表     *     * @return 流程定义列表     */    public Result<List<FlowDefinition>> getAllFlowDefinitions() {        try {            QueryWrapper<FlowDefinition> queryWrapper = new QueryWrapper<>();            queryWrapper.eq("deleted", 0)                    .orderByDesc("create_time");            List<FlowDefinition> list = flowDefinitionMapper.selectList(queryWrapper);            return Result.success(list);        } catch (Exception e) {            log.error("查询流程定义列表失败: {}", e.getMessage(), e);            return Result.fail("查询流程定义列表失败: " + e.getMessage());        }    }    /**     * 部署流程到Flowable引擎     *     * @param id 流程定义ID     * @return 部署结果     */    @Transactional(rollbackFor = Exception.class)    public Result<Deployment> deployFlowDefinition(Long id) {        try {            FlowDefinition flowDefinition = flowDefinitionMapper.selectById(id);            if (flowDefinition == null) {                return Result.fail("流程定义不存在");            }            if (flowDefinition.getDeployed() == 1) {                return Result.fail("该流程已经部署过了");            }            // 部署到Flowable引擎            String deploymentName = flowDefinition.getProcessName() + ".bpmn20.xml";            Deployment deployment = repositoryService.createDeployment()                    .name(flowDefinition.getProcessName())                    .category(flowDefinition.getCategory())                    .addBytes(deploymentName, flowDefinition.getBpmnXml().getBytes(StandardCharsets.UTF_8))                    .deploy();            // 更新部署状态            flowDefinitionMapper.updateDeployStatus(id, 1, deployment.getId());            log.info("部署流程成功: id={}, deploymentId={}", id, deployment.getId());            return Result.success("流程部署成功", deployment);        } catch (Exception e) {            log.error("部署流程失败: {}", e.getMessage(), e);            return Result.fail("部署流程失败: " + e.getMessage());        }    }    /**     * 获取Flowable流程定义列表     *     * @return 流程定义列表     */    public Result<List<ProcessDefinition>> getFlowableProcessDefinitions() {        try {            List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()                    .orderByProcessDefinitionVersion()                    .desc()                    .list();            return Result.success(list);        } catch (Exception e) {            log.error("获取Flowable流程定义列表失败: {}", e.getMessage(), e);            return Result.fail("获取流程定义列表失败: " + e.getMessage());        }    }    /**     * 根据流程Key获取最新版本的流程定义     *     * @param processKey 流程Key     * @return 流程定义     */    public Result<ProcessDefinition> getLatestProcessDefinitionByKey(String processKey) {        try {            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()                    .processDefinitionKey(processKey)                    .latestVersion()                    .singleResult();            if (processDefinition == null) {                return Result.fail("流程定义不存在");            }            return Result.success(processDefinition);        } catch (Exception e) {            log.error("获取流程定义失败: {}", e.getMessage(), e);            return Result.fail("获取流程定义失败: " + e.getMessage());        }    }    /**     * 导出流程定义XML     *     * @param id 流程定义ID     * @return BPMN XML字符串     */    public Result<String> exportFlowDefinitionXml(Long id) {        try {            FlowDefinition flowDefinition = flowDefinitionMapper.selectById(id);            if (flowDefinition == null) {                return Result.fail("流程定义不存在");            }            return Result.success(flowDefinition.getBpmnXml());        } catch (Exception e) {            log.error("导出流程定义失败: {}", e.getMessage(), e);            return Result.fail("导出流程定义失败: " + e.getMessage());        }    }    /**     * 获取流程定义的XML资源     *     * @param processDefinitionId 流程定义ID     * @return XML资源内容     */    public Result<String> getProcessDefinitionResource(String processDefinitionId) {        try {            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()                    .processDefinitionId(processDefinitionId)                    .singleResult();            if (processDefinition == null) {                return Result.fail("流程定义不存在");            }            String resourceName = processDefinition.getResourceName();            InputStream inputStream = repositoryService.getResourceAsStream(                    processDefinition.getDeploymentId(),                    resourceName);            byte[] bytes = readAllBytes(inputStream);            return Result.success(new String(bytes, StandardCharsets.UTF_8));        } catch (Exception e) {            log.error("获取流程定义资源失败: {}", e.getMessage(), e);            return Result.fail("获取流程定义资源失败: " + e.getMessage());        }    }    /**     * 读取输入流的所有字节(Java 8 兼容版本)     *     * @param inputStream 输入流     * @return 字节数组     * @throws IOException 读取异常     */    private byte[] readAllBytes(InputStream inputStream) throws IOException {        ByteArrayOutputStream buffer = new ByteArrayOutputStream();        int nRead;        byte[] data = new byte[4096];        while ((nRead = inputStream.read(data, 0, data.length)) != -1) {            buffer.write(data, 0, nRead);        }        buffer.flush();        return buffer.toByteArray();    }}

5.4 控制器层

RESTful API 接口:

package com.example.flowable.controller;import com.example.flowable.entity.FlowDefinition;import com.example.flowable.entity.Result;import com.example.flowable.service.FlowDesignService;import lombok.extern.slf4j.Slf4j;import org.flowable.engine.repository.Deployment;import org.flowable.engine.repository.ProcessDefinition;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;import java.util.List;/** * 流程设计控制器 * 提供流程设计的REST API接口 * @version 1.0.0 */@Slf4j@RestController@RequestMapping("/api/flow")@CrossOrigin(origins = "*")public class FlowDesignController {    @Autowired    private FlowDesignService flowDesignService;    /**     * 保存流程定义     *     * @param flowDefinition 流程定义实体     * @return 保存结果     */    @PostMapping("/save")    public Result<FlowDefinition> saveFlowDefinition(@Valid @RequestBody FlowDefinition flowDefinition) {        log.info("保存流程定义: key={}, name={}", flowDefinition.getProcessKey(), flowDefinition.getProcessName());        return flowDesignService.saveFlowDefinition(flowDefinition);    }    /**     * 更新流程定义     *     * @param flowDefinition 流程定义实体     * @return 更新结果     */    @PostMapping("/update")    public Result<FlowDefinition> updateFlowDefinition(@Valid @RequestBody FlowDefinition flowDefinition) {        log.info("更新流程定义: id={}", flowDefinition.getId());        return flowDesignService.updateFlowDefinition(flowDefinition);    }    /**     * 删除流程定义     *     * @param id 流程定义ID     * @return 删除结果     */    @DeleteMapping("/delete/{id}")    public Result<Void> deleteFlowDefinition(@PathVariable Long id) {        log.info("删除流程定义: id={}", id);        return flowDesignService.deleteFlowDefinition(id);    }    /**     * 根据ID查询流程定义     *     * @param id 流程定义ID     * @return 查询结果     */    @GetMapping("/get/{id}")    public Result<FlowDefinition> getFlowDefinitionById(@PathVariable Long id) {        return flowDesignService.getFlowDefinitionById(id);    }    /**     * 获取所有流程定义列表     *     * @return 流程定义列表     */    @GetMapping("/list")    public Result<List<FlowDefinition>> getAllFlowDefinitions() {        return flowDesignService.getAllFlowDefinitions();    }    /**     * 部署流程到Flowable引擎     *     * @param id 流程定义ID     * @return 部署结果     */    @PostMapping("/deploy/{id}")    public Result<Deployment> deployFlowDefinition(@PathVariable Long id) {        log.info("部署流程: id={}", id);        return flowDesignService.deployFlowDefinition(id);    }    /**     * 获取Flowable流程定义列表     *     * @return 流程定义列表     */    @GetMapping("/deployed/list")    public Result<List<ProcessDefinition>> getDeployedFlowDefinitions() {        return flowDesignService.getFlowableProcessDefinitions();    }    /**     * 根据流程Key获取最新版本的流程定义     *     * @param processKey 流程Key     * @return 流程定义     */    @GetMapping("/getByKey/{processKey}")    public Result<ProcessDefinition> getProcessDefinitionByKey(@PathVariable String processKey) {        return flowDesignService.getLatestProcessDefinitionByKey(processKey);    }    /**     * 导出流程定义XML     *     * @param id 流程定义ID     * @return BPMN XML字符串     */    @GetMapping("/export/{id}")    public Result<String> exportFlowDefinitionXml(@PathVariable Long id) {        log.info("导出流程定义XML: id={}", id);        return flowDesignService.exportFlowDefinitionXml(id);    }    /**     * 获取流程定义的XML资源     *     * @param processDefinitionId 流程定义ID     * @return XML资源内容     */    @GetMapping("/resource/{processDefinitionId}")    public Result<String> getProcessDefinitionResource(@PathVariable String processDefinitionId) {        return flowDesignService.getProcessDefinitionResource(processDefinitionId);    }}

六、前端实现

6.1 集成 bpmn-js

在 HTML 页面中引入 bpmn-js 库:

<script src="https://unpkg.com/bpmn-js@14.0.0/dist/bpmn-modeler.development.js"></script><link rel="stylesheet" href="https://unpkg.com/bpmn-js@14.0.0/dist/assets/diagram-js.css"/>

6.2 初始化建模器

// 创建 BPMN 建模器实例const bpmnModeler = new BpmnJS({    container: '#canvas',    height: '100%',    keyboard: { bindTo: window }});// 导入默认的 BPMN 模板bpmnModeler.importXML(defaultBpmnTemplate).then(() => {    bpmnModeler.get('canvas').zoom('fit-viewport');});

6.3 保存流程定义

function saveFlowDefinition() {    // 获取 BPMN XML    bpmnModeler.saveXML({ format: true }).then(result => {        const bpmnXml = result.xml;        // 发送到后端保存        $.ajax({            url: '/api/flow/save',            method: 'POST',            contentType: 'application/json',            data: JSON.stringify({                processKey: $('#processKey').val(),                processName: $('#processName').val(),                bpmnXml: bpmnXml            }),            success: function(response) {                if (response.code === 200) {                    showToast('保存成功', 'success');                }            }        });    });}

6.4 部署流程

function deployFlowDefinition(id) {    $.ajax({        url: '/api/flow/deploy/' + id,        method: 'POST',        success: function(response) {            if (response.code === 200) {                showToast('部署成功!部署ID: ' + response.data.id, 'success');            }        }    });}

七、流程部署完整流程

流程部署完整流程

前后端分离架构(Flowable在线流程图绘制:从入门到实战)

7.1 绘制阶段

  1. 用户打开流程设计器
  2. 使用 bpmn-js 拖拽组件绘制流程图
  3. 配置各元素的属性(任务名称、处理人、监听器等)

7.2 保存阶段

  1. 点击"保存"按钮
  2. 前端将 BPMN XML 发送到后端
  3. 后端验证数据并保存到 MySQL
  4. 返回保存结果

7.3 部署阶段

  1. 用户选择要部署的流程
  2. 点击"部署"按钮
  3. 后端调用 Flowable 的 RepositoryService
  4. Flowable 引擎解析 BPMN XML
  5. 创建部署对象并存储到数据库
  6. 更新流程定义的部署状态

八、数据库设计

数据库设计

8.1 核心表结构

flow_definition 表

  • 存储用户绘制的流程定义信息
  • 包含 BPMN XML、版本号、部署状态等字段

Flowable 核心表(自动创建)

  • ACT_RE_PROCDEF:流程定义表
  • ACT_RE_DEPLOYMENT:部署表
  • ACT_RU_EXECUTION:运行时执行实例表
  • ACT_RU_TASK:运行时任务表
  • ACT_HI_PROCINST:历史流程实例表
  • ACT_HI_TASKINST:历史任务表

8.2 表关系说明

ACT_RE_DEPLOYMENT (1) ───→ ACT_RE_PROCDEF (N)    一个部署包含多个流程定义ACT_RE_PROCDEF (1) ───→ ACT_RU_EXECUTION (N)    一个流程定义可启动多个流程实例ACT_RU_EXECUTION (1) ───→ ACT_RU_TASK (N)    一个流程实例包含多个任务

九、完整业务流程

完整业务流程

9.1 用户操作流程

  1. 登录系统:用户登录在线流程设计系统
  2. 创建流程:点击"新建流程"按钮
  3. 绘制流程图:使用 bpmn-js 设计器绘制 BPMN 2.0 流程图
  4. 配置属性:为各元素配置属性(名称、处理人等)
  5. 保存流程:将流程定义保存到 MySQL 数据库

9.2 设计师操作流程

  1. 绘制流程图:拖拽 BPMN 组件到画布
  2. 连接元素:使用顺序流连接各个元素
  3. 配置属性:设置任务名称、候选组、到期时间等
  4. 保存流程:将设计好的流程保存到数据库

9.3 系统处理流程

  1. 验证 XML:系统验证 BPMN XML 的格式和有效性
  2. 部署流程:将流程部署到 Flowable 引擎
  3. 存储数据:将流程数据持久化到数据库

十、实战案例

10.1 请假审批流程

下面以一个简单的请假审批流程为例,展示完整的实现过程。

流程需求

  1. 员工提交请假申请
  2. 部门经理审批
  3. 如果请假天数 > 3 天,需要 HR 复审
  4. 审批通过后,通知员工

BPMN 设计

<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">  <bpmn:process id="leaveRequest" name="请假审批流程" isExecutable="true">    <bpmn:startEvent id="start"/>    <bpmn:userTask id="managerApproval" name="经理审批" flowable:assignee="${manager}"/>    <bpmn:exclusiveGateway id="gateway"/>    <bpmn:userTask id="hrApproval" name="HR审批" flowable:candidateGroups="hr"/>    <bpmn:endEvent id="end"/>    <!-- 流转 -->    <bpmn:sequenceFlow sourceRef="start" targetRef="managerApproval"/>    <bpmn:sequenceFlow sourceRef="managerApproval" targetRef="gateway">      <bpmn:conditionExpression xsi:type="tFormalExpression">${approved != null}</bpmn:conditionExpression>    </bpmn:sequenceFlow>    <bpmn:sequenceFlow sourceRef="gateway" targetRef="hrApproval">      <bpmn:conditionExpression xsi:type="tFormalExpression">${days > 3}</bpmn:conditionExpression>    </bpmn:sequenceFlow>    <bpmn:sequenceFlow sourceRef="gateway" targetRef="end">      <bpmn:conditionExpression xsi:type="tFormalExpression">${days <= 3}</bpmn:conditionExpression>    </bpmn:sequenceFlow>    <bpmn:sequenceFlow sourceRef="hrApproval" targetRef="end"/>  </bpmn:process></bpmn:definitions>

部署和使用

// 启动流程实例ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(    "leaveRequest",    businessKey,    variables);// 完成经理审批任务Task task = taskService.createTaskQuery()    .processInstanceId(processInstance.getId())    .singleResult();taskService.complete(task.getId(), Collections.singletonMap("approved", true));

十一、单元测试

11.1 测试服务层

使用 JUnit 5 和 Mockito 进行单元测试:

@ExtendWith(MockitoExtension.class)class FlowDesignServiceTest {    @Mock    private FlowDefinitionMapper flowDefinitionMapper;    @Mock    private RepositoryService repositoryService;    @InjectMocks    private FlowDesignService flowDesignService;    @Test    @DisplayName("测试保存流程定义")    void testSaveFlowDefinition() {        // Given        FlowDefinition flow = new FlowDefinition();        flow.setProcessKey("test");        flow.setProcessName("测试流程");        flow.setBpmnXml(bpmnXml);        // When        Result<FlowDefinition> result = flowDesignService.saveFlowDefinition(flow);        // Then        assertTrue(result.isSuccess());        verify(flowDefinitionMapper).insert(any());    }}

十二、部署和运行

12.1 编译打包

# 编译项目mvn clean compile# 运行测试mvn test# 打包(可选)mvn clean package

12.2 启动应用

# 使用 Maven 启动mvn spring-boot:run# 或使用 java -jar 启动java -jar target/flowable_online-demo-1.0.0.jar

12.3 访问系统

  • 首页:http://localhost:8080
  • 流程设计器:http://localhost:8080/designer

十三、总结

本文介绍了如何基于 Flowable 6.8.0 和 bpmn-js 构建一个在线流程图绘制系统。 该系统采用前后端分离架构,易于扩展和集成。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有