Skip to content

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一个转发 controllerPOST /wf/{action} → 调用门面 flow(action, body),返回结构原样透传
3框架层登录 + 权限码校验登录用框架已有的全局拦截器;权限码由引擎 SPI 提供元数据(默认 wf:{action}),框架执行校验(superAdmin 万能放行)
4注入 operator + 适配细节body 注入当前登录用户 id;雪花 id 精度处理(见 §5)

2. 引入引擎依赖

语言坐标引擎核心
Javacom.mldong.jeeflow:jeeflow-core(starter: jeeflow-spring-bootX-starterJeeflowFacade / JeeflowEngine
Pythonjeeflow(PyPI)JeeflowFacade / EngineImpl
Nodejeeflow(npm)JeeflowFacade
Gogithub.com/mldong/jeeflow(go mod)facade.New
PHPmldong/jeeflow-php(Packagist)JeeflowFacade / JeeflowEngine
Rustjeeflow-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 连接池)+ JdbcProcessExtRepository
    • EngineImpl + 内置 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/listByTypeprocessDesign,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):ServiceContext builder (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/pagewf: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 框架接入(如若依)

不需要任何框架耦合,只需两步:

  1. 实现一个转发 controllerPOST /wf/{action} → 组装 body(注入 operator)→ 调用门面 flow(action, body) → 原样返回 {code, msg, data}
    • 若依(RuoYi-Vue)示例思路:@PostMapping("/wf/**") + 从 SecurityUtils.getLoginUser() 注入 operator;若依的 AjaxResult 与门面结构不同——直接透传门面返回,或 包一层 {code, msg, data} 对齐前端
  2. 登录鉴权:用框架已有的登录拦截器;权限码校验可选 (不校验则登录用户可访问全部 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 初始化)。

jeeflow · 轻量级多语言工作流引擎