现在根据我们前11章学的内容,练习一个小Demo来串通一下
如下是一个资金申请.bpmn20.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:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
<process id="money_apply_optimized" name="资金申请流程(优化版)" isExecutable="true">
<startEvent id="startEvent" name="开始"></startEvent>
<userTask id="task_submit_application" name="填写资金申请单" flowable:assignee="${initiator}">
<documentation>申请人填写并提交资金申请,需提供事由、金额(amount)等信息。</documentation>
</userTask>
<sequenceFlow id="flow_start_to_submit" sourceRef="startEvent" targetRef="task_submit_application"></sequenceFlow>
<userTask id="task_manager_approval" name="部门经理审批" flowable:assignee="${manager}">
<documentation>部门经理审批。审批时需设置流程变量 approved (true/false)。</documentation>
</userTask>
<sequenceFlow id="flow_submit_to_manager" sourceRef="task_submit_application" targetRef="task_manager_approval"></sequenceFlow>
<exclusiveGateway id="gateway_manager_decision" name="经理审批是否通过?"></exclusiveGateway>
<sequenceFlow id="flow_manager_to_gateway" sourceRef="task_manager_approval" targetRef="gateway_manager_decision"></sequenceFlow>
<exclusiveGateway id="gateway_check_amount" name="金额是否超过10000?" default="flow_amount_low"></exclusiveGateway>
<sequenceFlow id="flow_manager_approved" name="通过" sourceRef="gateway_manager_decision" targetRef="gateway_check_amount">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
</sequenceFlow>
<userTask id="task_modify_application" name="修改申请单" flowable:assignee="${initiator}">
<documentation>申请被驳回,请根据审批意见修改后重新提交。</documentation>
</userTask>
<sequenceFlow id="flow_manager_rejected" name="驳回修改" sourceRef="gateway_manager_decision" targetRef="task_modify_application">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_modify_to_manager" sourceRef="task_modify_application" targetRef="task_manager_approval"></sequenceFlow>
<userTask id="task_director_approval" name="部门总监审批" flowable:assignee="${director}">
<documentation>金额大于10000,需总监审批。审批时需设置流程变量 approved (true/false)。</documentation>
</userTask>
<sequenceFlow id="flow_amount_high" name="是 (>10000)" sourceRef="gateway_check_amount" targetRef="task_director_approval">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${amount > 10000}]]></conditionExpression>
</sequenceFlow>
<exclusiveGateway id="gateway_director_decision" name="总监审批是否通过?"></exclusiveGateway>
<sequenceFlow id="flow_director_to_gateway" sourceRef="task_director_approval" targetRef="gateway_director_decision"></sequenceFlow>
<userTask id="task_finance_review" name="财务复核" flowable:assignee="${finance_reviewer}">
<documentation>财务复核申请的合规性、票据等。复核时需设置流程变量 approved (true/false)。</documentation>
</userTask>
<sequenceFlow id="flow_director_approved" name="通过" sourceRef="gateway_director_decision" targetRef="task_finance_review">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_director_rejected" name="驳回修改" sourceRef="gateway_director_decision" targetRef="task_modify_application">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_amount_low" name="否 (默认)" sourceRef="gateway_check_amount" targetRef="task_finance_review"></sequenceFlow>
<exclusiveGateway id="gateway_finance_decision" name="财务复核是否通过?"></exclusiveGateway>
<sequenceFlow id="flow_finance_review_to_gateway" sourceRef="task_finance_review" targetRef="gateway_finance_decision"></sequenceFlow>
<userTask id="task_finance_payment" name="财务出纳付款" flowable:assignee="${finance_cashier}">
<documentation>所有审批完成,执行打款操作。</documentation>
</userTask>
<sequenceFlow id="flow_finance_approved" name="通过" sourceRef="gateway_finance_decision" targetRef="task_finance_payment">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_finance_rejected" name="驳回修改" sourceRef="gateway_finance_decision" targetRef="task_modify_application">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression>
</sequenceFlow>
<endEvent id="endEvent" name="结束"></endEvent>
<sequenceFlow id="flow_payment_to_end" sourceRef="task_finance_payment" targetRef="endEvent"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_money_apply_optimized">
<bpmndi:BPMNPlane bpmnElement="money_apply_optimized" id="BPMNPlane_money_apply_optimized">
<bpmndi:BPMNShape bpmnElement="startEvent" id="BPMNShape_startEvent">
<omgdc:Bounds height="30.0" width="30.0" x="200.0" y="50.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_submit_application" id="BPMNShape_task_submit_application">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="120.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_manager_approval" id="BPMNShape_task_manager_approval">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="240.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="gateway_manager_decision" id="BPMNShape_gateway_manager_decision">
<omgdc:Bounds height="40.0" width="40.0" x="195.0" y="360.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_modify_application" id="BPMNShape_task_modify_application">
<omgdc:Bounds height="80.0" width="100.0" x="350.0" y="240.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="gateway_check_amount" id="BPMNShape_gateway_check_amount">
<omgdc:Bounds height="40.0" width="40.0" x="195.0" y="440.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_director_approval" id="BPMNShape_task_director_approval">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="520.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="gateway_director_decision" id="BPMNShape_gateway_director_decision">
<omgdc:Bounds height="40.0" width="40.0" x="195.0" y="640.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_finance_review" id="BPMNShape_task_finance_review">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="720.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="gateway_finance_decision" id="BPMNShape_gateway_finance_decision">
<omgdc:Bounds height="40.0" width="40.0" x="195.0" y="840.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task_finance_payment" id="BPMNShape_task_finance_payment">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="920.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endEvent" id="BPMNShape_endEvent">
<omgdc:Bounds height="30.0" width="30.0" x="200.0" y="1040.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow_start_to_submit" id="BPMNEdge_flow_start_to_submit">
<omgdi:waypoint x="215.0" y="80.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="120.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_submit_to_manager" id="BPMNEdge_flow_submit_to_manager">
<omgdi:waypoint x="215.0" y="200.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="240.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_manager_to_gateway" id="BPMNEdge_flow_manager_to_gateway">
<omgdi:waypoint x="215.0" y="320.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="360.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_manager_approved" id="BPMNEdge_flow_manager_approved">
<omgdi:waypoint x="215.0" y="400.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="440.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_manager_rejected" id="BPMNEdge_flow_manager_rejected">
<omgdi:waypoint x="235.0" y="380.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="380.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="320.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_modify_to_manager" id="BPMNEdge_flow_modify_to_manager">
<omgdi:waypoint x="350.0" y="280.0"></omgdi:waypoint>
<omgdi:waypoint x="265.0" y="280.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_amount_high" id="BPMNEdge_flow_amount_high">
<omgdi:waypoint x="215.0" y="480.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="520.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_director_to_gateway" id="BPMNEdge_flow_director_to_gateway">
<omgdi:waypoint x="215.0" y="600.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="640.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_director_approved" id="BPMNEdge_flow_director_approved">
<omgdi:waypoint x="215.0" y="680.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="720.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_director_rejected" id="BPMNEdge_flow_director_rejected">
<omgdi:waypoint x="235.0" y="660.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="660.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="320.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_amount_low" id="BPMNEdge_flow_amount_low">
<omgdi:waypoint x="195.0" y="460.0"></omgdi:waypoint>
<omgdi:waypoint x="90.0" y="460.0"></omgdi:waypoint>
<omgdi:waypoint x="90.0" y="760.0"></omgdi:waypoint>
<omgdi:waypoint x="165.0" y="760.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_finance_review_to_gateway" id="BPMNEdge_flow_finance_review_to_gateway">
<omgdi:waypoint x="215.0" y="800.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="840.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_finance_approved" id="BPMNEdge_flow_finance_approved">
<omgdi:waypoint x="215.0" y="880.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="920.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_finance_rejected" id="BPMNEdge_flow_finance_rejected">
<omgdi:waypoint x="235.0" y="860.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="860.0"></omgdi:waypoint>
<omgdi:waypoint x="400.0" y="320.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow_payment_to_end" id="BPMNEdge_flow_payment_to_end">
<omgdi:waypoint x="215.0" y="1000.0"></omgdi:waypoint>
<omgdi:waypoint x="215.0" y="1040.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
那么接下来将要完善一下后台代码是如何实现的。
1、首先创建出对应的实体类以及DB表,完善一下Mapper
-- 资金申请业务表,用于存储与流程无关但又必须持久化的业务数据。
CREATE TABLE money_application (
-- 业务主键,自增ID
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
-- 申请人ID或用户名,与流程中的`initiator`变量对应
`applicant` VARCHAR(255) NOT NULL,
-- 申请事由,这是纯业务数据
`reason` VARCHAR(500) NOT NULL,
-- 申请金额,这是驱动流程走向的关键业务数据
`amount` DECIMAL(10, 2) NOT NULL,
-- 流程实例ID,这是业务表与Flowable流程实例建立关联的关键外键
`process_instance_id` VARCHAR(64) NULL,
-- 记录创建时间
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 流程状态
`status` VARCHAR(50) DEFAULT 'PROCESSING'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实体类
package com.example.flowabledemo.entity;
import lombok.Data; // 使用Lombok简化getter, setter, toString等方法的编写
import java.math.BigDecimal;
import java.util.Date;
/**
* @description: 资金申请实体类,映射数据库中的 `money_application` 表。
* 这个类是纯粹的业务领域模型(Domain Model),负责承载业务数据。
*/
@Data
public class MoneyApplication {
/**
* 业务主键ID
*/
private Long id;
/**
* 申请人
*/
private String applicant;
/**
* 申请事由
*/
private String reason;
/**
* 申请金额,使用BigDecimal以保证精度
*/
private BigDecimal amount;
/**
* 关联的Flowable流程实例ID
*/
private String processInstanceId;
/**
* 申请创建时间
*/
private Date createTime;
/**
* 流程状态
* /
private String status;
}
mapper类
package com.example.flowabledemo.mapper;
import com.example.flowabledemo.entity.MoneyApplication;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @description: 资金申请表的MyBatis Mapper接口。
* Spring Boot会自动扫描带有@Mapper注解的接口并为其创建代理实现。
*/
@Mapper
public interface MoneyApplicationMapper {
/**
* 插入一条新的资金申请记录到业务表中。
*
* @param application 包含申请信息的实体对象。
*/
void insert(MoneyApplication application);
/**
* 当流程启动后,将获取到的流程实例ID更新回对应的业务数据行。
* 这是建立业务数据与流程数据双向关联的关键步骤。
*
* @param id 业务数据的主键ID。
* @param processInstanceId Flowable返回的流程实例ID。
*/
void updateProcessInstanceId(@Param("id") Long id, @Param("processInstanceId") String processInstanceId);
/**
* 根据流程实例ID更新业务表的状态。
* @param processInstanceId 流程实例ID
* @param status 新的状态
*/
void updateStatusByProcessInstanceId(@Param("processInstanceId") String processInstanceId, @Param("status") String status);
/**
* 根据流程实例ID查询对应的业务申请详情。
*
* @param processInstanceId 流程实例ID
* @return 资金申请的业务实体对象
*/
MoneyApplication selectByProcessInstanceId(@Param("processInstanceId") String processInstanceId);
}
xml文件就不写了,纯纯的crud。
2、设置请求DTO
package com.example.flowabledemo.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* @description: 用于封装“启动流程”请求的数据传输对象 (Data Transfer Object)。
* 使用DTO可以使Controller接口的定义更清晰,与领域模型解耦,并且便于API文档生成。
*/
@Data
public class StartRequest {
/**
* 申请人ID
*/
private String applicant;
/**
* 申请事由
*/
private String reason;
/**
* 申请金额
*/
private BigDecimal amount;
}
3、设置service编写具体业务
ps:由于只写了一个工作流业务所以很多代码没有提取成公共代码,若是你的项目中引入工作流,简易先写一个service封装一下基本的工作流操作。
package com.example.flowabledemo.service;
import com.example.flowabledemo.entity.MoneyApplication;
import com.example.flowabledemo.mapper.MoneyApplicationMapper;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @description: 资金申请的核心业务服务类。
* 该类封装了所有与Flowable引擎的交互逻辑,以及相关的业务数据操作。
* Controller层只调用这个Service,不直接接触Flowable API。
*/
@Service
public class MoneyApplicationService {
// --- 注入Flowable核心服务API和MyBatis Mapper ---
@Autowired
private RuntimeService runtimeService; // 负责流程实例的启动和管理
@Autowired
private TaskService taskService; // 负责任务的查询、办理和完成
@Autowired
private HistoryService historyService; // 负责查询流程历史记录
@Autowired
private IdentityService identityService; // 注入IdentityService
@Autowired
private MoneyApplicationMapper applicationMapper; // 注入我们自己的MyBatis Mapper
/**
* 启动资金申请流程。
* 这是一个事务性操作,确保业务数据插入和流程启动要么都成功,要么都失败。
*
* @param applicant 申请人
* @param reason 申请事由
* @param amount 申请金额
* @return 启动的流程实例ID
*/
@Transactional
public String startProcess(String applicant, String reason, BigDecimal amount) {
// =================================================================
// 步骤1: 持久化业务数据 (使用MyBatis)
// =================================================================
MoneyApplication application = new MoneyApplication();
application.setApplicant(applicant);
application.setReason(reason);
application.setAmount(amount);
// 调用Mapper将业务数据插入到`money_application`表中
applicationMapper.insert(application); // 插入后,application.getId()将获得数据库生成的自增ID
// =================================================================
// 步骤2: 启动Flowable流程实例
// =================================================================
// 准备流程变量,这些变量将用于驱动流程的逻辑判断
Map<String, Object> variables = new HashMap<>();
// 'initiator' 是BPMN中定义的申请人变量 (e.g., flowable:assignee="${initiator}")
variables.put("initiator", applicant);
// 'amount' 是BPMN中定义的金额变量,用于金额判断网关 (e.g., ${amount > 10000})
// 注意:BPMN表达式通常使用标准Java类型,将BigDecimal转为Double
variables.put("amount", amount.doubleValue());
// 使用BPMN文件中的process id ("money_apply_optimized") 来启动流程。
// 第二个参数是 "businessKey",这是Flowable提供的用于关联业务数据的标准方式。
// 我们将业务表的主键ID作为businessKey,建立了从流程到业务数据的直接关联。
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("money_apply_optimized", String.valueOf(application.getId()), variables);
// =================================================================
// 步骤3: 将流程实例ID更新回业务表,建立双向关联
// =================================================================
applicationMapper.updateProcessInstanceId(application.getId(), processInstance.getId());
// =================================================================
// 步骤4 (可选但推荐): 自动完成第一个任务 "填写资金申请单"
// 因为启动API本身就代表了申请的提交,所以这个任务可以直接完成。
// 需要注意的是flowable6.7发起人跟第一个流程审批的人是同一个的话会自动完成,若是已经配置了true
//=================================================================
Task firstTask = taskService.createTaskQuery()
.processInstanceId(processInstance.getId()) // 限定在刚启动的流程实例中
.taskAssignee(applicant) // 任务的办理人是申请人
.singleResult(); // 确定只有一个结果
if(firstTask != null){
// 准备下一个任务(经理审批)所需的变量。
// 在实际项目中,审批人应根据申请人的部门、角色等动态查询得到。
// 这里为简化演示,我们硬编码。
Map<String, Object> taskVariables = new HashMap<>();
taskVariables.put("manager", "manager"); // 经理审批人
taskVariables.put("director", "director"); // 总监审批人
taskVariables.put("finance_reviewer", "finance_reviewer"); // 财务复核人
taskVariables.put("finance_cashier", "finance_cashier"); // 财务出纳
// 完成任务,并传入变量,这些变量会被设置到流程实例中
taskService.complete(firstTask.getId(), taskVariables);
}
// 返回流程实例ID给调用方
return processInstance.getId();
}
/**
* 根据办理人ID查询其名下的待办任务列表。
*
* @param assignee 办理人ID (例如: "manager", "director", "finance_reviewer")
* @return 一个简化的任务信息列表,方便前端展示。
*/
public List<Map<String, String>> getTasks(String assignee) {
// 使用TaskService创建一个任务查询
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(assignee) // 按办理人过滤
.orderByTaskCreateTime().desc() // 按创建时间降序排序
.list(); // 获取列表
// 将原始的Task对象转换为更简洁的Map结构,便于序列化为JSON
return tasks.stream()
.map(task -> {
Map<String, String> map = new HashMap<>();
map.put("taskId", task.getId());
map.put("taskName", task.getName());
map.put("processInstanceId", task.getProcessInstanceId());
// 也可以从流程变量中获取业务信息展示给审批人
// Map<String, Object> processVariables = taskService.getVariables(task.getId());
// map.put("applicant", (String) processVariables.get("initiator"));
// map.put("amount", String.valueOf(processVariables.get("amount")));
return map;
})
.collect(Collectors.toList());
}
/**
* 完成一个审批任务 (如经理审批, 总监审批, 财务复核)。
*
* @param taskId 要完成的任务的ID。
* @param approved 审批结果 (true=通过, false=驳回)。
*/
@Transactional
public void completeTask(String taskId, Boolean approved) {
// 准备流程变量,用于驱动审批网关的走向
Map<String, Object> variables = new HashMap<>();
// 'approved' 是我们在BPMN中定义的审批结果变量 (e.g., ${approved == true})
variables.put("approved", approved);
// 完成任务,Flowable引擎会自动根据传入的变量和BPMN定义,决定下一步流向哪里。
taskService.complete(taskId, variables);
}
/**
* 完成“修改申请单”这个特殊任务。
*
* @param taskId “修改申请单”任务的ID。
* @param newAmount (可选) 用户修改后的新金额。
*/
@Transactional
public void completeModifyTask(String taskId, BigDecimal newAmount) {
// 如果用户在修改时更新了金额,我们需要将这个变化同步到流程变量中
if (newAmount != null) {
// 先通过taskId找到任务对象,进而获取到流程实例ID
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
// 使用RuntimeService更新流程实例的'amount'变量
runtimeService.setVariable(task.getProcessInstanceId(), "amount", newAmount.doubleValue());
}
// 完成修改任务,流程将根据BPMN定义自动回到之前的审批节点
taskService.complete(taskId);
}
/**
* 获取指定流程实例的执行历史记录。
*
* @param processInstanceId 流程实例ID
* @return 一个包含历史活动信息的字符串列表。
*/
public List<String> getHistory(String processInstanceId) {
// 使用HistoryService创建一个历史活动实例查询
return historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId) // 按流程实例ID过滤
.orderByHistoricActivityInstanceStartTime().asc() // 按开始时间升序排序
.list()
.stream()
// 格式化输出,便于查看
.map(activity -> String.format("【%s】(节点类型: %s) 在 %s 完成",
activity.getActivityName(),
activity.getActivityType(),
activity.getEndTime() != null ? activity.getEndTime() : "进行中"))
.collect(Collectors.toList());
}
/**
* 撤回流程。
* 只有流程发起人可以撤回,且流程必须处于活动状态。
*
* @param processInstanceId 要撤回的流程实例ID
* @param operatorId 操作人ID,用于权限校验
* @param reason 撤回原因
*/
@Transactional
public void withdrawProcess(String processInstanceId, String operatorId, String reason) {
// 1. 校验流程实例是否存在且处于活动状态
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.active() // 关键:确保流程是活动的
.singleResult();
if (processInstance == null) {
throw new IllegalStateException("流程不存在或已结束,无法撤回。");
}
// =================================================================
// 步骤2: 【【【【【 核心修改 】】】】】
// 从我们自己的业务表中查询申请信息,以获取发起人ID
// =================================================================
MoneyApplication application = applicationMapper.selectByProcessInstanceId(processInstanceId);
if (application == null) {
// 这是一个数据不一致的异常情况,理论上不应发生
throw new IllegalStateException("数据异常:找不到与该流程关联的业务申请记录。");
}
// =================================================================
// 步骤3: 权限校验:使用业务表中的发起人信息进行判断
// =================================================================
if (!application.getApplicant().equals(operatorId)) {
throw new SecurityException("无权操作:只有流程发起人 (" + application.getApplicant() + ") 可以撤回。");
}
// 4. 删除Flowable流程实例
// deleteProcessInstance会删除所有运行时数据,并触发PROCESS_CANCELLED事件
// 历史记录默认是保留的
runtimeService.deleteProcessInstance(processInstanceId, reason);
// 5. 更新我们自己的业务表状态为“已撤回”
applicationMapper.updateStatusByProcessInstanceId(processInstanceId, "WITHDRAWN");
// 这里的日志可以通过全局监听器观察到 PROCESS_CANCELLED 事件
log.info("流程已撤回: 实例ID='{}', 操作人='{}', 原因='{}'", processInstanceId, operatorId, reason);
}
/**
* 获取指定任务之前的所有历史用户任务节点,作为可退回的目标。
*
* @param taskId 当前任务ID
* @return 一个包含节点ID和名称的Map列表
*/
public List<Map<String, String>> getReturnableNodes(String taskId) {
// 1. 获取当前任务信息
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
throw new IllegalStateException("任务不存在: " + taskId);
}
// 2. 获取该流程实例所有已完成的历史活动节点
List<HistoricActivityInstance> historyNodes = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.finished() // 只查询已完成的
.orderByHistoricActivityInstanceEndTime().asc() // 按时间排序
.list();
// 3. 过滤出用户任务节点,并且排除当前节点自身
return historyNodes.stream()
.filter(activity -> "userTask".equals(activity.getActivityType()) && !activity.getActivityId().equals(task.getTaskDefinitionKey()))
.map(activity -> {
Map<String, String> node = new HashMap<>();
node.put("activityId", activity.getActivityId()); // 节点ID
node.put("activityName", activity.getActivityName()); // 节点名称
return node;
})
.collect(Collectors.toList());
}
/**
* 将任务退回到指定的历史活动节点。
*
* @param taskId 当前任务ID
* @param targetActivityId 目标活动节点的ID (从getReturnableNodes获取)
* @param reason 退回原因
*/
@Transactional
public void returnTask(String taskId, String targetActivityId, String reason) {
// 1. 校验当前任务是否存在
Task currentTask = taskService.createTaskQuery().taskId(taskId).singleResult();
if (currentTask == null) {
throw new IllegalStateException("当前任务不存在: " + taskId);
}
// 2. 使用Flowable强大的 `ChangeActivityStateBuilder` API
// 这是实现“自由跳转”或“退回”等操作的核心
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId()) // 指定流程实例
.moveActivityIdTo(currentTask.getTaskDefinitionKey(), targetActivityId) // 将当前活动节点移动到目标节点
// .moveExecutionToActivityId(...) 如果有并行网关会更复杂,这里用简单版
.changeState();
// 3. (可选但推荐)在退回后,为当前已完成的任务添加评论,说明退回原因
taskService.addComment(taskId, currentTask.getProcessInstanceId(), "RETURN", "【退回】" + reason);
log.info("任务已退回: 从任务ID='{}' 退回到活动ID='{}', 原因='{}'", taskId, targetActivityId, reason);
}
}
4、最后再写一下Controller
package com.example.flowabledemo.controller;
import com.example.flowabledemo.dto.StartRequest;
import com.example.flowabledemo.service.MoneyApplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @description: 资金申请流程的RESTful API接口。
* 这是暴露给前端或第三方系统调用的入口,负责接收HTTP请求并将其委托给Service层处理。
*/
@RestController
@RequestMapping("/api/process") // 所有接口的基础路径
public class MoneyApplicationController {
@Autowired
private MoneyApplicationService moneyApplicationService;
/**
* API: 启动一个新的资金申请流程。
*
* @param request 包含申请人、事由和金额的JSON请求体。
* @return 包含新创建的流程实例ID的JSON响应。
* @example POST /api/process/start
* Body: {"applicant": "zhangsan", "reason": "购买办公用品", "amount": 5000}
*/
@PostMapping("/start")
public ResponseEntity<Map<String, String>> startProcessInstance(@RequestBody StartRequest request) {
String processInstanceId = moneyApplicationService.startProcess(
request.getApplicant(),
request.getReason(),
request.getAmount()
);
// 返回一个包含流程实例ID的JSON对象,状态码200 OK
return ResponseEntity.ok(Collections.singletonMap("processInstanceId", processInstanceId));
}
/**
* API: 获取指定用户的待办任务列表。
*
* @param assignee 用户的ID。
* @return 任务列表的JSON数组。
* @example GET /api/process/tasks?assignee=manager
*/
@GetMapping("/tasks")
public ResponseEntity<List<Map<String, String>>> getTasks(@RequestParam String assignee) {
return ResponseEntity.ok(moneyApplicationService.getTasks(assignee));
}
/**
* API: 完成一个审批任务 (通过或驳回)。
*
* @param taskId 任务ID
* @param approved 审批结果 (true/false)
* @return 成功时返回200 OK,无响应体。
* @example POST /api/process/approve?taskId=12345&approved=true
*/
@PostMapping("/approve")
public ResponseEntity<Void> approveTask(@RequestParam String taskId, @RequestParam Boolean approved) {
moneyApplicationService.completeTask(taskId, approved);
return ResponseEntity.ok().build();
}
/**
* API: 提交修改后的申请单。
*
* @param taskId “修改申请单”任务的ID
* @param newAmount (可选) 修改后的金额
* @return 成功时返回200 OK,无响应体。
* @example POST /api/process/modify?taskId=67890&newAmount=9500
*/
@PostMapping("/modify")
public ResponseEntity<Void> modifyTask(@RequestParam String taskId, @RequestParam(required = false) BigDecimal newAmount) {
moneyApplicationService.completeModifyTask(taskId, newAmount);
return ResponseEntity.ok().build();
}
/**
* API: 查看指定流程实例的执行历史。
*
* @param processInstanceId 流程实例ID
* @return 历史记录的JSON数组。
* @example GET /api/process/history/abcdef-12345
*/
@GetMapping("/history/{processInstanceId}")
public ResponseEntity<List<String>> getHistory(@PathVariable String processInstanceId) {
return ResponseEntity.ok(moneyApplicationService.getHistory(processInstanceId));
}
/**
* API: 撤回流程。
*
* @param processInstanceId 流程实例ID
* @param operatorId 操作人ID (模拟登录用户)
* @param reason 撤回原因
* @return 成功时返回200 OK
* @example POST /api/process/withdraw?processInstanceId=12345&operatorId=zhangsan&reason=信息填写错误
*/
@PostMapping("/withdraw")
public ResponseEntity<Void> withdrawProcess(
@RequestParam String processInstanceId,
@RequestParam String operatorId,
@RequestParam String reason) {
moneyApplicationService.withdrawProcess(processInstanceId, operatorId, reason);
return ResponseEntity.ok().build();
}
/**
* API: 获取可退回的历史节点列表。
*
* @param taskId 当前任务ID
* @return 节点列表
* @example GET /api/process/returnable-nodes?taskId=12345
*/
@GetMapping("/returnable-nodes")
public ResponseEntity<List<Map<String, String>>> getReturnableNodes(@RequestParam String taskId) {
return ResponseEntity.ok(moneyApplicationService.getReturnableNodes(taskId));
}
/**
* API: 执行退回操作。
*
* @param taskId 当前任务ID
* @param targetActivityId 目标节点ID
* @param reason 退回原因
* @return 成功时返回200 OK
* @example POST /api/process/return?taskId=12345&targetActivityId=task_manager_approval&reason=审批意见不明确
*/
@PostMapping("/return")
public ResponseEntity<Void> returnTask(
@RequestParam String taskId,
@RequestParam String targetActivityId,
@RequestParam String reason) {
moneyApplicationService.returnTask(taskId, targetActivityId, reason);
return ResponseEntity.ok().build();
}
}
当然也可以加上一个监听器来控制一下日志的输出或者根据业务干一些其他的事情。
package com.example.flowabledemo.listener;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.engine.delegate.event.impl.FlowableActivityEventImpl;
import org.flowable.task.api.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @description: 全局Flowable事件监听器,用于记录详细的流程日志。
* 实现了FlowableEventListener接口,可以捕获引擎触发的所有事件。
* 这是一个非常强大的工具,用于审计、监控和调试。
*/
public class FlowableGlobalEventListener implements FlowableEventListener {
// 获取一个SLF4J的Logger实例,用于记录日志
private static final Logger log = LoggerFactory.getLogger(FlowableGlobalEventListener.class);
@Override
public void onEvent(FlowableEvent event) {
// 使用switch语句根据事件类型进行不同的处理
switch (event.getType()) {
// 流程启动事件
case PROCESS_STARTED:
log.info("▶▶▶ 流程启动: 流程实例ID='{}', 流程定义ID='{}'",
event.getExecution().getProcessInstanceId(),
event.getExecution().getProcessDefinitionId());
break;
// 流程完成事件
case PROCESS_COMPLETED:
log.info("◀◀◀ 流程结束: 流程实例ID='{}', 流程定义ID='{}'",
event.getExecution().getProcessInstanceId(),
event.getExecution().getProcessDefinitionId());
break;
// 流程取消事件
case PROCESS_CANCELLED:
log.info("◀◀◀ 流程取消: 流程实例ID='{}'", event.getExecution().getProcessInstanceId());
break;
// 一个活动(流程中的一个节点)开始
case ACTIVITY_STARTED:
FlowableActivityEventImpl activityStartedEvent = (FlowableActivityEventImpl) event;
log.info(" → 活动开始: ID='{}', 名称='{}', 类型='{}'",
activityStartedEvent.getActivityId(),
activityStartedEvent.getActivityName(),
activityStartedEvent.getActivityType());
break;
// 一个活动(流程中的一个节点)完成
case ACTIVITY_COMPLETED:
FlowableActivityEventImpl activityCompletedEvent = (FlowableActivityEventImpl) event;
log.info(" ← 活动完成: ID='{}', 名称='{}', 类型='{}'",
activityCompletedEvent.getActivityId(),
activityCompletedEvent.getActivityName(),
activityCompletedEvent.getActivityType());
break;
// 一个用户任务被创建
case TASK_CREATED:
FlowableEngineEntityEvent taskCreatedEvent = (FlowableEngineEntityEvent) event;
Task task = (Task) taskCreatedEvent.getEntity();
log.info(" ◈ 任务创建: 任务ID='{}', 任务名称='{}', 办理人='{}'",
task.getId(),
task.getName(),
task.getAssignee());
break;
// 一个用户任务被分配给某人
case TASK_ASSIGNED:
FlowableEngineEntityEvent taskAssignedEvent = (FlowableEngineEntityEvent) event;
Task assignedTask = (Task) taskAssignedEvent.getEntity();
log.info(" ◈ 任务分配: 任务ID='{}', 任务名称='{}', 新办理人='{}'",
assignedTask.getId(),
assignedTask.getName(),
assignedTask.getAssignee());
break;
// 一个用户任务被完成
case TASK_COMPLETED:
FlowableEngineEntityEvent taskCompletedEvent = (FlowableEngineEntityEvent) event;
Task completedTask = (Task) taskCompletedEvent.getEntity();
log.info(" ✔ 任务完成: 任务ID='{}', 任务名称='{}', 办理人='{}'",
completedTask.getId(),
completedTask.getName(),
completedTask.getAssignee());
break;
default:
// 对于其他不关心的事件,可以忽略
// log.debug("捕获到其他事件: {}", event.getType());
}
}
@Override
public boolean isFailOnException() {
// 当监听器中的逻辑抛出异常时,是否希望它影响主流程的事务。
// false表示即使日志记录失败,也不要回滚主流程。这对于非关键的日志记录是安全的。
return false;
}
@Override
public boolean isFireOnTransactionLifecycleEvent() {
// 这个监听器是否应该在事务生命周期的特定点触发。
// null表示使用引擎的默认设置。
return false;
}
@Override
public String getOnTransaction() {
// 指定监听器应该在哪个事务状态下触发。
// null表示使用引擎的默认设置。
return null;
}
}
然后将其注册到流程引擎中去
package com.example.flowabledemo.config;
import com.example.flowabledemo.listener.FlowableGlobalEventListener;
import org.flowable.engine.RuntimeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @description: Flowable配置类,用于在应用启动后进行额外的配置,如此处的监听器注册。
*/
@Configuration
public class FlowableListenerConfiguration {
// 注入Flowable的RuntimeService,我们需要用它来添加监听器
@Autowired
private RuntimeService runtimeService;
/**
* 使用@PostConstruct注解,这个方法会在Spring容器完成所有bean的初始化之后自动执行。
* 这是注册全局监听器的理想时机。
*/
@PostConstruct
public void addGlobalEventListener() {
// 向Flowable引擎添加一个全局事件监听器。
// 从现在开始,引擎触发的任何事件都会通知这个监听器。
runtimeService.addEventListener(new FlowableGlobalEventListener());
System.out.println("Flowable全局日志监听器已成功注册!");
}
}
到这就完事了,目前功能只是简单的操作,基本可以完成大部分线性业务。