Appearance
规范 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。前提:聚合根内任务副本反映最新状态 (引擎在完成任务后会同步聚合内任务副本)。
各语言实现对照:
| 操作 | Java | Go | Node.js | Python |
|---|---|---|---|---|
| 查流程定义 | 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 起也有同名 actionprocessTask/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: StringIOrgUserProvider(v1.6.0)
组织维度取人接口——「部门领导/分管领导/按角色取人」是通用业务语义, 内置组织 handler(见 SPEC 流程定义 assignmentHandler)的数据源; 业务方只实现数据接口,不写 handler。
text
┌──────────────────────────────┐
│ <<interface>> │
│ IOrgUserProvider │
├──────────────────────────────┤
│ + findDeptLeaders(deptId): List<String> │ 部门领导
│ + findDeptMainLeaders(deptId): List<String> │ 部门分管领导
│ + findByRole(roleCode): List<String> │ 按角色取人
└──────────────────────────────┘| 方法 | Java | Go | Node.js | Python |
|---|---|---|---|---|
| 部门领导 | 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) |
行为约定:
- 返回
null/空列表 = 该组织维度无匹配用户 → 内置 handler 返回空参与者 → 任务不创建 - 消费方:内置组织 AssignmentHandler(任务参与者)+
candidateGroups候选解析 (candidatePage 可选名单,节点属性逗号分隔角色标识)——同一 SPI 双场景复用 - 组织数据只通过本接口获取,禁止在 handler 里直连组织服务(隔离可测试性)
- 未注入本 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 现读复核) |
| MoonBit | async 闭包((async (String) -> UserInfo? raise JeeflowError) 这类) | moonmysql/moondb 全 async,同步查库无正确实现路径;引擎方法本身全 async,await 穿透自然。纯内存实现另有 *_sync 便捷注册口(内部经统一异步桥),两通道注册完的注册表类型一致 |
八语言对齐豁免声明:MoonBit 走独立 0.x 版本线(mooncakes 发布),本轮只对齐行为 (宿主现场供数、运行期新数据免重启即可被流程解析、监听器可写库),不要求签名与其他七栈一致; 其余七栈不因本案改动。行为判据的实弹读数见 jeeflow-moon 的 T1-F 用例 F146 / F146E (真库跑:运行期插入新用户后下一次发起即时解析到该用户;async 监听器把事件行 INSERT 进真库)。
| 接口 | 方法 | 说明 |
|---|---|---|
IJsonProvider | toJson(obj) / fromJson(str) | JSON 序列化 |
IExpressionEvaluator | eval(expr, vars) | 决策表达式求值 |
ITransactionTemplate | execute(action) | 事务管理 |
IIdGenerator | nextId() | 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/FindByIdAsyncasync 面);go/py/node = 门面函数钩子(goUserSearch+UserSearchByID、 pythonuser_search+user_search_by_id、nodeUserSearch+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 已被该章更正。
原则:
- 引擎对事务是"可选参与",不是"不感知"(原写"引擎核心不感知事务",与八栈现状不符—— Java/PHP/C# 的引擎命令入口内就包了一层
runInTx)。合规形态有两种:引擎内包(未注册模板即裸跑)、 或宿主在外面自己包。判据只看行为,不看包在哪一层。 - 事务由调用方持有——业务代码是事务的 owner:开启 → 业务操作 + 引擎调用 → commit / rollback (引擎内包那一种,本质也是宿主注册进来的一次开启)。
- 事务是连接级的——同事务内所有仓储方法必须使用同一数据库连接,扩展仓储不豁免。
- 接口契约不变——仓储接口签名不携带事务参数,事务通过各语言的上下文绑定机制传递。 ⚠ 这里"上下文"必须是调用作用域(thread/task/request/调用方持有的实例), 不得是进程级可变句柄槽(教训见 issues/144)。
上下文绑定机制(八语言,允许各语言自定机制):
| 语言 | 机制 | 说明 |
|---|---|---|
| Java | ThreadLocal(Spring 事务管理器) | @Transactional 方法内仓储自动获得事务连接;非 Spring 用户用 ITransactionTemplate。⚠ 走 Spring 时必须经 DataSourceUtils(或 TransactionAwareDataSourceProxy)取连接,裸 dataSource.getConnection() 拿不到事务连接 |
| Go | context.Context | 引擎方法第一个参数为 ctx(Go 生态惯例),Tx 存入 ctx,仓储从 ctx 取出 |
| Python | contextvars.ContextVar | await repo.with_tx(fn) 内部把连接写入 ContextVar,引擎/业务 DAO 自动取用 |
| Node | AsyncLocalStorage(node:async_hooks) | await repo.withTx(async () => {...}) 内部 ALS.run() 绑定连接。⚠ 主仓储与扩展仓储必须共用同一枚 ALS,各绑一枚=扩展仓储的写逃逸出事务 |
| PHP | TransactionTemplateInterface(SPI) | 注入事务模板,PDO 仓储在同一连接内执行(对齐 Java 非 Spring 的 ITransactionTemplate);未注册时引擎内联裸跑(inTx 查不到注册件即直接执行闭包,等价 no-op;NoOpTransactionTemplate 件存在但引擎零引用,仅测试自装用,issues/223) |
| Rust | TransactionTemplate(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)
})仓储实现要求:
- 仓储方法优先使用上下文绑定的连接(同一事务内的多次调用走同一连接);无绑定连接时回退为独立连接(单操作场景)
- 事务包装(
WithTx/with_tx/withTx)由仓储提供——它持有连接池,事务时 checkout 一个连接并绑定到上下文,业务回调结束后 commit/rollback 并释放 - 业务 DAO 必须与流程仓储同源取连接(同样走上下文绑定),否则业务操作与流程操作不在同一事务
- 内存仓储为 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; Gospi/spi.go(ProcessExtRepository)+repository/jdbc;Pythonspi.py+repository/base.py; Nodespi.ts+src/jdbc/shared.ts;均配内存实现供测试
SurrogateInterceptor(委托生效,内置实现)
任务创建后(PROCESS_TASK_START 前),遍历任务参与者:
- 对每个 actor 调用
getSurrogate(actor, processName, now)(processName= 流程模型 name, 模型未带时回落wf_process_define.name—— 口径与理由见 06 §4.5 条款 1.1) - 命中委托 → 把代理人并入该任务的参与者集合(随任务一起落库),原授权人保留(任一可办)。 不走"事后
addTaskActor补写"路——见 06 §4.5 运行期语义条款 2 的 ⚠️(taskId 未分配时补写静默无效) - 代理人的委托不级联展开(防环状死循环):
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:
| 语言 | 共享核心 | 数据库适配 | 驱动安装 |
|---|---|---|---|
| Java | jeeflow-repository-jdbc → JdbcProcessRepository | javax.sql.DataSource(任意 JDBC 驱动) | Maven 依赖 |
| Go | repository/jdbc → jdbc.New(db) | database/sql 任意驱动(换驱动+DSN) | go get 驱动 |
| Python | jeeflow/repository/base.py → JdbcRepository(adapter) | mysql.py(aiomysql)/ postgres.py(asyncpg) | pip install jeeflow[mysql] / jeeflow[postgres] |
| Node.js | src/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分发到各语言。