尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

若依二开不碰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再单独写一套“审批引擎”模块为所有业务单据提供统一的发起、审批、驳回服务。这些服务里最核心的逻辑就是“从流程配置解析下一个审批人”。自研的好处在于流程配置可视化成 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB COMMENT审批记录表;这个表的价值有两个一是给前端的时间线组件提供数据二是在会签场景里用来统计“谁同意谁没同意”。待办表没有做成独立表而是直接查审批实例和节点配置得到原因是“待办”本质是一个派生数据。但如果你要展示的待办数量特别大建议还是建一张biz_process_todo实体表审批动作完成时同步删除旧待办、插入新待办。我在高并发场景下会单独加这张表平时直接用接口查也行。3.3 审批人解析模块面向若依权限体系的适配审批人配置可以灵活一点。我支持三种写法写死用户ID写角色编码运行时候通过sys_role关联sys_user_role查出用户写表达式比如“发起人的部门负责人”需要在解析时判断。解析逻辑最核心的一段伪码如下public ListLong 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代码都能写得非常顺。上面这套自研方案我已经在两个若依项目里跑过生产了到目前为止没有出过状态错乱的问题希望这些经验也能帮你做出合适的选择。
返回列表