引言
若依是国内最流行的快速开发框架之一,GitHub 镜像 4 万多 star,几乎每个 Java 后台项目都从它起步。但它有个大家都知道的"缺口":官方免费版不内置审批流。
用若依的人,迟早要碰审批——请假、报销、合同、采购。怎么补?
这篇文章给一个不绕弯子的答案:别上 Flowable,用 jeeflow。后端一个依赖一个薄壳,前端一个宿主组件九个薄页,配一条菜单 SQL,半天跑通一个带工作台、发起、待办、审批、流程设计器的完整流程中心。
我把整个过程真机走了一遍(若依 3.9.2 master 分支,Spring Boot 4.1.0 + JDK 17),造了一批发起、会签、网关分流、退回重提、拒绝、抄送的真实演示数据,下面全是实录截图。
一、先看清现状:若依到底有没有审批流
先把话说清楚,免得被标题党带节奏:
若依官方是活跃的。 master 分支最近还在升级 Spring Boot 4.1.0、poi 5.5.1(2026-08 的提交),项目本身没死。
但"活跃"和"有审批流"是两回事。若依官方只有三条主线:master(Boot 4)、springboot3、springboot2,没有工作流/审批流模块,也没有相关分支。它的定位是"用户管理、部门岗位、菜单权限、字典参数、代码生成"这一套基础脚手架,审批从来不在范围内。
所以给若依加审批流,实际上只有三条路:
| 路线 | 做法 | 代价 |
|---|---|---|
| ① 找付费/二开版 | 用 RuoYi-Vue-Plus、某些商业增强版里的工作流 | 绑定特定衍生版,升级跟着它走 |
| ② 自集成 Flowable | 引 Flowable 引擎,自己写业务适配 | 学习曲线陡、BPMN 模型重、和若依权限体系要硬接 |
| ③ 轻量引擎薄集成 | 用一个薄的工作流引擎,只写映射层 | 引擎得够轻、契约得够清 |
本文走第三条。选 jeeflow 的理由就一句话:它是为"快速开发框架集成"而生的薄引擎,门面契约、权限码、选人字典这些和后台框架的对接点都做好了,你只写"若依侧"那一薄层。
后面会看到,这一薄层有多薄。
二、为什么不用 Flowable
不踩一捧一,但对比是客观的。Flowable 是成熟的 BPMN 引擎,能力强,但它的"重量"和若依这种"薄脚手架"场景是错配的:
- 模型重。BPMN XML + 图形建模器,一个请假流程要维护一整套模型文件。jeeflow 的流程定义就是一段 JSON(本文第四节有完整示例),设计器也是 JSON 驱动。
- 学习曲线。Flowable 的 Process / Task / Execution / 监听器 / Delegate 那一套,上手要花时间。jeeflow 对外只有一个门面,45 个 action,POST 一下就完事。
- 权限对接。Flowable 自带一套 identity 用户体系,跟若依的
sys_user/sys_role/sys_menu是两套账本,得写桥。jeeflow 把"用户是谁""有没有权限""按什么角色找人"全做成 SPI 钩子,你填若依的 Service 就行(第三节)。 - 前端。Flowable 官方前端要自己拼。jeeflow 有个 npm 组件包
@mldong/jeeflow-ui,工作台/发起/待办/已办/我发起的/抄送/流程定义/流程设计/委托九个页面开箱即用,而且每个页面都能单独挂进宿主的菜单体系(第五节),不是塞给你一个iframe或者一套独立布局。
如果你的项目要上复杂 BPMN(会签矩阵、动态路由、跨系统编排),Flowable 依然是对的。但"若依加个请假/报销审批"这种 80% 的场景,jeeflow 的薄集成更省。
三、后端:一个依赖 + 一个薄壳
3.1 加依赖
若依 master 是 Spring Boot 4,对应 jeeflow 的 spring-boot4 starter:
1<!-- ruoyi-admin/pom.xml --> 2<dependency> 3 <groupId>com.mldong.jeeflow</groupId> 4 <artifactId>jeeflow-spring-boot4-starter</artifactId> 5 <version>1.8.27</version> 6</dependency> 7
这一个依赖进去,starter 自动装配好引擎核心:JeeflowEngine、JDBC 仓储、雪花 ID 生成、Jackson(Long→String,前端不丢精度)、事务模板、SpEL 表达式求值器、默认用户 Provider。全部分 @ConditionalOnMissingBean,你想覆盖哪个就自己 @Bean 哪个。
注意:不用覆盖 ID 生成器。若依没引 MyBatis-Plus,没有"雪花 vs 自增主键"的对齐问题,starter 内置雪花直接能用。这是"薄"的体现——少写一个 Bean。
3.2 只补两个 Bean
starter 不自动装配的就两样:设计/历史/委托三表的扩展仓储,和统一门面 JeeflowFacade。一个配置类搞定:
1@Configuration 2public class WfJeeflowConfig { 3 4 @Bean 5 @ConditionalOnMissingBean 6 public IProcessExtRepository jeeflowExtRepository(DataSource dataSource) { 7 return new JdbcProcessExtRepository(dataSource); 8 } 9 10 @Bean 11 @ConditionalOnMissingBean 12 public JeeflowFacade jeeflowFacade(JeeflowEngine engine, 13 IProcessRepository repository, 14 IProcessExtRepository extRepository, 15 ObjectProvider<IUserSearchProvider> userSearchProvider) { 16 JeeflowFacade facade = new JeeflowFacade(engine, repository, extRepository); 17 userSearchProvider.ifAvailable(facade::setUserSearchProvider); 18 return facade; 19 } 20} 21
3.3 一个 Controller 吃下所有请求
jeeflow 的门面是"一个入口,45 个 action"——POST /wf/{action},body 是参数 map,返回 {code: 0, msg, data}。所以若依侧只需要一个 Controller:
1@RestController 2@RequestMapping("/wf") 3public class WfFlowController { 4 5 @Autowired 6 private JeeflowFacade jeeflowFacade; 7 8 @PostMapping("/**") 9 public Map<String, Object> flow(HttpServletRequest request, 10 @RequestBody(required = false) Map<String, Object> body) { 11 String uri = request.getRequestURI(); 12 int idx = uri.indexOf("/wf/"); 13 String action = uri.substring(idx + "/wf/".length()); 14 15 checkPermission(action); // ① 权限校验 16 17 if (body == null) body = new LinkedHashMap<>(); 18 body.put("operator", SecurityUtils.getUserId().toString()); // ② 注入操作人 19 20 return jeeflowFacade.flow(action, body); // ③ 门面透传 21 } 22 23 private void checkPermission(String action) { 24 if (SecurityUtils.isAdmin()) return; // admin 万能放行 25 IActionPermissionProvider provider = ServiceContext.find(IActionPermissionProvider.class); 26 String[] codes = provider == null ? null : provider.permissionCodes(action); 27 if (codes != null && codes.length > 0) { 28 for (String code : codes) { 29 if (SecurityUtils.hasPermi(code)) return; // 任一权限码命中即可 30 } 31 throw new ServiceException("无权限执行操作:" + action, 403); 32 } 33 } 34} 35
三件事,全是若依现成的能力:
- 权限:引擎默认 Provider 把每个 action 映射成
wf:{action}权限码(如processTask/todoList → wf:processTask:todoList),SecurityUtils.hasPermi走若依的 RBAC(sys_menu按钮)。 - 操作人:
SecurityUtils.getUserId()从若依登录态(Spring Security 的LoginUser)取,塞进args.operator——门面靠它判断"我的待办/我发起的"。 - 透传:返回的是 jeeflow 门面原生结构
code: 0,不包若依的 AjaxResult(code: 200)。这是有意的——前端jeeflow-ui只认门面契约,包一层反而要拆。
3.4 三个 Provider:把若依的用户体系喂给引擎
引擎通过 SPI 问宿主三个问题:用户是谁、怎么搜人、按部门/角色怎么找人。各写一个 Provider 接若依 Service:
RyUserProvider(IUserProvider):selectUserById取nickName当姓名、取部门名。RyUserSearchProvider(IUserSearchProvider):候选人搜索,按昵称/账号模糊匹配。RyOrgUserProvider(IOrgUserProvider):findDeptLeaders(部门负责人)、findByRole(按角色找成员)——审批流里"派给部门主管""派给某角色"就靠它。
这三个是"若依侧"唯一要写的业务代码。没有之一。
3.5 种子流程:让系统开箱就有个流程
若依首次启动,我加了个 ApplicationRunner 幂等地上架一个"请假申请"流程(走的是和设计器完全相同的 processDesign/save → deploy 链路,不是直接插表):
1// WfFlowSeedRunner:启动时幂等上架 leave 流程 2// 1. 查 processDesign/page,name=leave 已发布就跳过 3// 2. processDesign/save(content = 流程 JSON 字符串)→ 拿 designId 4// 3. processDesign/deploy(designId) 5
为什么走门面而不是直接 insert?因为 listByType(发起页的数据源)读的是 design 表 + 历史表,不是 define 表。直接插 define 表,发起页看不到。走 save→deploy 才能保证和人工在界面上设计、发布走的是同一条路。
开箱只有请假一条还不够演示。我又写了三个流程定义(报销/采购/加班,JSON 见第四节),用同一条 save→deploy 链路上架——脚本调门面和在"流程设计"页面上点发布,后端是同一个动作。最终发起页有四张流程卡片:
四、四个流程定义:从直线到网关和会签
流程定义就是 JSON。四个流程由简到繁,正好把 jeeflow 的核心概念全覆盖:直线审批、决策网关、并行会签、字段权限、抄送。
4.1 leave:最简直线审批(全文)
1{ 2 "name": "leave", 3 "displayName": "请假申请", 4 "type": "approval", 5 "enableFieldPerm": true, 6 "__schema__": { 7 "columns": [ 8 { "fieldName": "reason", "remark": "请假事由", "component": "Textarea", "ext": { "required": 1, "placeholder": "请说明请假原因" } }, 9 { "fieldName": "days", "remark": "请假天数", "component": "Number", "ext": { "required": 1 } }, 10 { "fieldName": "leaveType", "remark": "请假类型", "component": "Select", 11 "ext": { "required": 1, "options": [ 12 { "label": "年假", "value": "annual" }, 13 { "label": "事假", "value": "personal" }, 14 { "label": "病假", "value": "sick" } ] } }, 15 { "fieldName": "startDate", "remark": "开始日期", "component": "Date" }, 16 { "fieldName": "endDate", "remark": "结束日期", "component": "Date" } 17 ] 18 }, 19 "nodes": [ 20 { "id": "start", "type": "snaker:start", "text": { "value": "开始" } }, 21 { "id": "apply", "type": "snaker:task", 22 "properties": { "form": "leave-form", "assignee": "applicant" }, 23 "text": { "value": "填写申请" } }, 24 { "id": "approve", "type": "snaker:task", 25 "properties": { "form": "leave-form", "assignee": "1", 26 "field": { "PERMISSION_f_reason": 1, "PERMISSION_f_days": 1, "PERMISSION_f_leaveType": 1 } }, 27 "text": { "value": "上级审批" } }, 28 { "id": "end", "type": "snaker:end", "text": { "value": "结束" } } 29 ], 30 "edges": [ 31 { "sourceNodeId": "start", "targetNodeId": "apply" }, 32 { "sourceNodeId": "apply", "targetNodeId": "approve" }, 33 { "sourceNodeId": "approve", "targetNodeId": "end" } 34 ] 35} 36
几个值得注意的点:
__schema__.columns就是表单,前端零代码渲染(第五节)。不用为"请假"写一个 Vue 表单组件。assignee: "applicant"= 发起人本人填;assignee: "1"= 字面 userId(这里指 admin)。还能写deptLeader、roleCode这种,由RyOrgUserProvider解析。field里的PERMISSION_f_xxx: 1= 该字段在审批节点只读(1=只读 / 2=可编辑 / 3=隐藏)。审批人能看到"请假事由"但改不了。
4.2 purchase:金额网关分流
采购申请,预算 5000 是分水岭:以下部门审批,以上总经理审批。核心就是一个决策节点 + 两条带表达式的边:
1{ "id": "decision1", "type": "snaker:decision", 2 "properties": { "expr": "#f_amount > 5000" }, 3 "text": { "value": "金额>5000?" } } 4
两条出边的表达式:
1{ "sourceNodeId": "decision1", "targetNodeId": "deptApprove", 2 "properties": { "expr": "#f_amount <= 5000" }, "text": { "value": "≤5000" } }, 3{ "sourceNodeId": "decision1", "targetNodeId": "gmApprove", 4 "properties": { "expr": "#f_amount > 5000" }, "text": { "value": ">5000" } } 5
这里有个真机踩出来的坑,值得单独说:表达式里引用表单字段,必须写成
#f_amount——井号引用 +f_前缀,两个都不能少。
f_前缀:jeeflow-ui 发起时给所有表单字段统一加f_前缀提交(f_amount、f_reason),这是引擎和前端约定好的业务参数命名空间,避免和引擎内置参数撞名。#引用:jeeflow 默认的 SpEL 求值器按#key从流程变量里取值。我第一次写成了裸的
amount <= 5000,发起 86000 的采购单,引擎直接报"decision节点无法确定下一步执行路线"——两条边都不命中。对着求值器源码才看清替换规则。写文章之前替你们踩了。
4.3 baoxiao:并行会签
报销申请,经理和财务并行会签,两人全部同意才过:
1{ "id": "countersign", "type": "snaker:task", 2 "properties": { 3 "form": "baoxiao-form", 4 "assignee": "1,2", 5 "performType": "1", 6 "countersignType": "PARALLEL", 7 "field": { "PERMISSION_f_reason": 1, "PERMISSION_f_amount": 1, 8 "PERMISSION_f_invoiceCount": 1, "PERMISSION_f_expenseDate": 1 } }, 9 "text": { "value": "经理财务会签" } } 10
三个属性缺一不可:assignee: "1,2" 逗号分隔多个审批人;performType: "1" 表示会签(每人一票);countersignType: "PARALLEL" 表示并行(任务同时产生)。改成 SEQUENTIAL 就是串行会签——按顺序逐个审。
4.4 overtime:最简审批 + 抄送
加班申请走"发起 → 上级审批"直线,抄送不占节点——发起时带 ccActors 参数就行,抄送对象会在发起瞬间收到一条抄送记录(我的抄送页可见)。
四个流程,定义文件加起来不到 400 行 JSON,没有一个 Java 类、没有一个 Vue 组件。
五、前端:一个宿主组件 + 九个薄页
若依前端是 RuoYi-Vue3(Vite + Vue3 + Element Plus + Pinia)。jeeflow-ui 的九个页面(工作台/发起/待办/已办/我发起的/抄送/流程定义/流程设计/委托)不是塞给你一坨独立布局,而是九个可以独立挂载的页面组件——正确姿势是把它们挂进若依自己的菜单体系,让"工作流"成为若依左侧菜单里的一级目录,跟"系统管理"平起平坐。
前端要写的东西分三块:main.js 两行、一个宿主组件、九个 8 行的薄页。
5.1 main.js:引一次样式
1import '@mldong/jeeflow-ui/dist/style.css' // jeeflow 流程中心组件样式 2
就一行。但这一行有个故事——见第八节,这行在 1.0.0 时代写了会报错,因为包漏导出了这个文件,1.0.1 才修。
5.2 wf-host.vue:全集成唯一值得细看的文件
九个页面共用一个宿主组件,职责就四件事:注入若依的身份与接口、桥接权限、映射页间跳转。
1<template> 2 <JeeflowUiProvider :config="jfConfig"> 3 <component :is="page" @goto="onGoto" /> 4 </JeeflowUiProvider> 5</template> 6 7<script setup> 8import { useRouter } from 'vue-router' 9import { JeeflowUiProvider } from '@mldong/jeeflow-ui' 10import request from '@/utils/request' 11import { getToken } from '@/utils/auth' 12import useUserStore from '@/store/modules/user' 13 14defineProps({ 15 page: { type: [Object, Function], required: true }, 16}) 17 18const router = useRouter() 19const userStore = useUserStore() 20 21// 通配权限匹配(对齐若依后端 PatternMatchUtils.simpleMatch) 22function globMatch(pattern, str) { 23 if (!pattern || !str) return false 24 if (pattern === '*:*:*') return true 25 if (pattern === str) return true 26 const re = '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$' 27 return new RegExp(re).test(str) 28} 29 30// hasPermission(codes[]):任一命中即放行 31function hasPermission(codes) { 32 const list = Array.isArray(codes) ? codes : [codes] 33 if (!list.length) return false 34 const mine = userStore.permissions || [] 35 return list.some(code => mine.some(perm => globMatch(perm, code))) 36} 37 38// 宿主 adapter:把若依 REST 喂给流程中心(选人/字典/上传) 39const adapters = { 40 getDict: async (code) => { 41 const res = await request.get([`/system/dict/data/type/${code}`](https://xplanc.org/primers/document/zh/02.Python/EX.%E5%86%85%E5%BB%BA%E5%87%BD%E6%95%B0/EX.dict.md)) 42 return (res.data || []).map(d => ({ value: d.dictValue, label: d.dictLabel })) 43 }, 44 upload: async (file) => { 45 const fd = new FormData() 46 fd.append('file', file) 47 const res = await request.post('/common/upload', fd, 48 { headers: { 'Content-Type': 'multipart/form-data' } }) 49 return res.url || res.fileName 50 }, 51 listUsers: async (keyword) => { 52 const res = await request.get('/system/user/list', 53 { params: { pageNum: 1, pageSize: 50, userName: keyword || undefined } }) 54 return (res.rows || []).map(u => ({ 55 userId: String(u.userId), realName: u.nickName, deptName: u.dept?.deptName || '' 56 })) 57 }, 58} 59 60const jfConfig = { 61 baseUrl: import.meta.env.VITE_APP_BASE_API, // 经 vite proxy → :8080 62 getToken: () => getToken(), // 若依 JWT(js-cookie Admin-Token) 63 getOperator: () => (userStore.id != null ? String(userStore.id) : ''), 64 hasPermission, 65 adapters, 66} 67 68// 组件内页间跳转(key 由组件 emit)→ 若依路由 69const routeMap = { 70 workbench: '/workflow/workbench', 71 apply: '/workflow/apply', 72 todo: '/workflow/todo', 73 done: '/workflow/done', 74 mine: '/workflow/mine', 75 cc: '/workflow/cc', 76 define: '/workflow/define', 77 design: '/workflow/design', 78 surrogate: '/workflow/surrogate', 79} 80 81function onGoto(key) { 82 if (routeMap[key]) router.push(routeMap[key]) 83} 84</script> 85
JeeflowUiProvider 是个依赖注入壳——它不绑死任何宿主 REST,token、当前用户、权限判断、选人/字典/上传全是你从外面塞进去的函数。你塞若依的,它就打若依的接口;塞别的系统的,它就打别的。
三个细节:
- token:若依用
js-cookie存Admin-Token,JWT 格式Bearer <token>,和 jeeflow-ui 要的一致,直接getToken()透传。 - 权限:若依前端
plugins/auth.js是精确匹配,没有通配;而菜单上我们把引擎细粒度码收成了通配按钮(wf:processDesign:*,第六节)。所以宿主里做了个globMatch桥接,让wf:processDesign:*能匹配wf:processDesign:listByType。 - 跳转:九个页面之间会互相跳(比如工作台点"待办数"跳待办页),jeeflow-ui 只 emit 一个
goto事件带页面 key,落地的路由由宿主决定——这就是它能"寄生"在任何宿主菜单体系里的关键。
5.3 九个薄页:每个 8 行
每个页面就是"宿主 + 对应的 Page 组件",以"我的待办"为例:
1<template> 2 <JfHost :page="JfTodoPage" /> 3</template> 4 5<script setup> 6import JfHost from '../wf-host.vue' 7import { JfTodoPage } from '@mldong/jeeflow-ui' 8</script> 9
其余八个(workbench/apply/done/mine/cc/define/design/surrogate)一模一样,只是 import 的 Page 组件不同。九个文件合计 70 行出头。
路由都不用手写——若依的动态路由会读 sys_menu 里 component 字段,指向 views/workflow/todo/index 就自动加载。所以挂菜单就是第六节那条 SQL 的事。
为什么前端这么少?因为表单也不归前端管——发起表单、审批回显全部由流程 JSON 的 __schema__ 驱动渲染,业务侧零表单代码。
六、RBAC:1 个目录 + 9 个页面 + 5 个通配按钮
菜单结构完全按若依的习惯来:一级目录"工作流",下面九个菜单项各挂一个页面,权限码用引擎的细粒度码(页面级入口),再配 5 个 F 型通配按钮把一族 action 的权限收口:
1-- 清旧段(2000-2099),保证可重复导入 2delete from sys_role_menu where menu_id between 2000 and 2099; 3delete from sys_menu where menu_id between 2000 and 2099; 4 5-- 一级目录:工作流 6insert into sys_menu values('2000', '工作流', 0, 5, 'workflow', null, '', '', 1, 0, 'M', '0', '0', '', 'tree', 'admin', sysdate(), '', null, 'jeeflow 工作流'); 7 8-- 九个页面菜单(动态路由,component 相对 views/) 9insert into sys_menu values('2001', '工作台', 2000, 1, 'workbench', 'workflow/workbench/index', '', '', 1, 0, 'C', '0', '0', 'wf:workbench', 'dashboard', 'admin', sysdate(), '', null, 'jeeflow 工作台'); 10insert into sys_menu values('2002', '发起申请', 2000, 2, 'apply', 'workflow/apply/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDesign:listByType', 'form', 'admin', sysdate(), '', null, 'jeeflow 发起申请'); 11insert into sys_menu values('2003', '我的待办', 2000, 3, 'todo', 'workflow/todo/index', '', '', 1, 0, 'C', '0', '0', 'wf:processTask:todoList', 'checkbox', 'admin', sysdate(), '', null, 'jeeflow 我的待办'); 12insert into sys_menu values('2004', '我的已办', 2000, 4, 'done', 'workflow/done/index', '', '', 1, 0, 'C', '0', '0', 'wf:processTask:doneList', 'time', 'admin', sysdate(), '', null, 'jeeflow 我的已办'); 13insert into sys_menu values('2005', '我发起的', 2000, 5, 'mine', 'workflow/mine/index', '', '', 1, 0, 'C', '0', '0', 'wf:processInstance:page', 'list', 'admin', sysdate(), '', null, 'jeeflow 我发起的'); 14insert into sys_menu values('2006', '我的抄送', 2000, 6, 'cc', 'workflow/cc/index', '', '', 1, 0, 'C', '0', '0', 'wf:processInstance:ccList', 'message', 'admin', sysdate(), '', null, 'jeeflow 我的抄送'); 15insert into sys_menu values('2007', '流程定义', 2000, 7, 'define', 'workflow/define/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDefine:page', 'documentation', 'admin', sysdate(), '', null, 'jeeflow 流程定义'); 16insert into sys_menu values('2008', '流程设计', 2000, 8, 'design', 'workflow/design/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDesign:page', 'build', 'admin', sysdate(), '', null, 'jeeflow 流程设计'); 17insert into sys_menu values('2009', '我的委托', 2000, 9, 'surrogate', 'workflow/surrogate/index', '', '', 1, 0, 'C', '0', '0', 'wf:processSurrogate:page', 'peoples', 'admin', sysdate(), '', null, 'jeeflow 我的委托'); 18 19-- 五个通配按钮(权限码收口:引擎细粒度码由若依 simpleMatch 通配命中) 20insert into sys_menu values('2020', '流程发起', 2000, 20, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processDesign:*', '#', 'admin', sysdate(), '', null, '发起/设计/上架/发布'); 21insert into sys_menu values('2021', '任务办理', 2000, 21, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processTask:*', '#', 'admin', sysdate(), '', null, '待办/已办/执行/转办'); 22insert into sys_menu values('2022', '实例查看', 2000, 22, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processInstance:*', '#', 'admin', sysdate(), '', null, '实例分页/详情/抄送'); 23insert into sys_menu values('2023', '定义管理', 2000, 23, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processDefine:*', '#', 'admin', sysdate(), '', null, '定义增删改/版本'); 24insert into sys_menu values('2024', '流程委托', 2000, 24, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processSurrogate:*','#', 'admin', sysdate(), '', null, '委托增删改查'); 25 26-- 授权普通角色(common, role_id=2):9 个页面 + 5 个按钮 27-- (按需裁剪:管理页 2007/2008/2009 与对应按钮可不给普通角色) 28insert into sys_role_menu values ('2','2000'),('2','2001'),('2','2002'),('2','2003'), 29 ('2','2004'),('2','2005'),('2','2006'),('2','2007'),('2','2008'),('2','2009'), 30 ('2','2020'),('2','2021'),('2','2022'),('2','2023'),('2','2024'); 31
能这么收,靠的是若依 SecurityUtils.hasPermi 底层用的 PatternMatchUtils.simpleMatch——天然支持 * 通配。引擎给 wf:processTask:todoList,按钮配 wf:processTask:*,一命中就放行。45 个 action 一个按钮都没漏。
真机验证了三种情况:
| 场景 | 账号 | 结果 |
|---|---|---|
| 正向:全流程 | admin(超级管理员) | 发起→待办→审批→记录 全通 |
| 正向:普通角色 | ry(common 角色) | 发起成功,走 admin 审批 |
| 负向:无权限 | noperm(无角色用户) | listByType / startAndExecute 返回 403,approvalRecord(免权限)放行 |
负向那条尤其关键——它证明了权限不是"登录就能全干",而是真落在若依 RBAC 上。
七、跑通实录
环境:若依 3.9.2(master,Boot 4.1.0 / JDK 17),MySQL 8(jeeflow 引擎 8 张 wf_* 表),Redis。
7.1 造一批真数据
空系统的截图是没有说服力的。我参考 jeeflow 商业演示站的数据语义,用脚本走真实 API(登录→发起→审批,一步没有跳过)造了一批演示数据——4 个流程、8 条实例,把审批流最常见的几种生命周期全覆盖:
| # | 流程 | 剧本 | 终态 |
|---|---|---|---|
| S1 | 请假 | ry 发起 → admin 同意 | ✅ 走完 |
| S2 | 请假 | admin 自己发起自己审 | 🔵 在途 |
| S3 | 报销 | ry 发起 → 经理+财务并行会签,两人全同意 | ✅ 走完 |
| S4 | 请假 | ry 发起 → admin 退回发起人 → ry 改理由重新提交 → admin 同意 | ✅ 走完 |
| S5 | 采购 | admin 发 4200 元(小额)→ 走部门审批 | 🔵 在途(ry 待办) |
| S6 | 采购 | ry 发 86000 元(大额)→ 走总经理审批 | 🔵 在途(admin 待办) |
| S7 | 加班 | ry 发起(抄送 admin)→ admin 拒绝 | ❌ 被拒 |
| S8 | 加班 | ry 发起 → admin 同意 | ✅ 走完 |
(S4 那条是故意演的:退回重提是审批系统里最考验数据完整性的场景,审批记录的时间线必须把"退回→重提→同意"全程记下来,见 7.3。)
7.2 接口层(curl):一条链走通
1# 1. 登录拿 token 2curl -s -X POST http://localhost:8080/login -H "Content-Type: application/json" \ 3 -d '{"username":"admin","password":"admin123","code":"13","uuid":"<验证码uuid>"}' 4# → {"code":200,"token":"eyJhbGciOiJIUzUxMiJ9..."} 5 6# 2. 发起(processDefine/startAndExecute,表单字段 f_ 前缀) 7curl -s -X POST http://localhost:8080/wf/processDefine/startAndExecute \ 8 -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ 9 -d '{"processDefineId":15328901147922432,"f_reason":"家里有事","f_days":2,"f_leaveType":"personal"}' 10# → {"code":0,"msg":"成功","data":{"processInstanceId":15328916897009664}} 11 12# 3. 查待办(processTask/todoList) 13# 4. 审批同意(processTask/execute,submitType=1) 14curl -s -X POST http://localhost:8080/wf/processTask/execute \ 15 -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ 16 -d '{"processTaskId":15328926470901760,"submitType":1,"tf_approvalComment":"同意,注意工作交接"}' 17# → {"code":0,"msg":"成功","data":null} 18 19# 5. 审批记录(processInstance/approvalRecord) 20# → apply 节点 + approve 节点,中文意见原样落库 21
⚠️ 一个 Windows 上的坑:用
curl -d '{...含中文...}'直接传中文,MSYS 的命令行参数编码会把中文变?(库里落一堆问号)。改用--data-binary @file.json从文件读,就正常了。跟若依、跟 jeeflow 都没关系,是 MSYS 的事——但如果你在 Windows 上用 curl 测,会遇到,先排掉这个再怀疑框架。
7.3 界面层:一个完整的流程中心
登录进首页,左侧菜单多了一级目录"工作流",九个菜单项全部就位:
工作台——在办/办结/发起/抄送四个统计卡、近 7 日发起趋势、热点流程 Top5、流程状态分布。这不是占位图表,数据全部来自引擎真实统计接口:
发起申请——四张流程卡片,还在途的流程会标"N 条在办"徽标。点开"报销申请",__schema__ 表单自动渲染:事由、金额、发票张数、费用日期,必填校验齐全:
我的待办——admin 登录,待办里躺着"总经理审批"(S6 采购 86000 大额)和"上级审批"(S2 自发起的请假):
点"办理",抽屉上半屏是申请信息只读回显——采购内容"测试服务器 2 台"、预算 86000、供应商、期望日期,审批人看得到但改不了(字段权限 PERMISSION_f_xxx: 1 在起作用);下半屏是审批意见和动作:
动作不止"同意/拒绝"——六种审批动作开箱即用:同意、拒绝、退回上一步、退回发起人、跳转指定节点、转办:
审批记录——S4 那条"退回重提"的时间线,四步全程可溯:发起 → 上级审批(退回发起人,带退回意见)→ 发起人重新提交 → 上级审批(同意)。审批系统最怕"口说无凭",这条时间线就是凭据:
流程定义——四个流程、版本号、发布状态一目了然:
流程设计——每个流程的设计快照可进可视化设计器调整再发布(本文的流程 JSON 就是在这里管理的):
我发起的——ry 视角看自己发起的 5 条,在途/完成/拒绝状态分明:
我的抄送——S7 加班单发起时抄送了 admin,这里多了一条带"抄送"标记的记录:
从零到这一屏,后端一个依赖 + 一个薄壳 + 三个 Provider + 一个种子 Runner,前端一个宿主组件 + 九个 8 行薄页 + 一行样式引入 + 一条菜单 SQL。没有写过一个表单,没有写过一个列表页。
八、首次第三方集成,反哺了上游三个问题
说个背景:jeeflow 此前的所有集成(十几个框架)都是我自己维护的 mldong 系列,"自家的引擎配自家的框架",很多坑是暴露不出来的。这次若依集成是第一次以第三方视角从零接一遍,一天之内挖出了三个问题——两个在 ui 组件包,当场修了发版;一个在引擎,记了 issue。
① ui 包漏导出样式文件。 @mldong/jeeflow-ui 的 dist/style.css 文件在包里,但 package.json 的 exports 字段没导出它——宿主 import '@mldong/jeeflow-ui/dist/style.css' 直接报 Missing specifier。Node 的 exports 语义下"包里有文件"和"允许被引用"是两回事。没有自己家的约定俗成兜底,第三方一接就现形。
② 办理抽屉画出重复的空表单。 审批人点"办理",抽屉下半屏本该只有审批意见框,却多画了一整份绑着空数据的业务表单——因为任务表单组件找不到宿主注册的审批表单时,回落渲染了流程级 __schema__ 表单,而 __schema__ 早就该由上半屏"申请信息"按字段权限渲染了。自家集成里表单注册习惯一致,这问题从来没冒过头。
这两个都在 jeeflow-ui 源头修复,发了 1.0.1(本文所有截图就是 1.0.1 的效果)。若依侧只需要 npm i @mldong/jeeflow-ui@^1.0.1。
③ 引擎的并行会签+退回组合缺陷。 并行会签节点上,一人执行"退回发起人"后,兄弟审批人的会签任务不自动作废——僵尸任务继续挂在别人待办里,还会卡死会签的"全员完成"判定,实例永远走不到结束。这个是引擎级问题,集成层修不了,已提 issue 记录在案,留待引擎侧修。演示数据里我把"退回重提"闭环放在单人节点上演(S4),绕开了这个组合——该有的场景一样不少。
这大概是第三方集成最大的价值:它是一台天然的模糊测试机。所有"我们一直都这么用"的隐式约定,在第一个真正的外人面前全部现形。
九、这个集成,我删了
写到这里,集成是通的、验证是齐的。但得跟你说清楚定位,免得有人真拿它当产品用。
jeeflow 是一个"多语言联邦"的工作流引擎,我维护的下游有一长串:Java 的 boot2/3/4、FastAPI、NestJS、Laravel、Go 的 goframe/gin/hertz、Rust 的 salvo、C#、Python 的 Flask/Django……每个框架都有一套"基础 + 1 commit"的集成壳,全部进了我的验收矩阵,长期维护。
若依不在这个列表里。
上面这套若依集成,是我为了写这篇文章,在一个临时目录里从零走一遍流程做的实践示例——它证明了"jeeflow 能接进任何主流框架,前后端各一两个文件"这件事本身。它不会进我的维护矩阵,没有持续升级承诺,文章发完我会把临时环境清掉。
所以:
- 如果你用 jeeflow,去看我维护的 mldong 快速开发框架系列(boot4 / FastAPI / goframe 等),那才是有长期保障的集成。
- 若依这套,当作"怎么给一个存量 Spring 项目接 jeeflow"的参考样本,代码逻辑是通的,照搬到你的若依项目能跑;但它不是成品,别直接当生产依赖。
一个第三方集成,最大的价值不是"我维护它",而是"它证明了接入成本有多低"。若依这套的价值就在最后——前后端加起来,真正要写的业务代码,就那么三个 Provider、一个宿主组件和九个薄页。
审批流这个东西,不该是若依用户"要不要上 Flowable"的难题。它应该是"加个依赖,配个菜单,半天跑通"的常规操作。
(全文完)
《给若依加审批流,不用 Flowable》 是转载文章,点击查看原文。
