当我们需要在Java应用中实现复杂的审批流程时,Activiti作为一款轻量级的工作流引擎,提供了强大的流程控制能力。本文将从一个真实的报销审批场景出发,逐步构建一个具备撤回、驳回和挂起功能的完整流程解决方案。
在开始之前,我们需要准备以下环境:
首先创建一个Spring Boot项目并添加Activiti依赖:
xml复制<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0</version>
</dependency>
基础报销流程的BPMN定义如下:
xml复制<process id="expense_approval" name="员工报销审批流程">
<startEvent id="startEvent" />
<userTask id="submitExpense" name="提交报销" activiti:assignee="${applicant}" />
<userTask id="deptManagerApprove" name="部门经理审批" activiti:assignee="deptManager" />
<userTask id="financeApprove" name="财务审批" activiti:assignee="financeStaff" />
<endEvent id="endEvent" />
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitExpense" />
<sequenceFlow id="flow2" sourceRef="submitExpense" targetRef="deptManagerApprove" />
<sequenceFlow id="flow3" sourceRef="deptManagerApprove" targetRef="financeApprove" />
<sequenceFlow id="flow4" sourceRef="financeApprove" targetRef="endEvent" />
</process>
这个简单的流程定义了三个审批节点:员工提交、部门经理审批和财务审批。接下来我们需要为这个基础流程添加更智能的控制功能。
在实际业务中,员工提交报销后可能发现填写错误,需要在部门经理审批前撤回申请。Activiti本身不直接提供撤回功能,但我们可以通过API组合实现。
撤回功能的实现原理:
以下是核心实现代码:
java复制public class ProcessRecallService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
public boolean recallProcess(String processInstanceId, String userId) {
// 获取当前任务
Task currentTask = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.singleResult();
// 验证用户权限
HistoricTaskInstance submitTask = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.taskDefinitionKey("submitExpense")
.taskAssignee(userId)
.singleResult();
if(submitTask == null) {
throw new IllegalStateException("只有提交人才能撤回申请");
}
// 修改流程定义
BpmnModel bpmnModel = repositoryService.getBpmnModel(
runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult()
.getProcessDefinitionId());
// 实现流程跳转逻辑
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdTo(currentTask.getTaskDefinitionKey(), "submitExpense")
.changeState();
// 添加撤回备注
taskService.addComment(currentTask.getId(), processInstanceId,
"申请已由" + userId + "撤回");
return true;
}
}
前端交互建议:
javascript复制// 在报销申请页面添加撤回按钮
function checkRecallStatus(processInstanceId, currentUser) {
fetch(`/api/process/canRecall?instanceId=${processInstanceId}&userId=${currentUser}`)
.then(response => {
if(response.canRecall) {
document.getElementById('recallBtn').style.display = 'block';
}
});
}
document.getElementById('recallBtn').addEventListener('click', () => {
fetch(`/api/process/recall`, {
method: 'POST',
body: JSON.stringify({
instanceId: processInstanceId,
userId: currentUser
})
}).then(() => {
alert('撤回成功');
location.reload();
});
});
驳回功能允许审批人将申请退回给提交人或之前的审批节点。与撤回不同,驳回是由审批人发起的操作。
驳回功能的业务规则:
实现代码如下:
java复制public class ProcessRejectService {
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
public void rejectToPreviousNode(String taskId, String rejectReason) {
Task currentTask = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
String processInstanceId = currentTask.getProcessInstanceId();
// 获取历史任务以确定退回节点
List<HistoricTaskInstance> history = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricTaskInstanceStartTime()
.asc()
.list();
// 找到当前任务的前一个用户任务
String previousTaskKey = findPreviousUserTask(history, currentTask.getTaskDefinitionKey());
// 执行驳回
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdTo(currentTask.getTaskDefinitionKey(), previousTaskKey)
.changeState();
// 记录驳回意见
taskService.addComment(taskId, processInstanceId, "驳回原因: " + rejectReason);
}
private String findPreviousUserTask(List<HistoricTaskInstance> history, String currentTaskKey) {
// 实现逻辑:从历史记录中找到当前任务的前一个用户任务节点
// ...
}
}
驳回操作的前端实现:
html复制<div class="reject-section">
<button onclick="showRejectDialog()">驳回申请</button>
<div id="rejectDialog" style="display:none;">
<select id="rejectTarget">
<option value="submitExpense">退回给申请人</option>
<option value="deptManagerApprove">退回给部门经理</option>
</select>
<textarea id="rejectReason" placeholder="请输入驳回原因"></textarea>
<button onclick="submitReject()">确认驳回</button>
</div>
</div>
<script>
function submitReject() {
const target = document.getElementById('rejectTarget').value;
const reason = document.getElementById('rejectReason').value;
fetch('/api/task/reject', {
method: 'POST',
body: JSON.stringify({
taskId: currentTaskId,
targetNode: target,
reason: reason
})
}).then(() => {
alert('驳回成功');
location.reload();
});
}
</script>
流程挂起功能可以暂停流程实例的执行,通常用于处理特殊情况或超时未处理的流程。
挂起功能的典型应用场景:
以下是实现代码:
java复制public class ProcessSuspendService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private ManagementService managementService;
// 挂起单个流程实例
public void suspendProcessInstance(String processInstanceId) {
runtimeService.suspendProcessInstanceById(processInstanceId);
}
// 自动挂起超时未处理的流程
@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void autoSuspendTimeoutProcesses() {
List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery()
.active()
.list();
for(ProcessInstance instance : instances) {
Task task = taskService.createTaskQuery()
.processInstanceId(instance.getId())
.active()
.singleResult();
if(task != null && isTaskTimeout(task)) {
runtimeService.suspendProcessInstanceById(instance.getId());
// 发送通知
sendTimeoutNotification(instance.getId(), task.getAssignee());
}
}
}
private boolean isTaskTimeout(Task task) {
// 判断任务是否超时(如超过3天未处理)
// ...
}
}
流程状态查询接口:
java复制@RestController
@RequestMapping("/api/process")
public class ProcessStatusController {
@GetMapping("/status/{instanceId}")
public ProcessStatus getProcessStatus(@PathVariable String instanceId) {
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
ProcessStatus status = new ProcessStatus();
status.setSuspended(instance.isSuspended());
// 获取当前任务信息
Task currentTask = taskService.createTaskQuery()
.processInstanceId(instanceId)
.singleResult();
if(currentTask != null) {
status.setCurrentTaskName(currentTask.getName());
status.setCurrentAssignee(currentTask.getAssignee());
}
return status;
}
}
一个完整的审批系统需要根据流程状态动态调整UI。以下是关键的状态处理逻辑:
流程状态枚举:
java复制public enum ProcessStatusEnum {
DRAFT("草稿"),
SUBMITTED("已提交"),
DEPT_APPROVING("部门审批中"),
FINANCE_APPROVING("财务审批中"),
REJECTED("已驳回"),
RECALLED("已撤回"),
SUSPENDED("已挂起"),
COMPLETED("已完成");
private String desc;
// constructor and getter
}
前端按钮状态控制逻辑:
javascript复制function updateButtonStatus(processStatus, currentUser) {
const submitBtn = document.getElementById('submitBtn');
const recallBtn = document.getElementById('recallBtn');
const approveBtn = document.getElementById('approveBtn');
const rejectBtn = document.getElementById('rejectBtn');
// 根据流程状态和当前用户角色控制按钮显示
switch(processStatus) {
case 'DRAFT':
submitBtn.style.display = currentUser === applicant ? 'block' : 'none';
break;
case 'SUBMITTED':
recallBtn.style.display = currentUser === applicant ? 'block' : 'none';
approveBtn.style.display = currentUser === deptManager ? 'block' : 'none';
rejectBtn.style.display = currentUser === deptManager ? 'block' : 'none';
break;
case 'DEPT_APPROVING':
approveBtn.style.display = currentUser === financeStaff ? 'block' : 'none';
rejectBtn.style.display = currentUser === financeStaff ? 'block' : 'none';
break;
case 'REJECTED':
submitBtn.style.display = currentUser === applicant ? 'block' : 'none';
break;
case 'SUSPENDED':
// 显示恢复按钮给管理员
break;
}
}
流程历史记录展示:
java复制public List<ProcessHistoryDTO> getProcessHistory(String processInstanceId) {
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricTaskInstanceStartTime()
.asc()
.list();
return tasks.stream().map(task -> {
ProcessHistoryDTO dto = new ProcessHistoryDTO();
dto.setTaskName(task.getName());
dto.setAssignee(task.getAssignee());
dto.setStartTime(task.getStartTime());
dto.setEndTime(task.getEndTime());
// 获取审批意见
List<Comment> comments = taskService.getTaskComments(task.getId());
if(!comments.isEmpty()) {
dto.setComment(comments.get(0).getFullMessage());
}
return dto;
}).collect(Collectors.toList());
}
在实际项目中,我们可能会遇到以下问题:
1. 历史数据膨胀问题
Activiti默认会保存所有流程历史数据,长期运行可能导致数据库膨胀。解决方案:
java复制// 在流程引擎配置中设置历史级别
@Bean
public ProcessEngineConfigurationImpl processEngineConfiguration() {
SpringProcessEngineConfiguration config = new SpringProcessEngineConfiguration();
config.setHistoryLevel(HistoryLevel.AUDIT); // 适当的历史级别
return config;
}
// 定期清理历史数据
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanHistoryData() {
historyService.createHistoricProcessInstanceQuery()
.finished()
.list()
.forEach(instance -> {
if(shouldClean(instance)) {
historyService.deleteHistoricProcessInstance(instance.getId());
}
});
}
2. 并发处理问题
当多个用户同时操作同一流程时可能出现并发冲突。解决方案:
java复制public void approveTask(String taskId, String userId) {
// 使用乐观锁控制并发
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
try {
taskService.claim(taskId, userId);
// 执行业务逻辑...
taskService.complete(taskId);
} catch (ActivitiOptimisticLockingException e) {
// 处理并发冲突
log.warn("任务已被其他用户处理: {}", taskId);
throw new BusinessException("当前任务已被其他用户处理,请刷新后重试");
}
}
3. 事务管理建议
流程操作通常需要与业务操作保持事务一致:
java复制@Transactional
public void completeExpenseApproval(String taskId, ExpenseApprovalDTO dto) {
// 1. 更新业务数据
expenseService.updateExpenseStatus(dto.getExpenseId(), "APPROVED");
// 2. 完成审批任务
taskService.complete(taskId, dto.getVariables());
// 3. 记录审批日志
approvalLogService.saveLog(dto);
// 如果任何一步失败,整个操作将回滚
}
动态审批人配置:
硬编码审批人不利于系统扩展,建议实现动态审批人配置:
java复制public class DynamicAssigneeService implements TaskAssignmentListener {
@Override
public void notify(TaskAssignmentEvent event) {
String taskDefinitionKey = event.getTask().getTaskDefinitionKey();
String processInstanceId = event.getTask().getProcessInstanceId();
// 从数据库或外部系统获取审批人
String assignee = findAssigneeByTaskKey(taskDefinitionKey, processInstanceId);
if(assignee != null) {
taskService.setAssignee(event.getTask().getId(), assignee);
}
}
private String findAssigneeByTaskKey(String taskKey, String processInstanceId) {
// 实现动态审批人查询逻辑
// ...
}
}
审批链模式:
对于更复杂的审批场景,可以实现审批链模式:
java复制public class ApprovalChainService {
public void startApprovalChain(String businessKey) {
// 根据业务规则确定审批流程
String processDefinitionKey = determineProcessDefinition(businessKey);
// 启动流程
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
processDefinitionKey, businessKey);
// 设置第一个审批人
Task firstTask = taskService.createTaskQuery()
.processInstanceId(instance.getId())
.singleResult();
taskService.setAssignee(firstTask.getId(), getFirstApprover(businessKey));
}
private String determineProcessDefinition(String businessKey) {
// 根据业务规则返回不同的流程定义Key
// ...
}
}
性能监控与统计:
java复制public class ProcessMonitorService {
public ProcessStatistics getProcessStatistics(String processDefinitionKey) {
ProcessStatistics stats = new ProcessStatistics();
// 运行中的实例数
stats.setActiveInstances(runtimeService.createProcessInstanceQuery()
.processDefinitionKey(processDefinitionKey)
.count());
// 平均审批时间
List<HistoricProcessInstance> instances = historyService.createHistoricProcessInstanceQuery()
.processDefinitionKey(processDefinitionKey)
.finished()
.list();
long totalDuration = instances.stream()
.mapToLong(i -> i.getDurationInMillis())
.sum();
stats.setAverageDuration(instances.isEmpty() ? 0 : totalDuration / instances.size());
return stats;
}
}
在实际项目中,我们还需要考虑以下实践: