先说明一下我的背景:我一直在用若依做二开,不管是分离版还是微服务版都碰过。上个月接到一个需求——给系统加审批流,业务方张口就是“能不能把审批做出来,像钉钉那样”。我第一反应也和很多人一样:上 Flowable。但真到落地的时候我犹豫了,因为我们的表单五花八门,流程也不算复杂,硬塞一个 Flowable 进去,后续的维护成本可能比需求本身还大。所以这篇就聊聊我最终选的方案:不碰 Flowable,用轻量自研的方式给若依加审批流,而且我认为对大多数中小项目来说,这才是更合适的路。
这篇文章适合正在用若依、被审批需求找上门、又不想把项目搞得像 Adidas 全家桶的开发者。我会把决策过程、表结构设计、核心代码逻辑、以及和若依权限体系打通的关键细节都写出来,配合真实踩坑经验,让你抄作业也能少走弯路。
1. 先想清楚:你的“审批流”到底需要多复杂?
很多人在给若依加审批流的一开始就选错了方向,原因是把“审批”和“工作流引擎”划了等号。但实际上,80% 的业务审批根本不需要引擎,需要的只是一个清晰的“状态机 + 审批人规则解析器”。
1.1 大多数业务嘴里的审批流,其实只有三层
我把这些年遇到过的审批需求做了个分类,大致可以分成三个层级:
第一层:单线审批。提交人发起,指定一个人或一个角色审批,通过就结束,驳回就退回。比如请假、用章申请、简单的报销单。这类需求用若依代码生成器生成一张业务表,表里加一个status字段就够用了,连独立的审批模块都不需要。
第二层:多级审批 + 条件分支 + 会签。比如“小于 5000 元主管审批,大于 5000 元还要总经理审批”,或者“部门负责人审核 + 财务复核”。这是最常见的真实需求,需要有个可配置的流程定义,能定义节点、审批人、分支条件。但节点数量一般不超过 10 个,流程类型不超过几十种。
第三层:复杂流程编排。子流程、并行网关、会签跑到飞起、流程版本热部署、还需要在线拖拽流程设计器。到了这个层级,Flowable 才真正有用武之地。
我见过不少项目,业务明明只在第二层,却硬上了第三层的工具,结果就是一堆表、一堆配置、一堆引擎报错把开发时间全吃掉了。
1.2 Flowable 这类引擎在若依里的真实代价
这里我简单算一笔账。引入 Flowable 意味着:
- 自动建表 60 多张,你不仅要懂业务表,还要弄懂
ACT_RU_*(运行时表)、ACT_HI_*(历史表)这一堆东西; - 流程定义文件(BPMN XML)要自己画或者集成设计器,前后端都要改造;
- 若依原来的 RBAC 权限体系(用户、角色、菜单、部门)和 Flowable 的用户组体系是两套东西,要做同步;
- 出问题的时候定位链路过长,最典型的就是 Flowable 的
CommandExecutor报错,很多时候查半天发现是事务或缓存问题,对运维人员不友好。
所以与其说“不能用 Flowable”,不如说“不要在若依这种轻量框架里,为了审批功能引入一个全家桶”。如果你没把握把引擎调明白,自研一套只够用的审批流,反而是更稳的选择。
2. 不碰 Flowable 的替代路线,我最终选了哪条
先说我调研过的三条路线,再解释我为什么选最后一条。
2.1 三条可落地的替代路线对比
| 方案 | 需要新建的表 | 复杂度 | 可维护性 | 适合场景 |
|---|---|---|---|---|
| 引入 SnakerFlow | 十几张左右 | 中等 | 一般,社区更新速度慢 | 对开源引擎有执念,但不想用 Flowable |
| 自研状态机 | 3~4 张表 | 低 | 高,完全可控 | 流程类型不复杂、团队能掌握全局 |
| 对接钉钉/企微审批 | 只存审批关联数据 | 低 | 依赖第三方平台 | 审批过程不需要留在自己系统内 |
SnakerFlow 确实比 Flowable 轻很多,表也少,刚看到的时候我很心动。但我在若依 + RuoYi-App 这种前后端分离的环境里做了个 Demo,发现它的设计器还是偏老,前后端改造工作量比预想大不少。而且开源项目更新缓慢,出了问题大概率只能自己看源码。
对接钉钉审批这条路线,因为业务要求审批记录、审批文件必须归档在自己的系统里,所以也排除了。
2.2 为什么最终自研了
我的决策依据很简单:若依本身有个非常好用的代码生成器。也就是说,我可以用它先搭建业务单据的 CRUD,再单独写一套“审批引擎”模块,为所有业务单据提供统一的发起、审批、驳回服务。这些服务里最核心的逻辑就是“从流程配置解析下一个审批人”。
自研的好处在于:
- 流程配置可视化成 JSON,存在一张配置表里,改配置不用改代码;
- 审批记录完整落在自己的库表,想怎么统计就怎么统计;
- 和若依的数据权限、部门权限自然打通,审批人就是
sys_user表里的真实用户; - 出了任何问题,我可以直接链路排查到 SQL,不用和引擎黑盒做斗争。
当然,自研并不是指从零造一个 BPMN 标准引擎出来,而是做一个“够用且好改”的审批骨架。BPMN 那些标准我们不需要。
3. 自研审批流的数据模型:四张表把流转逻辑装下
我设计的数据模型只有四张核心表,后来在多个单子里复用,效果很好。下面把每张表的职责和关键字段说清楚。
3.1 审批配置表与审批实例表
审批配置表存的是“流程模板”,比如:请假流程、报销流程、合同评审流程。关键字段如下:
CREATE TABLE `biz_process_definition` ( `id` bigint NOT NULL AUTO_INCREMENT, `process_code` varchar(64) NOT NULL COMMENT '流程编码,如 LEAVE_BILL', `process_name` varchar(128) NOT NULL COMMENT '流程名称', `version` int NOT NULL DEFAULT 1 COMMENT '版本号', `node_config_json` text NOT NULL COMMENT '节点配置JSON', `status` char(1) DEFAULT '0' COMMENT '状态 0启用 1停用', `create_by` varchar(64), `create_time` datetime, PRIMARY KEY (`id`), UNIQUE KEY `uk_code_version` (`process_code`, `version`) ) ENGINE=InnoDB COMMENT='审批流程定义表';node_config_json是灵魂字段,我举个例子你就明白:
{ "nodes": [ { "nodeId": "node_1", "nodeName": "主管审批", "nodeType": "approve", "approverType": "role", "approverValue": "dept_manager", "condition": "" }, { "nodeId": "node_2", "nodeName": "总经理审批", "nodeType": "approve", "approverType": "role", "approverValue": "general_manager", "condition": "amount > 5000" } ] }审批实例表则对应“某一次具体的审批单”,比如张三提交的请假单 A。
CREATE TABLE `biz_process_instance` ( `id` bigint NOT NULL AUTO_INCREMENT, `process_code` varchar(64) NOT NULL, `biz_type` varchar(64) NOT NULL COMMENT '业务类型,如 LEAVE_BILL', `biz_id` bigint NOT NULL COMMENT '业务单主键', `current_node_id` varchar(64) COMMENT '当前节点ID', `status` varchar(32) NOT NULL COMMENT '状态:approving/approved/rejected/canceled/draft', `apply_user_id` bigint NOT NULL COMMENT '发起人', `apply_dept_id` bigint COMMENT '发起人部门', `create_time` datetime, `update_time` datetime, PRIMARY KEY (`id`), UNIQUE KEY `uk_biz` (`biz_type`, `biz_id`) ) ENGINE=InnoDB COMMENT='审批实例表';这里建议加一个唯一索引uk_biz,避免同一条业务单被重复发起流程,这个坑我踩过:因为接口没做幂等,测试时双击提交按钮生成了两条实例,后面的审批记录全乱了。
3.2 审批记录表与待办表
审批记录表是审计日志,每次“同意/驳回/转办”都写一条,不能删改。
CREATE TABLE `biz_process_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `process_instance_id` bigint NOT NULL, `node_id` varchar(64) NOT NULL, `action` varchar(32) NOT NULL COMMENT 'agree/reject/transfer', `comment` varchar(500) COMMENT '审批意见', `approver_id` bigint NOT NULL COMMENT '审批人', `create_time` datetime, PRIMARY KEY (`id`), KEY `idx_instance` (`process_instance_id`) ) ENGINE=InnoDB COMMENT='审批记录表';这个表的价值有两个:一是给前端的时间线组件提供数据,二是在会签场景里用来统计“谁同意谁没同意”。
待办表没有做成独立表,而是直接查审批实例和节点配置得到,原因是“待办”本质是一个派生数据。但如果你要展示的待办数量特别大,建议还是建一张biz_process_todo实体表,审批动作完成时同步删除旧待办、插入新待办。我在高并发场景下会单独加这张表,平时直接用接口查也行。
3.3 审批人解析模块:面向若依权限体系的适配
审批人配置可以灵活一点。我支持三种写法:
- 写死用户ID;
- 写角色编码,运行时候通过
sys_role关联sys_user_role查出用户; - 写表达式,比如“发起人的部门负责人”,需要在解析时判断。
解析逻辑最核心的一段伪码如下:
public List<Long> resolveApprovers(String approverType, String approverValue, ProcessInstance instance) { if ("user".equals(approverType)) { return Collections.singletonList(Long.valueOf(approverValue)); } if ("role".equals(approverType)) { // 调用若依的 SysUserService,按角色编码查询用户列表 return sysUserService.selectUserIdsByRoleCode(approverValue); } if ("dept_leader".equals(approverType)) { // 查发起人的部门,再查部门负责人 SysDept dept = sysDeptService.selectDeptById(instance.getApplyDeptId()); return Collections.singletonList(dept.getLeaderUserId()); } return Collections.emptyList(); }建议给流程配置加一个简单的缓存,避免每次审批都去查角色关联。我用的 Redis 缓存,流程定义变更时直接删除 key 重新加载。
4. 在若依上落地自研审批流:接口、事务与数据权限
表结构设计好之后,重点就是接口了。不要把审批接口写散在每个业务 Controller 里,我建议单独建一个ProcessCoreController,提供统一入口。
4.1 核心接口清单
| 接口 | 作用 | 核心逻辑 |
|---|---|---|
POST /process/instance/start | 发起审批 | 创建实例,生成第一个待办 |
GET /process/todo/mylist | 我的待办 | 返回当前用户待审批的单据列表 |
POST /process/instance/approve | 审批同意 | 当前节点通过,推进到下一节点 |
POST /process/instance/reject | 驳回 | 驳回到指定节点或发起人 |
POST /process/instance/transfer | 转办 | 把当前待办转给他人 |
POST /process/instance/revoke | 撤单 | 发起人未审批前可撤回 |
GET /process/record/list | 审批记录 | 图表时间线展示 |
4.2 审批动作的事务一致性
一个“审批同意”动作,表面上是用户点了一下按钮,实际上后端做了四件事:
- 更新当前节点记录(如果会签,则更新该节点下的通过记录);
- 将当前节点推进到下一个节点;
- 为下一节点生成待办;
- 当整个流程完成时,把业务单的状态改为已通过。
这四件事必须在一个事务里完成,否则可能出现“记录已经写了,但是流程状态没变”的情况。实现上直接在 Service 层加@Transactional(rollbackFor = Exception.class)。
并发问题是这里最容易翻车的地方。两个人同时打开待办列表,同时点了同一个审批单的“同意”,如果不做控制,就可能出现重复审批。我的做法是给审批实例加乐观锁:
UPDATE biz_process_instance SET current_node_id = #{nextNodeId}, status = #{newStatus}, update_time = now() WHERE id = #{instanceId} AND current_node_id = #{currentNodeId}如果更新影响行数为 0,说明当前节点已经被别人处理了,直接抛出业务异常提示“该单据已被处理”。
4.3 待办列表与若依数据权限的融合
若依的数据权限通过@DataScope注解实现,但待办列表的逻辑不是“根据部门过滤全部数据”,而是“根据当前登录用户查他作为审批人的单子”。所以这里的过滤条件是approver_id = 当前用户ID,或者approver_role 包含当前用户角色。
我实现了一个查询方法:
selectTodoListByUserId(@Param("userId") Long userId)如果查询慢,可以考虑把待办独立成表,我在前面已经讲过了。
另外,审批消息提醒可以接若依的站内信sys_notice,也可以接企业微信/钉钉机器人。这个不强求,但如果业务方有强提醒需求,建议在生成待办的同时异步推一条通知,不要阻塞主流程。
5. 业务膨胀后怎么扩展:会签、条件分支、驳回到中间节点
很多决策者担心的是:“你现在不用 Flowable,以后需求复杂了怎么办?”我承认这种担心是合理的,但自研方案也可以通过扩展来覆盖大部分复杂需求。
5.1 会签、或签的写法
业务场景:一个合同需要财务、法务、总经理三方都同意才能通过,这就是会签(AND);三方中任一人同意即可,是或签(OR)。
我在节点配置里加两个字段:
{ "nodeId": "node_5", "nodeName": "合规会签", "nodeType": "countersign", "signType": "AND", "approverList": [ { "approverType": "role", "approverValue": "finance" }, { "approverType": "role", "approverValue": "legal" } ] }审批时,先查询biz_process_record表中该节点已有多少条审批记录,当满足“所有审批人都已审批,且全部同意”时,才推进到下一个节点;否则继续保持当前节点待办状态。或签的差别只在于“只要有一个人同意就推进”。
统计逻辑看起来简单,但要注意:不要用“同意数 >= 审批人总数”来判断,因为审批人可能是动态解析的,比如一个角色下有 5 个人,配置要求任意 3 人同意即可。所以更稳妥的做法是在节点配置里增加一个passCount属性,默认 -1 表示全部通过。
5.2 条件分支的配置化
条件分支我见过很多写法,最实用的一种是“在节点 JSON 里写 SpEL 表达式或简单比较表达式”。我们的场景基本都是金额、部门、城市这类简单字段判断,所以我封装了一个规则解析工具:
if ("amount > 5000".equals(condition)) { // 从业务表单数据中取 amount 字段比较 }这样流程配置人员(其实还是开发自己)可以直接改 JSON,不用重新编译。真正复杂的规则建议还是落到代码里,配置里写表达式只适合简单场景,不要追求大而全的规则引擎。
5.3 驳回到中间节点的处理
驳回是审批流里最容易出 bug 的地方,尤其是“驳回到指定节点,而不是直接退回发起人”。
我的处理策略是:驳回时把current_node_id改成目标节点,同时把该目标节点之后产生的所有审批记录标记为“已撤销”。注意这里的“已撤销”不能直接DELETE,否则审计日志就断了。
UPDATE biz_process_record SET revoked = 1 WHERE process_instance_id = #{instanceId} AND create_time > (SELECT create_time FROM biz_process_record WHERE id = #{targetRecordId})然后重新生成目标节点的待办。这里有个细节:如果目标节点原来是会签,重新审批时是否需要把之前已同意的人重置?我的做法是重置该节点及之后所有节点的审批状态,避免出现“少了一个人的同意记录”却不自知的情况。
5.4 流程进度展示
没有 Flowable 自带的流程设计器,前端进度展示我们自己写了一个组件,用时间线形式展示每个节点的状态:待处理、处理中、已完成、已驳回。数据来源就是biz_process_record表,按时间排序即可。对于节点状态为“处理中”的,我们从biz_process_instance.current_node_id拿到当前节点,加上高亮。
不过我必须承认,如果一个甲方明确要求在系统里“像 Visio 一样拖拽画流程”,自研的性价比就很低了。这种明确偏好可视化的场景,老老实实上 Flowable 或者购买商业工作流产品更现实。
6. 在若依(Vue3 + TS 版本)集成的实操碎片
这里说几个网上问得比较多的问题,因为我就是在这个版本上踩过来的。
6.1 代码生成器生成的模块报 “error adding module to project: null”
这个问题在若依的 Vue3 + TS 前端项目里很常见。大概率是代码生成器把文件生成到磁盘后,你在前端项目里没有手动把路由模块加入菜单,或者接口地址和后端 controller 的 RequestMapping 前缀对不上。
我之前踩过的详细坑是这样的:代码生成时填写的“模块名”和“请求路径前缀”不一致,导致前端生成的index.ts里的url指向了/xxx/yyy,而后端实际挂在/system/yyy下。解决办法就是统一生成时的请求路径,比如统一用业务模块名bizLeave,前后端都照着这个来。
有时候这个问题也可能出在 IDEA 导入 Maven 模块上。新加一个 RuoYi 子模块后,IDEA 右侧 Maven 面板没有自动加载,你手动刷新一下就行,同时检查pom.xml是否被父模块正确 include。
6.2 若依验证码不显示,前端能做什么
验证码不显示,大多数人第一反应就是查代码,但我想提醒你按顺序排查:
- 后端
sys_config表中sys.account.captchaEnabled是否为 true; - 后端服务是否输出了验证码图片,可以通过直接访问验证码接口确认;
- 前端
captchaImage接口是否正常返回img字段,图片 base64 是否能被显示; - 如果部署在 Nginx 下,要注意反向代理是否丢失了 Cookie 或者请求头。验证码是存在服务端 Redis 里的,和 Session 关系不大,但跨域场景下要保证请求携带正确上下文。
我那次遇到的原因是 Redis 里验证码 key 过期时间设置太短,结果前端刷新慢一点就过期了。大家如果调整过 Redis 配置,可以优先检查这一块。
6.3 建议保留统一的审批模块名
在若依的多模块架构里,我建议不要给每个业务独自塞一张待办表,而是把审批相关的逻辑收拢成一个ruoyi-process模块。业务模块只需要在发起审批时调用processInstanceService.start(),在业务状态变化时同步更新业务单状态。这样可以避免以后业务多了,审批逻辑散落各处。
7. 哪些场景我会坚决劝你用 Flowable
聊了这么多自研,我也想客观收个尾。如果有人再问我“若依到底能不能不用 Flowable”,我的回答是:能,但要分场景。
如果你的系统里流程类型本身就非常固定,例如就是请假、报销、合同审批,流程节点最多五六个,并发量也不高,那么自研完全绰绰有余。我甚至建议把自研审批流沉淀成公司内部基础组件,后续新项目直接复用。
但如果出现下面任何一个条件,我还是会劝你老老实实上 Flowable 或者 Activiti:
- 业务要求 BPMN2.0 标准,后续可能和其他系统做流程文件交换;
- 甲方明确指定需要在线流程设计器,让业务人员自己画流程;
- 会签、子流程、并行网关用得非常多,而且流程经常动态变化;
- 团队有专门的流程开发人员,能把控引擎并处理复杂排错。
自研和引擎不是谁替代谁的关系,而是尺度问题。我见过太多团队为了“技术先进”引入 Flowable,最后连 BPMN 的边界事件都搞不清楚,反而拖垮了交付进度。反过来说,自研也要有克制力,不要在发现了复杂场景之后还硬造轮子去实现子流程功能,那会把自己逼疯。
我个人现在处理审批需求有一个习惯:接到需求先不急着写代码,先把所有的审批节点、审批人规则、驳回策略画到一张纸上,和业务确认清楚。这一步做完,不管后面用不用 Flowable,代码都能写得非常顺。上面这套自研方案,我已经在两个若依项目里跑过生产了,到目前为止没有出过状态错乱的问题,希望这些经验也能帮你做出合适的选择。