引言
若依是国内最流行的快速开发框架之一,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:
xml
<!-- ruoyi-admin/pom.xml -->
<dependency>
<groupId>com.mldong.jeeflow</groupId>
<artifactId>jeeflow-spring-boot4-starter</artifactId>
<version>1.8.27</version>
</dependency>
这一个依赖进去,starter 自动装配好引擎核心:JeeflowEngine、JDBC 仓储、雪花 ID 生成、Jackson(Long→String,前端不丢精度)、事务模板、SpEL 表达式求值器、默认用户 Provider。全部分 @ConditionalOnMissingBean,你想覆盖哪个就自己 @Bean 哪个。
注意:不用覆盖 ID 生成器。若依没引 MyBatis-Plus,没有"雪花 vs 自增主键"的对齐问题,starter 内置雪花直接能用。这是"薄"的体现------少写一个 Bean。
3.2 只补两个 Bean
starter 不自动装配的就两样:设计/历史/委托三表的扩展仓储,和统一门面 JeeflowFacade。一个配置类搞定:
java
@Configuration
public class WfJeeflowConfig {
@Bean
@ConditionalOnMissingBean
public IProcessExtRepository jeeflowExtRepository(DataSource dataSource) {
return new JdbcProcessExtRepository(dataSource);
}
@Bean
@ConditionalOnMissingBean
public JeeflowFacade jeeflowFacade(JeeflowEngine engine,
IProcessRepository repository,
IProcessExtRepository extRepository,
ObjectProvider<IUserSearchProvider> userSearchProvider) {
JeeflowFacade facade = new JeeflowFacade(engine, repository, extRepository);
userSearchProvider.ifAvailable(facade::setUserSearchProvider);
return facade;
}
}
3.3 一个 Controller 吃下所有请求
jeeflow 的门面是"一个入口,45 个 action"------POST /wf/{action},body 是参数 map,返回 {code: 0, msg, data}。所以若依侧只需要一个 Controller:
java
@RestController
@RequestMapping("/wf")
public class WfFlowController {
@Autowired
private JeeflowFacade jeeflowFacade;
@PostMapping("/**")
public Map<String, Object> flow(HttpServletRequest request,
@RequestBody(required = false) Map<String, Object> body) {
String uri = request.getRequestURI();
int idx = uri.indexOf("/wf/");
String action = uri.substring(idx + "/wf/".length());
checkPermission(action); // ① 权限校验
if (body == null) body = new LinkedHashMap<>();
body.put("operator", SecurityUtils.getUserId().toString()); // ② 注入操作人
return jeeflowFacade.flow(action, body); // ③ 门面透传
}
private void checkPermission(String action) {
if (SecurityUtils.isAdmin()) return; // admin 万能放行
IActionPermissionProvider provider = ServiceContext.find(IActionPermissionProvider.class);
String[] codes = provider == null ? null : provider.permissionCodes(action);
if (codes != null && codes.length > 0) {
for (String code : codes) {
if (SecurityUtils.hasPermi(code)) return; // 任一权限码命中即可
}
throw new ServiceException("无权限执行操作:" + action, 403);
}
}
}
三件事,全是若依现成的能力:
- 权限 :引擎默认 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 链路,不是直接插表):
java
// WfFlowSeedRunner:启动时幂等上架 leave 流程
// 1. 查 processDesign/page,name=leave 已发布就跳过
// 2. processDesign/save(content = 流程 JSON 字符串)→ 拿 designId
// 3. processDesign/deploy(designId)
为什么走门面而不是直接 insert?因为 listByType(发起页的数据源)读的是 design 表 + 历史表,不是 define 表。直接插 define 表,发起页看不到。走 save→deploy 才能保证和人工在界面上设计、发布走的是同一条路。
开箱只有请假一条还不够演示。我又写了三个流程定义(报销/采购/加班,JSON 见第四节),用同一条 save→deploy 链路上架------脚本调门面和在"流程设计"页面上点发布,后端是同一个动作。最终发起页有四张流程卡片:

四、四个流程定义:从直线到网关和会签
流程定义就是 JSON。四个流程由简到繁,正好把 jeeflow 的核心概念全覆盖:直线审批、决策网关、并行会签、字段权限、抄送。
4.1 leave:最简直线审批(全文)
json
{
"name": "leave",
"displayName": "请假申请",
"type": "approval",
"enableFieldPerm": true,
"__schema__": {
"columns": [
{ "fieldName": "reason", "remark": "请假事由", "component": "Textarea", "ext": { "required": 1, "placeholder": "请说明请假原因" } },
{ "fieldName": "days", "remark": "请假天数", "component": "Number", "ext": { "required": 1 } },
{ "fieldName": "leaveType", "remark": "请假类型", "component": "Select",
"ext": { "required": 1, "options": [
{ "label": "年假", "value": "annual" },
{ "label": "事假", "value": "personal" },
{ "label": "病假", "value": "sick" } ] } },
{ "fieldName": "startDate", "remark": "开始日期", "component": "Date" },
{ "fieldName": "endDate", "remark": "结束日期", "component": "Date" }
]
},
"nodes": [
{ "id": "start", "type": "snaker:start", "text": { "value": "开始" } },
{ "id": "apply", "type": "snaker:task",
"properties": { "form": "leave-form", "assignee": "applicant" },
"text": { "value": "填写申请" } },
{ "id": "approve", "type": "snaker:task",
"properties": { "form": "leave-form", "assignee": "1",
"field": { "PERMISSION_f_reason": 1, "PERMISSION_f_days": 1, "PERMISSION_f_leaveType": 1 } },
"text": { "value": "上级审批" } },
{ "id": "end", "type": "snaker:end", "text": { "value": "结束" } }
],
"edges": [
{ "sourceNodeId": "start", "targetNodeId": "apply" },
{ "sourceNodeId": "apply", "targetNodeId": "approve" },
{ "sourceNodeId": "approve", "targetNodeId": "end" }
]
}
几个值得注意的点:
__schema__.columns就是表单,前端零代码渲染(第五节)。不用为"请假"写一个 Vue 表单组件。assignee: "applicant"= 发起人本人填;assignee: "1"= 字面 userId(这里指 admin)。还能写deptLeader、roleCode这种,由RyOrgUserProvider解析。field里的PERMISSION_f_xxx: 1= 该字段在审批节点只读(1=只读 / 2=可编辑 / 3=隐藏)。审批人能看到"请假事由"但改不了。
4.2 purchase:金额网关分流
采购申请,预算 5000 是分水岭:以下部门审批,以上总经理审批。核心就是一个决策节点 + 两条带表达式的边:
json
{ "id": "decision1", "type": "snaker:decision",
"properties": { "expr": "#f_amount > 5000" },
"text": { "value": "金额>5000?" } }
两条出边的表达式:
json
{ "sourceNodeId": "decision1", "targetNodeId": "deptApprove",
"properties": { "expr": "#f_amount <= 5000" }, "text": { "value": "≤5000" } },
{ "sourceNodeId": "decision1", "targetNodeId": "gmApprove",
"properties": { "expr": "#f_amount > 5000" }, "text": { "value": ">5000" } }
这里有个真机踩出来的坑,值得单独说 :表达式里引用表单字段,必须写成
#f_amount------井号引用 +f_前缀,两个都不能少。
f_前缀:jeeflow-ui 发起时给所有表单字段统一加f_前缀提交(f_amount、f_reason),这是引擎和前端约定好的业务参数命名空间,避免和引擎内置参数撞名。#引用:jeeflow 默认的 SpEL 求值器按#key从流程变量里取值。我第一次写成了裸的
amount <= 5000,发起 86000 的采购单,引擎直接报"decision节点无法确定下一步执行路线"------两条边都不命中。对着求值器源码才看清替换规则。写文章之前替你们踩了。
4.3 baoxiao:并行会签
报销申请,经理和财务并行会签,两人全部同意才过:
json
{ "id": "countersign", "type": "snaker:task",
"properties": {
"form": "baoxiao-form",
"assignee": "1,2",
"performType": "1",
"countersignType": "PARALLEL",
"field": { "PERMISSION_f_reason": 1, "PERMISSION_f_amount": 1,
"PERMISSION_f_invoiceCount": 1, "PERMISSION_f_expenseDate": 1 } },
"text": { "value": "经理财务会签" } }
三个属性缺一不可: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:引一次样式
js
import '@mldong/jeeflow-ui/dist/style.css' // jeeflow 流程中心组件样式
就一行。但这一行有个故事------见第八节,这行在 1.0.0 时代写了会报错,因为包漏导出了这个文件,1.0.1 才修。
5.2 wf-host.vue:全集成唯一值得细看的文件
九个页面共用一个宿主组件,职责就四件事:注入若依的身份与接口、桥接权限、映射页间跳转。
vue
<template>
<JeeflowUiProvider :config="jfConfig">
<component :is="page" @goto="onGoto" />
</JeeflowUiProvider>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { JeeflowUiProvider } from '@mldong/jeeflow-ui'
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
defineProps({
page: { type: [Object, Function], required: true },
})
const router = useRouter()
const userStore = useUserStore()
// 通配权限匹配(对齐若依后端 PatternMatchUtils.simpleMatch)
function globMatch(pattern, str) {
if (!pattern || !str) return false
if (pattern === '*:*:*') return true
if (pattern === str) return true
const re = '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$'
return new RegExp(re).test(str)
}
// hasPermission(codes[]):任一命中即放行
function hasPermission(codes) {
const list = Array.isArray(codes) ? codes : [codes]
if (!list.length) return false
const mine = userStore.permissions || []
return list.some(code => mine.some(perm => globMatch(perm, code)))
}
// 宿主 adapter:把若依 REST 喂给流程中心(选人/字典/上传)
const adapters = {
getDict: async (code) => {
const res = await request.get(`/system/dict/data/type/${code}`)
return (res.data || []).map(d => ({ value: d.dictValue, label: d.dictLabel }))
},
upload: async (file) => {
const fd = new FormData()
fd.append('file', file)
const res = await request.post('/common/upload', fd,
{ headers: { 'Content-Type': 'multipart/form-data' } })
return res.url || res.fileName
},
listUsers: async (keyword) => {
const res = await request.get('/system/user/list',
{ params: { pageNum: 1, pageSize: 50, userName: keyword || undefined } })
return (res.rows || []).map(u => ({
userId: String(u.userId), realName: u.nickName, deptName: u.dept?.deptName || ''
}))
},
}
const jfConfig = {
baseUrl: import.meta.env.VITE_APP_BASE_API, // 经 vite proxy → :8080
getToken: () => getToken(), // 若依 JWT(js-cookie Admin-Token)
getOperator: () => (userStore.id != null ? String(userStore.id) : ''),
hasPermission,
adapters,
}
// 组件内页间跳转(key 由组件 emit)→ 若依路由
const routeMap = {
workbench: '/workflow/workbench',
apply: '/workflow/apply',
todo: '/workflow/todo',
done: '/workflow/done',
mine: '/workflow/mine',
cc: '/workflow/cc',
define: '/workflow/define',
design: '/workflow/design',
surrogate: '/workflow/surrogate',
}
function onGoto(key) {
if (routeMap[key]) router.push(routeMap[key])
}
</script>
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 组件",以"我的待办"为例:
vue
<template>
<JfHost :page="JfTodoPage" />
</template>
<script setup>
import JfHost from '../wf-host.vue'
import { JfTodoPage } from '@mldong/jeeflow-ui'
</script>
其余八个(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 的权限收口:
sql
-- 清旧段(2000-2099),保证可重复导入
delete from sys_role_menu where menu_id between 2000 and 2099;
delete from sys_menu where menu_id between 2000 and 2099;
-- 一级目录:工作流
insert into sys_menu values('2000', '工作流', 0, 5, 'workflow', null, '', '', 1, 0, 'M', '0', '0', '', 'tree', 'admin', sysdate(), '', null, 'jeeflow 工作流');
-- 九个页面菜单(动态路由,component 相对 views/)
insert into sys_menu values('2001', '工作台', 2000, 1, 'workbench', 'workflow/workbench/index', '', '', 1, 0, 'C', '0', '0', 'wf:workbench', 'dashboard', 'admin', sysdate(), '', null, 'jeeflow 工作台');
insert into sys_menu values('2002', '发起申请', 2000, 2, 'apply', 'workflow/apply/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDesign:listByType', 'form', 'admin', sysdate(), '', null, 'jeeflow 发起申请');
insert into sys_menu values('2003', '我的待办', 2000, 3, 'todo', 'workflow/todo/index', '', '', 1, 0, 'C', '0', '0', 'wf:processTask:todoList', 'checkbox', 'admin', sysdate(), '', null, 'jeeflow 我的待办');
insert into sys_menu values('2004', '我的已办', 2000, 4, 'done', 'workflow/done/index', '', '', 1, 0, 'C', '0', '0', 'wf:processTask:doneList', 'time', 'admin', sysdate(), '', null, 'jeeflow 我的已办');
insert into sys_menu values('2005', '我发起的', 2000, 5, 'mine', 'workflow/mine/index', '', '', 1, 0, 'C', '0', '0', 'wf:processInstance:page', 'list', 'admin', sysdate(), '', null, 'jeeflow 我发起的');
insert into sys_menu values('2006', '我的抄送', 2000, 6, 'cc', 'workflow/cc/index', '', '', 1, 0, 'C', '0', '0', 'wf:processInstance:ccList', 'message', 'admin', sysdate(), '', null, 'jeeflow 我的抄送');
insert into sys_menu values('2007', '流程定义', 2000, 7, 'define', 'workflow/define/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDefine:page', 'documentation', 'admin', sysdate(), '', null, 'jeeflow 流程定义');
insert into sys_menu values('2008', '流程设计', 2000, 8, 'design', 'workflow/design/index', '', '', 1, 0, 'C', '0', '0', 'wf:processDesign:page', 'build', 'admin', sysdate(), '', null, 'jeeflow 流程设计');
insert into sys_menu values('2009', '我的委托', 2000, 9, 'surrogate', 'workflow/surrogate/index', '', '', 1, 0, 'C', '0', '0', 'wf:processSurrogate:page', 'peoples', 'admin', sysdate(), '', null, 'jeeflow 我的委托');
-- 五个通配按钮(权限码收口:引擎细粒度码由若依 simpleMatch 通配命中)
insert into sys_menu values('2020', '流程发起', 2000, 20, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processDesign:*', '#', 'admin', sysdate(), '', null, '发起/设计/上架/发布');
insert into sys_menu values('2021', '任务办理', 2000, 21, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processTask:*', '#', 'admin', sysdate(), '', null, '待办/已办/执行/转办');
insert into sys_menu values('2022', '实例查看', 2000, 22, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processInstance:*', '#', 'admin', sysdate(), '', null, '实例分页/详情/抄送');
insert into sys_menu values('2023', '定义管理', 2000, 23, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processDefine:*', '#', 'admin', sysdate(), '', null, '定义增删改/版本');
insert into sys_menu values('2024', '流程委托', 2000, 24, '', '', '', '', 1, 0, 'F', '0', '0', 'wf:processSurrogate:*','#', 'admin', sysdate(), '', null, '委托增删改查');
-- 授权普通角色(common, role_id=2):9 个页面 + 5 个按钮
-- (按需裁剪:管理页 2007/2008/2009 与对应按钮可不给普通角色)
insert into sys_role_menu values ('2','2000'),('2','2001'),('2','2002'),('2','2003'),
('2','2004'),('2','2005'),('2','2006'),('2','2007'),('2','2008'),('2','2009'),
('2','2020'),('2','2021'),('2','2022'),('2','2023'),('2','2024');
能这么收,靠的是若依 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):一条链走通
bash
# 1. 登录拿 token
curl -s -X POST http://localhost:8080/login -H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","code":"13","uuid":"<验证码uuid>"}'
# → {"code":200,"token":"eyJhbGciOiJIUzUxMiJ9..."}
# 2. 发起(processDefine/startAndExecute,表单字段 f_ 前缀)
curl -s -X POST http://localhost:8080/wf/processDefine/startAndExecute \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d '{"processDefineId":15328901147922432,"f_reason":"家里有事","f_days":2,"f_leaveType":"personal"}'
# → {"code":0,"msg":"成功","data":{"processInstanceId":15328916897009664}}
# 3. 查待办(processTask/todoList)
# 4. 审批同意(processTask/execute,submitType=1)
curl -s -X POST http://localhost:8080/wf/processTask/execute \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d '{"processTaskId":15328926470901760,"submitType":1,"tf_approvalComment":"同意,注意工作交接"}'
# → {"code":0,"msg":"成功","data":null}
# 5. 审批记录(processInstance/approvalRecord)
# → apply 节点 + approve 节点,中文意见原样落库
⚠️ 一个 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"的难题。它应该是"加个依赖,配个菜单,半天跑通"的常规操作。
(全文完)