feat(wm-bpm-engine): #2 业务流程引擎

- 流程定义 CRUD + 版本管理 + 发布
- 流程实例启动/列表/状态跟踪
- 待办/已办任务管理(审批/驳回/转办)
- 流程模板快速创建
- 统计评估(完成率/运行中实例)
- DDL: bpm_process_definition/instance/task_item/template
- 增强 wm-bpm 模块(新增 FormTemplate/Orchestration/ProcessNode/ProcessStat/TodoTask)
This commit is contained in:
2026-06-14 13:52:36 +08:00
parent f808efdc80
commit c2295fba2c
34 changed files with 1269 additions and 24 deletions
+63
View File
@@ -0,0 +1,63 @@
<?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>
<parent>
<groupId>com.water</groupId>
<artifactId>wm-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>wm-bpm-engine</artifactId>
<name>wm-bpm-engine</name>
<description>BPM 业务流程引擎模块</description>
<dependencies>
<dependency>
<groupId>com.water</groupId>
<artifactId>wm-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,7 @@
package com.water.bpmengine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BpmEngineApplication {
public static void main(String[] args) { SpringApplication.run(BpmEngineApplication.class, args); }
}
@@ -0,0 +1,77 @@
package com.water.bpmengine.controller;
import com.water.common.core.result.R;
import com.water.bpmengine.entity.*;
import com.water.bpmengine.service.BpmEngineService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Tag(name = "业务流程引擎")
@RestController @RequestMapping("/bpm") @RequiredArgsConstructor
public class BpmEngineController {
private final BpmEngineService svc;
@GetMapping("/definition/list")
public R<List<ProcessDefinition>> listDefs(
@RequestParam(required=false) String category,
@RequestParam(required=false) Integer status) {
return R.ok(svc.listDefinitions(category, status));
}
@PostMapping("/definition")
public R<Long> createDef(@RequestBody Map<String,Object> req) {
return R.ok(svc.createDefinition(req));
}
@PostMapping("/definition/{id}/publish")
public R<String> publish(@PathVariable Long id) {
svc.publishDefinition(id); return R.ok("OK");
}
@PostMapping("/process/start")
public R<Map<String,Object>> start(
@RequestParam Long definitionId,
@RequestParam String title,
@RequestParam String initiator) {
return R.ok(svc.startProcess(definitionId, title, initiator));
}
@GetMapping("/process/list")
public R<List<ProcessInstance>> listInstances(
@RequestParam(required=false) String initiator,
@RequestParam(required=false) Integer status) {
return R.ok(svc.listInstances(initiator, status));
}
@GetMapping("/task/todo")
public R<List<TaskItem>> todo(@RequestParam String assignee) {
return R.ok(svc.getTodoTasks(assignee));
}
@GetMapping("/task/done")
public R<List<TaskItem>> done(@RequestParam String assignee) {
return R.ok(svc.getDoneTasks(assignee));
}
@PostMapping("/task/{id}/approve")
public R<Map<String,Object>> approve(@PathVariable Long id,
@RequestParam(required=false) String comment) {
return R.ok(svc.approveTask(id, comment));
}
@PostMapping("/task/{id}/reject")
public R<Map<String,Object>> reject(@PathVariable Long id,
@RequestParam(required=false) String comment) {
return R.ok(svc.rejectTask(id, comment));
}
@PostMapping("/task/{id}/transfer")
public R<Map<String,Object>> transfer(@PathVariable Long id,
@RequestParam String newAssignee) {
return R.ok(svc.transferTask(id, newAssignee));
}
@GetMapping("/statistics")
public R<Map<String,Object>> stats(@RequestParam(required=false) String processKey) {
return R.ok(svc.getStatistics(processKey));
}
@GetMapping("/template/list")
public R<List<ProcessTemplate>> listTemplates() {
return R.ok(svc.listTemplates());
}
@PostMapping("/template")
public R<Long> createTemplate(@RequestBody Map<String,Object> req) {
return R.ok(svc.createTemplate(req));
}
}
@@ -0,0 +1,13 @@
package com.water.bpmengine.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("bpm_process_definition")
public class ProcessDefinition {
@TableId(type = IdType.AUTO) private Long id;
private String processKey, name, category;
private String bpmnXml;
private Integer version; private Integer status; // 0草稿 1已发布 2已停用
private String description;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
@TableField(fill=FieldFill.INSERT_UPDATE) private LocalDateTime updatedTime;
}
@@ -0,0 +1,13 @@
package com.water.bpmengine.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("bpm_process_instance")
public class ProcessInstance {
@TableId(type = IdType.AUTO) private Long id;
private Long definitionId; private String processKey;
private String title, initiator;
private Integer status; // 0运行中 1已完成 2已驳回 3已撤销
private String currentNodeName;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
private LocalDateTime completedTime;
}
@@ -0,0 +1,11 @@
package com.water.bpmengine.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("bpm_process_template")
public class ProcessTemplate {
@TableId(type = IdType.AUTO) private Long id;
private String name, category, description;
private String bpmnXml;
private Integer status;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
}
@@ -0,0 +1,13 @@
package com.water.bpmengine.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("bpm_task_item")
public class TaskItem {
@TableId(type = IdType.AUTO) private Long id;
private Long instanceId; private String taskName, taskType;
private String assignee;
private Integer status; // 0待办 1已办 2驳回 3转办
private String comment;
@TableField(fill=FieldFill.INSERT) private LocalDateTime createdTime;
private LocalDateTime completedTime;
}
@@ -0,0 +1,5 @@
package com.water.bpmengine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.bpmengine.entity.ProcessDefinition;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface ProcessDefinitionMapper extends BaseMapper<ProcessDefinition> {}
@@ -0,0 +1,5 @@
package com.water.bpmengine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.bpmengine.entity.ProcessInstance;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface ProcessInstanceMapper extends BaseMapper<ProcessInstance> {}
@@ -0,0 +1,5 @@
package com.water.bpmengine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.bpmengine.entity.ProcessTemplate;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface ProcessTemplateMapper extends BaseMapper<ProcessTemplate> {}
@@ -0,0 +1,5 @@
package com.water.bpmengine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.bpmengine.entity.TaskItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface TaskItemMapper extends BaseMapper<TaskItem> {}
@@ -0,0 +1,122 @@
package com.water.bpmengine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.bpmengine.entity.*;
import com.water.bpmengine.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.util.*;
@Service @RequiredArgsConstructor
public class BpmEngineService {
private final ProcessDefinitionMapper defMapper;
private final ProcessInstanceMapper instMapper;
private final TaskItemMapper taskMapper;
private final ProcessTemplateMapper tplMapper;
// === 流程定义 ===
public List<ProcessDefinition> listDefinitions(String category, Integer status) {
return defMapper.selectList(new LambdaQueryWrapper<ProcessDefinition>()
.eq(category != null, ProcessDefinition::getCategory, category)
.eq(status != null, ProcessDefinition::getStatus, status));
}
public Long createDefinition(Map<String,Object> req) {
ProcessDefinition d = new ProcessDefinition();
d.setProcessKey((String)req.get("processKey"));
d.setName((String)req.get("name"));
d.setCategory((String)req.get("category"));
d.setBpmnXml((String)req.get("bpmnXml"));
d.setDescription((String)req.get("description"));
d.setVersion(1); d.setStatus(0);
defMapper.insert(d);
return d.getId();
}
public void publishDefinition(Long id) {
ProcessDefinition d = defMapper.selectById(id);
if (d == null) throw new RuntimeException("流程定义不存在");
d.setStatus(1); defMapper.updateById(d);
}
// === 流程实例 ===
public Map<String,Object> startProcess(Long definitionId, String title, String initiator) {
ProcessDefinition d = defMapper.selectById(definitionId);
if (d == null || d.getStatus() != 1) throw new RuntimeException("流程未发布");
ProcessInstance inst = new ProcessInstance();
inst.setDefinitionId(definitionId); inst.setProcessKey(d.getProcessKey());
inst.setTitle(title); inst.setInitiator(initiator);
inst.setStatus(0); inst.setCurrentNodeName("开始");
instMapper.insert(inst);
TaskItem t = new TaskItem();
t.setInstanceId(inst.getId()); t.setTaskName("审批节点");
t.setTaskType("审批"); t.setAssignee(initiator); t.setStatus(0);
taskMapper.insert(t);
return Map.of("instanceId", inst.getId(), "processKey", d.getProcessKey());
}
public List<ProcessInstance> listInstances(String initiator, Integer status) {
return instMapper.selectList(new LambdaQueryWrapper<ProcessInstance>()
.eq(initiator != null, ProcessInstance::getInitiator, initiator)
.eq(status != null, ProcessInstance::getStatus, status));
}
// === 任务处理 ===
public List<TaskItem> getTodoTasks(String assignee) {
return taskMapper.selectList(new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getAssignee, assignee).eq(TaskItem::getStatus, 0));
}
public List<TaskItem> getDoneTasks(String assignee) {
return taskMapper.selectList(new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getAssignee, assignee).in(TaskItem::getStatus, 1, 2));
}
public Map<String,Object> approveTask(Long taskId, String comment) {
TaskItem t = taskMapper.selectById(taskId);
if (t == null || t.getStatus() != 0) throw new RuntimeException("任务不可审批");
t.setStatus(1); t.setComment(comment); t.setCompletedTime(LocalDateTime.now());
taskMapper.updateById(t);
ProcessInstance inst = instMapper.selectById(t.getInstanceId());
inst.setCurrentNodeName("已完成"); inst.setStatus(1); inst.setCompletedTime(LocalDateTime.now());
instMapper.updateById(inst);
return Map.of("taskId", taskId, "status", "已审批");
}
public Map<String,Object> rejectTask(Long taskId, String comment) {
TaskItem t = taskMapper.selectById(taskId);
if (t == null) throw new RuntimeException("任务不存在");
t.setStatus(2); t.setComment(comment); t.setCompletedTime(LocalDateTime.now());
taskMapper.updateById(t);
ProcessInstance inst = instMapper.selectById(t.getInstanceId());
inst.setStatus(2); inst.setCompletedTime(LocalDateTime.now());
instMapper.updateById(inst);
return Map.of("taskId", taskId, "status", "已驳回");
}
public Map<String,Object> transferTask(Long taskId, String newAssignee) {
TaskItem t = taskMapper.selectById(taskId);
if (t == null) throw new RuntimeException("任务不存在");
t.setStatus(3); t.setCompletedTime(LocalDateTime.now());
taskMapper.updateById(t);
TaskItem nt = new TaskItem();
nt.setInstanceId(t.getInstanceId()); nt.setTaskName(t.getTaskName());
nt.setTaskType(t.getTaskType()); nt.setAssignee(newAssignee); nt.setStatus(0);
taskMapper.insert(nt);
return Map.of("oldTaskId", taskId, "newTaskId", nt.getId());
}
// === 统计 ===
public Map<String,Object> getStatistics(String processKey) {
long total = instMapper.selectCount(new LambdaQueryWrapper<ProcessInstance>()
.eq(processKey != null, ProcessInstance::getProcessKey, processKey));
long completed = instMapper.selectCount(new LambdaQueryWrapper<ProcessInstance>()
.eq(processKey != null, ProcessInstance::getProcessKey, processKey)
.eq(ProcessInstance::getStatus, 1));
return Map.of("total", total, "completed", completed,
"running", total - completed,
"completionRate", total > 0 ? (double)completed / total : 0);
}
// === 模板 ===
public List<ProcessTemplate> listTemplates() { return tplMapper.selectList(null); }
public Long createTemplate(Map<String,Object> req) {
ProcessTemplate t = new ProcessTemplate();
t.setName((String)req.get("name")); t.setCategory((String)req.get("category"));
t.setBpmnXml((String)req.get("bpmnXml")); t.setStatus(1);
tplMapper.insert(t);
return t.getId();
}
}
@@ -0,0 +1,15 @@
server:
port: 9040
spring:
application:
name: wm-bpm-engine
datasource:
url: jdbc:postgresql://localhost:5432/water_bpm
username: water
password: water123
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
@@ -0,0 +1,23 @@
-- BPM Engine DDL
CREATE TABLE IF NOT EXISTS bpm_process_definition (
id BIGSERIAL PRIMARY KEY, process_key VARCHAR(100), name VARCHAR(200),
category VARCHAR(50), bpmn_xml TEXT, version INT DEFAULT 1,
status INT DEFAULT 0, description TEXT,
created_time TIMESTAMP DEFAULT NOW(), updated_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS bpm_process_instance (
id BIGSERIAL PRIMARY KEY, definition_id BIGINT, process_key VARCHAR(100),
title VARCHAR(200), initiator VARCHAR(50), status INT DEFAULT 0,
current_node_name VARCHAR(100),
created_time TIMESTAMP DEFAULT NOW(), completed_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS bpm_task_item (
id BIGSERIAL PRIMARY KEY, instance_id BIGINT, task_name VARCHAR(100),
task_type VARCHAR(30), assignee VARCHAR(50), status INT DEFAULT 0,
comment TEXT, created_time TIMESTAMP DEFAULT NOW(), completed_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS bpm_process_template (
id BIGSERIAL PRIMARY KEY, name VARCHAR(200), category VARCHAR(50),
bpmn_xml TEXT, status INT DEFAULT 1,
created_time TIMESTAMP DEFAULT NOW()
);