Skip to content

规范 04 · 引擎核心操作 ​

方法语义契约。引擎一次执行旅程的时序与设计动机见 设计原理 03 · 执行引擎。

启动流程 ​

text
startProcessInstanceById(defineId, operator, args) → ProcessInstance

语义:

  1. 加载流程定义,解析为 ProcessModel
  2. 创建 ProcessInstance(state=10)
  3. 保存实例到仓库
  4. 执行 start 节点 → 遍历输出边 → 创建第一个任务(发起申请节点,actor=发起人)
  5. 任务的 actor 根据 assignee / assignmentHandler 解析
  6. 保存任务和参与者记录

startAndExecute 契约(调用方约定,引擎不内置):

text
startAndExecute(defineId, operator, args) → ProcessInstance
  1. 调用 startProcessInstanceById
  2. 获取所有进行中任务,逐个自动完成(submitType=0 APPLY)
  3. 流程由此推进到第一个真正的审批节点

引擎不自动执行第一个任务——这是调用方的职责(mldong 框架模式), 六版 demo 的 /wf/processInstance/startAndExecute 端点均遵循此契约。

完成任务 ​

text
executeProcessTask(taskId, operator, args) → ProcessTask[]

语义:

  1. 加载任务 → 校验权限(actor 包含 operator;flow.admin / flow.auto 恒放行,见下)
  2. 任务状态 10→20,记录操作人和完成时间
  3. 执行当前节点 → 遍历输出边 → 创建下一批任务
  4. 若下一节点为 end → 实例状态 10→20(完成)
  5. 若下一节点为 task → 创建新任务(解析 actor)
  6. 若下一节点为 decision → 评估表达式 → 选择分支
  7. 若下一节点为 fork → 并行创建多条路径的任务
  8. 保存变更

系统代执行(flow.auto / flow.admin) ​

v1.0.1(集成反馈④):对齐 mldong 框架 boot2/boot3 语义。

  • 权限放行:operator = "flow.auto"(自动执行,发起即提交)或 "flow.admin" (超级管理员)时,任务权限校验直接通过,无需在参与者列表内(忽略大小写)。
  • 变量注入跳过:flow.auto / flow.admin 不是真实用户——引擎执行时不调用 UserProvider 注入 u_* 变量,流程变量保持既有值。需要 u_* 反映实际执行人时, 由调用方(如 startAndExecute)在 args 中显式携带。
  • 发起即提交(startAndExecute):启动流程后,调用方获取 doing 任务,以 flow.auto 逐个自动完成(submitType=APPLY),把流程推进到第一个真正的审批节点——apply 节点 参与者为发起人(applicant),由 flow.auto 代执行,语义等价于发起人亲自提交。

参与者解析(assignee) ​

字典面(assignee / assignmentHandler 词条)见 规范 02 §4;本节钉引擎侧解析义务。

  • assignee 逗号分隔多人,逐个解析后落任务参与者快照(建单时落库,非求值期);
  • 特殊值 applicant ⇒ 建单落库时字面代换为流程发起人(实例 operator)——参与者快照、 后续权限校验、doneList 可见性都按解析后的人,不得把字面值 applicant 留进参与者。 基准=java CreateTaskHandler(token.replace("applicant", processInstance.getOperator())); boot2 血统无字面代换、经 assignment handler 等价表达(语义相同,实现面各栈自定)。

驳回 ​

text
executeAndJumpToEnd(taskId, operator, args) → ProcessTask[]

语义:

  1. 校验权限
  2. 任务状态 10→20
  3. 实例状态 10→45(已拒绝)
  4. 其余进行中任务的处置:实现自定(可选超集,不承诺八栈统一)——清理(→99)或留存(保持 10)均合规; 清理档为超集增强(rust/moon 现行),留存档对齐 boot2 血统(java/php/csharp/go/py/node 现行); 调用方不应依赖他支行终态

退回发起人(mldong 框架契约):业务上"驳回后让发起人改单重提"不使用 executeAndJumpToEnd(那是拒绝,实例直接 45),而是 submitType=6(ROLLBACK_TO_OPERATOR) 调 executeAndJumpToFirstTaskNode——第一个任务节点重执行且参与者强制为发起人, 发起人收到新待办,实例保持 10 进行中。六版 demo 的 /wf/processTask/execute 遵循此行为 (submitType 全枚举见 设计原理 06 §3-4)。

退回修改、重提直返(reapplyMode) ​

审批节点可在 properties.reapplyMode 上选择退回发起人后的重提方式:

值语义
NORMAL(缺省)退回发起人后,重提按申请节点的正常出边重新审批
RETURN_TO_SOURCE退回发起人后,重提直接回到本次退回的来源节点建待办

非法值必须报配置错误,不得静默按缺省处理。

submitType=6(来源节点配置 RETURN_TO_SOURCE)

  1. 校验来源任务、操作权限、实例状态与准入条件(申请节点唯一、来源为普通审批任务,会签与 子流程不在本契约内)。
  2. 完成来源任务(10→20,留痕 submitType=6),实例保持 10、业务单据与原实例不变。
  3. 在唯一申请节点创建发起人新待办,parentTaskId = 来源任务;该任务变量写入引擎保留键 _reapplyContext = {mode, sourceTaskId, sourceNode, branch}。
  4. 不创建来源之后节点的任务、不重放来源出边;并行分支中其他分支可继续办理。

submitType=5(申请待办带 _reapplyContext)

  1. 校验当前待办有效、办理人为发起人;来源任务属于同一实例且确为本次退回来源。
  2. 表单写入沿用业务既有校验与字段权限,不额外识别"关键字段"、不限制只能补材料。
  3. 完成申请待办(留痕 submitType=5),在来源节点直接创建新待办,parentTaskId = 本轮申请任务; 不执行申请节点出边、不重算来源前的条件、不执行来源节点出边。
  4. 参与者按来源节点规则重新解析(不锁定原退回人);遗留的 tf_nextNodeOperator 不参与本次建单。
  5. 新来源任务办理通过后才继续后续流程;后续条件节点按当时数据正常求值。

带 _reapplyContext 的申请待办只接受 submitType=5;改传 0/1/4 必须失败,不得绕过。

并行分支

  • 任务变量保留键 _branchCtx 记录分支归属({fork, join, round, root},外层→内层); round 标识分叉轮次、root 标识分支身份,用于区分重复执行与多分支分别退回。
  • 汇合判据:本轮全部分支根下不得有进行中任务,且不得有申请任务携带指向该根、该轮的 _reapplyContext.branch 等待;满足后放行,且只放行一次。
  • 退回后申请人在分叉之外修改时,来源分支仍未完成——不得仅凭"当前没有分支待办"放行。
  • 存量任务没有 _branchCtx 时回落既有按分支节点名扫描的判据。

内部键与并发

  • _branchCtx / _reapplyContext 是引擎内部状态:办理入口必须先剥离客户端传入的同名键; 含标记但结构损坏、来源丢失或关联不符时明确失败,不按无标记处理。
  • 同一实例的办理需串行化(重复提交至多一个成功、汇合不重复放行)。单进程部署可用引擎内 按实例互斥;多进程部署需仓储侧提供跨进程锁。

跳转 ​

text
executeAndJumpTask(taskId, operator, args, targetTaskName) → ProcessTask[]

语义:

  1. 校验权限
  2. 任务状态 10→20
  3. 在已完成的节点中找到 targetTaskName,重新执行其输出边
  4. 废弃当前未完成的任务

退回上一步(submitType=3 / ROLLBACK) ​

text
executeAndJumpTask(taskId, operator, args, targetTaskName = null) → ProcessTask[]

语义(血缘版,八栈统一):

  1. 与办理/跳转共用序言:校验权限 → 当前任务 10→20;
  2. 上一步来源=数据血缘:读当前任务行的 task_parent_id,按它取那条历史任务行(下称"上一步行")。 不按模型入边拓扑推——拓扑版在分支/回环流会回到本实例没走过的节点;
  3. 守卫 canRejected(current, parent):自当前节点的入边递归回溯。命中 parent 即放行; 入边来源是 fork/join/start 时跳过该条入边、不再深入(boot2 是 continue, 不是"穿越"——所以分支任务想退到 fork 之前的节点会被判不通过);其余来源节点递归下去。 subprocess 在 boot2 里被注释掉了,等同普通节点递归处理(各栈照此,不要当白名单)。 不通过 ⇒ 引擎异常 20010008「无法驳回至上一步处理,请确认上一步骤并非fork、join、suprocess以及会签任务」;
  4. 无血缘(task_parent_id 为 NULL 或 0)⇒ 引擎异常 20010007 「上一步任务ID为空,无法驳回至上一步处理」,不得静默不建单 (拓扑版"什么都不做、实例保持 10 却零待办"的行为作废);
  5. 新任务=上一步行复活:节点属性(taskName / displayName / taskType / performType / formKey)取该行节点,状态 10;variable 只带数据类键——保留 f_*(申请内容)、 u_*(发起人快照)、autoGenTitle、isFirstTaskNode,剔除 submitType / taskName / tf_* / csv_* 等控制类残留。剔 tf_* 是刻意为之:非必填字段第一次填了、第二次不填时, 整包克隆会把上次提交值带进新待办,用户会看到"我没提交这个怎么显示了"; ⇒ 新待办的任务级表单从空白开始;
  6. 参与者=上一步行的 operator(当年办上一步那个人);若该行 variable.isFirstTaskNode == true 即首任务节点行,参与者改取该行 variable.u_userId=流程发起人。 委托(见 门面契约 06 §4.5)在建单落库前并入 ⇒ actors = [该人, 代理人…];
  7. expireTime 按被回退掉的那条任务所属节点的到期表达式重算——身份是 boot2 rejectTask 里的 current = model.getNode(currentTask.getTaskName()), 不是复活行所属的上一个节点(那一档在 boot2 里只喂 form 等随行数据); 实例保持 DOING(10);
  8. 新行的 task_parent_id 随行拷贝(="上一步的上一步",与 mldong-boot2 现状一致)。

出口错码与文案:20010007 / 20010008 是引擎层 WfErrEnum 内部码,只用于引擎内部区分这两格,严禁出现在对外 msg 里(不带码数字前缀)。 对外恒 code=99999999 + 上面第 3/4 条引号里的逐字中文文案(全栈以 Java 参考实现为准,见 门面契约 06 §4.3 失败 msg 跨栈统一文案)。异常无码位的栈也不得把码写进 msg 来补码位——两格语义由这两条不同文案自证区分。

建单不变量(适用于全部建任务路径):引擎每次新建任务行必写 task_parent_id (发起 execution 无当前任务时写 0)与 variable.isFirstTaskNode(该行节点是否 start 直接后继)。 该标记属引擎控制键:出口 taskRowToMap 判「任务变量是否为空 ⇒ 回退实例变量」(issues/82-3 契约)时 必须先忽略控制键——否则新建任务的 ext 从此永不再回退,实例变量在待办列表里静默丢失。

测试纪律:落点/参与者判据的夹具必须 ≥3 个任务节点且至少一条含分支或回环—— 两步流里"上一节点"与"首任务节点"同格,断言恒真、缺陷恒绿。

会签 ​

会签在普通任务基础上增加:

  • 并行会签:为每个 actor 创建独立任务,全部完成才驱动下一步
  • 串行会签:一次只创建一个任务,完成后创建下一个 actor 的任务
  • 按比例会签:完成率达到阈值即驱动下一步

串行会签的全量办理人名单写在任务变量上:键名 operatorList_{taskName}(值=字符串数组), 建单时写在首个成员任务的变量里,推进时由上一成员任务抄给下一成员任务;读侧(processTask/detail 的 ext、进度/高亮还原)按同一键从当前 DOING 任务取。参考实现(锚到方法不写行号——行号随迭代漂移): ProcessInstance#createCountersignTasks(串行分支)写、CountersignHandler#createNextCountersignTask 抄、 JeeflowFacade#readCountersignOperatorList 读。串行簿记另两键同批落任务变量: loopCounter_{taskName}(当前位序)、nrOfInstances_{taskName}(总人数)。

为什么必须在任务上:键名带 _{taskName} 后缀 ⇒ 落在实例上攒的是"走过的那几个会签节点各一条链", 一条链走完三个会签节点后实例里同时留着 operatorList_a/b/c,谁都不等于"当前会签名单"; 而串行会签的"一个实例一条链"正是排他会签的互斥前提,读实例那条链会把排他会签的名单一起卷进来。 实例变量侧的会签键只保留并行会签计数(csv_{taskName}_nrOfInstances 等,见 CountersignHandler#buildCountersignVars)。

放行表达式的求值原料(issues/165 立法,2026-10-11 owner 拍 A):countersignCompletionCondition 交给求值器时,原料必须同时含前缀键(csv_{taskName}_nrOfInstances / nrOfActivateInstances / nrOfCompletedInstances,既有定义兼容)与裸名(nrOfInstances / nrOfActivateInstances / nrOfCompletedInstances,文档/设计器/教程给用户的形状,如 #nrOfCompletedInstances>=2)。 只挂前缀键 ⇒ 按文档配的会签在生产求值器上永不按阈值放行(java SpelExpressionEvaluator 的 #变量 精确查表、php WfExpressionEvaluator 的 # 臂剥前缀查表,都查不到裸名;活栈实测 3/3 全办完仍停住)。基准=CountersignHandler.buildCountersignVars;裸名以本节点为准 (同名实例变量在求值上下文里被覆盖)。⚠ 判据必须走生产求值器(活栈或按生产 evaluator 实例化的单测)——测试求值器的 endsWith 后缀桥会把"裸名没进原料"在单测侧遮住(测试绿生产红, java/php 两栈 2026-10-11 同批拆桥)。

前缀键半边的射程(issues/231 附带发现=队列 R89,2026-10-15 owner 拍 B=行为等价解释档,零代码):上面「必须同时含」里硬承诺只有裸名那一半——裸名是文档/设计器/教程交给用户写的形状,八栈原料都必须给得出。前缀键(csv_{taskName}_nrOfInstances 等)=引擎内部命名,只在自家求值通道能命中前缀键的栈顺带提供(csharp DefaultExpressionEvaluator 的 EndsWith 后缀桥、java SpEL 精确查表),不构成跨栈义务:rust engine.rs gate_vars 与 moon engine_ops.mbt 只挂裸名、两栈求值器都精确查表无后缀桥 ⇒ 前缀键表达式在这两栈结构性不可解析,判许可、不打红。现读依据(2026-10-15,逐仓 cd 进去跑):前缀形状在共享流程定义 16 份、mldong-boot2(6 处表达式全裸名)、文档站 60 处样本里零命中,唯一在场处=java 自家求值器级用例 CountersignBareNameI165Test:143(注释自述「引擎内部命名保留兼容」)——字面执法换来的是一档无真实定义样本的形状。⚠ 反噬提醒(写给用户):手抄过引擎内部命名而写成 #csv_… 的定义,在 rust/moon 会求值失败并落入高亮补全腿的「求值失败留痕」档(规范 06 §4.6 义务 2),表达式一律用裸名。

实例变量侧是否允许再镜像一份串行名单:允许,但不作要求(issues/136,owner 2026-09-28 拍「能」)。 名单的权威落点始终是任务变量 operatorList_{taskName}(即上面那段),实例侧另放一份属各栈自由: 现状 jeeflow-go / python / node 三族镜像、java / php / csharp / rust 四族不镜像——这不构成跨栈缺陷、不要求八栈统一, 但读侧(processTask/detail 的 ext、进度/高亮还原)不得改成读实例侧那一份(理由同上:实例侧攒的是"走过的各节点各一条链")。 ⇒ 这一半不进对拍判据:L2-23 只钉任务侧(where == "taskExt" ∧ 键名 operatorList_{taskName}), 实例侧有没有那一份都不判红。

三种模式的引擎实现细节见 设计原理 03 §6。

任务行 expire_time 由谁写(issues/126) ​

引擎在每一处新建任务行的路径上按节点属性 properties.expireTime 计算并写入任务行的 expire_time, 共五处(缺任何一处都会让"只有走过那条路径的任务才有到期时间",逾期统计在常规流上恒 0):

#新建任务行的路径表达式取哪个节点变量源
1普通建单该任务所属节点实例变量(boot2 的 execution.getArgs() 同档)
2串行会签首位成员该会签节点实例变量
3并行会签全员(含未配类型那一档)该会签节点实例变量
4退回上一步 / 跳转新建被回退掉的那条任务所属节点(current)随行拷贝那份变量(boot2 的 hisVariable)
5串行会签推进出的下一位成员该会签节点实例变量

第 5 处最容易漏:这一支通常绕过本栈的建单 helper 直接构造任务行。基准侧 boot2 的串行推进是回调 createCountersignTask(ProcessTaskServiceImpl:485,内含 :524 那处到期写),所以"第二、第三位成员"同样带到期时间。

表达式三档语义(逐字取 Java FlowUtil.processTime,顺序不可变):

  1. args 里存在键名等于表达式原串的项 ⇒ 取该项的值(日期对象 / 毫秒时间戳 / yyyy-MM-dd HH:mm:ss 字符串); 值类型不认识则落穿到第 2、3 档(不提前返回 null)。
  2. 以 s/m/h/d 结尾且前缀是整数 ⇒ 当前时间 + N 秒/分/时/天(d 走日历加天,不按 86400 秒乘)。
  3. 否则把表达式本身按 yyyy-MM-dd HH:mm:ss 解析;失败 ⇒ NULL。

误配档统一"落穿 → NULL",任何栈都不得抛错(issues/137 C · 2026-09-29 owner 拍): 形如 xh(前缀非整数)、2.5h(小数)、3hh(后缀不认识)这类写坏的相对档,按上面的档位天然落穿到第 3 档、 解析失败即 NULL,不得抛异常、不得整条建单失败——理由是"节点属性配错了不该把流程炸掉", 与第 190 行"不允许退化成取当前时间"配套:错配的结果是没有到期时间,不是报错也不是默认逾期。 八栈这条已一致(java 参考实现 6bdf41b 随 1.8.36 改成落穿,其余七栈本就落穿)。

相对档前缀必须是「非负」整数(issues/137 D · 2026-10-01 owner 拍"判非负"): -5h / -5d 这类负数前缀同样算不合法,按上面的落穿路径 ⇒ NULL。理由是放行它会算出一个 过去的时刻 ⇒ 新建的行当场即逾期,比"没配到期时间"更难发现,且与"不许退化成取当前时间"同向。 只裁负不裁加号:各栈整数解析(python [+-]?、node [-+]?\d+、php [+-]?\d{1,18})都收 +, 裁掉加号等于新造一处跨栈分叉。d 档同判(它走日历加天,负数=历日倒退,不是乘 86400 秒)。 判据:各栈 T0 钉"配 -5h ⇒ 该列 NULL",跨栈门禁沿用 L2-27 的尺子反面(不得早于同行 create_time)。

相对档前缀允许两端空白(issues/137 E · 2026-10-01 owner 拍"统一 trim"):各栈在判整数之前 裁掉前缀的两端空白,之后才走"非负整数"那一档判定(137 D)。理由与各栈整数解析的容忍度差异有关: go 在 strconv.Atoi 前显式 TrimSpace、rust .trim()、.NET int.TryParse 与 python int() 默认收前后空白,而 java Integer.parseInt(" 2")、node/php 的校验正则原本不吃空白 ⇒ 同一份流程定义会出现"别家有到期时间、这一家没有"(表达式是设计器手填/JSON 搬运的字符串, 夹一个空格是常态)。

裁的边界只到前缀,三条分界逐栈一致(基准=jeeflow-java bf1f401): ① " 2h"、"2 h"(空格落在前缀区内、末位仍是单位符)⇒ 照样算得出 now+2h; ② "2h "(单位符后面还带空白)⇒ 末位不是 s/m/h/d、认不出单位,按误配落穿到绝对档 ⇒ NULL; ③ " 2.5h" ⇒ trim 之后仍是误配(小数)⇒ 仍落穿。trim 不是把"裁空白"顺手做成"裁容错"。 ⚠️ 变量档(表达式名去 args 里取值)与绝对档的字符串都不做 trim——那是键名/时间串本身, 裁它会改的是另一件事。判负(137 D)在 trim 之后仍然生效:" -5h" ⇒ NULL。 判据:各栈 T0 至少钉上面①②③三档;go/rust 本就合规(现读已 trim),无需改动只补断言。

三处「不承诺统一」(owner 2026-10-02 拍定,裁定原文=「一般不会那么长的时间,不要过渡设计」)—— 都不是遗漏、是既定状态,写下是为了让下一轮普查别把它们当新缺陷再立一遍案:

  1. 超大偏移不裁位宽:本条只裁"非负"。同一句 3000000000h 在 java(Integer.parseInt 溢出)与 node(intPrefix 有 2147483647 上限)判"解析不出"⇒ NULL,在 php(正则收 18 位)/go(Atoi 走 int64) 会算出荒谬年份(php 实测 344265-02-15)。⚠️ php tryInt 旧注释那句"位宽超 int 视同解析不出" 是与代码不符的假账,已按实读更正(jeeflow-php d15f0b2);别"顺手对齐"去加这个上限。
  2. 绝对档的尾部空白:"2026-12-31 10:00:00 " 在 java SimpleDateFormat 忽略尾部文本 ⇒ 照样解出, 在 python/node/php/go/rust 判解析不出 ⇒ 两个答案都算合规。
  3. 上面「两端空白」的字符集:只指 ASCII <= U+0020 那一族,不含全角空格 U+3000。 实测 " 2h"(全角)在 java 落穿 ⇒ NULL,在 go/rust/node 算得出——各栈"空白"定义本就不同 (java trim() 剥 <= U+0020、go/rust/JS 走 Unicode 空白、moon 惯用四字符 " \t\r\n")。 要统一得先给"空白"下定义并八栈同批改,owner 判断不值得。

节点没配(null 或空串)⇒ 这一列保持 NULL,不造任何默认值;任何一档都不允许退化成"取当前时间"—— expire_time = create_time 等价于"建单即逾期",比不写更难发现(owner 2026-09-28 口径)。

判据:跨栈门禁 L2-27(jeeflow-integrations/verify/contract/runner.py)钉三档—— ① 未配 ⇒ 该列为空;② 配 2h ⇒ 同一行 expire_time − create_time ≈ 表达式偏移; ③ 配 -5h ⇒ 该列必须为空(本条判非负的镜像层落点;八栈判负落地前这一档必然红, 故判据抽成纯函数 judge_expire_cells 并由 --selftest-l2-27 把五种坏形状各自钉红一次—— "恒红"与"判据没牙"在报告里长得一模一样)。 样本取 processTask/doneList 并要求同行 create_time 投得出来(processInstance/detail.tasks[] 那一层不投时间列,拿它判会把"出口没投影"当成"库里没值")。第 5 处与各栈求值器档位由各自 T0 钉。 实例级 expire_time(流程定义级表达式算到实例上)与任务行这一列是两件事,本条只管任务行。

实例行 expire_time 由谁写(issues/137 A · 裁定 A) ​

wf_process_instance.expire_time 与上面任务行那一列同源不同档:表达式来自流程定义顶层expireTime(02-flow-definition.md:21/55「流程期望完成时间」),写点在发起那一步,实例行只有一条, 所以八栈曾经给出过三种不同的答案(原串搬运/压根不写/求值结果)。owner 拍 A 案,口径与任务行同尺:

引擎在发起路径上按定义级表达式求值,把「求值结果」写进实例列。

基准=java JeeflowEngineImpl.java:93-96(取 model.getExpireTime(),非空才instance.setExpireTime(FlowUtil.processTime(expireTime, args))),同形状 boot2 内置版 ProcessInstanceServiceImpl.java:157-160;c# 现状同形(JeeflowEngine.cs:59-61,钟由调用点注入)。 五条口径(各栈 T0 逐条钉住。整改面实读是八栈、不是案文写的六栈——批二 §3-4 的 go 腿普查查出: 被当作"现状就是这个形状"的两栈基准各自都有一处断链:java JeeflowEngineImpl.java:93 与 c# JeeflowEngine.cs:59-61 读的都是 ProcessModel.ExpireTime,而两栈的解析器(java ModelParser.java:58-66、 c# ModelParser.cs:81-92)逐字段搬了九项、唯独没搬 expireTime(LfModel/ProcessModel 那个字段 一直在、全仓零赋值点)⇒ 守卫永远读到 null、实例那一列在参考实现里恒 NULL,"形状是 A"只是形。 三派失效形状因此是:php/rust/moon「把表达式原串搬进这一列」;go/python/node「压根没有这一处写点」; java/c#「有写点、读的却是没被填过的字段」。八栈随批二 §3-4 一起统一到 A):

  1. 进列的是时刻,不是表达式原串。原串("2h")直接绑进 DATETIME 列在严格模式下是服务端硬错: 160 那台 MySQL(@@sql_mode 含 STRICT_TRANS_TABLES)实测给 1292 / SQLSTATE 22007 Incorrect datetime value: '2h' for column 'expire_time'(⚠️ 本案文此前写的是 1366——那是字符列 那一族的码,DATETIME/TIMESTAMP 列这台给 1292,php/rust 两栈真库腿各自实测并复现); 非严格模式则静默存 0000-00-00。两种都是"内存绿、真库红",而 php 栈的实际后果更重: 配了顶层 expireTime 的流程根本发起不了(saveInstance 抛异常), 这一点长期没被发现只是因为共享夹具的顶层 expireTime 全部 absent。
  2. 变量源=发起参数那一份(已注入 u_* 用户信息与 autoGenTitle 之后的那份 args), 与节点级表达式用的实例变量是两档;取错那份会让"表达式是变量名"这一档跨栈给出不同答案。
  3. 没配 ⇒ 一个字都不写:该列保持建行时的 NULL。不赋 now()、不赋空串(owner 2026-09-28 口径: "不设置就是没有到期时间")。四态都算没配:缺键/JSON null/空串/纯空白。
  4. 配了但算不出 ⇒ NULL:沿用上一条「任务行」那节的落穿语义(误配、负数档 ⇒ 解析不出), 任何一档都不许兜底成当前时间——那等于给实例造一个"发起即逾期"的假到期。
  5. 求值器复用任务行那一枚(同一份档位实现),不许为实例级新造第二把尺子。
  6. 解析层必须把根上那个键搬进模型(本条的第二只鞋,八栈各自的落点不同但判点同一): 表达式住在流程定义 JSON 的根上,任何"引擎读 model.expireTime"的实现都必须先证明那一跳存在—— java/c# 的缺陷正是"写点在、映射缺",两栈各自补上一行(processModel.setExpireTime(lfModel.getExpireTime()) / ExpireTime = Str(root, "expireTime"))并配解析层取证格:摘掉那行映射,取证格与"配了该有值"那几格一起红, 而"该留 NULL"那两族反而恒绿(这就是为什么判据里必须有一格直接打在 ProcessModel 上, 只打实例列会被"恒 NULL"伪装成"合规")。

判据形状与各栈 T0 的对应关系同「任务行」那节:未配/相对档/绝对档/变量档/算不出, 且断言一律打在仓储读回的实例行上(不是引擎返回的那个聚合对象)——只有读回值能证明这一列真进了库。

流程变量约定 ​

引擎自动注入以下变量(前缀 u_ 表示用户信息):

变量名来源说明
u_userIdIUserProvider当前操作人 ID
u_realNameIUserProvider当前操作人姓名
u_deptIdIUserProvider部门 ID
u_deptNameIUserProvider部门名称
u_postIdIUserProvider岗位 ID
u_postNameIUserProvider岗位名称
autoGenTitle引擎自动生成${realName}的${displayName}-${时间}——${时间} 钉 yyyy-MM-dd HH:mm:ss(秒档,issues/232 owner 拍 A);实现须用标题专用格式化点,不得复用 expire_time 解析腿的共用格式常量/函数
BUSINESS_NO业务方传入业务流水号
submitType操作时传入全枚举 0=发起 1=同意 2=拒绝 3=退回上一步 4=跳转 5=重新提交 6=退回发起人 7=转办 20=会签拒绝(见 设计原理 06 §3-4)。仅当次操作留痕,不随行复活(退回上一步新建的待办不带该键,见「退回上一步」第 5 条)
isFirstTaskNode引擎建单时写入bool——该行节点是否 start 直接后继(引擎控制键:判「任务变量是否为空」时须忽略)。建单不变量:必写进行变量;「退回上一步」据此决定参与者取发起人还是该行 operator。存量行缺该键时由门面按拓扑现算兜底(现算带"仅进行中"判定,只用于展示,不得当引擎判据)

聚合根方法清单(跨语言契约) ​

八版实现遵循统一 DDD 结构:聚合根封装业务规则,引擎只做编排。 设计论证见 设计原理 02 · 领域模型。

ProcessInstance(聚合根) ​

行为说明
create工厂——创建流程实例(state=10)
completeTask(task, operator, vars)完成任务(子实体状态转换 + 实例变量合并)
abandonTask(task)废弃单个任务
abandonAllDoing()(可选)废弃所有进行中任务(可选聚合方法;有则语义为逐条 DOING→99,见 §驳回 步 4)
finish()流程完成(state 10→20)
reject()驳回(state 10→45)
addVariable(vars)追加流程变量
createTask(...)创建任务(子实体工厂)
getDoingTasks() / getDoneTasks()查询
isAllTasksFinished()join 合并判断

ProcessTask(子实体) ​

行为说明
finish(operator, vars)完成任务(10→20,记录操作人/完成时间)
abandon()废弃(10→99)
isAllowed(operator)参与者权限判断
isDoing() / isFinished()状态判断

各语言实现位置 ​

语言聚合根引擎(薄编排)
Javadomain/ProcessInstance.java + domain/ProcessTask.javacore/JeeflowEngineImpl.java
Gomodel/instance.goengine/engine_impl.go
Node.jssrc/model.ts(class)src/engine.ts
Pythonjeeflow/model.py(dataclass + 方法)jeeflow/engine.py
PHPpackages/core/src/Domain/ProcessInstance.php + ProcessTask.phppackages/core/src/JeeflowEngine.php
Rustjeeflow-core/src/model.rsjeeflow-core/src/engine.rs
MoonBitcore/model/aggregate.mbtcore/engine/engine.mbt
C#src/Mldong.Jeeflow.Core/ProcessInstance.cs + ProcessTask.cssrc/Mldong.Jeeflow.Core/Engine/JeeflowEngine.cs

引擎保留:流程遍历(executeNode)、决策求值、参与者解析、仓储调用。 任务状态转换/创建等业务规则一律收敛到聚合根。

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