RuoYi集成Flowable工作流引擎的OA审批实战指南
2026/9/11 22:10:36 网站建设 项目流程

简介:一套面向Java开发者的OA办公系统项目源码,基于RuoYi框架集成Flowable工作流引擎实现,适合需要学习SpringBoot+MyBatis实战或快速搭建后台管理系统的读者。项目可在Eclipse与IntelliJ IDEA中直接运行,前后端技术栈包括Layui、Ajax、Json以及SpringBoot、MyBatis,推荐JDK1.8+Maven+MySQL环境。系统包含管理员与用户两种角色,覆盖登录注册、工作计划、待办已办、通知公告、公章申请、项目管理、流程管理、权限设置、定时任务等典型OA模块,业务完整度较高。资源压缩包共2000个文件,其中HTML与JS前端页面约占多数,另有259个Java源文件、SQL数据库脚本及CSS、JSON、XML等配置,整体大小321.39MB,便于按目录结构查阅学习。内含完整数据库脚本和前后端分离逻辑,可快速启动企业级后台系统原型;已有375人学习,适合希望从零理解工作流集成与权限设计的中级Java开发者。

1. OA 的流程不只是状态字段:RuoYi 和 Flowable 各管哪一段

OA 项目最常在验收前一个月出问题:请假单能提交,但流程走到一半卡死;报销单审批通过后,列表里状态还是“审批中”;一个人请假,部门主管和人事同时看到待办,谁批都生效。这些问题大多不是表单字段设计不够,也不是 SQL 写错,而是把“审批流程”硬编码进了业务表的状态字段。RuoYi 提供了部门、用户、角色、菜单、代码生成这些组织与管理能力,但它不会替你管理流程节点;Flowable 是 BPMN 2.0 规范的工作流引擎,负责流程定义、任务分派、驳回、会签。两者通过 Spring Boot 拼在一起,形成“RuoYi 管组织与数据权限、Flowable 管审批流转、业务表只存业务数据”的三角结构。这套组合也是目前 Java 生态里做 OA 类系统最常见的技术选型,正文按集成建表、部署发起、审批回退、表单绑定、踩坑验证的顺序展开。

2. Flowable 的表和 RuoYi 的业务表怎么分家:从 ACT_ 前缀说起

2.1 Flowable 都需要创建哪些表:四组前缀各管一摊

Flowable 启动后会创建 60 多张表,第一次接触的人看到这个数量通常会犹豫,以为是引错了依赖。其实按表名前缀划分只有四组,职责非常明确:ACT_RE_ 存流程定义和模型,ACT_RU_ 存运行中的实例与任务,ACT_HI_ 存审批历史和足迹,ACT_GE_ 存通用数据。RuoYi 自带的业务表以 sys_ 为前缀,两者互不干扰。

前缀表族典型表职责
ACT_RE_流程定义存储ACT_RE_DEPLOYMENT、ACT_RE_PROCDEF部署包、流程定义及版本
ACT_RU_运行时数据ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE、ACT_RU_IDENTITYLINK运行中的实例、任务、变量、参与者
ACT_HI_历史数据ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST、ACT_HI_VARINST已结束实例、任务足迹、审批记录
ACT_GE_通用数据ACT_GE_BYTEARRAY、ACT_GE_PROPERTY部署资源字节流、引擎属性

这四组表里,ACTHI_ 和 ACT_RU_ 是日常运维关注的重点。审批中的任务一定在 ACT_RU_TASK 里有记录,流程结束后会写到 ACT_HI_PROCINST,RuoYi 的业务表不会知道这些细节,它只需要记住一个流程实例 ID。

2.2 自动建表配置:一步到位但生产环境要改掉

RuoYi 的数据源默认走 Druid,集成 Flowable 时不需要单独配置数据源,让 Flowable 与 RuoYi 共用业务库即可。首次启动时用自动建表最省事,配置如下:

flowable: database-schema-update: true async-executor-activate: false history: full db-history-used: true check-process-definitions: true process-definition-location-prefix: classpath:/processes/

database-schema-update: true表示启动时自动创建或升级 ACT_ 表,第一次跑通项目可以开着;生产环境建议改为 false,用官方 SQL 脚本在发布流程里手动初始化,避免引擎在启动时做不可控的 DDL。async-executor-activate: false很关键,没有定时器事件、异步消息的流程不需要激活异步执行器,置为 false 可以减少一批后台线程。history: full表示记录全部历史数据,审批意见、变量变更都能追溯;如果项目对历史要求不高,可以改成 audit 减少存储量。process-definition-location-prefix让 Flowable 启动时扫描 classpath 下指定目录里的 BPMN 文件并自动部署,本地开发和测试环境非常方便。

MySQL 8 环境下最容易踩的坑是启动日志没有报错,但 ACT_ 表一张都没建。原因是驱动拿不到正确的 catalog,需要在 RuoYi 的 Druid URL 上追加参数:

url: jdbc:mysql://localhost:3306/ry_oa?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true

nullCatalogMeansCurrent=true告诉 MySQL 驱动在 catalog 为空时使用当前数据库,Flowable 的表结构初始化语句才能落到ry_oa库,而不是报 “Table doesn't exist” 或建到奇怪的位置。这个参数也是网上搜“flowable 工作流数据库报错”时最常看到的解药。

2.3 RuoYi 业务表怎么挂流程:proc_inst_id 和 status 是标配

Flowable 不管业务数据,RuoYi 的代码生成器也不懂流程节点,两边的连接点就是流程实例 ID。以请假单为例,业务表最少要有三个与流程相关的字段:proc_inst_id存 Flowable 的流程实例 ID,status存业务自己的审批状态,apply_user_id存发起人。流程发起后用businessKey记录业务主键,反过来在历史表里也能通过BUSINESS_KEY_查到这张请假单。

CREATE TABLE ob_leave ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT COMMENT '申请人', leave_type VARCHAR(20) COMMENT '事假/病假/年假', days DECIMAL(4,1) COMMENT '请假天数', reason VARCHAR(500) COMMENT '请假事由', proc_inst_id VARCHAR(64) COMMENT 'Flowable流程实例ID', status CHAR(1) DEFAULT '0' COMMENT '0草稿 1审批中 2通过 3驳回', create_time DATETIME, KEY idx_proc_inst_id (proc_inst_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='请假业务表';

proc_inst_id上要建索引,因为待办列表和详情回显都要靠它关联任务表,没有索引的话数据量上来之后关联查询会明显变慢。status只反映业务视角的状态,它和 Flowable 里的任务状态是两回事,前者给列表页展示用,后者驱动流程走向。

2.4 部署一次后 BPMN 变更怎么生效:靠版本号而不是覆盖

Flowable 每次部署同一 key 的 BPMN 文件都会生成新版本,ACT_RE_PROCDEF里的VERSION_会递增。老版本的流程实例继续按老定义走完,新发起的流程使用最新版本。这个机制避免了“流程改到一半,存量单子全乱掉”的问题。RuoYi 这类管理后台里,版本管理一般不做成可视化界面,直接用部署文件命名区分即可,例如leave-process-v1.bpmn20.xmlleave-process-v2.bpmn20.xml

3. Spring Boot 集成 RuoYi 的流程最小闭环:部署、发起、审批

3.1 依赖选择:flowable-spring-boot-starter-process 就够了

RuoYi 分离版常见基于 Spring Boot 2.5 或 2.7,搭配 Flowable 6.7.2 是比较成熟的选择;如果项目已经升级到 Spring Boot 3,则要使用 Flowable 7.x。依赖放到 ruoyi-framework 模块或独立的 OA 模块均可:

<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter-process</artifactId> <version>6.7.2</version> </dependency>

不需要引入flowable-ui-modeler,那是独立的 Web 应用,和 RuoYi 的前后端分离结构不好融合。流程设计可以用 bpmn-js 在前端画图,将生成的 XML 传给后端部署,也可以先用手头的 BPMN 设计器导出 XML 再放到 resources 目录。starter-process会带来RepositoryServiceRuntimeServiceTaskServiceHistoryService一组 Spring Bean,直接注入使用,不需要手动初始化 ProcessEngine。

3.2 最小可跑的 BPMN:请假超过 3 天走总经理审批

Flowable 的流程文件后缀可以是.bpmn20.xml,放在src/main/resources/processes/下,配合前面的process-definition-location-prefix配置,项目启动时自动部署。一个能覆盖“条件分支、多人审批”的最小请假流程如下:

<?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:flowable="http://flowable.org/bpmn" targetNamespace="http://flowable.org/bpmn"> <process id="leaveProcess" name="请假审批" isExecutable="true"> <startEvent id="startEvent" flowable:initiator="applyUserId"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="deptUserTask"/> <userTask id="deptUserTask" name="部门审批" flowable:candidateGroups="deptLeader"/> <sequenceFlow id="flow2" sourceRef="deptUserTask" targetRef="gateway1"/> <exclusiveGateway id="gateway1"/> <sequenceFlow id="flow3" sourceRef="gateway1" targetRef="managerUserTask"> <conditionExpression xsi:type="tFormalExpression">${days > 3}</conditionExpression> </sequenceFlow> <sequenceFlow id="flow4" sourceRef="gateway1" targetRef="endEvent"> <conditionExpression xsi:type="tFormalExpression">${days <= 3}</conditionExpression> </sequenceFlow> <userTask id="managerUserTask" name="总经理审批" flowable:candidateGroups="manager"/> <sequenceFlow id="flow5" sourceRef="managerUserTask" targetRef="endEvent"/> <endEvent id="endEvent"/> </process> </definitions>

userTask上的candidateGroups定义谁能处理这个任务,deptLeadermanager是组名,不是 RuoYi 的用户名。exclusiveGateway根据流程变量days的值决定走总经理审批还是直接结束,days必须在发起流程时传入变量。

3.3 发起流程:把 RuoYi 当前用户和业务主键传进去

发起流程的代码放在 Service 层,通过runtimeService.startProcessInstanceByKey启动。流程定义 ID 是上面的leaveProcess,业务主键作为businessKey传给引擎,方便以后从 ACT_HI_PROCINST 反查业务单:

public void startLeaveProcess(ObLeave leave) { Map<String, Object> vars = new HashMap<>(); vars.put("days", leave.getDays()); vars.put("applyUserId", SecurityUtils.getUserId()); vars.put("applyUserName", SecurityUtils.getUsername()); ProcessInstance pi = runtimeService.startProcessInstanceByKey( "leaveProcess", leave.getId().toString(), vars); leave.setProcInstId(pi.getId()); leave.setStatus("1"); leaveMapper.updateProcInstId(leave); }

startProcessInstanceByKey的三个参数分别是流程定义 key、业务 key、流程变量。days会在网关条件表达式里被读取,applyUserIdapplyUserName用于记录发起人,审批节点如果需要按发起人查部门负责人,可以在这一步先查好放入变量。businessKey这里传的是业务表主键字符串,引擎不会解析它,但查询历史时会非常有用。

3.4 待办查询和审批:RuoYi 的分页插件可以继续用

RuoYi 使用 PageHelper 做分页,查询待办任务时照常用startPage(),再调用taskService.createTaskQuery()

public TableDataInfo getTodoList(String userId, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<Task> tasks = taskService.createTaskQuery() .taskCandidateOrAssigned(userId) .processDefinitionKey("leaveProcess") .orderByTaskCreateTime().desc() .list(); return getDataTable(convertTaskToVo(tasks)); }

taskCandidateOrAssigned会同时匹配已经分配给这个人的任务(assignee)和这个人所在候选组里的任务(candidate),适合“同一节点多人可处理”的场景。如果流程里用的是 candidateUsers 而不是 candidateGroups,这里可以直接换成taskCandidateUser(userId)。审批动作本质上是往任务上写审批意见和变量,然后调用complete

public void completeTask(String taskId, String approved, String comment) { taskService.addComment(taskId, null, comment); Map<String, Object> vars = new HashMap<>(); vars.put("approved", "Y".equals(approved) ? "Y" : "N"); taskService.complete(taskId, vars); }

addComment把审批意见写入历史,前端详情页可以按任务 ID 查出来展示。complete是让流程继续往下走的唯一入口,流程走到哪个节点完全由 BPMN 定义决定,后端代码里不需要再写 if/else 判断下一级是谁。

3.5 驳回不能删流程实例:用变量控制网关走向

很多从零开始写 OA 的人会想到用runtimeService.deleteProcessInstance模拟驳回,这是错误做法。删除实例会把整个流程痕迹抹掉,审批历史也没了。常见做法是在 BPMN 里设计排他网关,审批人驳回时传入approved=N,网关条件判断后走“驳回结束”或“退回发起节点”的路径。更复杂的退回上一节点操作,Flowable 支持changeActivityId动态跳转,但需要在 Service 层做节点校验,写清楚“当前节点能否退到目标节点”,否则容易把流程实例改乱。

4. 表单数据进流程变量:RuoYi 代码生成器与 Flowable 变量的绑定

4.1 业务表单和流程表单是两套数据:先分清一个原则

RuoYi 代码生成器按业务表生成列表页、表单页、Controller、Service、Mapper,它解决的是“CRUD 页面快速开发”的问题。但流程表单的要求不一样:每个节点的字段可见性可能不同,审批人要在表单里填写意见,发起人提交的数据要跟着实例走。常见做法是业务数据存业务表,流程变量只存流程走向需要的控制字段。days存业务表,但也要在发起时同步成流程变量,因为网关表达式${days > 3}读的是变量不是数据库。

4.2 提交时业务入表、变量入流程:前后端各做一半

RuoYi 前端提交表单后,把需要参与流程判断的字段单独放在variables对象里传给后端:

const data = { id: this.form.id, days: this.form.days, reason: this.form.reason, variables: { days: this.form.days, approved: 'Y' } }; saveAndStart(data).then(res => { this.$modal.msgSuccess("流程已发起"); });

后端接收后先保存业务数据,再带着variables启动流程:

@PostMapping("/leave/start") public AjaxResult saveAndStart(@RequestBody ObLeaveBody body) { ObLeave leave = obLeaveService.saveBusiness(body); Map<String, Object> vars = new HashMap<>(); if (body.getVariables() != null) { vars.putAll(body.getVariables()); } vars.putIfAbsent("applyUserId", SecurityUtils.getUserId()); ProcessInstance pi = runtimeService.startProcessInstanceByKey( "leaveProcess", leave.getId().toString(), vars); return AjaxResult.success("流程已发起", pi.getId()); }

代码里vars.putIfAbsent保证前端传的变量不会被后端覆盖,applyUserId缺失时自动拿当前登录用户补上。businessKey用业务主键,后面流程结束回写状态时,通过BusinessKey找到对应业务记录并更新status

4.3 会签和或签:用多实例与候选组实现

会签需要多个人全部审批完,流程才继续走。BPMN 里用multiInstanceLoopCharacteristics实现,给任务节点配置一个审批人列表变量:

<userTask id="multiApproveTask" name="会签审批"> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="assigneeList" elementVariable="assignee"> <completionCondition>${nrOfCompletedInstances >= nrOfInstances}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>

assigneeList是流程变量里的 List 类型,来自 RuoYi 的部门人员查询结果。isSequential="false"表示并行会签,所有人同时批;completionCondition控制完成条件,默认全部通过才算过。或签更简单,用candidateGroups的候选人组,谁先处理完任务就归谁。

4.4 RuoYi 的 @DataScope 不能直接用:流程表没有部门字段

RuoYi 的数据权限注解@DataScope是在 Mapper 层拼接部门数据权限 SQL,但 ACT_RU_TASK 里没有dept_idcreate_by这类 RuoYi 约定字段,所以不能直接对 Flowable 表使用数据权限过滤。可靠做法是分两步:先按当前用户查出他可见的业务表数据,再把这些数据的proc_inst_id集合拿去匹配 Flowable 任务表:

public List<TaskVO> getTodoListWithDataScope(Long userId, Integer pageNum, Integer pageSize) { List<Task> tasks = taskService.createTaskQuery() .taskCandidateOrAssigned(userId.toString()) .orderByTaskCreateTime().desc() .listPage((pageNum - 1) * pageSize, pageSize); List<String> procInstIds = tasks.stream() .map(Task::getProcessInstanceId) .collect(Collectors.toList()); if (procInstIds.isEmpty()) { return new ArrayList<>(); } List<ObLeave> leaves = leaveMapper.selectByProcInstIds(procInstIds); Set<Long> visibleIds = leaves.stream() .map(ObLeave::getId) .collect(Collectors.toSet()); return tasks.stream() .filter(t -> visibleIds.contains(Long.valueOf(t.getBusinessKey()))) .map(t -> convertToVO(t, leaves)) .collect(Collectors.toList()); }

这个思路是先取任务,再用业务表做权限过滤。leaveMapper.selectByProcInstIds的 Mapper 方法上可以加@DataScope注解,让 RuoYi 自动拼上部门过滤条件,这样数据权限边界仍然由 RuoYi 控制,Flowable 只负责找出候选人任务。

4.5 流程结束状态回写:用事件监听器而不是在 complete 后写

审批流最后一步执行完complete,流程实例在引擎侧已经结束,此时如果只在前端代码里更新业务状态,异步场景下很容易漏更新。推荐注册一个FlowableEventListener监听流程结束事件,在回调里根据businessKey回写业务表状态:

@Component public class ProcessEndListener implements FlowableEventListener { @Resource private ObLeaveMapper obLeaveMapper; @Override public void onEvent(FlowableEvent event) { FlowableEntityEvent entityEvent = (FlowableEntityEvent) event; ProcessInstanceEntity instanceEntity = (ProcessInstanceEntity) entityEvent.getEntity(); String businessKey = instanceEntity.getBusinessKey(); if (businessKey != null) { Long leaveId = Long.valueOf(businessKey); obLeaveMapper.updateStatus(leaveId, "2"); } } @Override public boolean isFailOnException() { return false; } @Override public boolean isFireOnTransactionLifecycleEvent() { return false; } @Override public String getOnTransaction() { return null; } }

isFailOnException返回 false 表示监听器抛异常不会影响流程事务,回写失败可以由日志告警兜底。监听器需要注册到引擎配置,在 RuoYi 中通过ProcessEngineConfigurationConfigurer追加即可,这样流程无论走哪条分支结束,状态都会落到业务表。

5. RuoYi + Flowable 集成避坑:从版本冲突到数据权限

5.1 版本搭配要提前定:Spring Boot 3 配 Flowable 6 直接启动失败

RuoYi 框架目前有基于 Spring Boot 2.x 和 3.x 的分支,选 Flowable 时一定要先看 Spring Boot 大版本。Flowable 6.x 基于 javax 命名空间,遇到 Spring Boot 3 的 jakarta 会直接 Bean 创建失败,错误信息通常是ClassNotFoundException: javax.xml.bind.*

Spring Boot 版本Flowable 版本备注
2.5.x6.7.2若依分离版常见组合
2.7.x6.7.2 或 7.0.0兼容性较好
3.x7.0.0+必须用 7.x,注意依赖版本对齐

5.2 ACT_ 表没自动创建:先检查 Druid URL 和数据库账号权限

启动日志里没有报错,但数据库里看不到 ACT_ 表,最常见的是 MySQL 8 的nullCatalogMeansCurrent问题,另外还要确认数据库账号有 CREATE 权限。database-schema-update: true只在第一次启动时建表,如果项目之前用旧账号启动过并失败,表目录可能已经不完整,此时需要手动执行官方 SQL 脚本或删除相关表重新建。

5.3 申请人看不到待办:candidateGroups 里的组名和 RuoYi 角色要对上

BPMN 里的candidateGroups是流程角色组,RuoYi 里一个用户对多个角色,角色标识存sys_role.role_key。如果两边名称不一致,比如流程里写deptLeader,RuoYi 角色表里维护的是dept_manager,任务就不会出现在任何人的待办列表里。建议建一张映射表或统一角色编码规范,部署流程前用脚本检查 BPMN 里的组名在sys_role中是否存在。

5.4 历史表膨胀:定时清理要按子表到主表顺序删

Flowable 的history: full会记录每个节点、每个变量的完整变更,跑几年后 ACT_HI_ 系列表会非常大。清理已结束实例的历史数据时,要按从子表到主表的顺序删除,否则外键约束会报错:

DELETE FROM ACT_HI_VARINST WHERE PROC_INST_ID_ IN (SELECT ID_ FROM ACT_HI_PROCINST WHERE END_TIME_ IS NOT NULL); DELETE FROM ACT_HI_ACTINST WHERE PROC_INST_ID_ IN (SELECT ID_ FROM ACT_HI_PROCINST WHERE END_TIME_ IS NOT NULL); DELETE FROM ACT_HI_TASKINST WHERE PROC_INST_ID_ IN (SELECT ID_ FROM ACT_HI_PROCINST WHERE END_TIME_ IS NOT NULL); DELETE FROM ACT_HI_PROCINST WHERE END_TIME_ IS NOT NULL;

生产环境不建议手写定时 SQL 直接删,可以基于 Flowable 的 History Job 或者自研幂等任务,按业务保留期限分批清理,避免大事务锁表。

5.5 验证一套流程是否真正跑通:看三张表和一条 SQL

集成完成后,用一个真实的请假单走完全流程,检查三个指标:发起后 ACT_RU_TASK 里出现当前审批人的任务;审批通过后 ACT_HI_PROCINST 的 END_TIME_ 不是 NULL;业务表 ob_leave 的 status 变成 2。用一条 SQL 同时观察流程侧和业务侧:

SELECT p.ID_ AS 实例ID, p.BUSINESS_KEY_ AS 业务单号, p.START_TIME_ AS 发起时间, p.END_TIME_ AS 结束时间, t.NAME_ AS 当前节点 FROM ACT_HI_PROCINST p LEFT JOIN ACT_RU_TASK t ON t.PROC_INST_ID_ = p.ID_ ORDER BY p.START_TIME_ DESC LIMIT 10;

END_TIME_为 NULL 表示流程还在运行,当前节点显示的是引擎此刻停在哪,配合业务列表页的审批状态一起看,就能区分是流程没走完还是状态没回写。这个查询是判断集成是否正常最直接的入口,建议直接保存为开发期常用 SQL。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询