news 2026/9/24 5:05:42

若依二开不碰Flowable,自研轻量审批流实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
若依二开不碰Flowable,自研轻量审批流实战指南

先说明一下我的背景:我一直在用若依做二开,不管是分离版还是微服务版都碰过。上个月接到一个需求——给系统加审批流,业务方张口就是“能不能把审批做出来,像钉钉那样”。我第一反应也和很多人一样:上 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,再单独写一套“审批引擎”模块,为所有业务单据提供统一的发起、审批、驳回服务。这些服务里最核心的逻辑就是“从流程配置解析下一个审批人”。

自研的好处在于:

  1. 流程配置可视化成 JSON,存在一张配置表里,改配置不用改代码;
  2. 审批记录完整落在自己的库表,想怎么统计就怎么统计;
  3. 和若依的数据权限、部门权限自然打通,审批人就是sys_user表里的真实用户;
  4. 出了任何问题,我可以直接链路排查到 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 审批动作的事务一致性

一个“审批同意”动作,表面上是用户点了一下按钮,实际上后端做了四件事:

  1. 更新当前节点记录(如果会签,则更新该节点下的通过记录);
  2. 将当前节点推进到下一个节点;
  3. 为下一节点生成待办;
  4. 当整个流程完成时,把业务单的状态改为已通过。

这四件事必须在一个事务里完成,否则可能出现“记录已经写了,但是流程状态没变”的情况。实现上直接在 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 若依验证码不显示,前端能做什么

验证码不显示,大多数人第一反应就是查代码,但我想提醒你按顺序排查:

  1. 后端sys_config表中sys.account.captchaEnabled是否为 true;
  2. 后端服务是否输出了验证码图片,可以通过直接访问验证码接口确认;
  3. 前端captchaImage接口是否正常返回img字段,图片 base64 是否能被显示;
  4. 如果部署在 Nginx 下,要注意反向代理是否丢失了 Cookie 或者请求头。验证码是存在服务端 Redis 里的,和 Session 关系不大,但跨域场景下要保证请求携带正确上下文。

我那次遇到的原因是 Redis 里验证码 key 过期时间设置太短,结果前端刷新慢一点就过期了。大家如果调整过 Redis 配置,可以优先检查这一块。

6.3 建议保留统一的审批模块名

在若依的多模块架构里,我建议不要给每个业务独自塞一张待办表,而是把审批相关的逻辑收拢成一个ruoyi-process模块。业务模块只需要在发起审批时调用processInstanceService.start(),在业务状态变化时同步更新业务单状态。这样可以避免以后业务多了,审批逻辑散落各处。

7. 哪些场景我会坚决劝你用 Flowable

聊了这么多自研,我也想客观收个尾。如果有人再问我“若依到底能不能不用 Flowable”,我的回答是:能,但要分场景。

如果你的系统里流程类型本身就非常固定,例如就是请假、报销、合同审批,流程节点最多五六个,并发量也不高,那么自研完全绰绰有余。我甚至建议把自研审批流沉淀成公司内部基础组件,后续新项目直接复用。

但如果出现下面任何一个条件,我还是会劝你老老实实上 Flowable 或者 Activiti:

  • 业务要求 BPMN2.0 标准,后续可能和其他系统做流程文件交换;
  • 甲方明确指定需要在线流程设计器,让业务人员自己画流程;
  • 会签、子流程、并行网关用得非常多,而且流程经常动态变化;
  • 团队有专门的流程开发人员,能把控引擎并处理复杂排错。

自研和引擎不是谁替代谁的关系,而是尺度问题。我见过太多团队为了“技术先进”引入 Flowable,最后连 BPMN 的边界事件都搞不清楚,反而拖垮了交付进度。反过来说,自研也要有克制力,不要在发现了复杂场景之后还硬造轮子去实现子流程功能,那会把自己逼疯。

我个人现在处理审批需求有一个习惯:接到需求先不急着写代码,先把所有的审批节点、审批人规则、驳回策略画到一张纸上,和业务确认清楚。这一步做完,不管后面用不用 Flowable,代码都能写得非常顺。上面这套自研方案,我已经在两个若依项目里跑过生产了,到目前为止没有出过状态错乱的问题,希望这些经验也能帮你做出合适的选择。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 5:03:44

SPI四种模式详解:从CPOL/CPHA原理到实战调试避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 5:01:20

4-28GHz宽带威尔金森功分器:从ADS原理图到Momentum版图仿真实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:55:26

瑞芯微RV1106产线级环境搭建与AI部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:45:21

SSM毕设项目:基于 SSM+Vue 的线上体检预约管理系统的设计与实现 基于 SSM 的居民健康体检档案管理系统的设计与实现 (源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/24 4:44:11

DaVinci Configurator AUTOSAR CAN七层配置实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华