Appearance
10 · mldong 快速开发框架集成 jeeflow 指南
面向新框架接入:怎么把一个 jeeflow 工作流引擎集成进自己的快速开发框架。 mldong 系列五框架(Java boot4 / Python FastAPI / Node NestJS / Go GoFrame / PHP Laravel)已有 参考实现(见文末 §7),本文档是五仓集成面的统一归纳——引擎零改动, 集成方只需做 4 件事。
前端对接不依赖本文档:只要后端实现了统一门面,前端一律按 规范 06 · 统一门面完整接口文档 的 40+ 个 action 直接接入。
1. 接入总览(4 件事)
框架层 jeeflow 引擎层
┌──────────────────────┐ ┌──────────────────────┐
│ /wf/** 转发 controller │ POST │ JeeflowFacade │
│ (一个入口,全部 action)│ ────────→ │ flow(action, args) │
│ │ body │ 40+ 个 action 内置路由 │
│ ① 登录校验(框架已有) │ ├──────────────────────┤
│ ② 权限码动态校验 │ │ IProcessRepository │
│ ③ operator 注入 │ │ IProcessExtRepository │
│ ④ listByType 结构转换 │ │ (引擎提供 JDBC 默认) │
└──────────────────────┘ └──────────────────────┘集成方只做 4 件事:
| # | 事项 | 说明 |
|---|---|---|
| 1 | 引入引擎依赖 | 六语言各自:Maven / pip / npm / go mod / composer / cargo |
| 2 | 一个转发 controller | POST /wf/{action} → 调用门面 flow(action, body),返回结构原样透传 |
| 3 | 框架层登录 + 权限码校验 | 登录用框架已有的全局拦截器;权限码由引擎 SPI 提供元数据(默认 wf:{action}),框架执行校验(superAdmin 万能放行) |
| 4 | 注入 operator + 适配细节 | body 注入当前登录用户 id;雪花 id 精度处理(见 §5) |
2. 引入引擎依赖
| 语言 | 坐标 | 引擎核心 |
|---|---|---|
| Java | com.mldong.jeeflow:jeeflow-core(starter: jeeflow-spring-bootX-starter) | JeeflowFacade / JeeflowEngine |
| Python | jeeflow(PyPI) | JeeflowFacade / EngineImpl |
| Node | jeeflow(npm) | JeeflowFacade |
| Go | github.com/mldong/jeeflow(go mod) | facade.New |
| PHP | mldong/jeeflow-php(Packagist) | JeeflowFacade / JeeflowEngine |
| Rust | jeeflow-facade(crates.io;jeeflow-core / jeeflow-persist / jeeflow-repository-sqlx 同版本) | JeeflowFacade::flow / JeeflowEngine |
可选组件:
jeeflow-persist(业务数据自动落表/回显,见 指南 08、指南 09); 仓储默认实现jeeflow-repository-jdbc(Java,给 DataSource 即用)。
3. 六框架参考实现
Laravel(jeeflow-php,PHP 8.4)的薄映射与 Java 同构:
WfFlowController(40+ action 转发JeeflowFacade)
JeeflowUserProvider/WfExpressionEvaluator/WfTransactionTemplate+WfJeeflowServiceProvider注册装配,代码结构对齐 §3.1,此处不重复展开。
3.1 Java(boot4,Spring Boot 4 + sa-token)
java
@RestController
@RequiredArgsConstructor
public class WfFlowController {
private final JeeflowFacade jeeflowFacade;
/** 统一入口:一个 /wf/** 转发全部 action */
@PostMapping("/wf/**")
public Map<String, Object> flow(HttpServletRequest request,
@RequestBody(required = false) Map<String, Object> body) {
String uri = request.getRequestURI();
String contextPath = request.getContextPath();
String action = uri.substring(contextPath.length() + "/wf/".length());
// ① 权限校验(权限码 SPI,引擎内置默认映射)
checkPermission(action);
// ② 注入操作人(门面约定 args.operator)
if (body == null) body = new LinkedHashMap<>();
body.put("operator", LoginUserHolder.getUserId().toString());
// ③ listByType 返回结构转换(boot3 前端约定,见 §5.4)
if ("processDesign/listByType".equals(action)) {
return designListByType(body);
}
// ④ 门面转发(字段契约引擎已内置)
return jeeflowFacade.flow(action, body);
}
/** 权限校验:superAdmin 万能放行;其余按权限码 SPI(OR 语义) */
private void checkPermission(String action) {
LoginUser loginUser = LoginUserHolder.me();
if (loginUser != null && loginUser.isSuperAdmin()) return;
IActionPermissionProvider provider = ServiceContext.find(IActionPermissionProvider.class);
String[] codes = provider == null ? null : provider.permissionCodes(action);
if (codes != null && codes.length > 0) {
StpUtil.checkPermissionOr(codes); // sa-token 校验
}
}
}- facade 装配:
WfJeeflowConfig(@Configuration)中new JeeflowFacade(engine, repository, extRepository)注册为 bean, 注入框架UserProvider(用户体系映射)、metaTableReader(persist 回显)等 SPI - id 字符串契约:Java 集成层配置
ToStringSerializer,出口 id 一律字符串 - 权限码 SPI:引擎内置
DefaultActionPermissionProvider,无需手动实现
3.2 Python(FastAPI)
python
@router.post("/wf/{action:path}", summary="jeeflow 门面转发")
async def wf_flow(action: str, body: dict = Body(default=None)):
current_user = UserContext.get_current_user() # 框架登录上下文
# ① 权限码动态校验(superAdmin 万能放行)
if not current_user.superAdmin:
codes = permission_codes(action) # 引擎内置规则同款
if codes and not any(c in current_user.permissions for c in codes):
raise NotPermissionException()
# ② 注入操作人
body = dict(body or {})
body["operator"] = current_user.id
# ③ 门面转发
facade = await get_facade()
result = await facade.flow(action, body)
# ④ listByType 结构转换(boot3 前端约定)
if action == "processDesign/listByType" and result.get("code") == 0:
data = result.get("data") or {}
result["data"] = [{"type": k, "title": "", "items": v} for k, v in data.items()]
return result- facade 装配(
jeeflow_factory.py):JdbcRepository(aiomysql 连接池)+JdbcProcessExtRepositoryEngineImpl+ 内置 AssignmentHandler 注册 + 框架 SPI 适配:
MldongUserProvider/MldongOrgUserProvider:sys_user / sys_dept / sys_post / sys_role 映射FrameworkSnowflakeIDGen:框架雪花注入(与 Java MyBatis-Plus IdWorker 同构, 保证跨语言共用流程定义时 id 体系一致)
- id 字符串契约:引擎出口
stringifyIDs已保证
3.3 Node(NestJS)
typescript
@Controller()
export class WfController {
constructor(private readonly wfService: WfJeeflowService) {}
// Express 5(path-to-regexp v8)命名通配符:wf/*splat 匹配 wf/{action 多段},
// 捕获值中多段分隔符是 `,`(v8 行为)——必须还原为 `/`
@Post('wf/*splat')
async flow(@Param('splat') splat: string, @Body() body: Record<string, any>) {
const action = splat.replace(/,/g, '/');
const user = LoginUserHolder.getCurrentUser();
if (!user) AssertTool.raiseBizWithCodeMsg(99990403, 'token不存在或无效!');
// ① 权限码动态校验(superAdmin 万能放行)
if (!user.superAdmin) {
const codes = permissionCodes(action);
if (codes && !codes.some((c) => user.permissions.includes(c))) {
AssertTool.raiseBizWithCodeMsg(99990406, `您没有资源wf:${action}访问权限,请联系管理员!`);
}
}
// ② 注入操作人
const args: Record<string, any> = { ...(body ?? {}) };
args.operator = user.id;
// ③ 门面转发
const result = await this.wfService.flow(action, args);
// ④ listByType 结构转换
if (action === 'processDesign/listByType' && result?.code === 0) {
const data = (result.data ?? {}) as Record<string, any>;
result.data = Object.entries(data).map(([type, items]) => ({ type, title: '', items }));
}
return result;
}
}- facade 装配(
wf-jeeflow.service.ts):new JeeflowFacade(engine, repo, extRepo),setOrgProvider(...)组织体系 SPI、setUserSearch(...)用户搜索 SPI - 坑(必踩):Express 5 的
*splat通配符把多段路径捕获成逗号分隔 (processDesign/listByType→processDesign,listByType),必须splat.replace(/,/g, '/') - id 字符串契约:Node 引擎全链路 string,天然满足
3.4 Go(GoFrame)
go
func (ctrl *WfController) Flow(ctx context.Context, req *wfApi.FlowReq) (res *base.CommonResult, err error) {
r := g.RequestFromCtx(ctx)
// ① 权限码动态校验(superAdmin 万能放行)
if !utility.SuperAdmin(ctx) {
user, _ := utility.GetCurrentUser(ctx)
codes := core.PermissionCodes(req.Action)
if len(codes) > 0 {
ok := false
for _, c := range codes {
for _, pc := range user.PermCodes { if c == pc { ok = true; break } }
if ok { break }
}
if !ok {
r.Response.WriteJson(base.Fail(99990406, "您没有资源wf:"+req.Action+"访问权限,请联系管理员!"))
return
}
}
}
// ② 解析 body:UseNumber + 精确转 int64(雪花 id >2^53 精度)
body := parseBody(r.GetBody())
body["operator"] = utility.GetCurrentUserId(ctx)
// ③ 门面转发
result := core.GetFacade().Flow(req.Action, body)
// ④ listByType 结构转换
if req.Action == "processDesign/listByType" && result["code"] == 0 { /* Map → [{type,title,items}] */ }
// ⑤ id 兜底:facade stringifyIDs 不处理结构体——统一 marshal→unmarshal 再 idToString
if data, err := toGeneric(result["data"]); err == nil {
result["data"] = idToString(data)
}
r.Response.WriteJson(result)
return
}- 坑(必踩):
json.Unmarshal默认把数字解析为 float64,雪花 id(>2^53)直接丢精度 (...4290→...4288)——必须dec.UseNumber()+ 递归精确转 int64(parseBody)- 引擎
stringifyIDs只处理 map/slice,结构体切片原样返回——集成方 marshal→unmarshal 成通用 map 后再idToString兜底
- facade 装配(
wf_factory.go):facade.New(engine, repo, extRepo, opts...), 注入框架用户/组织 SPI
3.5 Rust(mldong-salvo,Salvo 0.76 + sqlx)
rust
// controller/wf_controller.rs —— 唯一 /wf/** 入口(40+ action 全覆盖,与 boot4 同款单 controller 转发)
#[handler]
pub async fn flow(req: &mut Request, depot: &mut Depot, res: &mut Response) {
let action = req.param::<String>("action").unwrap_or_default();
let user = get_login_user(depot).unwrap(); // 框架登录上下文
// ① 权限码动态校验:wf:{action 的 / 换 :}(OR 规则表 / NO_PERM_ACTIONS 登录即可;superAdmin 万能放行)
if let Err(resp) = check_wf_permission(&user, &action).await { res.render(Json(resp)); return; }
// ② 解析 body + 注入 operator(门面约定 args.operator)
let mut args: HashMap<String, Json> = req.parse_json::<Json>().await /* → HashMap */;
args.insert("operator".into(), json!(user.user_id));
// ③ 门面转发:flow 内部完成 C1/C14 id 字符串化、C4 camelCase、C5 时间格式、
// C6 五键分页、C7 错误码 99999999——controller 层零业务
let mut result = get_facade().flow(&action, &args).await;
// ④ listByType 结构转换(boot3 前端约定,Map → [{type,title,items}])
if action == "processDesign/listByType" { adapt_list_by_type(&mut result); }
res.render(Json(result));
}- facade 装配(
core/wf_factory.rs):ServiceContextbuilder (with_repository/with_user_provider/with_org_user_provider/with_user_search_provider/with_id_generator…全部Arc<dyn Trait>)→JeeflowFacade单例 - 坑(必踩):Rust
serde_json默认u64序列化不丢雪花精度(数值直接进 JSON 数字, JS 侧仍需按 C1 收字符串——facade 出口已 stringify);Docker 需seccomp:unconfined(老 runc 拦新 glibc 系统调用) - 完整集成文档见 Rust 指南 · mldong-salvo 集成
4. 引擎初始化(SPI 注册清单)
| SPI | 必须 | 六框架注入 |
|---|---|---|
| 仓储(IProcessRepository 等) | 是 | Java: starter 自动;Python/Node/Go: 构造参数传 JdbcRepository;Rust: ServiceContext::with_repository(SqlxRepository) |
| 扩展仓储(设计/委托) | 设计功能需要 | 同上(JdbcProcessExtRepository) |
| 用户/组织 Provider | 强烈建议 | Java: UserProvider bean;Python: MldongUserProvider;Node: setOrgProvider;Go: 构造参数;Rust: with_user_provider / with_org_user_provider |
| 用户搜索(候选) | 候选人搜索需要 | Java: setUserSearchProvider;Node: setUserSearch;Rust: with_user_search_provider |
| 雪花 ID 生成器 | 跨语言共用定义时需要 | Python: FrameworkSnowflakeIDGen(与 MyBatis-Plus 同构);Rust: with_id_generator(SalvoIdGenerator 包框架雪花,EPOCH 对齐 1288834974657);其余语言引擎内置 |
| 表达式求值器 | 决策节点需要 | SimpleExprEvaluator(各框架同款实现) |
| persist(业务落表) | 业务数据持久化需要 | 按需引入,注册 PostInterceptor/MetaTableReader |
5. 共性要点(六框架一致)
5.1 权限码
- 引擎内置默认映射:
wf:{action.replace('/', ':')}(processDefine/page→wf:processDefine:page) - 部分 action OR 语义(
processDefine/detail→ detail 或 listByType);只读轻量 action 放行 - 校验一律在框架层执行:superAdmin 万能放行 + 权限码命中任一
- 六框架实现参考:
WfFlowController.checkPermission(Java)/permission_codes(Python)/permissionCodes(Node)/core.PermissionCodes(Go)/wf_permission::permission_codes(Rust,OR 规则表 + NO_PERM_ACTIONS);Laravel 同 Java 在WfFlowController内校验(见 §3 开头说明)
5.2 operator 注入
门面不感知登录态:controller 层强制 body.operator = 当前登录用户 id(覆盖用户传参)。 涉及"我的"语义的 action(待办/已办/我发起/抄送)依赖此字段过滤。
5.3 id 字符串契约(跨语言统一)
| 语言 | 处理位置 |
|---|---|
| Java | 集成层 ToStringSerializer(全局序列化) |
| Node | 引擎全链路 string,无需额外处理 |
| Python | 引擎出口 stringifyIDs |
| Go | 引擎出口 stringifyIDs + 集成层 idToString 兜底(结构体) |
前端一律把 id 当字符串处理(雪花 id 超 JS 2^53 安全整数)。
5.4 listByType 返回结构转换
引擎 processDesign/listByType 返回 Map<type, items>;boot3 前端(apply-list.vue) 约定数组 [{type, title, items}]。四个参考实现都在 controller 层转换——新框架接入时 照抄即可(不做也兼容:前端按统一接口文档取值)。
6. 非 mldong 框架接入(如若依)
不需要任何框架耦合,只需两步:
- 实现一个转发 controller:
POST /wf/{action}→ 组装 body(注入 operator)→ 调用门面flow(action, body)→ 原样返回{code, msg, data}- 若依(RuoYi-Vue)示例思路:
@PostMapping("/wf/**")+ 从SecurityUtils.getLoginUser()注入operator;若依的AjaxResult与门面结构不同——直接透传门面返回,或 包一层{code, msg, data}对齐前端
- 若依(RuoYi-Vue)示例思路:
- 登录鉴权:用框架已有的登录拦截器;权限码校验可选 (不校验则登录用户可访问全部 action;建议按 §5.1 接上)
前端接入:同栈 Vue3 用 指南 13 · jeeflow-ui(组件包 / iframe); 付费完整后台用 vben5-wf。两种前端都只认 /wf/**,不关心后端是什么框架。 只要后端实现了转发,按 规范 06 即可对接。
7. 参考实现
mldong 系列五框架(Java / Python / Node / Go / PHP Laravel)已有内部参考实现,接入时对照本文档 第 3 节代码即可(权限码、operator 注入、id 精度处理、listByType 转换四个差异点 已在 §3.1~3.4 完整给出;Laravel 见 §3 开头说明)。
8. 一键部署
十三套框架集成版已打包为 Docker 镜像(后端 + MySQL + Redis + Nginx 前端),一条命令即可在本机跑起完整业务系统,用于快速体验或二次开发参考。统一账号 superAdmin / 123456,要求 Docker Compose v2(Docker 20.10+)。
- Java · Spring Boot 4(jeeflow-java · boot4j)bash
curl -fsSL https://www.mldong.com/deploy/mldong-boot4-jeeflow/deploy.sh | bash # 访问 http://localhost:21080 - Java · Spring Boot 3(jeeflow-java · boot3j)bash
curl -fsSL https://www.mldong.com/deploy/mldong-boot3-jeeflow/deploy.sh | bash # 访问 http://localhost:22180 - Java · Spring Boot 2(jeeflow-java · boot2j)bash
curl -fsSL https://www.mldong.com/deploy/mldong-boot2-jeeflow/deploy.sh | bash # 访问 http://localhost:22080 - Go · GoFrame(jeeflow-go · goframe)bash
curl -fsSL https://www.mldong.com/deploy/mldong-goframe-jeeflow/deploy.sh | bash # 访问 http://localhost:20080 - Node.js · NestJS(jeeflow-node · nestjsj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-nestjs-jeeflow/deploy.sh | bash # 访问 http://localhost:27080 - Python · FastAPI(jeeflow-python · fastapij)bash
curl -fsSL https://www.mldong.com/deploy/mldong-fastapi-jeeflow/deploy.sh | bash # 访问 http://localhost:23180 - PHP · Laravel(jeeflow-php · laravelj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-laravel-jeeflow/deploy.sh | bash # 访问 http://localhost:18082 - Rust · Salvo(jeeflow-rust · salvoj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-salvo-jeeflow/deploy.sh | bash # 访问 http://localhost:28080 - Python · Flask(jeeflow-python · flaskj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-flask-jeeflow/deploy.sh | bash # 访问 http://localhost:29080 - Python · Django(jeeflow-python · djangoj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-django-jeeflow/deploy.sh | bash # 访问 http://localhost:30080 - Go · Gin(jeeflow-go · ginj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-gin-jeeflow/deploy.sh | bash # 访问 http://localhost:31080 - Go · Hertz(jeeflow-go · hertzj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-hertz-jeeflow/deploy.sh | bash # 访问 http://localhost:32080 - C# · ASP.NET Core(jeeflow-csharp · csharpj)bash
curl -fsSL https://www.mldong.com/deploy/mldong-csharp-jeeflow/deploy.sh | bash # 访问 http://localhost:33080
停止并清理:
docker compose -p <项目名> down -v(项目名即上面括号里的 boot4j / boot3j / …)。首次启动约 2 分钟(等待 MySQL 初始化)。