基于 Camunda BPM 的工作流引擎示例项目

发布于:2025-07-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

项目介绍

这是一个基于 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

功能特性

  • 流程定义管理
    • 流程设计器
    • 流程部署
    • 流程定义列表
    • 流程图查看
  • 流程实例管理
    • 启动流程
    • 实例列表
    • 实例流程图
  • 任务管理
    • 任务列表
    • 任务处理

使用说明

环境准备

  1. 安装 JDK 1.8 或以上版本
  2. 安装 MySQL 8.0
  3. 安装 Node.js 16.0 或以上版本

数据库配置

  1. 创建数据库
CREATE DATABASE camunda DEFAULT CHARACTER SET utf8mb4;
  1. 修改数据库配置
    编辑 server/src/main/resources/application.yaml 文件:
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/camunda
    username: your_username
    password: your_password

后端启动

  1. 进入后端项目目录
cd server
  1. 编译打包
mvn clean package
  1. 运行项目
java -jar target/camunda-demo-1.0-SNAPSHOT.jar

前端启动

  1. 进入前端项目目录
cd web
  1. 安装依赖
npm install
  1. 启动开发服务器
npm run dev
  1. 访问项目
    打开浏览器访问: http://localhost:5173

使用流程

  1. 流程设计

    • 点击"新建流程"进入流程设计器
    • 使用工具栏组件设计流程
    • 设计完成后点击"部署"按钮部署流程
  2. 流程管理

    • 在流程定义列表中查看所有已部署的流程
    • 可以查看流程图、编辑流程、启动新的流程实例
  3. 流程执行

    • 在流程实例列表中查看所有运行中的流程实例
    • 点击查看实例图可以查看当前流程的执行状态
  4. 任务处理

    • 在任务列表中查看待处理的任务
    • 点击"完成任务"按钮处理任务

开发说明

  • 后端接口统一以 /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 转换为 String
    private 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 的 title
  function 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 = url
  a.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) return

  const 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>