目录
-
- 相关框架官方资源
- 写在前面:我们要做什么?
- [一、5 个核心概念](#一、5 个核心概念)
- 二、环境准备
- [三、搭建后端(Spring Boot + Flowable)](#三、搭建后端(Spring Boot + Flowable))
-
- [3.1 创建 Maven 工程](#3.1 创建 Maven 工程)
- [3.2 `pom.xml`(完整)](#3.2
pom.xml(完整)) - [3.3 启动类](#3.3 启动类)
- [3.4 `application.properties`(完整)](#3.4
application.properties(完整)) - [3.5 请假审批 BPMN 流程(完整)](#3.5 请假审批 BPMN 流程(完整))
- [3.6 请求 DTO(3 个类)](#3.6 请求 DTO(3 个类))
- [3.7 核心 Controller(完整)](#3.7 核心 Controller(完整))
- [3.8 跨域与异常处理](#3.8 跨域与异常处理)
- [3.9 启动后端并验证](#3.9 启动后端并验证)
- [四、搭建前端(bpmn-js 在线建模)](#四、搭建前端(bpmn-js 在线建模))
-
- [4.1 初始化 Vite + Vue 项目](#4.1 初始化 Vite + Vue 项目)
- [4.2 安装后的 `package.json` 依赖段](#4.2 安装后的
package.json依赖段) - [4.3 `vite.config.ts`(完整)](#4.3
vite.config.ts(完整)) - [4.4 入口文件](#4.4 入口文件)
- [4.5 API 封装 `src/api/workflow.ts`(完整)](#4.5 API 封装
src/api/workflow.ts(完整)) - [4.6 默认 BPMN 模板 `src/bpmn/defaultDiagram.ts`](#4.6 默认 BPMN 模板
src/bpmn/defaultDiagram.ts) - [4.7 bpmn-js 建模器 `src/components/BpmnModelerView.vue`(完整)](#4.7 bpmn-js 建模器
src/components/BpmnModelerView.vue(完整)) - [4.8 主页面 `src/App.vue`(完整)](#4.8 主页面
src/App.vue(完整)) - [4.9 样式文件](#4.9 样式文件)
- [4.10 启动前端](#4.10 启动前端)
- 五、页面操作:走完审批闭环
- [六、从 Demo 到生产的 4 个建议](#六、从 Demo 到生产的 4 个建议)
- 写在最后

后端 :Spring Boot 2.4 + Flowable 6.8 + H2
前端 :Vite + Vue 3 + bpmn-js
阅读本文并照做,无需下载任何源码仓库,即可从零搭建一套「在线画流程 → 部署 → 发起 → 审批」的完整 Demo。
相关框架官方资源
Flowable(后端)
Flowable 官网:https://www.flowable.com/open-source
Flowable 官方文档:https://www.flowable.com/open-source/docs/
Flowable GitHub:https://github.com/flowable/flowable-engine
bpmn-js(前端)
bpmn-js 官网:https://bpmn.io/toolkit/bpmn-js/
bpmn-js 官方文档:https://github.com/bpmn-io/bpmn-js#documentation
bpmn-js GitHub:https://github.com/bpmn-io/bpmn-js
Vite 官网:https://vitejs.dev/
Vite 官方文档:https://cn.vitejs.dev/guide/
Vite GitHub:https://github.com/vitejs/vite
写在前面:我们要做什么?
企业里审批场景很多:请假、报销、采购......如果流转逻辑全写在 if/else 里,每次改流程都要发版。
BPMN + Flowable 的做法是:
- 用 BPMN 标准把流程画成图,保存为 XML
- Flowable 引擎负责解析 XML、驱动流程运转
- 业务系统只对接:部署、发起、查待办、提交审批
本文用「请假审批」串联整条链路:
bpmn-js 在线建模 → 部署到 Flowable → 发起流程 → 查待办 → 审批完成
最终你会得到两个工程:
| 工程 | 技术 | 端口 |
|---|---|---|
| 后端 | Spring Boot + Flowable + H2 | 8088 |
| 前端 | Vite + Vue 3 + bpmn-js | 5173 |
后端框架选型说明
Java 侧常见工作流引擎均支持 BPMN 2.0,本文后端选用 Flowable,主要因为:它由 Activiti 原核心团队维护,API 与 Activiti 5/6 相近、迁移成本低;官方提供 Spring Boot Starter,几行配置即可跑起来;社区活跃、文档齐全,适合作为审批类系统的入门选型。
| 引擎 | 维护背景 | Spring Boot 集成 | 特点简述 | 本文选用 |
|---|---|---|---|---|
| Flowable | Activiti 原团队 fork | Starter 开箱即用 | 轻量、上手快,XML 兼容性好 | ✅ |
| Activiti | 社区分裂,7.x 架构变化大 | 有 Starter,版本需注意 | 存量项目多,新项需谨慎选型 | --- |
| Camunda | Camunda 公司主导 | 集成成熟 | 功能强、生态完善,部分能力偏商业 | --- |
| jBPM | Red Hat 系 | 偏 JBoss 体系 | 常与 Drools 规则引擎配合使用 | --- |
前端框架选型说明
流程建模前端的核心需求是:在浏览器里画 BPMN 图、导出标准 XML、交给后端部署 。本文选用 Vue 3 + bpmn-js :bpmn-js 是 Camunda 开源的 BPMN 2.0 专用库,与 Flowable 使用同一套 XML 标准;Vue 3 组合式 API 便于在 onMounted 中初始化建模器、在 onUnmounted 中销毁实例,比桌面建模器更适合嵌入 Web 审批平台。
| 方案 | 类型 | 输出格式 | 嵌入 Web | 本文选用 |
|---|---|---|---|---|
| bpmn-js + Vue 3 | 浏览器建模库 + 前端框架 | 标准 BPMN 2.0 XML | ✅ | ✅ |
| bpmn-js + React | 同上,换用 React 封装 | 标准 BPMN 2.0 XML | ✅ | --- |
| Camunda Modeler | 桌面建模工具 | 标准 BPMN 2.0 XML | ❌ 独立应用 | --- |
| Activiti Modeler | 浏览器建模(较老) | BPMN XML | ✅ 可嵌入 | --- |
| LogicFlow / AntV X6 | 通用图编辑库 | 自定义 JSON | ✅ 灵活 | --- |
通用图编辑库自由度更高,但需自行映射 BPMN 语义;若目标是直接对接 Flowable,bpmn-js + Vue 3 是最省事的选择。
一、5 个核心概念
| 概念 | 含义 | 本文示例 |
|---|---|---|
| BPMN | 流程建模标准,落地为 .bpmn20.xml |
请假审批流程图 |
| Process Definition | 已部署的流程定义 | leaveApproval |
| Process Instance | 一次真实业务流转 | 张三的请假单 |
| Task | 待用户处理的任务 | 经理审批 |
| Variables | 流程变量,驱动分支 | applicant、approved |
主线:XML → 部署 → 启动实例 → 产生任务 → 变量决定走向。
二、环境准备
| 工具 | 版本建议 |
|---|---|
| JDK | 1.8+ |
| Maven | 3.6+ |
| Node.js | 18+(推荐 20+) |
| IDE | IntelliJ IDEA / VS Code 均可 |
三、搭建后端(Spring Boot + Flowable)
在写代码之前,先弄清 Flowable 在后端扮演的角色。
Flowable 是一个 BPMN 2.0 流程引擎,核心思路是:把 BPMN XML 解析为可执行的流程定义,运行时按节点顺序推进,遇到 UserTask 就生成待办任务,由业务系统查询并完成。

图 5:Flowable 核心服务分工 --- RepositoryService 管部署,RuntimeService 管运行,TaskService 管待办
几个关键 Service 的职责:
| Service | 作用 | 本文中的用法 |
|---|---|---|
| RepositoryService | 部署 BPMN、管理流程定义 | /deploy-xml 部署 XML |
| RuntimeService | 启动流程实例、管理运行时变量 | /start 发起请假 |
| TaskService | 查询待办、完成任务 | /tasks、/tasks/complete |
| HistoryService | 查询已结束的流程实例 | /history/process-instances |
一条数据流转路径:
BPMN XML → 部署 → ProcessDefinition(流程定义)
↓ startProcessInstanceByKey + variables
ProcessInstance(流程实例)
↓ 到达 UserTask
Task(待办任务)→ complete + variables → 继续流转或结束
Spring Boot 通过 flowable-spring-boot-starter-process 自动注入上述 Service,我们只需在 Controller 中调用即可。
3.1 创建 Maven 工程
用 IDEA 新建 Spring Boot 项目,或手动创建如下目录结构:
java
flowable-demo/
├── pom.xml
└── src/main/
├── java/com/demo/
│ ├── FlowableDemoApplication.java
│ └── bpmn/
│ ├── api/
│ │ ├── FlowableWorkflowController.java
│ │ ├── ApiExceptionHandler.java
│ │ └── dto/
│ │ ├── DeployXmlRequest.java
│ │ ├── StartProcessRequest.java
│ │ └── CompleteTaskRequest.java
│ └── config/
│ └── CorsConfig.java
└── resources/
├── application.properties
└── processes/
└── leave-approval.bpmn20.xml
3.2 pom.xml(完整)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>flowable-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>1.8</java.version>
<flowable.version>6.8.0</flowable.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>${flowable.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Demo 使用 H2 内存库,零配置即可启动。生产环境把数据源换成 MySQL,Flowable 配置基本不变。
3.3 启动类
java
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FlowableDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FlowableDemoApplication.class, args);
}
}
3.4 application.properties(完整)
properties
server.port=8088
# H2 内存数据库(Demo 用,重启后数据清空)
spring.datasource.url=jdbc:h2:mem:flowable-demo;DB_CLOSE_DELAY=-1;MODE=MySQL
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# Flowable:自动建表 + 自动部署 classpath 下的 BPMN
flowable.database-schema-update=true
flowable.async-executor-activate=false
flowable.process-definition-location-prefix=classpath:/processes/
flowable.process-definition-location-suffixes=**.bpmn20.xml,**.bpmn
三个配置项的含义:
database-schema-update=true:首次启动自动创建 Flowable 所需表async-executor-activate=false:关闭异步执行器,Demo 日志更干净process-definition-location-*:启动时自动部署processes/目录下的 BPMN 文件
3.5 请假审批 BPMN 流程(完整)
在 src/main/resources/processes/ 下新建 leave-approval.bpmn20.xml:
xml
<?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"
targetNamespace="http://demo.bpmn/leave">
<process id="leaveApproval" name="Leave Approval" isExecutable="true">
<startEvent id="start" name="Start"/>
<sequenceFlow id="flow_start_apply" sourceRef="start" targetRef="apply"/>
<!-- 第一步:申请人提交,指派给 ${applicant} -->
<userTask id="apply" name="Submit Leave Request" flowable:assignee="${applicant}">
<extensionElements>
<flowable:formProperty id="days" name="Days" type="long" required="true"/>
<flowable:formProperty id="reason" name="Reason" type="string" required="true"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow_apply_mgr" sourceRef="apply" targetRef="managerApprove"/>
<!-- 第二步:经理审批,指派给 ${manager} -->
<userTask id="managerApprove" name="Manager Approval" flowable:assignee="${manager}">
<extensionElements>
<flowable:formProperty id="approved" name="Approved" type="boolean" required="true"/>
<flowable:formProperty id="comment" name="Comment" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow_mgr_gateway" sourceRef="managerApprove" targetRef="approvedGateway"/>
<!-- 排他网关:根据 approved 变量分支 -->
<exclusiveGateway id="approvedGateway" name="Approved?"/>
<sequenceFlow id="flow_gateway_ok" sourceRef="approvedGateway" targetRef="endApproved">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_gateway_reject" sourceRef="approvedGateway" targetRef="endRejected">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
</sequenceFlow>
<endEvent id="endApproved" name="Approved"/>
<endEvent id="endRejected" name="Rejected"/>
</process>
</definitions>
流程说明:
- 启动时注入变量
applicant、manager,分别作为两个 UserTask 的处理人 - 经理审批节点写入
approved变量 - 排他网关根据
approved == true/false走向不同结束事件

图 1:请假审批流程 --- 提交 → 经理审批 → 网关分支 → 通过/驳回
3.6 请求 DTO(3 个类)
DeployXmlRequest.java
java
package com.demo.bpmn.api.dto;
import javax.validation.constraints.NotBlank;
public class DeployXmlRequest {
@NotBlank private String name;
@NotBlank private String bpmnXml;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getBpmnXml() { return bpmnXml; }
public void setBpmnXml(String bpmnXml) { this.bpmnXml = bpmnXml; }
}
StartProcessRequest.java
java
package com.demo.bpmn.api.dto;
import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
public class StartProcessRequest {
@NotBlank private String processDefinitionKey;
@NotBlank private String applicant;
@NotBlank private String manager;
private Long days;
private String reason;
public String getProcessDefinitionKey() { return processDefinitionKey; }
public void setProcessDefinitionKey(String key) { this.processDefinitionKey = key; }
public String getApplicant() { return applicant; }
public void setApplicant(String applicant) { this.applicant = applicant; }
public String getManager() { return manager; }
public void setManager(String manager) { this.manager = manager; }
public Long getDays() { return days; }
public void setDays(Long days) { this.days = days; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public Map<String, Object> toVariables() {
Map<String, Object> vars = new HashMap<>();
vars.put("applicant", applicant);
vars.put("manager", manager);
if (days != null) vars.put("days", days);
if (reason != null) vars.put("reason", reason);
return vars;
}
}
CompleteTaskRequest.java
java
package com.demo.bpmn.api.dto;
import javax.validation.constraints.NotBlank;
public class CompleteTaskRequest {
@NotBlank private String taskId;
@NotBlank private String userId;
private Boolean approved;
private String comment;
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Boolean getApproved() { return approved; }
public void setApproved(Boolean approved) { this.approved = approved; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
}
3.7 核心 Controller(完整)
新建 FlowableWorkflowController.java,统一前缀 /api/workflow:
java
package com.demo.bpmn.api;
import com.demo.bpmn.api.dto.*;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.task.api.Task;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Validated
@RestController
@RequestMapping("/api/workflow")
public class FlowableWorkflowController {
private final RepositoryService repositoryService;
private final RuntimeService runtimeService;
private final TaskService taskService;
private final HistoryService historyService;
public FlowableWorkflowController(RepositoryService repositoryService,
RuntimeService runtimeService,
TaskService taskService,
HistoryService historyService) {
this.repositoryService = repositoryService;
this.runtimeService = runtimeService;
this.taskService = taskService;
this.historyService = historyService;
}
/** 查询已部署的流程定义 */
@GetMapping("/process-definitions")
public List<Map<String, Object>> listProcessDefinitions() {
List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
.latestVersion().orderByProcessDefinitionKey().asc().list();
List<Map<String, Object>> out = new ArrayList<>();
for (ProcessDefinition pd : list) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", pd.getId());
row.put("key", pd.getKey());
row.put("name", pd.getName());
row.put("version", pd.getVersion());
out.add(row);
}
return out;
}
/** 部署前端传来的 BPMN XML */
@PostMapping("/deploy-xml")
public Map<String, Object> deployXml(@Valid @RequestBody DeployXmlRequest req) {
String resourceName = req.getName().endsWith(".bpmn20.xml")
? req.getName() : req.getName() + ".bpmn20.xml";
Deployment deployment = repositoryService.createDeployment()
.name(req.getName())
.addBytes(resourceName, req.getBpmnXml().getBytes(StandardCharsets.UTF_8))
.deploy();
Map<String, Object> out = new LinkedHashMap<>();
out.put("deploymentId", deployment.getId());
out.put("name", deployment.getName());
return out;
}
/** 导出流程 XML */
@GetMapping(value = "/process-definition/{id}/xml", produces = MediaType.APPLICATION_XML_VALUE)
public String getProcessDefinitionXml(@PathVariable("id") String processDefinitionId) throws IOException {
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(processDefinitionId).singleResult();
if (pd == null) throw new IllegalArgumentException("ProcessDefinition not found");
try (InputStream in = repositoryService.getProcessModel(pd.getId())) {
return new String(readAllBytes(in), StandardCharsets.UTF_8);
}
}
/** 发起流程实例 */
@PostMapping("/start")
public Map<String, Object> start(@Valid @RequestBody StartProcessRequest req) {
org.flowable.engine.runtime.ProcessInstance pi = runtimeService.startProcessInstanceByKey(
req.getProcessDefinitionKey(), req.toVariables());
Map<String, Object> out = new LinkedHashMap<>();
out.put("processInstanceId", pi.getId());
out.put("processDefinitionKey", pi.getProcessDefinitionKey());
return out;
}
/** 查询待办 */
@GetMapping("/tasks")
public List<Map<String, Object>> listTasks(@RequestParam String assignee) {
List<Task> list = taskService.createTaskQuery()
.taskAssignee(assignee).orderByTaskCreateTime().desc().list();
List<Map<String, Object>> out = new ArrayList<>();
for (Task t : list) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", t.getId());
row.put("name", t.getName());
row.put("processInstanceId", t.getProcessInstanceId());
row.put("createTime", t.getCreateTime());
out.add(row);
}
return out;
}
/** 查看任务变量 */
@GetMapping("/tasks/{taskId}/variables")
public Map<String, Object> getTaskVariables(@PathVariable String taskId) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) throw new IllegalArgumentException("Task not found: " + taskId);
Map<String, Object> vars = taskService.getVariables(taskId);
return vars == null ? Collections.emptyMap() : vars;
}
/** 完成 / 审批任务 */
@PostMapping("/tasks/complete")
public Map<String, Object> complete(@Valid @RequestBody CompleteTaskRequest req) {
Task task = taskService.createTaskQuery().taskId(req.getTaskId()).singleResult();
if (task == null) throw new IllegalArgumentException("Task not found");
if (task.getAssignee() != null && !Objects.equals(task.getAssignee(), req.getUserId())) {
throw new IllegalArgumentException("Task is not assigned to: " + req.getUserId());
}
Map<String, Object> vars = new HashMap<>();
if (req.getApproved() != null) vars.put("approved", req.getApproved());
if (req.getComment() != null) vars.put("comment", req.getComment());
taskService.complete(req.getTaskId(), vars);
Map<String, Object> out = new LinkedHashMap<>();
out.put("completed", true);
out.put("taskId", req.getTaskId());
return out;
}
/** 查询历史流程实例 */
@GetMapping("/history/process-instances")
public List<Map<String, Object>> listHistory() {
List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery()
.orderByProcessInstanceStartTime().desc().listPage(0, 50);
List<Map<String, Object>> out = new ArrayList<>();
for (HistoricProcessInstance h : list) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", h.getId());
row.put("processDefinitionKey", h.getProcessDefinitionKey());
row.put("startTime", h.getStartTime());
row.put("endTime", h.getEndTime());
out.add(row);
}
return out;
}
private static byte[] readAllBytes(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
return out.toByteArray();
}
}

图 2:Vue 3 前端 bpmn-ui 通过 REST API 调用 Flowable 引擎,数据持久化到 H2 数据库
3.8 跨域与异常处理
前端开发时需要跨域支持,新建 CorsConfig.java:
java
package com.demo.bpmn.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.*;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
新建 ApiExceptionHandler.java,统一返回错误格式:
java
package com.demo.bpmn.api;
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
public ResponseEntity<Map<String, Object>> handleBadRequest(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBody("BAD_REQUEST", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleInvalid(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getAllErrors().isEmpty()
? "Validation failed"
: ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBody("VALIDATION_ERROR", msg));
}
private static Map<String, Object> errorBody(String code, String message) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("code", code);
body.put("message", message);
return body;
}
}
3.9 启动后端并验证
bash
mvn spring-boot:run
浏览器访问 http://localhost:8088/api/workflow/process-definitions,应返回包含 leaveApproval 的 JSON。
也可以用 curl 走一遍审批:
bash
# 1. 发起流程
curl -X POST http://localhost:8088/api/workflow/start \
-H "Content-Type: application/json" \
-d '{"processDefinitionKey":"leaveApproval","applicant":"alice","manager":"bob","days":1,"reason":"年假"}'
# 2. 查 bob 的待办(注意:发起后第一个任务是 alice 的,需 alice 先完成提交任务)
curl "http://localhost:8088/api/workflow/tasks?assignee=alice"
# 3. 完成 alice 的任务(把 {taskId} 替换为上一步返回的 id)
curl -X POST http://localhost:8088/api/workflow/tasks/complete \
-H "Content-Type: application/json" \
-d '{"taskId":"{taskId}","userId":"alice"}'
# 4. 再查 bob 的待办,此时应出现经理审批任务
curl "http://localhost:8088/api/workflow/tasks?assignee=bob"
# 5. bob 审批通过
curl -X POST http://localhost:8088/api/workflow/tasks/complete \
-H "Content-Type: application/json" \
-d '{"taskId":"{taskId}","userId":"bob","approved":true,"comment":"同意"}'
四、搭建前端(bpmn-js 在线建模)
前端侧,bpmn-js 是 Camunda 开源的 BPMN 2.0 建模与渲染库,让我们在浏览器里直接画流程图、导入/导出 XML。

图 6:bpmn-js 分层架构 + Vue 3 组件封装(onMounted 初始化 / onUnmounted 销毁)
bpmn-js 的分层结构:
| 层级 | 模块 | 职责 |
|---|---|---|
| 数据层 | bpmn-moddle |
BPMN XML ↔ 内存对象模型的解析与序列化 |
| 渲染层 | diagram-js |
在 Canvas 上用 SVG 绘制流程图元素 |
| 交互层 | BpmnModeler |
提供拖拽建模、连线、属性编辑等能力 |
| 事件层 | EventBus + CommandStack |
监听编辑变更、支持撤销/重做 |
与我们 Demo 相关的封装方式(Vue 3 组合式 API):
ts
// onMounted:初始化建模器并加载 XML
onMounted(() => {
modeler = new BpmnModeler({ container: containerRef.value })
await modeler.importXML(bpmnXml)
})
// commandStack.changed:编辑后导出 XML
const { xml } = await modeler.saveXML({ format: true })
// onUnmounted:销毁实例,防止内存泄漏
onUnmounted(() => modeler?.destroy())
前端画好的 XML 通过 /deploy-xml 发给后端,由 Flowable 的 RepositoryService 完成部署------前端负责「画」,后端负责「跑」。
4.1 初始化 Vite + Vue 项目
bash
npm create vite@4.4.1 flowable-ui -- --template vue-ts
cd flowable-ui
npm install
npm install bpmn-js axios
Node 18 请使用
create-vite@4.4.1;Node 20+ 可用最新版。
4.2 安装后的 package.json 依赖段
确认 dependencies / devDependencies 包含:
json
{
"dependencies": {
"axios": "^1.16.1",
"bpmn-js": "^18.16.1",
"vue": "^3.4.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.6.2",
"typescript": "~5.3.3",
"vite": "^4.4.5",
"vue-tsc": "^1.8.27"
}
}
4.3 vite.config.ts(完整)
配置 Vue 插件与开发代理,把 /api 转发到后端 8088:
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8088',
changeOrigin: true,
},
},
},
})
4.4 入口文件
index.html
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flowable + bpmn-js 审批工作流</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
src/main.ts
ts
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
4.5 API 封装 src/api/workflow.ts(完整)
ts
import axios from 'axios';
const http = axios.create({ baseURL: '/api/workflow' });
export async function listProcessDefinitions() {
const { data } = await http.get('/process-definitions');
return data;
}
export async function deployXml(name: string, bpmnXml: string) {
const { data } = await http.post('/deploy-xml', { name, bpmnXml });
return data;
}
export async function startProcess(req: {
processDefinitionKey: string;
applicant: string;
manager: string;
days?: number;
reason?: string;
}) {
const { data } = await http.post('/start', req);
return data;
}
export async function listTasks(assignee: string) {
const { data } = await http.get('/tasks', { params: { assignee } });
return data;
}
export async function getTaskVariables(taskId: string) {
const { data } = await http.get(`/tasks/${taskId}/variables`);
return data;
}
export async function completeTask(req: {
taskId: string; userId: string; approved?: boolean; comment?: string;
}) {
const { data } = await http.post('/tasks/complete', req);
return data;
}
4.6 默认 BPMN 模板 src/bpmn/defaultDiagram.ts
ts
export const DEFAULT_BPMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
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"
xmlns:flowable="http://flowable.org/bpmn"
id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="demoProcess" name="Demo Process" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="Start" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="demoProcess">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>`;
4.7 bpmn-js 建模器 src/components/BpmnModelerView.vue(完整)
Vue 组件中用 onMounted 初始化 bpmn-js,用 onUnmounted 销毁实例:
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import BpmnModeler from 'bpmn-js/lib/Modeler'
import { DEFAULT_BPMN_XML } from '../bpmn/defaultDiagram'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
const props = defineProps<{ initialXml?: string }>()
const emit = defineEmits<{ xmlChanged: [xml: string] }>()
const containerRef = ref<HTMLDivElement | null>(null)
const error = ref<string | null>(null)
let modeler: BpmnModeler | null = null
onMounted(() => {
if (!containerRef.value) return
modeler = new BpmnModeler({ container: containerRef.value, keyboard: { bindTo: window } })
void modeler.importXML(props.initialXml ?? DEFAULT_BPMN_XML)
const eventBus = modeler.get('eventBus') as { on: (e: string, fn: () => void) => void }
eventBus.on('commandStack.changed', async () => {
const { xml } = await modeler!.saveXML({ format: true })
if (typeof xml === 'string') emit('xmlChanged', xml)
})
})
onUnmounted(() => { modeler?.destroy(); modeler = null })
</script>
<template>
<div class="modelerRoot">
<div v-if="error" class="modelerError">{{ error }}</div>
<div ref="containerRef" class="modelerCanvas" />
</div>
</template>
4.8 主页面 src/App.vue(完整)
页面分为左侧建模画布 + 右侧操作面板。使用 :key="modelerKey" 在「新建 / 导入」时重建建模器,避免编辑中重复挂载:
vue
<template>
<div class="appShell">
<div class="topbar">
<div class="title">Flowable + bpmn-js 审批工作流 Demo</div>
<div class="btnRow">
<button class="btn" @click="resetDiagram">新建</button>
<button class="btn" @click="onImportXml">导入XML</button>
<button class="btn" @click="downloadText(`${deployName}.bpmn20.xml`, xml)">导出XML</button>
</div>
</div>
<div class="body">
<div class="canvasWrap">
<BpmnModelerView :key="modelerKey" :initial-xml="xml" @xml-changed="xml = $event" />
</div>
<div class="side">
<!-- 部署 / 发起 / 待办审批 三个 card,调用 workflow.ts 中的 API -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import BpmnModelerView from './components/BpmnModelerView.vue'
import { completeTask, deployXml, getTaskVariables, listProcessDefinitions, listTasks, startProcess } from './api/workflow'
import { DEFAULT_BPMN_XML } from './bpmn/defaultDiagram'
const xml = ref(DEFAULT_BPMN_XML)
const modelerKey = ref(0)
const deployName = ref('leave-approval')
// ... applicant、manager、taskUser 等 ref 状态
function resetDiagram() {
xml.value = DEFAULT_BPMN_XML
modelerKey.value += 1
}
onMounted(() => { listProcessDefinitions().then(/* 填充流程定义下拉框 */) })
</script>
<style src="./App.css"></style>
完整的
App.vue逻辑(部署、发起、待办刷新、审批完成)与 React 版一致,只是状态管理改为 Vue 的ref/computed/watch。照抄上文第三节后端 API,把axios调用填入即可。

图 3:Vue 3 + bpmn-js 前端 --- 左侧在线建模,右侧部署 / 发起 / 待办审批
4.9 样式文件
src/index.css --- 让页面铺满视口:
css
html, body, #app {
width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
}
body { min-width: 320px; font-family: Inter, system-ui, sans-serif; }
src/App.css --- 布局与暗色主题(完整):
css
*, *::before, *::after { box-sizing: border-box; }
.appShell {
width: 100%; height: 100%; display: grid;
grid-template-rows: minmax(56px, auto) 1fr;
background: #0b1220; color: #e5e7eb; overflow: hidden;
}
.topbar {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 0 16px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.title { font-weight: 700; }
.btnRow { display: flex; gap: 8px; flex-wrap: wrap; }
.btn {
border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06);
color: #e5e7eb; padding: 8px 10px; border-radius: 10px; font-size: 12px; cursor: pointer;
}
.btnPrimary { background: #2563eb; border-color: #2563eb; }
.body {
display: grid; grid-template-columns: minmax(0,1fr) minmax(280px,360px);
min-height: 0; overflow: hidden;
}
.canvasWrap { min-width: 0; min-height: 0; background: #111827; }
.side {
border-left: 1px solid rgba(255,255,255,0.08); padding: 12px;
display: flex; flex-direction: column; gap: 12px; overflow: auto;
}
.card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px; padding: 12px;
}
.cardTitle { font-weight: 700; font-size: 13px; margin-bottom: 8px; }
.input, .select {
width: 100%; height: 34px; margin-top: 6px; border-radius: 10px;
border: 1px solid rgba(255,255,255,0.12); background: rgba(0,0,0,0.25);
color: #e5e7eb; padding: 0 10px;
}
.hint { font-size: 12px; color: rgba(229,231,235,0.7); }
.mono {
font-family: monospace; font-size: 12px; white-space: pre-wrap;
margin-top: 8px; color: rgba(229,231,235,0.8);
}
@media (max-width: 960px) {
.body { grid-template-columns: 1fr; grid-template-rows: minmax(280px,1fr) auto; }
.side { max-height: 42vh; border-left: none; border-top: 1px solid rgba(255,255,255,0.08); }
}
4.10 启动前端
bash
npm run dev
浏览器打开 http://localhost:5173,即可看到左侧建模器 + 右侧操作面板。
五、页面操作:走完审批闭环
按以下顺序在页面上操作:
| 步骤 | 操作人 | 页面操作 | 预期 |
|---|---|---|---|
| 1 | --- | 确认流程定义列表中有 leaveApproval |
后端启动时已自动部署 |
| 2 | alice | 发起流程:applicant=alice,manager=bob |
提示发起成功 |
| 3 | alice | 待办:assignee=alice → 刷新 → 完成提交任务 |
任务消失 |
| 4 | bob | 待办:assignee=bob → 刷新 |
出现「Manager Approval」 |
| 5 | bob | 选择「通过」→ 审批/完成 | 任务消失,流程结束 |
若 bob 选择「驳回」,流程走到 Rejected 结束节点。

图 4:经理审批后,根据 approved 变量走通过或驳回结束节点
六、从 Demo 到生产的 4 个建议
- 身份体系 :Demo 用字符串
assignee,生产应接入用户 / 角色 / 候选组 - 业务关联 :启动时写入
businessKey(如请假单 ID),串联业务表与流程实例 - 变量规范 :前后端约定统一变量命名,避免
approved/isApproved漂移 - 权限校验 :
tasks/complete必须校验当前用户是否为任务处理人
写在最后
本文从零搭建了一套完整的审批工作流 Demo:
- 后端:Flowable 引擎 + REST API,H2 零配置启动
- 前端:bpmn-js 在线建模 + 一键部署 + 发起审批
- 流程:请假审批,两个 UserTask + 排他网关
实际项目中你还可以继续扩展:接入 Camunda Modeler 画复杂流程、用监听器发消息通知、把 H2 换成 MySQL 并对接现有用户体系。
难的不是引擎本身,而是把流程、表单、权限三件事设计清楚。 希望这篇实战能帮你快速上手。