Skip to content

规范 05 · SPI 接口 ​

接口签名契约。接口为什么这样划界的论证见 设计原理 05 · SPI 设计; 扩展仓储(管理扩展)的设计动机见 设计原理 07 · 管理扩展与门面。

IProcessRepository(必须实现) ​

引擎与存储层的唯一接口。各语言遵循自身命名约定(Java/Node=camelCase,Go=PascalCase),语义一致。

text
┌──────────────────────────────────────────────┐
│         <<interface>>                        │
│        IProcessRepository                    │
├──────────────────────────────────────────────┤
│ + findDefineById(id): ProcessDefine          │
│ + saveDefine(def): void          (v1.0.1)    │
│ + updateDefine(def): void        (v1.0.1)    │
│ + updateDefineState(id, state): void (v1.0.1)│
│ + removeDefine(id): void         (v1.0.1)    │
│ + findInstanceById(id): ProcessInstance      │
│ + saveInstance(inst): void                   │
│ + updateInstance(inst): void                 │
│ + findTaskById(taskId): ProcessTask          │
│ + saveTask(task): void                       │
│ + updateTask(task): void                     │
│ + findDoingTasks(instanceId, taskNames): []  │
│ + findDoneTasks(instanceId, taskNames): []   │
│ + findHistoryTasks(instanceId): []           │
│ + findTaskActors(taskId): []                 │
│ + addTaskActor(taskId, actors): void         │
│ + removeTaskActor(taskId, actors): void      │
│ + createCcInstance(instId, creator, ...ids)  │
│ + updateCcStatus(instId, actorId): void      │
│ + pageCcInstances(query): Page               │
│ # cc dedup (default, issues/141 G2)          │
│ + findCcActorIds(instId): []       (default) │
│ + createCcInstanceIfAbsent(instId, creator,  │
│     ...ids): []                   (default)  │
│ # frontend paging family (jeeflow 2.0+)      │
│ + pageTodoTasks(query): Page[TaskRow]        │
│ + pageDoneTasks(query): Page[TaskRow]        │
│ + pageInstances(query): Page[InstanceRow]    │
│ + pageDefines(query): Page[DefineRow]        │
│ + countTodoTasks(userId): int                │
│ # stats family (issues/103, spec06 stats)    │
│ + queryInstancesForStats(stateIn, timeField, │
│     start, end): []InstanceStatsRow          │
│ + queryTasksForStats(state, start, end): []  │
│ + statsAvgCompletedDurationSeconds(s, e)     │
│ + statsPendingAndOverdueCount(): int[]       │
│ + statsCompletedTaskAggregate(): int[]       │
│ + statsStuckNodeGroup(limit): [{key, count}] │
│ + statsStuckApproverGroup(limit): [{key,n}]  │
│ + statsDefineGroup(start, end, limit)        │
│ + statsCompletedInstanceDurations(s, e): []  │
└──────────────────────────────────────────────┘

图谱完整性(2026-10 补收,issues/222):本图谱=参考实现(Java)IProcessRepository 的完整签名面,共 36 法=引擎运行时 21(含 cc 写侧判重 default 双法)+前端分页族 6 +统计族 9。此前版本只收 20 法——那是一份不完整的签名面,第三方照旧 20 法图谱实现仓储, 接参考实现引擎/门面即缺方法(接口-实现契约破口,非图谱缺项)。各族的跨语言分布并不同构, 不承诺八栈统一:

  • 统计族 9 法(喂规范 06 §4.2 processInstance/stats/* 视图端点):Java/Go/Python/Node/C# 五栈按此形状实现;PHP/Rust/MoonBit 三栈结构性无统计族,统计面由 getAllInstances/ getAllTasks(Rust/MoonBit 蛇形同名)两法把全表拉回进程内过滤——两套形状都合法。
  • countTodoTasks:Java/Rust/MoonBit/C# 有;Go/Python/Node/PHP 无。
  • findDefineByName:Java/Rust/MoonBit/C# 无;Go/Python/Node 有;PHP 为改名版 findLatestDefineByName。
  • PHP 另砍图谱内 findTaskActors/findDoneTasks 两法(参与者读侧走聚合根 ProcessTask::getActorIds()),且 saveDefine 改名 addDefine。
  • cc 写侧判重双法八栈都有,语言形态五派(Java default/Rust provided method/ Python 无 @abstractmethod 的具体法/Node 可选成员+自由函数兜底/Go·PHP·MoonBit 必选成员),语义同:default 档返回空集=不判重(issues/141 G2 立法)。

下方「各语言实现对照」表只覆盖图谱既有 20 法(Java/Go/Node.js/Python 四列); 分页/统计族与 cc 判重双法的跨语言签名不在表内逐栈列表,以本图谱(参考实现) +各语言仓源码为准。

updateInstance 级联(v1.0.1,集成反馈):updateInstance(inst) 级联持久化聚合根内 任务状态变更——撤回/挂起/废弃等聚合命令改完任务状态后,随 updateInstance 同一连接落库, 集成方无需再逐个 updateTask。前提:聚合根内任务副本反映最新状态 (引擎在完成任务后会同步聚合内任务副本)。

各语言实现对照:

操作JavaGoNode.jsPython
查流程定义findDefineById(Long)FindDefineByID(int64)findDefineById(id: number)find_define_by_id(id)
保存定义saveDefine(ProcessDefine)SaveDefine(*ProcessDefine)saveDefine(def)save_define(def)
更新定义updateDefine(ProcessDefine)UpdateDefine(*ProcessDefine)updateDefine(def)update_define(def)
定义启停updateDefineState(Long, int)UpdateDefineState(int64, int)updateDefineState(id, state)update_define_state(id, state)
删除定义removeDefine(Long)RemoveDefine(int64)removeDefine(id)remove_define(id)
查实例findInstanceById(Long)FindInstanceByID(int64)findInstanceById(id: number)find_instance_by_id(id)
保存实例saveInstance(ProcessInstance)SaveInstance(*ProcessInstance)saveInstance(inst)save_instance(inst)
更新实例updateInstance(ProcessInstance)UpdateInstance(*ProcessInstance)updateInstance(inst)update_instance(inst)
查任务findTaskById(Long)FindTaskByID(int64)findTaskById(id: number)find_task_by_id(id)
保存任务saveTask(ProcessTask)SaveTask(*ProcessTask)saveTask(task)save_task(task)
更新任务updateTask(ProcessTask)UpdateTask(*ProcessTask)updateTask(task)update_task(task)
进行中任务findDoingTasks(Long, String[])FindDoingTasks(int64, []string)findDoingTasks(id, taskNames?)find_doing_tasks(id, task_names)
已完成任务findDoneTasks(Long, String[])FindDoneTasks(int64, []string)findDoneTasks(id, taskNames?)find_done_tasks(id, task_names)
历史任务findHistoryTasks(Long)FindHistoryTasks(int64)findHistoryTasks(id)find_history_tasks(id)
查参与者findTaskActors(Long)FindTaskActors(int64)findTaskActors(id)find_task_actors(id)
添加参与者addTaskActor(Long, List)AddTaskActor(int64, []string)addTaskActor(id, actors)add_task_actor(id, actors)
移除参与者removeTaskActor(Long, List)RemoveTaskActor(int64, []string)removeTaskActor(id, actors)remove_task_actor(id, actors)
抄送创建createCcInstance(Long, String, String...)CreateCcInstance(int64, string, ...string)createCcInstance(id, creator, ...ids)create_cc_instance(id, creator, *ids)
抄送状态updateCcStatus(Long, String)UpdateCcStatus(int64, string)updateCcStatus(id, actorId)update_cc_status(id, actor_id)
抄送分页(v1.3.0)pageCcInstances(PageQuery)PageCcInstances(ctx, PageQuery, string)pageCcInstances(pageNum, pageSize, actorId, conditions?)page_cc_instances(page_num, page_size, actor_id, conditions=None)

addTaskActor 语义(v1.3.0 明确):追加——查已有参与者、去重后仅插入新增, 不清空原参与者(对齐 boot2/boot3;加签/委托后原处理人保留可办)。 JDBC 参考实现 v1.2.0 曾为覆盖语义(先删后插),v1.3.0 修复; 全量重置场景用 removeTaskActor + addTaskActor 组合。 转办(摘原人)走门面 processTask/transfer,内部即 removeTaskActor(fromActor) + addTaskActor(toActor)(见 06-facade);集成方不要在薄壳里自拼摘人 SQL 绕过引擎。 ⚠️ removeTaskActor 是 SPI 必选方法(八语言 SPI 面都要有,含内存仓 + SQL 仓两实现)—— PHP 曾整条缺失,属 SPI 面不合规,已随 issues/115 补齐。 门面侧自 1.8.37 起也有同名 action processTask/removeTaskActor(只摘不加、不留痕, 归属/留人下限见 门面 06)——集成方需要"摘人"时走门面这一条,仍然不要自拼摘人 SQL。

pageCcInstances 的形参个数不承诺八栈统一(2026-10-15 补收,issues/237):本图谱按参考实现记 单查询对象形,而对照表四格是各栈 SPI 声明面的现读形状——Java/PHP/C#/Rust/MoonBit 五栈=单 PageQuery 形,Go/Python/Node.js 三栈=多参形(归属值另给一枚专用形参,py/node 还各带一枚可选 conditions)。归属值的证据通道也因此逐栈不同:Java/PHP/C# 由门面把 cc.actor_id 挂进 conditions; Rust 判据三通道并查(operator/打在 cc.actor_id 上的 m_ 过滤/conditions);MoonBit 取 PageQuery.operator;Go/Python/Node.js 用专用形参(与 conditions 两形并立,任一有效即算给了归属)。 要承诺的从来不是形参个数,而是归属义务本身:cc.actor_id 上没有有效归属证据(缺键/空串/全空白) ⇒ 返回空页,且 SQL 仓与内存仓必须同判据同答案——条文见 门面 06 §2.5「抄送分页同一条尺子」 (issues/141 G1)。第三方换宿主实现仓储时按所在栈的签名面对齐,不必向 Java 的形参个数看齐;把三栈 改成单查询对象形要动 9 处声明+3 处门面消费点,代价与收益不成立,故本条明文记为分布差而非缺陷。

IUserProvider ​

text
┌────────────────────────┐
│    <<interface>>       │
│     IUserProvider      │
├────────────────────────┤
│ + getUser(userId): UserInfo │
└────────────────────────┘

UserInfo
├── userId: String
├── realName: String
├── deptId: String
├── deptName: String
├── postId: String
└── postName: String

IOrgUserProvider(v1.6.0) ​

组织维度取人接口——「部门领导/分管领导/按角色取人」是通用业务语义, 内置组织 handler(见 SPEC 流程定义 assignmentHandler)的数据源; 业务方只实现数据接口,不写 handler。

text
┌──────────────────────────────┐
│      <<interface>>           │
│       IOrgUserProvider       │
├──────────────────────────────┤
│ + findDeptLeaders(deptId): List<String>     │  部门领导
│ + findDeptMainLeaders(deptId): List<String> │  部门分管领导
│ + findByRole(roleCode): List<String>        │  按角色取人
└──────────────────────────────┘
方法JavaGoNode.jsPython
部门领导findDeptLeaders(String)FindDeptLeaders(string)findDeptLeaders(deptId)find_dept_leaders(dept_id)
部门分管领导findDeptMainLeaders(String)FindDeptMainLeaders(string)findDeptMainLeaders(deptId)find_dept_main_leaders(dept_id)
按角色取人findByRole(String)FindByRole(string)findByRole(roleCode)find_by_role(role_code)

行为约定:

  1. 返回 null/空列表 = 该组织维度无匹配用户 → 内置 handler 返回空参与者 → 任务不创建
  2. 消费方:内置组织 AssignmentHandler(任务参与者)+ candidateGroups 候选解析 (candidatePage 可选名单,节点属性逗号分隔角色标识)——同一 SPI 双场景复用
  3. 组织数据只通过本接口获取,禁止在 handler 里直连组织服务(隔离可测试性)
  4. 未注入本 SPI 时组织维度 handler 失效,assignee/表单字段/发起人语义不受影响

可选 SPI ​

宿主 I/O 类 SPI 的语言形状豁免 ​

立案:issues/146(MoonBit 供数 SPI 是同步闭包 ⇒ 集成方只能启动期快照,之后新人/调岗/换领导引擎看不见)。

规范钉的是"宿主供数"这个行为,不是闭包/接口的签名形状。 一条 SPI 是否允许异步形状,判据只有一个: 这一档 SPI 的合规实现要不要做宿主 I/O。要做的(IUserProvider / IOrgUserProvider / IUserSearchProvider / 业务数据读取 / 元数据供给 / 事件监听器 / 拦截器 / 取人与决策 handler), 在"查库只有异步一条路"的语言里 SPI 就必须是异步形状——同步形状等于把集成方逼成启动期全量预载, 而那是行为缺陷(数据不新鲜 + 规模上限),不是实现风格差异。 纯计算档(JSON 序列化、表达式求值、id 生成、权限码推导)不在豁免面内,保持同步。⚠ 例外登记:Python/Node 两栈未拆「宿主 I/O/纯计算」两档、其求值器随栈为 async(两栈 id 生成仍同步)——无 I/O 行为差异;其余六栈该档全同步。

各栈现读形状:

语言形状依据
Java / Go / PHP / Rust同步接口/闭包JVM 线程模型、各栈同步驱动可用;注入的是宿主现场查库的 bean/句柄
Python / Node / C#宿主 I/O 类 async(Python/Node 宿主 I/O 面全 async;C# 仅宿主 I/O 档 async、纯计算档保持同步)Python/Node 的查库驱动只有异步一路(asyncio / Promise);C# 的 MySqlConnector 只有 async 面(宿主 I/O 三件均 Task<…>);同步形状等于把集成方逼成启动期全量预载(issues/221 现读复核)
MoonBitasync 闭包((async (String) -> UserInfo? raise JeeflowError) 这类)moonmysql/moondb 全 async,同步查库无正确实现路径;引擎方法本身全 async,await 穿透自然。纯内存实现另有 *_sync 便捷注册口(内部经统一异步桥),两通道注册完的注册表类型一致

八语言对齐豁免声明:MoonBit 走独立 0.x 版本线(mooncakes 发布),本轮只对齐行为 (宿主现场供数、运行期新数据免重启即可被流程解析、监听器可写库),不要求签名与其他七栈一致; 其余七栈不因本案改动。行为判据的实弹读数见 jeeflow-moon 的 T1-F 用例 F146 / F146E (真库跑:运行期插入新用户后下一次发起即时解析到该用户;async 监听器把事件行 INSERT 进真库)。

接口方法说明
IJsonProvidertoJson(obj) / fromJson(str)JSON 序列化
IExpressionEvaluatoreval(expr, vars)决策表达式求值
ITransactionTemplateexecute(action)事务管理
IIdGeneratornextId()ID 生成
IUserSearchProvider(v1.2.0)page(query) / findById(userId)candidatePage 用户分页搜索+候选映射单用户查询(消费场景见规范 06 §4.3 candidatePage)
IActionPermissionProvider(v1.8.3)permissionCodes(action)action → 权限码映射元数据,引擎只供映射、校验由集成方完成(默认规则/OR/放行清单见规范 06 §2.6)

语言形状(八栈现读,issues/225):

  • IUserSearchProvider 两派:java/php/rust/csharp = 两方法 SPI 接口(page+findById; csharp 为 PageAsync/FindByIdAsync async 面);go/py/node = 门面函数钩子(go UserSearch+UserSearchByID、 python user_search+user_search_by_id、node UserSearch+UserSearchById——issues/178 起分页+单查两臂齐); moon = 闭包组 UserSearchProviderFns(page+find_by_id,async 形状见上文豁免段)。
  • IActionPermissionProvider 单方法:java/rust/csharp = 接口+内置 DefaultActionPermissionProvider; moon = 闭包槽(未注入回落内置默认 wf:{action /→ :});go/py/node/php 引擎仓无此注入件, action→权限码映射由集成层按规范 06 §2.6 规约实现。

事务约定 ​

⚠ 本节自 2026-10-06 起只登记各栈机制,行为判据(原子面/覆盖面/归属面/副作用面 + 三档门禁)一律见 规范 12 · 事务契约。下面的原则 1 已被该章更正。

原则:

  1. 引擎对事务是"可选参与",不是"不感知"(原写"引擎核心不感知事务",与八栈现状不符—— Java/PHP/C# 的引擎命令入口内就包了一层 runInTx)。合规形态有两种:引擎内包(未注册模板即裸跑)、 或宿主在外面自己包。判据只看行为,不看包在哪一层。
  2. 事务由调用方持有——业务代码是事务的 owner:开启 → 业务操作 + 引擎调用 → commit / rollback (引擎内包那一种,本质也是宿主注册进来的一次开启)。
  3. 事务是连接级的——同事务内所有仓储方法必须使用同一数据库连接,扩展仓储不豁免。
  4. 接口契约不变——仓储接口签名不携带事务参数,事务通过各语言的上下文绑定机制传递。 ⚠ 这里"上下文"必须是调用作用域(thread/task/request/调用方持有的实例), 不得是进程级可变句柄槽(教训见 issues/144)。

上下文绑定机制(八语言,允许各语言自定机制):

语言机制说明
JavaThreadLocal(Spring 事务管理器)@Transactional 方法内仓储自动获得事务连接;非 Spring 用户用 ITransactionTemplate。⚠ 走 Spring 时必须经 DataSourceUtils(或 TransactionAwareDataSourceProxy)取连接,裸 dataSource.getConnection() 拿不到事务连接
Gocontext.Context引擎方法第一个参数为 ctx(Go 生态惯例),Tx 存入 ctx,仓储从 ctx 取出
Pythoncontextvars.ContextVarawait repo.with_tx(fn) 内部把连接写入 ContextVar,引擎/业务 DAO 自动取用
NodeAsyncLocalStorage(node:async_hooks)await repo.withTx(async () => {...}) 内部 ALS.run() 绑定连接。⚠ 主仓储与扩展仓储必须共用同一枚 ALS,各绑一枚=扩展仓储的写逃逸出事务
PHPTransactionTemplateInterface(SPI)注入事务模板,PDO 仓储在同一连接内执行(对齐 Java 非 Spring 的 ITransactionTemplate);未注册时引擎内联裸跑(inTx 查不到注册件即直接执行闭包,等价 no-op;NoOpTransactionTemplate 件存在但引擎零引用,仅测试自装用,issues/223)
RustTransactionTemplate(SPI,execute_in_tx 闭包)with_transaction_template(...) 注入;引擎入口与门面动作级经 ctx.transaction_template 包裹(有则用、无则裸跑);事务连接由实现件经线程本地槽在动作执行期路由(仓储与集成写腿同源取用;同线程嵌套并入外层不重开)
C#AsyncLocal<MySqlConnection?>/AsyncLocal<MySqlTransaction?>挂在仓储实例上,但 AsyncLocal 是 task 作用域(不是进程级槽,符合规范 12 归属面);ITransactionTemplate 形态与 Java 非 Spring 同构
MoonBit随仓储实例携带(MysqlRepository.tx : Ref[...] + new_request() 派生请求级实例)本栈无 task-local,故不用全局槽:拷贝共享槽 ⇒ 同实例族的仓储调用自动同事务;派生切开 ⇒ 请求之间互不可见。execute_in_tx(op) 把绑定后的实例交给回调。⚠ execute_in_tx 是 MysqlRepository 具体类型上的扩展函数、不在 ProcessRepository trait(SPI 契约)上——内存仓与 MySQL 仓在这一点上不是同一个接口面,可选 SPI 表里的 ITransactionTemplate 对本栈无对应件;「内存仓储为 no-op、切换真实仓储时业务代码零改动」在本栈不成立(内存仓无此方法)

⚠ 上表只登记机制,不构成合规声明。各栈现状是否真过 规范 12 · 事务契约 §12.4 的三档判据, 以实跑读数为唯一依据(普查账目见 issues/145)。

业务层形态:

java
// Java(Spring)
@Transactional
public void approve(Long taskId, String operator) {
    orderService.updateStatus(orderId, "APPROVED");   // 业务表(同事务连接)
    engine.executeProcessTask(taskId, operator, args); // 仓储同事务
}
go
// Go(ctx 携带 Tx)
ctx := context.WithValue(r.Context(), TxKey, tx)
err := repo.WithTx(ctx, func(ctx context.Context) error {
    orderSvc.UpdateStatus(ctx, orderID, "APPROVED")
    _, e := engine.ExecuteProcessTask(ctx, taskID, op, args)
    return e
})
python
# Python(with_tx 回调包裹)
async def do_biz():
    await order_svc.update_status(order_id, "APPROVED")
    await engine.execute_process_task(task_id, op, args)

await repo.with_tx(do_biz)
ts
// Node(withTx 包装)
await repo.withTx(async () => {
  await orderSvc.updateStatus(orderId, 'APPROVED')
  await engine.executeProcessTask(taskId, op, args)
})

仓储实现要求:

  1. 仓储方法优先使用上下文绑定的连接(同一事务内的多次调用走同一连接);无绑定连接时回退为独立连接(单操作场景)
  2. 事务包装(WithTx / with_tx / withTx)由仓储提供——它持有连接池,事务时 checkout 一个连接并绑定到上下文,业务回调结束后 commit/rollback 并释放
  3. 业务 DAO 必须与流程仓储同源取连接(同样走上下文绑定),否则业务操作与流程操作不在同一事务
  4. 内存仓储为 no-op——事务包装直接执行回调(内存操作天然原子),接口形态与其他语言一致,切换真实仓储时业务代码零改动(⚠ MoonBit 例外:本栈事务通道是仓储具体件、不在 SPI 契约面上,内存仓无此方法——见上方机制表)

约束:

  • 跨数据库/分布式事务(2PC/Saga)不在本规范范围——工作流引擎按单库事务设计,跨服务一致性由业务层自管
  • 引擎对事务是"可选参与":引擎在命令入口内包事务钩子(有则用、无则裸跑)与宿主在外面包,两种形态都合规——判据只看行为,一律见 规范 12 · 事务契约 §12.1 原则 3

扩展仓储(IProcessExtRepository,可选) ​

引擎核心不依赖本节内容。设计稿 / 历史 / 委托是"周边管理能力",通过扩展仓储 SPI 提供统一读写,集成方可选接入;SurrogateInterceptor(委托自动生效)是引擎内置、默认注册的 拦截器,但扩展仓储缺席时必须静默跳过——见下文同名小节与 06 §4.5 运行期语义。

text
┌──────────────────────────────────────────────┐
│      <<interface>>   (可选 SPI)              │
│        IProcessExtRepository                 │
├──────────────────────────────────────────────┤
│ # 流程设计(wf_process_design)              │
│ + findDesignById(id): ProcessDesign          │
│ + saveDesign(design): void                   │
│ + updateDesign(design): void                 │
│ + removeDesign(id): void                     │
│ + pageDesigns(query): Page<DesignRow>        │
│ # 设计历史(wf_process_design_his)          │
│ + saveDesignHis(his): void                   │
│ + listDesignHis(designId): []DesignHis       │
│ # 委托代理(wf_process_surrogate)           │
│ + findSurrogateById(id): ProcessSurrogate    │
│ + saveSurrogate(s): void                     │
│ + updateSurrogate(s): void                   │
│ + removeSurrogate(id): void                  │
│ + pageSurrogates(query): Page<SurrogateRow>  │
│ + getSurrogate(operator, processName, time)  │
│     : ProcessSurrogate|null   # 生效中的委托 │
└──────────────────────────────────────────────┘

⚠️ 接口-实现契约破口(2026-10 登记,issues/222):PHP 扩展仓 SPI ProcessExtRepositoryInterface 实有 16 法=上图 13 法+3 枚本卷未收法 (findLatestDesignHis/updateDesignDeployed/listDesignsByType)。这 3 枚不是冗余件—— PHP 门面设计族(designDetail/designSave/designUpdateDefine/designDeploy/ designRedeploy/designListByType 六个动作)对它们有多处硬依赖调用,第三方照上图 13 法 实现 PHP 栈的扩展仓储=门面设计族动作运行期 fatal error。其余七栈扩展仓均 13 法与上图一致 (Rust 逐字同形核实,MoonBit/C# 系计数核对)。这 3 枚的终局去向(立法进 SPI 契约 vs 改门面不依赖)归 owner 拍板,本节先如实登记破口——在 PHP 栈自实现扩展仓储的集成方 须连带补齐这 3 法。

  • 分页查询参数/返回约定与 IProcessRepository 的 pageDefines 等一致(表别名 t) 但 pageSurrogates 的 t.operator 是归属列,不适用「空值当没填」那条通用放行:门面注入归一后的 operator,仓储侧归属列空值档返回空页,判据见 06 §4.5「processSurrogate/page 归属不变式」(issues/152 ②)
  • getSurrogate 生效规则:enabled=1 且 start_time <= time <= end_time(时间窗为空表示不限), 优先匹配 process_name 精确命中;process_name 为空(全流程委托)作为兜底; 且 surrogate <> operator(自己委托给自己不生效) ⚠️ 上述三档方向与内置 mldong-wf 版相反(那边全流程优先、先按判据过滤再取最新、无自委托过滤), 迁移必读 06 §4.5「与内置版(mldong-wf)的三条方向差异」(issues/152 ①,owner 2026-10-08 拍:引擎不反向迁就)
  • enabled 取值:只有整数 1 生效;不可解析为整数的脏值不得当作启用(各栈脏值默认方向必须一致, 曾出现 PHP 侧 (int)'abc'→0 停用、C# 侧 ToInt 回落 1 启用 的相反默认)。 接 issues/130(v1.8.34 起)立法收严:接受集合=数值 1(严格类型判据——'1'/1.0/true/2 一律停用);驱动或序列化把整数列变成字符串属边界事实,SPI 层负责在交判据前还原成数值 (只还原规范整数串 -?(0|[1-9]\d*),'abc'/'1.0'/' 1' 不还原),不得为此放宽引擎判据。 门面写侧的入参归一(processSurrogate 收到 "1" 存成数值 1)保留不动。
  • 内存仓与 SQL 仓两条路径都要满足上述全部判据:同栈两仓对同一份数据给出不同结论即缺陷
  • 各语言实现位置:Java spi/IProcessExtRepository.java + JdbcProcessExtRepository; Go spi/spi.go(ProcessExtRepository)+ repository/jdbc;Python spi.py + repository/base.py; Node spi.ts + src/jdbc/shared.ts;均配内存实现供测试

SurrogateInterceptor(委托生效,内置实现) ​

任务创建后(PROCESS_TASK_START 前),遍历任务参与者:

  1. 对每个 actor 调用 getSurrogate(actor, processName, now)(processName = 流程模型 name, 模型未带时回落 wf_process_define.name —— 口径与理由见 06 §4.5 条款 1.1)
  2. 命中委托 → 把代理人并入该任务的参与者集合(随任务一起落库),原授权人保留(任一可办)。 不走"事后 addTaskActor 补写"路——见 06 §4.5 运行期语义条款 2 的 ⚠️(taskId 未分配时补写静默无效)
  3. 代理人的委托不级联展开(防环状死循环):getSurrogate 只对建单那一刻的参与者 快照逐个查,步骤 2 新并入的代理人自身不回灌查询——A→B 且 B→C 时 C 不会被拉进 参与者集合,环状委托(A→B→A)天然免疫死循环。各栈实现须满足同一判据: 遍历中新增的 actor 不得触发新的 getSurrogate 调用(显式复制快照/只遍历原集合的值副本 等价形均可,issues/227)

v1.9.0 起为引擎内置、默认开启(issues/116):此前写作"参考实现放在各语言 demo / starter, 默认不注册",结果是八语言里只有 Java 有个没人挂的类,用户配好委托后单子仍只发给本人—— 从内置引擎迁过来的用户等于白丢功能(内置版由 Spring 容器自动装配,零配置即生效)。 现规定:各语言引擎内置该拦截器并默认注册;集成方可显式关闭(不注册 / 注册空实现 / 配置开关)。 未配置 IProcessExtRepository 时必须静默跳过,不得抛错打断建单流程。 委托在 todoList 的合并展示属于集成方视图层职责(参考 boot3:ProcessTaskServiceImpl.todoList)。

参考实现(多数据库适配架构) ​

各语言仓库内置,可直接用于生产;另附内存实现供测试。

每语言一个共享核心(SQL 逻辑唯一维护点,占位符统一 ?)+ 每库一个薄适配器 (连接 + 占位符风格),新增数据库 ≈ 适配器 80 行 + 建表 SQL:

语言共享核心数据库适配驱动安装
Javajeeflow-repository-jdbc → JdbcProcessRepositoryjavax.sql.DataSource(任意 JDBC 驱动)Maven 依赖
Gorepository/jdbc → jdbc.New(db)database/sql 任意驱动(换驱动+DSN)go get 驱动
Pythonjeeflow/repository/base.py → JdbcRepository(adapter)mysql.py(aiomysql)/ postgres.py(asyncpg)pip install jeeflow[mysql] / jeeflow[postgres]
Node.jssrc/jdbc/shared.ts → JdbcRepository(adapter)mysql.ts(mysql2)/ postgres.ts(pg)npm i mysql2 / pg(optionalDependencies)

事务绑定:Java = Spring @Transactional / ITransactionTemplate;Go = WithTx(ctx, fn); Python = with_tx(fn)(ContextVar);Node = withTx(fn)(AsyncLocalStorage)。 建表 SQL 各语言自带(使用者单语言下载即用):tests/schema/schema-<db>.sql (Go 为 repository/jdbc/schema/)。维护约定:编辑源在 jeeflow-java 仓 jeeflow-repository-jdbc/src/test/resources/(schema-h2/mysql/postgres 并排), 改表结构后跑 jeeflow-hub/scripts/sync-schema.sh 分发到各语言。

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