一、主办为什么先把自己挂起
一个事项走到主办节点,主办看完材料,发现自己一个人拍不了板:需要财务看预算、法务看条款,可能还要问人事。
名单不是流程设计时能定死的。得到这一刻,主办才知道该拉谁;少拉了财务,还得再拉一轮;每条意见都要能独立出证,不能被下一轮覆盖。
这不是会签节点。会签池在进节点之前就定死了,谁同意、谁反对、几个人算过,都是静态的。也不是后加签。后加签是主办已经办结、把同一手交给下一位;动态会签是主办还没拍板,先把意见收进来。
它的名字叫动态会签(征询)。这篇只回答三件事:主办为什么挂起;收齐前为什么连驳回都按不了;撤回和多轮怎么处理。
二、先和后加签分开
后加签和动态会签共用同一个协议键 tf_addSignOperators,但行为由节点属性分流,解析期互斥:
| 维度 | 后加签 | 动态会签 |
|---|---|---|
| 主办任务 | 办结,原人退出 | 挂起,不办结 |
| 新任务 | 同节点新一轮接力 | 按人建成员行收意见 |
| 节点推进 | 接力链没人再点时流出 | 收齐意见、主办再办结才流出 |
| 拍板权 | 随 token 前移 | 始终在主办 |
| 成员角色 | 后继办理人 | 出意见的人,无表决 |
| 什么时候用 | 我办完,把这一手交出去 | 我不能独自拍板,先收意见 |
一句话:后加签是主办把这一手交出去;动态会签是主办把意见收进来,自己拍板。

三、发起:主办不办结,按人建成员行
主办在同意时带上成员名单,必要时再带一个发起原因:
text
POST /wf/processTask/execute
{
"operator": "<当前用户 id>",
"processTaskId": "<主办任务 id>",
"submitType": 1,
"tf_approvalComment": "先收集财务、法务意见",
"tf_addSignOperators": ["<吴昊 id>", "<赵敏 id>"],
"tf_addSignReason": "需要财务、法务专业意见"
}
引擎做完这件事,数据上是四件:
- 主办行仍然是
DOING(10),不办结; - 按人建成员行:
task_parent_id指向主办行,节点同名同节点,参与者就是那名成员; - 每条成员行盖轮次戳
addSignRound_{节点名}(这里就是节点 id,例子里的consult)和发起原因tf_addSignReason; - 主办行账本追加一条
tf_addSignHistory:{round:1, fromActor:<主办 id>, toActors:[<成员 id>...], time, operator}。
响应里会带 data.dynamicCountersign={round,total,submitted};集成层可以据此提示"已发起会签"。这次真跑的 vben5 演示前端提交后仍提示"提交成功",提示文案归集成层。
成员行是普通任务行,不是会签池;一人一行,一条意见,不新增表。意见的物理隔离由"一行一条"保证:每个人的意见落在自己那一行的变量里,finish 一次写死,没有原地改意见的通道。
成员行可以转办、委托、抄送,但不能带拉人参数------成员不能再拉人,只有主办能把人拉进本轮征询。多选时一次建多行,轮次上限 addSignMaxRounds 由后加签和动态会签共用。

四、收齐前,连驳回都按不了
主办再提交时,引擎先看主办行下面还有没有 DOING 的成员行:
- 有:报错,零写入。对外文案是
节点[{节点名}]动态会签意见未收齐(已提交 {m}/{n}),不能办理; - 没有:放行,主办按普通办理档办结,节点流出。
关键点是:闸门挡全部 submitType,包括驳回。未收齐时,同意(1)、不同意(2)、退回上一步(3)、退回发起人(6)、跳转(4)全部不放行;唯一出口是撤回动态会签。
这里的"唯一出口"指办结/离开节点 。审批意见框、附件、发起控件还在------发起或补人不是办结动作,走带人 execute;但想把主办这一手办结、让节点往下走,只能先撤回。未收齐时主办自己的拒绝、退回也会被同一道闸门挡住,走不到清场。
前端把这件事做成"整排置灰":主办视角的五个办理动作全灰,只剩一条警告、一个进度和"撤回动态会签";审批意见框、附件、发起控件还在,它们不是办结动作。进度是 已提交 m/n;1/2 时五个动作按钮全灰,只剩撤回。置灰是体验,服务端闸门是权威------绕过前端直接调接口,同样过不去。

收齐判据只有一条:主办行下没有 DOING 成员行。废弃行不进分母;撤回清掉未答行之后,闸门自然恢复。

流程图里也不会把主办和成员画成三个节点:还是同一个「专业意见征询」节点,节点里竖排主办和成员。进行时主办是蓝点、已交的成员绿勾、未交的灰点;收齐后节点变绿,终审才变当前节点。

五、撤回与多轮:只废未答行,已答意见保留
撤回是置灰态的唯一恢复出口,不是"暂停"。
- 废弃本主办行下全部未答成员行;
- 已提交的意见行原样保留,审计凭证不动;
- 账本追加撤回条目;
- 闸门自然恢复,轮次不消耗。
撤回是独立 action,不在 submitType 枚举里:
text
POST /wf/processTask/revokeDynamicCountersign
{
"operator": "<主办 id>",
"processTaskId": "<主办任务 id>",
"reason": "成员名单有变化"
}
主办行的 processTask/detail 里,ext.dynamicCountersign 给出 {active,round,total,submitted,actors};前端的进度和置灰就是读它。
我这次真跑撤回的一单:主办拉了吴昊、赵敏,吴昊交了一条意见,赵敏还没交;撤回后清场的是赵敏,吴昊那条已交意见保留。库里读回来:吴昊行 state=20(FINISHED,已交意见还在),赵敏行 state=99(废弃,未答被清场),主办行仍 10(DOING)。这个结果和"只废未答行"的契约一致。
撤回后再拉一轮,不是再点一次撤回,而是主办再提一次带 tf_addSignOperators 的 execute:round+1,闸门重新关闭,跨轮意见互不覆盖。每一轮的发起原因、成员、意见都留在各自的账本/任务行里,读侧按轮分组。
未收齐时,主办自己的拒绝、退回会被同一道闸门挡住,走不到清场;真正可达的是实例撤回(以及流程定义允许的其他离开本节点路径)。这些路径离开本节点时,pending 成员行一并废弃,不留孤儿待办。

图里成员意见行显示姓名;上面的加签账本列存的是用户 id,两个要分开看。征询原因、轮次、每条意见都在主办那一行下面按轮展开。
六、怎么配,前端怎么接
节点上和后加签有关的两个键仍然只有两个:
| 键 | 说明 |
|---|---|
dynamicCountersign |
bool,不写键=false;开启后该节点主办可在运行期拉人收意见。 |
addSignMaxRounds |
int ≥ 0,不写键=0=不限;和后加签共用。 |
解析期会拦下四类:
- 非法值:解析期报错,不静默缺省;
- 会签节点配
dynamicCountersign:报错,会签池推进和征询挂起是两种语义; - 首任务节点(申请节点)配
dynamicCountersign:报错,申请节点走发起/重提语义; - 后加签和动态会签双开:报错,两者共用
tf_addSignOperators,双开等于行为歧义。
设计器把它做成三选一的"办理协同":无、后加签(接力)、动态会签(征询)。选动态会签写 dynamicCountersign、删 allowAddSign;选无则把这一组键一起清掉。
这次真跑的 vben5 演示前端里,主办看到的是"发起动态会签"和"会签人员";公开 npm 包的 jeeflow-ui 这一版仍写"加签给下一位",文案归集成层。发起动作仍然是同意(1)的附属效果,不新增 submitType。
发起原因 tf_addSignReason 是协议选填;演示前端这一版没有单独放原因输入框,原因由集成层/接口传入,审批记录会显示出来。这是前端形态,不是引擎缺字段。

前端还有两件事:
- 未收齐时,执行弹窗整排动作置灰,只留进度和"撤回动态会签";
- 审批记录按轮分组:主办那一行下面列征询原因、成员意见、轮次;成员意见一行一条,不聚合。
服务端闸门和前端置灰是双保险:前端负责让人看懂"为什么点不动",服务端负责让绕过前端的人也过不去。
七、边界:v1 不做的
- 成员不递归拉人:只有主办能把人拉进本轮征询;成员行携带拉人参数会报错。
- 没有表决语义:成员提交类型只作记录,无否决、比例、一票否决;投票应该用会签节点,不是这里。
- 只做并行征询:不做串行逐个征询。
- 表单快照另案:本轮账本已有 round、reason、成员和意见,快照存储形态还没拍。
- 多进程锁不在承诺内:单进程部署按实例串行;多进程部署的跨进程锁仍要仓储侧提供,本特性不宣称已支持。
- 离开节点时的清场:实例撤回/上游退回等真正离开本节点的路径会把 pending 成员行一并废弃,不留孤儿待办;未收齐时主办自己的拒绝、退回被闸门挡住,走不到清场。
八、什么时候选它
- 选动态会签:主办不能独自拍板,需要先收意见、自己再决定;人员运行期才定。
- 选后加签:主办已经表过态,只需要把同一手交给下一位专业复核。
- 选会签节点:所有人在流出前都要表态,或者需要比例、一票否决------这是静态会签的语义,不是动态会签。
- 选转办:我不办,换人办。
- 选委托:建单时就把代理人并进名册,不摘原人。
一句话:动态会签是"收意见",不是"投票";拍板权始终在主办手里。
八种语言的引擎按同一份契约实现,参考实现是 Java。我这次真跑的是 boot3 演示栈(Java 参考实现 + vben5 前端),实例、任务行、账本都在上面;其余七种语言这次只对照了规范和共享测试流程,没有逐个做 UI 走查。
落地清单
- 确认你的引擎代次带
dynamicCountersign和addSignMaxRounds,设计器有"办理协同"三选一。 - 只在普通审批节点上开;会签节点、申请节点解析期就会拒。
- 需要限制轮次就设
addSignMaxRounds;0是不限。 - 前端未收齐时置灰全部办理动作,只留"撤回动态会签";服务端闸门不要省。
- 上线前跑一遍自己的样本:主办拉两人 → 一人交意见看 1/2 置灰 → 撤回看已答意见是否保留 → 再拉一轮 → 两人交齐 → 主办拍板,看节点是否在收齐后才流出。
参考资料
jeeflow 文档站 规范 04「引擎操作」------动态会签(征询模式) jeeflow-doc.mldong.com/spec/04-eng...
jeeflow 文档站 规范 02「流程定义」------办理协同开关(§4.3 allowAddSign / §4.4 dynamicCountersign) jeeflow-doc.mldong.com/spec/02-flo...
jeeflow 文档站 规范 06「统一门面」------tf_addSignOperators / tf_addSignReason / 投影 jeeflow-doc.mldong.com/spec/06-fac...
jeeflow 文档站 更新日志(八引擎各自代次) jeeflow-doc.mldong.com/changelog/
jeeflow Java 参考实现 github.com/mldong/jeef...
八语言演示站(在线可试) jeeflow-demo.mldong.com/