(1)认识 Flowable 流程引擎
(2)SpringBoot 3 + Vue 3 前后端分离项目,集成 Flowable(正在浏览)
1. pom.xml 添加 Maven 依赖
<!-- Flowable 流程引擎 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql 连接驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector-j.version}</version>
</dependency>
<!-- spring-boot3 , mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- mybatis-plus 分页插件,确保版本和 MyBatis Plus 主包一致 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus-jsqlparser.version}</version>
</dependency>
<!-- mybatis-plus-join 多表查询 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. application.yml 配置 Flowable,项目启动会自动在数据库创建 Flowable 需要的表
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/springboot3_vue3_satoken
username: ***
password: ***
driver-class-name: com.mysql.cj.jdbc.Driver
flowable:
# 是否开启异步执行器
async-executor-activate: false
# 建议初始化设置 true,生成所需要的表;之后设置为 false,则不会自动检查和更新数据库
database-schema-update: false

3. 创建用户、组,并绑定用户-组的关系
3.1 根据 act_id_user、act_id_group、act_id_membership 表,分别创建 Entity、Controller、Service、ServiceImp、Mapper 。
3.2 Flowable 用户组 API
package com.dragon.springboot3vue3.controller.flowable;
import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringsDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActIdGroupPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdGroup;
import com.dragon.springboot3vue3.service.flowableService.ActIdGroupService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Tag(name = "Flowable 用户组 API")
@RestController
@RequestMapping("/actIdGroup")
public class ActIdGroupController {
@Autowired
private ActIdGroupService groupService;
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody ActIdGroupPageDto pageDto){
// 创建分页对象
Page<ActIdGroup> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造多表查询条件
MPJLambdaWrapper<ActIdGroup> qw = new MPJLambdaWrapper<ActIdGroup>()
.like(StringUtils.isNotBlank(pageDto.getName()), ActIdGroup::getName, pageDto.getName());
// 根据查询条件,将结果封装到分页对象
Page<ActIdGroup> response = groupService.page(page, qw);
return SaResult.ok().setData(response);
}
@Operation(summary = "新增或更新")
@PostMapping("/saveOrUpdate")
public SaResult saveOrUpdate(@RequestBody @Validated ActIdGroup actIdGroup){
ActIdGroup group = new ActIdGroup();
BeanUtils.copyProperties(actIdGroup, group);
groupService.saveOrUpdate(group);
return SaResult.ok();
}
@Operation(summary = "删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody @Validated StringsDTO stringsDTO){
groupService.removeByIds(stringsDTO.getStrings());
return SaResult.ok();
}
@Operation(summary = "所有列表")
@GetMapping("/getAll")
public SaResult getAll(){
return SaResult.ok().setData(groupService.list());
}
}

3.3 Flowable 用户 API
package com.dragon.springboot3vue3.controller.flowable;
import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringsDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActIdUserDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActIdUserPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdGroup;
import com.dragon.springboot3vue3.entity.flowable.ActIdMembership;
import com.dragon.springboot3vue3.entity.flowable.ActIdUser;
import com.dragon.springboot3vue3.service.flowableService.ActIdMembershipService;
import com.dragon.springboot3vue3.service.flowableService.ActIdUserService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Tag(name = "Flowable 用户 API")
@RestController
@RequestMapping("/actIdUser")
public class ActIdUserController {
@Autowired
private ActIdUserService actIdUserService;
@Autowired
private ActIdMembershipService actIdMembershipService;
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody ActIdUserPageDto pageDto){
// 创建分页对象
Page<ActIdUserDto> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造多表查询条件
MPJLambdaWrapper<ActIdUser> qw = new MPJLambdaWrapper<ActIdUser>()
.selectAs(ActIdUser::getId, ActIdUserDto::getId)
.selectAs(ActIdUser::getDisplayName, ActIdUserDto::getDisplayName)
.selectAs(ActIdGroup::getId, ActIdUserDto::getGroupId)
.selectAs(ActIdGroup::getName, ActIdUserDto::getGroupName)
.leftJoin(ActIdMembership.class, ActIdMembership::getUserId, ActIdUser::getId)
.leftJoin(ActIdGroup.class, ActIdGroup::getId, ActIdMembership::getGroupId)
.like(StringUtils.isNotBlank(pageDto.getDisplayName()), ActIdUser::getDisplayName, pageDto.getDisplayName())
.like(StringUtils.isNotBlank(pageDto.getGroupName()), ActIdGroup::getName, pageDto.getGroupName());
// 根据查询条件,将结果封装到分页对象
Page<ActIdUserDto> response = actIdUserService.selectJoinListPage(page, ActIdUserDto.class,qw);
return SaResult.ok().setData(response);
}
@Transactional
@Operation(summary = "新增或更新")
@PostMapping("/saveOrUpdate")
public SaResult saveOrUpdate(@RequestBody @Validated ActIdUserDto userDto){
// 1. 保存用户
ActIdUser user = new ActIdUser();
BeanUtils.copyProperties(userDto, user);
actIdUserService.saveOrUpdate(user);
// 2. 保存用户-组关联关系
ActIdMembership membership = new ActIdMembership();
membership.setUserId(userDto.getId());
membership.setGroupId(userDto.getGroupId());
actIdMembershipService.saveOrUpdate(membership);
return SaResult.ok();
}
@Operation(summary = "删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody @Validated StringsDTO stringsDTO){
actIdUserService.removeByIds(stringsDTO.getStrings());
return SaResult.ok();
}
@Operation(summary = "查询所有用户列表")
@GetMapping("/getAll")
public SaResult getAll(){
return SaResult.ok().setData(actIdUserService.list());
}
}

4. 流程定义 API(首先部署流程定义)
@Slf4j
@Tag(name = "Flowable 流程引擎 API")
@RestController
@RequestMapping("/process")
public class ProcessController {
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private IdentityService identityService;
@Autowired
private HistoryService historyService;
@Autowired
private ActReProcDefService actReProcDefService;
@Autowired
private ActIdMembershipService actIdMembershipService;
@Operation(summary = "上传文件部署流程定义")
@PostMapping("/uploadFileDeploy")
public SaResult uploadFileDeploy(@RequestParam("file") MultipartFile file) {
try {
repositoryService.createDeployment()
.addBytes(file.getOriginalFilename(), file.getBytes())
.name(file.getOriginalFilename())
.deploy();
return SaResult.ok("流程部署成功 ");
} catch (Exception e) {
return SaResult.error("流程部署失败 ");
}
}
@Operation(summary = "本地文件部署流程定义")
@PostMapping("/localFileDeploy")
public SaResult localFileDeploy() {
// 本地文件部署,resources -> process -> leave-request.bpmn20.xml
repositoryService.createDeployment()
.addClasspathResource("process/leave-request.bpmn20.xml")
.name("请假流程")
.deploy();
return SaResult.ok("部署成功");
}
@Operation(summary = "流程定义分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody ActReProcDefPageDto pageDto) {
// 创建分页对象
Page<ActReProcDef> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造多表查询条件
MPJLambdaWrapper<ActReProcDef> qw = new MPJLambdaWrapper<ActReProcDef>()
.like(StringUtils.isNotBlank(pageDto.getName()), ActReProcDef::getName, pageDto.getName());
// 根据查询条件,将结果封装到分页对象
Page<ActReProcDef> response = actReProcDefService.page(page, qw);
return SaResult.ok().setData(response);
}
@Operation(summary = "流程定义删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody StringDTO stringDTO) {
// 根据 deploymentId 删除,是否级联删除(流程启动了也可以删除)
repositoryService.deleteDeployment(stringDTO.getStr(), true);
return SaResult.ok();
}
@Operation(summary = "挂起流程定义")
@PostMapping("/suspend")
public SaResult suspend(@RequestBody StringDTO stringDTO) {
repositoryService.suspendProcessDefinitionByKey(stringDTO.getStr());
return SaResult.ok("挂起流程定义");
}
@Operation(summary = "激活流程定义")
@PostMapping("/activate")
public SaResult activate(@RequestBody StringDTO stringDTO) {
repositoryService.activateProcessDefinitionByKey(stringDTO.getStr());
return SaResult.ok("激活流程定义");
}
}

5. 申请者开启流程实例(以请假流程为例)
/**
* 一个流程实例通常会包含多个任务节点,这些任务会在流程执行过程中逐步生成和分配
*/
@Operation(summary = "开启请假流程实例")
@PostMapping("/startLeave")
public SaResult startLeave(@RequestBody HandleTaskDto handleTaskDto) {
// 设置启动人
identityService.setAuthenticatedUserId(handleTaskDto.getUserId());
// 请假实例参数
Map<String, Object> variables = new HashMap<>();
variables.put("userId", handleTaskDto.getUserId());
variables.put("groupId", handleTaskDto.getGroupId());
// 启动流程实例,并设置流程变量
runtimeService.startProcessInstanceByKey(handleTaskDto.getProcDefKey(), variables);
return SaResult.ok();
}

6. 申请者处理任务(填写请假信息)
@Operation(summary = "用户处理任务")
@PostMapping("/assigneeHandle")
public SaResult assigneeHandle(@RequestBody HandleTaskDto handleTaskDto) {
Task task = taskService.createTaskQuery()
.taskAssignee(handleTaskDto.getUserId())
.singleResult();
// 请假实例参数
Map<String, Object> variables = new HashMap<>();
variables.put("userId", handleTaskDto.getUserId());
variables.put("startTime", handleTaskDto.getStartTimeValue());
variables.put("endTime", handleTaskDto.getEndTimeValue());
variables.put("days", String.valueOf(handleTaskDto.getDays()));
variables.put("reason", handleTaskDto.getReason());
// 处理任务
taskService.complete(task.getId(), variables);
return SaResult.ok();
}

7. 查看申请记录
@Operation(summary = "查询流程实例历史记录列表")
@PostMapping("/getProcessInstanceHistory")
public SaResult getProcessInstanceHistory(@RequestBody ProcessInstanceHistoryPageDto pageDto) {
// 创建分页对象
Page<ActHiProcinstDto> page = new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());
// 创建查询对象
HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
.orderByProcessInstanceStartTime().desc();
// 根据启动人筛选流程实例
if (StringUtils.isNotBlank(pageDto.getUserId())) {
query = query.startedBy(pageDto.getUserId());
}
// 查询当前页的数据
List<HistoricProcessInstance> historicTasks = query.listPage((int) ((pageDto.getCurrentPage() - 1) * pageDto.getPageSize()), Math.toIntExact(pageDto.getPageSize()));
// 转换为 DTO 列表
List<ActHiProcinstDto> list = historicTasks.stream()
.map(task -> {
// 获取变量
Map<String, Object> variables = getApplyVariables(task.getId());
// 获取任务ID
HistoricTaskInstance managerApprovalTask = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(task.getId())
.taskDefinitionKey("managerApproval")
.unfinished()
.singleResult();
return ActHiProcinstDto.builder()
.id(task.getId())
.name(task.getName())
.startTime(DateUtil.toLocalDateTime(task.getStartTime()))
.endTime(DateUtil.toLocalDateTime(task.getEndTime()))
.procDefId(task.getProcessDefinitionId())
.procDefName(task.getProcessDefinitionName())
.procDefKey(task.getProcessDefinitionKey())
.procInstId(task.getId())
.startUserId(task.getStartUserId())
.startTimeValue( (String) variables.get("startTime"))
.endTimeValue((String) variables.get("endTime"))
.days((String) variables.get("days"))
.reason((String) variables.get("reason"))
.userId((String) variables.get("userId"))
.result(variables.get("result") != null ? (Boolean) variables.get("result") : null)
.managerApprovalTaskId(managerApprovalTask!= null ? managerApprovalTask.getId() : null)
.build();
}
).toList();
// 查询总记录数
long total = query.count();
// 封装分页结果
page.setRecords(list);
page.setTotal(total);
return SaResult.ok().setData(page);
}
@Operation(summary = "查询历史记录中流程实例变量")
private Map<String, Object> getApplyVariables(String processInstanceId) {
HistoricVariableInstance days = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("days")
.singleResult();
HistoricVariableInstance startTime = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("startTime")
.singleResult();
HistoricVariableInstance endTime = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("endTime")
.singleResult();
HistoricVariableInstance userId = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("userId")
.singleResult();
HistoricVariableInstance reason = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("reason")
.singleResult();
HistoricVariableInstance result = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("result")
.singleResult();
HashMap<String, Object> map = new HashMap<>();
map.put("days", days != null ? days.getValue() : null);
map.put("startTime", startTime != null ? startTime.getValue() : null);
map.put("endTime", endTime != null ? endTime.getValue() : null);
map.put("userId", userId != null ? userId.getValue() : null);
map.put("reason", reason != null ? reason.getValue() : null);
map.put("result", result != null ? result.getValue() : null);
return map;
}

8. 查看流程进度
@Operation(summary = "查询历史活动详情列表")
@GetMapping("/getHistoryTask")
public SaResult getHistoryTask(@RequestParam String processInstanceId) {
List<HistoricActivityInstance> historicTasks = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceEndTime().asc()
.list();
List<ActRuTaskDto> list = historicTasks.stream()
.map(task -> ActRuTaskDto.builder()
.id(task.getActivityId())
.name(task.getActivityName())
.assignee(task.getAssignee())
.createTime(DateUtil.toLocalDateTime(task.getStartTime()))
.endTime(DateUtil.toLocalDateTime(task.getEndTime()))
.procInstId(task.getProcessInstanceId())
.procDefId(task.getProcessDefinitionId())
.build()
).toList();
return SaResult.ok().setData(list);
}

9. 审批者认领任务(认领候选组任务)
@Operation(summary = "认领候选组任务")
@PostMapping("/claimGroupTask")
public SaResult claimGroupTask(@RequestBody HandleTaskDto handleTaskDto) {
// 判断用户是否属于该用户组
ActIdMembership one = actIdMembershipService.lambdaQuery()
.eq(ActIdMembership::getUserId, handleTaskDto.getHandleUserId())
.eq(ActIdMembership::getGroupId, handleTaskDto.getGroupId())
.one();
if(one == null){
return SaResult.error("您无权限认领此任务");
}
// 认领任务
taskService.claim(handleTaskDto.getManagerApprovalTaskId(), handleTaskDto.getHandleUserId());
return SaResult.ok("任务已认领");
}
10. 审批者处理任务(处理认领的任务)
@Operation(summary = "认领用户处理认领的任务")
@PostMapping("/handleClaimTask")
public SaResult handleClaimTask(@RequestBody HandleTaskDto handleTaskDto) {
Task task = taskService.createTaskQuery()
.taskAssignee(handleTaskDto.getHandleUserId())
.singleResult();
if(task == null){
return SaResult.error("任务已处理");
}
// 是否同意
Map<String, Object> variables = new HashMap<>();
variables.put("result", handleTaskDto.getResult());
taskService.complete(task.getId(), variables);
return SaResult.ok("任务处理完成");
}

11. DTO
11.1 ActHiProcinstDto
package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Builder
@Data
public class ActHiProcinstDto {
@Schema(description = "主键ID")
private String id;
@Schema(description = "修订版本号(用于乐观锁控制)")
private Integer rev;
@Schema(description = "业务键")
private String businessKey;
@Schema(description = "开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@Schema(description = "结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
@Schema(description = "启动用户ID")
private String startUserId;
@Schema(description = "开始活动ID")
private String startActId;
@Schema(description = "结束活动ID")
private String endActId;
@Schema(description = "删除原因")
private String deleteReason;
@Schema(description = "租户ID")
private String tenantId;
@Schema(description = "流程实例名称")
private String name;
@Schema(description = "流程实例ID")
private String procInstId;
@Schema(description = "流程定义ID")
private String procDefId;
@Schema(description = "流程定义Key")
private String procDefKey;
@Schema(description = "流程定义名称")
private String procDefName;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "开始时间")
private String startTimeValue;
@Schema(description = "结束时间")
private String endTimeValue;
@Schema(description = "请假天数")
private String days;
@Schema(description = "请假理由")
private String reason;
@Schema(description = "审批是否通过")
private Boolean result;
@Schema(description = "管理员审批的任务ID")
private String managerApprovalTaskId;
}
11.2 ActIdUserDto
package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class ActIdUserDto {
@Schema(description = "用户ID")
private String id;
@Schema(description = "用户显示名称")
private String displayName;
@Schema(description = "邮箱")
private String email;
@Schema(description = "密码")
private String pwd;
@Schema(description = "租户ID")
private String tenantId;
@NotEmpty
@Schema(description = "用户组ID")
private String groupId;
@Schema(description = "用户组名称")
private String groupName;
}
11.3 ActRuTaskDto
package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Builder
@Data
public class ActRuTaskDto {
@Schema(description = "主键")
private String id;
@Schema(description = "修订版本号(用于乐观锁控制)")
private Integer rev;
@Schema(description = "执行实例ID")
private String executionId;
@Schema(description = "流程实例ID")
private String procInstId;
@Schema(description = "流程定义ID")
private String procDefId;
@Schema(description = "任务定义ID")
private String taskDefId;
@Schema(description = "任务状态")
private String state;
@Schema(description = "任务名称")
private String name;
@Schema(description = "任务描述")
private String description;
@Schema(description = "任务定义键")
private String taskDefKey;
@Schema(description = "任务处理人")
private String assignee;
@Schema(description = "优先级")
private Integer priority;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "开始处理时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime inProgressTime;
@Schema(description = "开始处理人")
private String inProgressStartedBy;
@Schema(description = "签收时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime claimTime;
@Schema(description = "签收人")
private String claimedBy;
@Schema(description = "挂起时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime suspendedTime;
@Schema(description = "挂起人")
private String suspendedBy;
@Schema(description = "处理中截止时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime inProgressDueDate;
@Schema(description = "截止时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dueDate;
@Schema(description = "分类")
private String category;
@Schema(description = "挂起状态(1-活跃,2-挂起)")
private Integer suspensionState;
@Schema(description = "开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@Schema(description = "结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
@Schema(description = "请假天数")
private String days;
@Schema(description = "请假理由")
private String reason;
}
11.4 HandleTaskDto
package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class HandleTaskDto {
@Schema(description = "开始时间")
private String startTimeValue;
@Schema(description = "结束时间")
private String endTimeValue;
@Schema(description = "请假天数")
private float days;
@Schema(description = "请假理由")
private String reason;
// 开启流程
@Schema(description = "申请人ID")
private String userId;
@Schema(description = "流程定义Key")
private String procDefKey;
// 认领和处理任务
@Schema(description = "审批者ID(认领任务者ID)")
private String handleUserId;
@Schema(description = "任务ID")
private String managerApprovalTaskId;
@Schema(description = "用户组ID")
private String groupId;
@Schema(description = "处理结果")
private Boolean result;
}
11.5 PageDTO
package com.dragon.springboot3vue3.controller.dto.pageDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "基础分页")
public class PageDTO {
@NotNull
@Schema(description = "当前页码")
public Long currentPage;
@NotNull
@Schema(description = "每页记录数")
public Long pageSize;
}
11.6 ActIdGroupPageDto
package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ActIdGroupPageDto extends PageDTO {
@Schema(description = "组名")
private String name;
}
11.7 ActIdUserPageDto
package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ActIdUserPageDto extends PageDTO {
@Schema(description = "用户显示名称")
private String displayName;
@Schema(description = "用户组名")
private String groupName;
}
11.8 ActReProcDefPageDto
package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ActReProcDefPageDto extends PageDTO {
@Schema(description = "流程定义名称")
private String name;
}
11.9 ProcessInstanceHistoryPageDto
package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ProcessInstanceHistoryPageDto extends PageDTO {
@Schema(description = "申请人ID")
private String userId;
}
12. 完整的流程处理代码(包含部署流程定义、开启流程实例、处理任务、查看历史任务等)
package com.dragon.springboot3vue3.controller.flowable;
import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActHiProcinstDto;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActRuTaskDto;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.HandleTaskDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActReProcDefPageDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ProcessInstanceHistoryPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdMembership;
import com.dragon.springboot3vue3.entity.flowable.ActReProcDef;
import com.dragon.springboot3vue3.entity.flowable.ActRuTask;
import com.dragon.springboot3vue3.service.flowableService.ActIdMembershipService;
import com.dragon.springboot3vue3.service.flowableService.ActReProcDefService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Tag(name = "Flowable 流程引擎 API")
@RestController
@RequestMapping("/process")
public class ProcessController {
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private IdentityService identityService;
@Autowired
private HistoryService historyService;
@Autowired
private ActReProcDefService actReProcDefService;
@Autowired
private ActIdMembershipService actIdMembershipService;
@Operation(summary = "上传文件部署流程定义")
@PostMapping("/uploadFileDeploy")
public SaResult uploadFileDeploy(@RequestParam("file") MultipartFile file) {
try {
repositoryService.createDeployment()
.addBytes(file.getOriginalFilename(), file.getBytes())
.name(file.getOriginalFilename())
.deploy();
return SaResult.ok("流程部署成功 ");
} catch (Exception e) {
return SaResult.error("流程部署失败 ");
}
}
@Operation(summary = "本地文件部署流程定义")
@PostMapping("/localFileDeploy")
public SaResult localFileDeploy() {
// 本地文件部署,resources -> process -> leave-request.bpmn20.xml
repositoryService.createDeployment()
.addClasspathResource("process/leave-request.bpmn20.xml")
.name("请假流程")
.deploy();
return SaResult.ok("部署成功");
}
@Operation(summary = "流程定义分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody ActReProcDefPageDto pageDto) {
// 创建分页对象
Page<ActReProcDef> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造多表查询条件
MPJLambdaWrapper<ActReProcDef> qw = new MPJLambdaWrapper<ActReProcDef>()
.like(StringUtils.isNotBlank(pageDto.getName()), ActReProcDef::getName, pageDto.getName());
// 根据查询条件,将结果封装到分页对象
Page<ActReProcDef> response = actReProcDefService.page(page, qw);
return SaResult.ok().setData(response);
}
@Operation(summary = "流程定义删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody StringDTO stringDTO) {
// 根据 deploymentId 删除,是否级联删除(流程启动了也可以删除)
repositoryService.deleteDeployment(stringDTO.getStr(), true);
return SaResult.ok();
}
@Operation(summary = "挂起流程定义")
@PostMapping("/suspend")
public SaResult suspend(@RequestBody StringDTO stringDTO) {
repositoryService.suspendProcessDefinitionByKey(stringDTO.getStr());
return SaResult.ok("挂起流程定义");
}
@Operation(summary = "激活流程定义")
@PostMapping("/activate")
public SaResult activate(@RequestBody StringDTO stringDTO) {
repositoryService.activateProcessDefinitionByKey(stringDTO.getStr());
return SaResult.ok("激活流程定义");
}
/**
* 一个流程实例通常会包含多个任务节点,这些任务会在流程执行过程中逐步生成和分配
*/
@Operation(summary = "开启请假流程实例")
@PostMapping("/startLeave")
public SaResult startLeave(@RequestBody HandleTaskDto handleTaskDto) {
// 设置启动人
identityService.setAuthenticatedUserId(handleTaskDto.getUserId());
// 请假实例参数
Map<String, Object> variables = new HashMap<>();
variables.put("userId", handleTaskDto.getUserId());
variables.put("groupId", handleTaskDto.getGroupId());
// 启动流程实例,并设置流程变量
runtimeService.startProcessInstanceByKey(handleTaskDto.getProcDefKey(), variables);
return SaResult.ok();
}
@Operation(summary = "删除流程实例历史数据")
@DeleteMapping("/deleteHistoricProcessInstance")
public SaResult deleteHistoricProcessInstance(@RequestParam String processInstanceId) {
try {
// 查询流程实例是否存在(包括历史和运行中)
HistoricProcessInstance historicInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
if (historicInstance == null) {
return SaResult.error("流程实例不存在");
}
// 如果流程在运行中,先删除运行中实例
if (historicInstance.getEndTime() == null) {
runtimeService.deleteProcessInstance(processInstanceId, "管理员删除");
}
// 再删除历史流程实例
historyService.deleteHistoricProcessInstance(processInstanceId);
return SaResult.ok("流程实例删除成功");
} catch (FlowableObjectNotFoundException e) {
return SaResult.error("指定的流程实例不存在");
} catch (Exception e) {
return SaResult.error("删除历史流程实例失败: " + e.getMessage());
}
}
@Operation(summary = "查询用户待办任务")
@GetMapping("/getUserTasks")
public SaResult getUserTasks(@RequestParam String userId) {
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.list();
List<ActRuTask> list = tasks.stream()
.map(task -> ActRuTask.builder()
.id(task.getId())
.name(task.getName())
.assignee(task.getAssignee())
.createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
.procInstId(task.getProcessInstanceId())
.procDefId(task.getProcessDefinitionId())
.formKey(task.getFormKey())
.build())
.toList();
return SaResult.ok().setData(list);
}
@Operation(summary = "用户处理任务")
@PostMapping("/assigneeHandle")
public SaResult assigneeHandle(@RequestBody HandleTaskDto handleTaskDto) {
Task task = taskService.createTaskQuery()
.taskAssignee(handleTaskDto.getUserId())
.singleResult();
// 请假实例参数
Map<String, Object> variables = new HashMap<>();
variables.put("userId", handleTaskDto.getUserId());
variables.put("startTime", handleTaskDto.getStartTimeValue());
variables.put("endTime", handleTaskDto.getEndTimeValue());
variables.put("days", String.valueOf(handleTaskDto.getDays()));
variables.put("reason", handleTaskDto.getReason());
// 处理任务
taskService.complete(task.getId(), variables);
return SaResult.ok();
}
@Operation(summary = "认领用户处理认领的任务")
@PostMapping("/handleClaimTask")
public SaResult handleClaimTask(@RequestBody HandleTaskDto handleTaskDto) {
Task task = taskService.createTaskQuery()
.taskAssignee(handleTaskDto.getHandleUserId())
.singleResult();
if(task == null){
return SaResult.error("任务已处理");
}
// 是否同意
Map<String, Object> variables = new HashMap<>();
variables.put("result", handleTaskDto.getResult());
taskService.complete(task.getId(), variables);
return SaResult.ok("任务处理完成");
}
@Operation(summary = "查询历史活动详情列表")
@GetMapping("/getHistoryTask")
public SaResult getHistoryTask(@RequestParam String processInstanceId) {
List<HistoricActivityInstance> historicTasks = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceEndTime().asc()
.list();
List<ActRuTaskDto> list = historicTasks.stream()
.map(task -> ActRuTaskDto.builder()
.id(task.getActivityId())
.name(task.getActivityName())
.assignee(task.getAssignee())
.createTime(DateUtil.toLocalDateTime(task.getStartTime()))
.endTime(DateUtil.toLocalDateTime(task.getEndTime()))
.procInstId(task.getProcessInstanceId())
.procDefId(task.getProcessDefinitionId())
.build()
).toList();
return SaResult.ok().setData(list);
}
@Operation(summary = "查询流程实例历史记录列表")
@PostMapping("/getProcessInstanceHistory")
public SaResult getProcessInstanceHistory(@RequestBody ProcessInstanceHistoryPageDto pageDto) {
// 创建分页对象
Page<ActHiProcinstDto> page = new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());
// 创建查询对象
HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
.orderByProcessInstanceStartTime().desc();
// 根据启动人筛选流程实例
if (StringUtils.isNotBlank(pageDto.getUserId())) {
query = query.startedBy(pageDto.getUserId());
}
// 查询当前页的数据
List<HistoricProcessInstance> historicTasks = query.listPage((int) ((pageDto.getCurrentPage() - 1) * pageDto.getPageSize()), Math.toIntExact(pageDto.getPageSize()));
// 转换为 DTO 列表
List<ActHiProcinstDto> list = historicTasks.stream()
.map(task -> {
// 获取变量
Map<String, Object> variables = getApplyVariables(task.getId());
// 获取任务ID
HistoricTaskInstance managerApprovalTask = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(task.getId())
.taskDefinitionKey("managerApproval")
.unfinished()
.singleResult();
return ActHiProcinstDto.builder()
.id(task.getId())
.name(task.getName())
.startTime(DateUtil.toLocalDateTime(task.getStartTime()))
.endTime(DateUtil.toLocalDateTime(task.getEndTime()))
.procDefId(task.getProcessDefinitionId())
.procDefName(task.getProcessDefinitionName())
.procDefKey(task.getProcessDefinitionKey())
.procInstId(task.getId())
.startUserId(task.getStartUserId())
.startTimeValue( (String) variables.get("startTime"))
.endTimeValue((String) variables.get("endTime"))
.days((String) variables.get("days"))
.reason((String) variables.get("reason"))
.userId((String) variables.get("userId"))
.result(variables.get("result") != null ? (Boolean) variables.get("result") : null)
.managerApprovalTaskId(managerApprovalTask!= null ? managerApprovalTask.getId() : null)
.build();
}
).toList();
// 查询总记录数
long total = query.count();
// 封装分页结果
page.setRecords(list);
page.setTotal(total);
return SaResult.ok().setData(page);
}
@Operation(summary = "查询活跃任务列表(可认领和处理)")
@GetMapping("/getActiveTask")
public SaResult getActiveTask() {
List<Task> tasks = taskService.createTaskQuery().active().list();
List<ActRuTaskDto> list = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
// 将日期字符串转为LocalDateTime
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime startTime = LocalDateTime.parse((CharSequence) variables.get("startTime"), formatter);
LocalDateTime endTime = LocalDateTime.parse((CharSequence) variables.get("endTime"), formatter);
return ActRuTaskDto.builder()
.id(task.getId())
.name(task.getName())
.assignee(task.getAssignee())
.createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
.procInstId(task.getProcessInstanceId())
.procDefId(task.getProcessDefinitionId())
.startTime(startTime)
.endTime(endTime)
.days((String) variables.get("days"))
.reason((String) variables.get("reason"))
.build();
}).toList();
return SaResult.ok().setData(list);
}
@Operation(summary = "查询挂起的任务列表(任务暂停,不可处理)")
@GetMapping("/getSuspendedTask")
public SaResult getSuspendedTask() {
List<Task> list = taskService.createTaskQuery().suspended().list();
return SaResult.ok().setData(list);
}
@Operation(summary = "根据流程实例查询任务ID")
@GetMapping("/getTaskId")
public SaResult getTaskId(@RequestParam String processInstanceId) {
String managerApprovalTaskId = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.taskDefinitionKey("managerApproval")
.unfinished()
.singleResult().getId();
return SaResult.ok().setData(managerApprovalTaskId);
}
@Operation(summary = "查询历史记录中流程实例变量")
@GetMapping("/getHistoryInstanceVariables")
public SaResult getHistoryInstanceVariables(@RequestParam String processInstanceId) {
return SaResult.ok().setData(getApplyVariables(processInstanceId));
}
@Operation(summary = "查询历史记录中流程实例变量")
private Map<String, Object> getApplyVariables(String processInstanceId) {
HistoricVariableInstance days = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("days")
.singleResult();
HistoricVariableInstance startTime = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("startTime")
.singleResult();
HistoricVariableInstance endTime = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("endTime")
.singleResult();
HistoricVariableInstance userId = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("userId")
.singleResult();
HistoricVariableInstance reason = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("reason")
.singleResult();
HistoricVariableInstance result = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.variableName("result")
.singleResult();
HashMap<String, Object> map = new HashMap<>();
map.put("days", days != null ? days.getValue() : null);
map.put("startTime", startTime != null ? startTime.getValue() : null);
map.put("endTime", endTime != null ? endTime.getValue() : null);
map.put("userId", userId != null ? userId.getValue() : null);
map.put("reason", reason != null ? reason.getValue() : null);
map.put("result", result != null ? result.getValue() : null);
return map;
}
@Operation(summary = "查询候选组任务")
@GetMapping("/getGroupTasks")
public SaResult getGroupTasks(@RequestParam String groupId) {
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateGroup(groupId)
.list();
List<ActRuTaskDto> list = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
// 将日期字符串转为LocalDateTime
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime startTime = LocalDateTime.parse((CharSequence) variables.get("startTime"), formatter);
LocalDateTime endTime = LocalDateTime.parse((CharSequence) variables.get("endTime"), formatter);
return ActRuTaskDto.builder()
.id(task.getId())
.name(task.getName())
.assignee(task.getAssignee())
.createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
.procInstId(task.getProcessInstanceId())
.procDefId(task.getProcessDefinitionId())
.startTime(startTime)
.endTime(endTime)
.days((String) variables.get("days"))
.reason((String) variables.get("reason"))
.build();
}).toList();
return SaResult.ok().setData(list);
}
@Operation(summary = "认领候选组任务")
@PostMapping("/claimGroupTask")
public SaResult claimGroupTask(@RequestBody HandleTaskDto handleTaskDto) {
// 判断用户是否属于该用户组
ActIdMembership one = actIdMembershipService.lambdaQuery()
.eq(ActIdMembership::getUserId, handleTaskDto.getHandleUserId())
.eq(ActIdMembership::getGroupId, handleTaskDto.getGroupId())
.one();
if(one == null){
return SaResult.error("您无权限认领此任务");
}
// 认领任务
taskService.claim(handleTaskDto.getManagerApprovalTaskId(), handleTaskDto.getHandleUserId());
return SaResult.ok("任务已认领");
}
}