SpringBoot 2.6.2 + Flowable 6.7.2 整合实战:从零搭建一个报销审批系统(附源码)
SpringBoot + Flowable 报销审批系统实战:从流程设计到权限控制的全链路实现
在企业的日常运营中,报销审批是一个高频且重要的业务流程。传统的手工审批方式效率低下,而基于工作流引擎的系统可以实现审批流程的自动化、规范化和可追溯。本文将带你从零开始,基于SpringBoot 2.6.2和Flowable 6.7.2构建一个完整的报销审批系统,涵盖流程设计、表单开发、权限控制等核心环节。
1. 技术选型与环境搭建
1.1 为什么选择Flowable
Flowable是一个轻量级、高性能的BPMN 2.0流程引擎,相比Activiti具有以下优势:
- 更活跃的社区支持:Flowable是Activiti的分支,社区更新更频繁
- 更简洁的API设计:减少了冗余配置,开发体验更好
- 更好的Spring集成:原生支持Spring Boot Starter
- 更完善的功能模块:包括Modeler、IDM等配套工具
1.2 基础环境配置
首先创建Spring Boot项目并添加必要依赖:
<dependencies> <!-- Flowable核心依赖 --> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.7.2</version> </dependency> <!-- 数据库相关 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <!-- Web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>application.yml配置示例:
spring: datasource: url: jdbc:mysql://localhost:3306/flowable_expense?useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource flowable: database-schema-update: true # 自动创建表结构 async-executor-activate: false # 关闭异步任务执行器 history-level: full # 完整的历史记录提示:生产环境应将database-schema-update设置为false,避免意外修改表结构
2. 报销流程设计与实现
2.1 业务流程分析
典型的报销审批流程包含以下环节:
- 员工提交报销单:填写报销金额、事由、附件等
- 部门经理审批:金额≤500元时直接审批
- 财务总监审批:金额>500元时需要额外审批
- 财务处理:出纳打款
- 流程结束:归档记录
2.2 BPMN流程定义
使用Flowable Modeler设计报销流程,对应的BPMN XML核心部分:
<process id="expense_process" name="报销审批流程" isExecutable="true"> <startEvent id="startEvent" name="开始"></startEvent> <!-- 员工填写报销单 --> <userTask id="fillExpense" name="填写报销单" flowable:assignee="${applicant}"> <extensionElements> <flowable:formProperty id="amount" name="报销金额" type="double" required="true"/> <flowable:formProperty id="reason" name="报销事由" type="string" required="true"/> </extensionElements> </userTask> <!-- 部门经理审批 --> <userTask id="deptManagerApprove" name="部门经理审批" flowable:candidateGroups="dept_manager"> <extensionElements> <flowable:formProperty id="deptComment" name="审批意见" type="string"/> </extensionElements> </userTask> <!-- 金额判断网关 --> <exclusiveGateway id="amountJudge" name="金额判断"></exclusiveGateway> <!-- 财务总监审批 --> <userTask id="financeDirectorApprove" name="财务总监审批" flowable:candidateGroups="finance_director"> <extensionElements> <flowable:formProperty id="financeComment" name="审批意见" type="string"/> </extensionElements> </userTask> <!-- 财务处理 --> <userTask id="financeProcess" name="财务处理" flowable:candidateGroups="finance_staff"> <extensionElements> <flowable:formProperty id="paymentInfo" name="付款信息" type="string"/> </extensionElements> </userTask> <endEvent id="endEvent" name="结束"></endEvent> <!-- 序列流定义 --> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="fillExpense"/> <sequenceFlow id="flow2" sourceRef="fillExpense" targetRef="deptManagerApprove"/> <sequenceFlow id="flow3" sourceRef="deptManagerApprove" targetRef="amountJudge"/> <!-- 金额<=500流向财务处理 --> <sequenceFlow id="flow4" sourceRef="amountJudge" targetRef="financeProcess"> <conditionExpression xsi:type="tFormalExpression"> ${amount <= 500} </conditionExpression> </sequenceFlow> <!-- 金额>500流向财务总监审批 --> <sequenceFlow id="flow5" sourceRef="amountJudge" targetRef="financeDirectorApprove"> <conditionExpression xsi:type="tFormalExpression"> ${amount > 500} </conditionExpression> </sequenceFlow> <sequenceFlow id="flow6" sourceRef="financeDirectorApprove" targetRef="financeProcess"/> <sequenceFlow id="flow7" sourceRef="financeProcess" targetRef="endEvent"/> </process>2.3 流程部署与启动
通过RepositoryService部署流程定义:
@Service public class ProcessDeployService { @Autowired private RepositoryService repositoryService; public void deployProcess(String processName, String bpmnPath) { Deployment deployment = repositoryService.createDeployment() .name(processName) .addClasspathResource(bpmnPath) .deploy(); System.out.println("流程部署成功,部署ID:" + deployment.getId()); } }启动流程实例:
@RestController @RequestMapping("/expense") public class ExpenseController { @Autowired private RuntimeService runtimeService; @PostMapping("/start") public String startExpenseProcess(@RequestBody ExpenseRequest request) { Map<String, Object> variables = new HashMap<>(); variables.put("applicant", request.getApplicant()); variables.put("amount", request.getAmount()); variables.put("reason", request.getReason()); ProcessInstance instance = runtimeService.startProcessInstanceByKey( "expense_process", variables); return "流程启动成功,流程实例ID:" + instance.getId(); } }3. 业务数据与流程的关联
3.1 数据库设计
为报销系统设计业务表结构:
CREATE TABLE `expense_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '报销单号', `applicant` varchar(64) NOT NULL COMMENT '申请人', `amount` decimal(10,2) NOT NULL COMMENT '报销金额', `reason` varchar(255) NOT NULL COMMENT '报销事由', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待审批,1-审批中,2-已通过,3-已驳回', `process_instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_process_instance` (`process_instance_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报销单表'; CREATE TABLE `expense_approval` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL COMMENT '报销单ID', `approver` varchar(64) NOT NULL COMMENT '审批人', `approval_result` tinyint(4) NOT NULL COMMENT '审批结果:1-通过,2-驳回', `approval_comment` varchar(255) DEFAULT NULL COMMENT '审批意见', `task_id` varchar(64) DEFAULT NULL COMMENT '任务ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批记录表';3.2 业务与流程的同步
使用ExecutionListener实现业务状态同步:
public class ExpenseStatusListener implements ExecutionListener { @Override public void notify(DelegateExecution execution) { String processInstanceId = execution.getProcessInstanceId(); String eventName = execution.getEventName(); // 获取业务服务Bean ExpenseOrderService orderService = SpringContextUtil.getBean(ExpenseOrderService.class); if ("start".equals(eventName)) { // 流程启动时更新状态为"审批中" orderService.updateStatusByProcessInstance(processInstanceId, 1); } else if ("end".equals(eventName)) { // 流程结束时判断最终状态 boolean approved = execution.getVariable("approved", Boolean.class); orderService.updateStatusByProcessInstance( processInstanceId, approved ? 2 : 3); } } }4. 审批功能实现
4.1 任务查询接口
实现获取用户待办任务列表:
@RestController @RequestMapping("/task") public class TaskController { @Autowired private TaskService taskService; @GetMapping("/todo") public List<TaskDTO> getTodoTasks( @RequestParam String userId, @RequestParam(required = false) List<String> groups) { // 创建任务查询 TaskQuery query = taskService.createTaskQuery() .taskAssignee(userId); // 添加候选组查询 if (groups != null && !groups.isEmpty()) { query.taskCandidateGroupIn(groups); } // 执行查询并转换为DTO return query.orderByTaskCreateTime().desc().list() .stream() .map(task -> { TaskDTO dto = new TaskDTO(); dto.setTaskId(task.getId()); dto.setName(task.getName()); dto.setCreateTime(task.getCreateTime()); dto.setProcessInstanceId(task.getProcessInstanceId()); return dto; }) .collect(Collectors.toList()); } }4.2 任务审批接口
实现任务审批通过/驳回操作:
@PostMapping("/approve") public String approveTask(@RequestBody ApproveRequest request) { // 获取当前任务 Task task = taskService.createTaskQuery() .taskId(request.getTaskId()) .singleResult(); if (task == null) { throw new RuntimeException("任务不存在或已完成"); } // 设置审批变量 Map<String, Object> variables = new HashMap<>(); variables.put("approved", request.isApproved()); variables.put("comment", request.getComment()); // 完成任务 taskService.complete(task.getId(), variables); // 记录审批记录 approvalService.recordApproval( task.getProcessInstanceId(), task.getId(), SecurityUtils.getCurrentUserId(), request.isApproved(), request.getComment()); return "审批操作成功"; }5. 高级功能实现
5.1 动态审批人设置
通过TaskListener实现动态审批人分配:
public class DeptManagerTaskListener implements TaskListener { @Override public void notify(DelegateTask delegateTask) { // 获取流程变量 String applicant = (String) delegateTask.getVariable("applicant"); // 根据申请人查询部门经理 UserService userService = SpringContextUtil.getBean(UserService.class); String deptManager = userService.getDeptManagerByUser(applicant); // 设置任务处理人 delegateTask.setAssignee(deptManager); } }5.2 驳回功能实现
在BPMN中定义驳回路线:
<sequenceFlow id="rejectFlow" sourceRef="deptManagerApprove" targetRef="fillExpense"> <conditionExpression xsi:type="tFormalExpression"> ${!approved} </conditionExpression> </sequenceFlow>5.3 流程图表生成
实现流程进度图生成接口:
@GetMapping("/diagram") public void generateDiagram( HttpServletResponse response, @RequestParam String processInstanceId) throws IOException { ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult(); if (processInstance == null) { return; } // 获取活动节点 List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstanceId); // 获取BPMN模型 BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId()); // 生成流程图 ProcessDiagramGenerator diagramGenerator = processEngineConfiguration.getProcessDiagramGenerator(); InputStream imageStream = diagramGenerator.generateDiagram( bpmnModel, "png", activeActivityIds, Collections.emptyList(), processEngineConfiguration.getActivityFontName(), processEngineConfiguration.getLabelFontName(), processEngineConfiguration.getAnnotationFontName(), null, 1.0, true); // 输出图片 response.setContentType("image/png"); try (OutputStream out = response.getOutputStream()) { IOUtils.copy(imageStream, out); } }6. 前端集成与权限控制
6.1 集成Flowable Modeler
添加Modeler依赖并配置:
<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-ui-modeler-rest</artifactId> <version>6.7.2</version> </dependency> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-ui-modeler-conf</artifactId> <version>6.7.2</version> </dependency>配置安全规则绕过授权:
@Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER - 1) public class ModelerSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/modeler/**").permitAll() .anyRequest().authenticated(); } }6.2 自定义用户集成
重写用户信息接口:
@RestController @RequestMapping("/app") public class CustomUserResource { @GetMapping("/rest/account") public UserRepresentation getCurrentUser() { UserRepresentation user = new UserRepresentation(); user.setId(SecurityUtils.getCurrentUserId()); user.setFirstName(SecurityUtils.getCurrentUserName()); // 设置权限 List<String> privileges = new ArrayList<>(); privileges.add("access-idm"); privileges.add("access-modeler"); user.setPrivileges(privileges); return user; } }7. 系统优化与扩展
7.1 性能优化建议
启用异步执行器:对于耗时操作启用异步处理
flowable: async-executor-activate: true async-executor-thread-pool-size: 10历史数据归档:定期归档已完成流程的历史数据
缓存流程定义:避免频繁读取数据库
7.2 扩展功能思路
- 多级审批:支持无限级审批层级配置
- 金额分段审批:不同金额范围走不同审批路径
- 附件管理:集成文件上传服务
- 移动端支持:开发微信小程序审批端
7.3 常见问题解决方案
问题1:流程图中中文乱码
解决方案:配置字体
@Configuration public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> { @Override public void configure(SpringProcessEngineConfiguration configuration) { configuration.setActivityFontName("宋体"); configuration.setLabelFontName("宋体"); configuration.setAnnotationFontName("宋体"); } }问题2:MyBatis与Flowable冲突
解决方案:使用多数据源配置,为MyBatis指定主数据源
@MapperScan(basePackages = "com.example.mapper", sqlSessionFactoryRef = "businessSqlSessionFactory") @Configuration public class BusinessDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.business") public DataSource businessDataSource() { return DataSourceBuilder.create().build(); } @Bean public SqlSessionFactory businessSqlSessionFactory( @Qualifier("businessDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } }8. 项目部署与测试
8.1 测试用例设计
编写集成测试验证核心流程:
@SpringBootTest class ExpenseProcessTest { @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Test void testExpenseProcess() { // 1. 启动流程 Map<String, Object> vars = new HashMap<>(); vars.put("applicant", "user1"); vars.put("amount", 1000.00); ProcessInstance instance = runtimeService.startProcessInstanceByKey( "expense_process", vars); // 2. 验证任务 Task task = taskService.createTaskQuery() .processInstanceId(instance.getId()) .singleResult(); assertEquals("填写报销单", task.getName()); // 3. 完成任务 taskService.complete(task.getId()); // 4. 验证下一任务 task = taskService.createTaskQuery() .processInstanceId(instance.getId()) .singleResult(); assertEquals("部门经理审批", task.getName()); } }8.2 部署建议
- 数据库:生产环境建议使用主从配置
- 应用服务器:至少2个节点实现高可用
- 监控:集成Prometheus监控流程引擎指标
- 日志:集中式日志收集分析
9. 总结与源码
通过本文的实践,我们完成了基于SpringBoot和Flowable的报销审批系统,实现了以下功能:
- 可视化流程设计:通过Flowable Modeler设计业务流程
- 灵活的任务分配:支持固定审批人和动态审批人
- 完整的审批功能:包括通过、驳回、撤销等操作
- 业务集成:流程与业务数据紧密关联
- 权限控制:与现有系统用户体系集成
项目源码结构:
flowable-expense/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── config/ # 配置类 │ │ │ ├── controller/ # 控制器 │ │ │ ├── listener/ # 流程监听器 │ │ │ ├── model/ # 数据模型 │ │ │ ├── service/ # 业务服务 │ │ │ └── FlowableExpenseApplication.java │ │ └── resources/ │ │ ├── static/ # 静态资源 │ │ ├── templates/ # 模板文件 │ │ ├── processes/ # BPMN流程文件 │ │ └── application.yml # 应用配置 │ └── test/ # 测试代码 ├── pom.xml # Maven配置 └── README.md # 项目说明