项目介绍
这是一个基于 Camunda BPM 的工作流引擎示例项目,包含完整的后台接口和前端页面,实现了流程的设计、部署、执行等核心功能。
技术栈
后端
- Spring Boot 2.7.9
- Camunda BPM 7.18.0
- MySQL 8.0
- JDK 1.8
前端
- Vue 3
- Element Plus
- Bpmn.js
- Vite
功能特性
- 流程定义管理
- 流程设计器
- 流程部署
- 流程定义列表
- 流程图查看
- 流程实例管理
- 启动流程
- 实例列表
- 实例流程图
- 任务管理
- 任务列表
- 任务处理
使用说明
环境准备
- 安装 JDK 1.8 或以上版本
- 安装 MySQL 8.0
- 安装 Node.js 16.0 或以上版本
数据库配置
- 创建数据库
CREATE DATABASE camunda DEFAULT CHARACTER SET utf8mb4;
- 修改数据库配置
编辑server/src/main/resources/application.yaml
文件:
spring:datasource:url: jdbc:mysql://localhost:3306/camundausername: your_usernamepassword: your_password
后端启动
- 进入后端项目目录
cd server
- 编译打包
mvn clean package
- 运行项目
java -jar target/camunda-demo-1.0-SNAPSHOT.jar
前端启动
- 进入前端项目目录
cd web
- 安装依赖
npm install
- 启动开发服务器
npm run dev
- 访问项目
打开浏览器访问: http://localhost:5173
使用流程
-
流程设计
- 点击"新建流程"进入流程设计器
- 使用工具栏组件设计流程
- 设计完成后点击"部署"按钮部署流程
-
流程管理
- 在流程定义列表中查看所有已部署的流程
- 可以查看流程图、编辑流程、启动新的流程实例
-
流程执行
- 在流程实例列表中查看所有运行中的流程实例
- 点击查看实例图可以查看当前流程的执行状态
-
任务处理
- 在任务列表中查看待处理的任务
- 点击"完成任务"按钮处理任务
开发说明
- 后端接口统一以
/api
开头 - 前端开发服务器已配置代理,自动转发
/api
请求到后端服务 - 流程设计器基于 bpmn-js,支持标准 BPMN 2.0 规范
注意事项
- 首次启动时,Camunda 会自动创建所需的数据库表
- 建议使用 Chrome 或 Firefox 浏览器访问
- 流程图导出支持 BPMN 格式
源码下载
Camunda Demo
演示截图
1.流程定义列表
2.流程实例列表
3.任务列表
4.流程定义页面
5.流程定义编辑页面
核心源码
server/pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>camunda-demo</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.9</version></parent><properties><java.version>1.8</java.version><camunda.spring-boot.version>7.18.0</camunda.spring-boot.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-rest</artifactId><version>${camunda.spring-boot.version}</version></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId><version>${camunda.spring-boot.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies>
</project>
server/src/main/java/com/example/controller/ProcessController.java
package com.example.controller;import com.example.dto.ProcessDefinitionDTO;
import com.example.dto.ProcessInstanceDTO;
import com.example.dto.TaskDTO;
import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.repository.Deployment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;@RestController
@RequestMapping("/api/process")
@RequiredArgsConstructor
public class ProcessController {private final ProcessEngine processEngine;@GetMapping("/definitions")public List<ProcessDefinitionDTO> getProcessDefinitions() {List<ProcessDefinitionDTO> list = processEngine.getRepositoryService().createProcessDefinitionQuery().latestVersion().list().stream().map(def -> new ProcessDefinitionDTO(def.getId(),def.getDeploymentId(),null,null,def.getVersion())).collect(Collectors.toList());for (ProcessDefinitionDTO dto : list) {Deployment deployment = processEngine.getRepositoryService().createDeploymentQuery().deploymentId(dto.getDeploymentId()).singleResult();dto.setName(deployment.getName());dto.setCreateTime(deployment.getDeploymentTime());}return list;}@GetMapping("/instances")public List<ProcessInstanceDTO> getProcessInstances() {return processEngine.getRuntimeService().createProcessInstanceQuery().list().stream().map(instance -> new ProcessInstanceDTO(instance.getId(),instance.getProcessDefinitionId(),instance.getBusinessKey())).collect(Collectors.toList());}@GetMapping("/tasks")public List<TaskDTO> getTasks() {return processEngine.getTaskService().createTaskQuery().list().stream().map(task -> new TaskDTO(task.getId(),task.getName(),task.getAssignee(),task.getProcessInstanceId())).collect(Collectors.toList());}@PostMapping("/start")public ResponseEntity<Void> startProcess(@RequestParam String processDefinitionId, @RequestBody(required = false) Map<String, Object> variables) {processEngine.getRuntimeService().startProcessInstanceById(processDefinitionId, variables);return ResponseEntity.ok().build();}@PostMapping("/tasks/{taskId}/complete")public void completeTask(@PathVariable String taskId, @RequestBody(required = false) Map<String, Object> variables) {processEngine.getTaskService().complete(taskId, variables);}@GetMapping("/definition/xml")public String getProcessDefinitionXml(@RequestParam String processDefinitionId) {InputStream inputStream = processEngine.getRepositoryService().getProcessModel(processDefinitionId);return convertStreamToString(inputStream);}@GetMapping("/instance/xml")public String getProcessInstanceXml(@RequestParam String processDefinitionId, @RequestParam String procInstId) {InputStream inputStream = processEngine.getRepositoryService().getProcessModel(processDefinitionId);return convertStreamToString(inputStream);}// 将 InputStream 转换为 Stringprivate String convertStreamToString(InputStream is) {if (is == null) {return null;}BufferedReader reader = new BufferedReader(new InputStreamReader(is));StringBuilder sb = new StringBuilder();String line;try {while ((line = reader.readLine()) != null) {sb.append(line).append("\n");}} catch (IOException e) {e.printStackTrace();} finally {try {is.close();} catch (IOException e) {e.printStackTrace();}}return sb.toString();}@PostMapping("/deploy")public ResponseEntity<Void> deployProcess(@RequestParam String processName, @RequestBody Map<String, Object> map) {String bpmnXml = (String) map.get("bpmnXml");processEngine.getRepositoryService().createDeployment().name(processName).addString("process.bpmn", bpmnXml).deploy();return ResponseEntity.ok().build();}
}
web/src/main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'import 'bpmn-js/dist/assets/bpmn-js.css'
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 app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')
web/src/views/ProcessDefinitionList.vue
<template><el-table :data="definitions" border><el-table-column prop="deploymentId" label="流程定义ID" align="center"></el-table-column><el-table-column prop="name" label="流程名称" width="200" align="center"></el-table-column><el-table-column label="创建时间" width="200" align="center"><template #default="scope">{{ formatDate(scope.row.createTime) }}</template></el-table-column><el-table-column prop="version" label="版本" width="80" align="center"></el-table-column><el-table-column label="操作" width="360" align="center"><template #default="scope"><el-button type="success" @click="viewDefinition(scope.row.processDefinitionId)">查看流程图</el-button><el-button type="warning" @click="editDefinition(scope.row.processDefinitionId)">编辑流程</el-button><el-button type="primary" @click="startProcess(scope.row.processDefinitionId)">启动流程</el-button></template></el-table-column></el-table>
</template><script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import {useRouter} from "vue-router";const definitions = ref([])const fetchDefinitions = async () => {const response = await axios.get('/api/process/definitions')definitions.value = response.data
}const startProcess = async (processDefinitionId) => {try {await axios.post(`/api/process/start?processDefinitionId=${processDefinitionId}`)ElMessage.success('流程启动成功')} catch (error) {ElMessage.error('流程启动失败')}
}const router = useRouter()const viewDefinition = (processDefinitionId) => {router.push({path: "/view/definition",query: {processDefinitionId},})
}const editDefinition = (processDefinitionId) => {router.push({path: "/definition",query: {processDefinitionId},})
}onMounted(() => {fetchDefinitions()
})function formatDate(isoString) {if (!isoString) return '';const date = new Date(isoString);const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
</script>
web/src/views/ProcessEditor.vue
<template><div style="margin-bottom: 20px"><el-button type="primary" @click="saveBpmn">部署</el-button><el-button type="info" @click="exportBpmn">导出 BPMN</el-button><el-button type="danger" @click="triggerImport">导入 BPMN</el-button><!-- 隐藏的文件输入 --><input type="file" ref="fileInput" @change="importBpmn" accept=".bpmn,.xml" hidden /></div><div ref="canvas" class="canvas"></div>
</template><script setup>
import {onMounted, ref} from 'vue'
import BpmnModeler from 'bpmn-js/lib/Modeler'
import {ElMessage, ElMessageBox} from "element-plus";
import axios from "axios";
import {useRoute, useRouter} from "vue-router";const canvas = ref()
let bpmnModeler = null// 示例 BPMN 内容(你可以从后端获取或创建空流程)
let defaultBpmnXml = `
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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:camunda="http://camunda.org/schema/1.0/bpmn"id="Definitions_0fr9mxs"targetNamespace="http://bpmn.io/schema/bpmn"><bpmn:process id="process" name="流程" isExecutable="true"></bpmn:process><bpmndi:BPMNDiagram id="BPMNDiagram-process"><bpmndi:BPMNPlane id="BPMNPlane-process" bpmnElement="process"></bpmndi:BPMNPlane></bpmndi:BPMNDiagram>
</bpmn:definitions>
`.trim()const route = useRoute()onMounted(async () => {bpmnModeler = new BpmnModeler({container: canvas.value,})if (route.query.processDefinitionId) {const res = await axios.get(`/api/process/definition/xml?processDefinitionId=${route.query.processDefinitionId}`)if (res.data) {defaultBpmnXml = res.data;}}// 加载默认 BPMN 或从后端获取现有流程定义await bpmnModeler.importXML(defaultBpmnXml)// 中英文映射表const titleMap = new Map([["Activate global connect tool", "连线"],["Create start event", "创建开始事件"],["Create end event", "创建结束事件"],["Create task", "创建任务"]]);// 白名单:只显示这些功能项(基于 data-action)const allowedActions = ['create.start-event','create.end-event','global-connect-tool','create.task',];// 白名单:只显示这些功能项(基于 data-action)const contextPadAllowedActions = ['replace','delete','connect',];// 更新所有 .entry 的 titlefunction updateTitles() {document.querySelectorAll('.djs-palette-entries .entry').forEach(entry => {const originalTitle = entry.getAttribute('title');if (originalTitle && titleMap.has(originalTitle)) {entry.setAttribute('title', titleMap.get(originalTitle));}});}// 控制显示哪些条目(基于 data-action)function setDisplayByAction(allowedActions) {document.querySelectorAll('.djs-palette-entries .entry').forEach(entry => {const action = entry.getAttribute('data-action');entry.style.display = allowedActions.includes(action) ? 'block' : 'none';});}// 控制显示哪些条目(基于 data-action)function setContextPadDisplayByAction(allowedActions) {document.querySelectorAll('.djs-context-pad-parent .djs-context-pad .group .entry').forEach(entry => {const action = entry.getAttribute('data-action');entry.style.display = allowedActions.includes(action) ? 'block' : 'none';});}// 执行函数updateTitles();setDisplayByAction(allowedActions);// 新增:监听上下文菜单变化const observer = new MutationObserver(() => {setContextPadDisplayByAction(contextPadAllowedActions)})const contextPadParent = document.querySelector('.djs-context-pad-parent')if (contextPadParent) {observer.observe(contextPadParent, {childList: true,subtree: true})} else {// 如果还没渲染出来,可以稍后重试setTimeout(() => {const contextPadParent = document.querySelector('.djs-context-pad-parent')if (contextPadParent) {observer.observe(contextPadParent, {childList: true,subtree: true})}}, 1000)}
})const router = useRouter()const saveBpmn = async () => {const {xml} = await bpmnModeler.saveXML({format: true})ElMessageBox.prompt('请输入流程名称', '流程部署', {confirmButtonText: '确认',cancelButtonText: '取消',}).then(({value}) => {try {axios.post('/api/process/deploy', {bpmnXml: xml}, {params: {processName: value}})ElMessage.success('流程部署成功')router.push({path: "/",})} catch (error) {ElMessage.error('流程部署失败')console.error(error)}}).catch(() => {})
}const fileInput = ref();// ===== 导出 BPMN 文件 =====
const exportBpmn = async () => {const { xml } = await bpmnModeler.saveXML({ format: true })const blob = new Blob([xml], { type: "application/xml;charset=utf-8" })const url = URL.createObjectURL(blob)const a = document.createElement("a")a.href = urla.download = "process.bpmn"a.click()URL.revokeObjectURL(url)
}// ===== 触发文件选择 =====
const triggerImport = () => {fileInput.value.click()
}// ===== 导入 BPMN 文件 =====
const importBpmn = async (event) => {const file = event.target.files[0]if (!file) returnconst reader = new FileReader()reader.onload = async () => {try {await bpmnModeler.importXML(reader.result + "")ElMessage.success('BPMN 文件导入成功')} catch (err) {ElMessage.error('BPMN 文件导入失败')console.error(err)}}reader.readAsText(file)
}
</script><style scoped>
.canvas {height: calc(100% - 60px);border: 1px solid #ccc;
}
</style>