一、一个反直觉的现场:审批办完了,节点还在原地
合同审批走到「合同复核」这一格,李娜看完了,也点了同意。
按直觉,这一格应该结束了,单子该去终审。但在后加签里,李娜同意之后,单子还在这一格------她只是把自己这一手办结了,然后又指定吴昊在同一格接手;吴昊办完再指定赵敏;赵敏确认不再加人,这一格才真正流出。
这不是转办,也不是往同一轮里加一个参与人,更不是改流程图。
它叫后加签(可重复)。这篇只回答三件事:后加签改的是哪一行;节点什么时候才动;配的时候哪些边界不能踩。
二、先把「加人」分成四档
在同一个审批节点上,往里面加人其实有四个动作。它们长得很像,改的东西完全不同:
| 档位 | 它改了什么,什么时候用 |
|---|---|
并加签(surrogate / addCandidate) |
不办结,追加参与人;同一轮、计票共享、原人保留。适合同一轮多人共享一票,谁办都算这一手办完。 |
后加签 (allowAddSign) |
办结我这一手,原人退出;同节点新一轮、计票不共享。适合我办完,指定下一位接力。 |
转办(transfer) |
我不办,换人办;同一任务行摘原人换新人。 |
委托(SurrogateInterceptor) |
建单时把代理人并进名册,不摘原人、不新建。 |
只有第二档同时满足三个条件:我办结、原人退出、同一节点新一轮。它的特殊之处就在这里。
先解释几个词:DOING 是任务状态 10(待办);token 是流程令牌,代表流程当前停在哪;merged 是本次提交要不要把节点汇合流出。这里说的「计票」指同节点多行任务要不要合并成一次结果------并加签合并,后加签不合并。
后加签不新增节点类型、不画自环边、不新增 submitType。提交动作仍然是同意(1)或不同意(2),加签只是随这次提交带的一个参数 tf_addSignOperators;账本 tf_addSignHistory 也是任务变量上的一棵追加式列表,不新增表。

三、办结的是我这一手,不是这个节点
李娜在同意时带上吴昊的用户 id:
text
POST /wf/processTask/execute
{
"operator": "<当前用户 id>",
"processTaskId": "2118334755562127362",
"submitType": 1,
"tf_approvalComment": "技术复核通过,请财务接力",
"tf_addSignOperators": ["2086772723853299712"]
}
通过集成壳调用时,operator 由壳从登录态注入;直连门面时要显式带。
引擎做完这件事,数据上是三件:
- 李娜那一行置为
FINISHED(20),不改回DOING; - 同一节点新建一行,
task_parent_id指向李娜刚办结的那一行,参与者只有吴昊,performType=NORMAL,expire_time按本节点表达式同尺重算; - 李娜那一行追加一条账本
tf_addSignHistory:{round:1, fromActor:<李娜 id>, toActors:[<吴昊 id>], time, operator}。
吴昊办完再加赵敏时,同样的三件事再来一次:吴昊那一行置 FINISHED,同节点新建赵敏那一行,账本 round:2 落在吴昊那一行上。赵敏办完不再加人,这一轮就没有新行了。
所以「节点有三行」不是某个任务被改了三次状态,而是三个独立的任务行:
- 第 1 行:李娜,
FINISHED,账本 round 1 → 吴昊; - 第 2 行:吴昊,
FINISHED,账本 round 2 → 赵敏; - 第 3 行:赵敏,
FINISHED,无加签账本。

两个数据细节值得单独说:
- 原行状态与办理结果不改写 :jeeflow 没有一条独立的历史表,任务行本身即审批历史。把已办结行翻回
DOING等于改写已经出证的记录;所以撤回、退回这些动作也都是「旧行留痕、另起一行」,后加签沿用同一个原则。唯一追加在原行上的,是它的账本tf_addSignHistory。 - 上一行的账本不会复制到新行 :
tf_addSignHistory留在已经完成的那一行上;新行只带自己的轮次键addSignRound_{节点 id}(例子里的节点 id 是review)。注意这个键记的是创建它的那一轮:吴昊那一行是 1,但他再点赵敏时,账本写的是 round 2。tf_控制键也不会并进实例变量,避免上一轮的控制参数在后续请求里复活。
多选时,同一圈会建多行任务,每行一个参与者;它们共享这一圈的轮次,但计票不共享。加签人如果是通过委托进来的,新行照普通建单的规则把当时生效的受托人并进参与者,不摘原人。
四、节点什么时候才往下走:三个判据,点人排第一
任务节点执行闸分档:会签节点走会签档;开了 allowAddSign 的节点走加签闸;开了 dynamicCountersign 的节点走征询档(下一篇);其余走普通档。后加签闸的 merged 判据只有三步,点人排在最前面:
text
本次给了 tf_addSignOperators → 在本节点建单,不流转
否则,本节点还有 DOING 任务 → 不流转
否则 → 流出本节点
为什么点人排第一:这次点了人,就是要新建一行;先把新行建出来,再等所有行办完。哪怕同节点还有别的 DOING 任务,也先建单------否则这次加签请求会被静默吞掉。
对着真机记录看就很清楚:
- 李娜办结后,吴昊那一行还是
DOING,所以节点不流出; - 吴昊办结后,赵敏那一行还是
DOING,所以节点不流出; - 赵敏办结、且没有点人,本节点没有
DOING行,也没有新的加签人,节点才流出,终审任务才出现。
多选时,同一圈建了多行;某一行既办结又点人,仍然先建新行、节点继续等。只有最后一行办结且没再点人,merged 才为真。停止条件就是这个判据本身,不设第二判据源------否则就会出现「刚建完接力任务,节点已经走了」的假完成。

同一节点三行,在流程图上不会画成三个节点。它仍然是一个「合同复核」节点,节点里竖排三个人名:
- 停在中间时,review 是橙色边框,李娜、吴昊已经绿勾,赵敏还是蓝点,终审还没动;
- 三个人都办完后,review 变绿,三个人名全绿,终审才变成当前节点。
这张图不是示意图,是同一套定义在两个时间点的任务详情「流程图」。

下面是真跑的审批记录:

图里账本显示的是用户 id;三行办理人姓名在每行上方,第 1 轮对应第二行的吴昊,第 2 轮对应第三行的赵敏。终审那一行在赵敏办结之后才出现;它还是待办,所以办理人一栏是空的------王强还没办。
五、怎么配:两个键、四条解析期拦截、四个写入口
和后加签有关的键只有两个:
| 键 | 说明 |
|---|---|
allowAddSign |
bool,不写键=false;开启后该节点办理人可在办结时指定后继,在同一节点续办。 |
addSignMaxRounds |
int ≥ 0,不写键=0=不限;后加签和动态会签共用。 |
落在节点上就是两个属性:
json
"properties": {
"allowAddSign": true,
"addSignMaxRounds": 0
}
把它加在普通审批节点的 properties 上;其余节点字段按你项目的流程定义保留。
解析期会拦下四类:
- 非法值:解析期报错,不静默缺省;别指望它悄悄当成 false 或 0;
- 会签节点配
allowAddSign:报错。会签池内加人和会签之后加人是两种语义,推进闸会打架; - 首任务节点(申请节点)配
allowAddSign:报错。申请节点的办理是发起/重提语义,和加签路由打架; - 后加签和动态会签双开:报错。两者共用
tf_addSignOperators,双开等于行为歧义。
设计器把这个互斥做成了一个三选一的「办理协同」:无、后加签(接力)、动态会签(征询)。选后加签写 allowAddSign、删 dynamicCountersign;选无则把 allowAddSign、dynamicCountersign、addSignMaxRounds 一起清掉;已有 allowAddSign:true 的存量定义回显为「后加签」。
写入口还有四个判据,全部在状态变更之前,报错即回滚整次提交:
- 节点没开
allowAddSign却传了tf_addSignOperators:报错; - 把加签加给自己:报错;
- 超过
addSignMaxRounds:报错,不强制放行------放行等于伪造一次审批完成; tf_addSignOperators和tf_nextNodeOperator同传:报错。一个管本节点再来一轮,一个管流出后下一节点交给谁;加签让 token 停在本节点,流出发生在后续请求,届时先传的下一节点处理人已经读不到了,同传时它是死参数,所以宁可报错,也不静默择一。
空串、纯空白、null 会被归一成「没传」,按普通流转处理;别拿空值当加签参数。

图里还能看到「允许已办撤回」,那是另一项能力,不是后加签的开关。
前端提交时,两组互斥的控件会做第二步清理:勾了加签就删掉「指定下一节点处理人」那组键,没勾就删掉加签那组,避免残留值把服务端判成同传。这一步不是可选的------只把控件隐藏、不删值,隐藏的那一半仍会进 payload。同意以外的动作(不同意、退回、跳转)会把两组键都删掉;别在这些动作里带加签参数。
账本在任务行的 ext.tf_addSignHistory 里,processTask/detail 或流程详情的审批记录都能看到。
这里最容易混的是动态会签:它复用的也是 tf_addSignOperators,但主办行不办结、挂起收意见,收齐后主办再拍板。后加签是主办已经表过态、把同一手交给下一位;动态会签是主办还没拍板、先收集意见。两件事,下一篇单独讲。
六、边界:哪些事后加签不做
- 加签链上不能退回上一步 :当前行和它的血缘 parent 在同一节点,既有的
canRejected守卫会拦下。要往回走,用「退回发起人(6)」或「跳转(4)」。 - 不同意(2):按拒绝档结束实例,并废弃本节点未完成的加签任务;**退回发起人(6)**同样废弃,不留孤儿待办。
- 加给自己报错 ;加给已经办过的人允许 :A→B→A 是合法业务,靠
addSignMaxRounds兜上限。 - 加签任务自身的能力:可以再后加签、可以不同意、可以被转办/委托/抄送。
- 并发承诺边界:同一实例的办理在单进程部署下串行化;多进程部署的跨进程锁不在本特性的承诺内。这一点如实写在这里,别把它当成已经支持。
- 不改流程图:不新增节点类型、不画自环边;任务行在同一节点续建,图上仍然是一个节点。
七、什么时候选它
- 选后加签:主办已经表过态,只需要把同一手交给下一位专业复核;需要一轮一轮接力;不想改流程图。
- 选动态会签:主办不能独自拍板,需要先收意见、自己再决定;人员运行期才定。
- 选转办:我不办,换人办。
- 选并加签:同一轮多人共享一票,谁办都算这一手办完。
- 选委托:建单时就把代理人并进名册,不摘原人。
一句话:后加签不是会签的替代品。如果要求所有人在流出前都表态,应该用会签节点;后加签解决的是「我这一手已经办结,但这一手还没完」的接力问题。
八、八种语言同一套契约
后加签的参考实现是 Java;八种语言的引擎按同一份契约实现,八仓里放着同一份测试流程 19-add-sign-after.json。这一批代次发版后,各集成壳和演示镜像都已跟上。
我这次真跑的是 boot3 演示栈(Java 参考实现 + vben5 前端),实例 2118334755528572930:review 节点三行分别是李娜、吴昊、赵敏,前两行各带一条加签账本,第三行不再加签,终审任务在第三行办结后才创建。其余七种语言我这次只对照了规范和共享测试流程,没有逐个做 UI 走查------跨栈断言按这个口径看。
版本怎么确认:在装好的引擎里搜 allowAddSign,或看文档站更新日志里你那一语言从哪版起带上它。八种语言版本线独立,正文不写死版本号。
落地清单
- 确认你的引擎代次带
allowAddSign和addSignMaxRounds,前端设计器有「办理协同」三选一。 - 只在普通审批节点上开;会签节点、申请节点解析期就会拒。
- 想限制 A→B→A 的来回加签,就把
addSignMaxRounds设成非 0;0是不限。 - 前端两组控件必须互斥清理:勾加签删「指定下一节点处理人」,不勾删加签那组键。
- 上线前跑一遍自己的样本:主办办结时点人 → 下一位再点人 → 最后一位不点人,看节点是否在最后一位办结后才流出,审批记录里是否三行都在。
参考资料
jeeflow 文档站 规范 04「引擎操作」------后加签(可重复) jeeflow-doc.mldong.com/spec/04-eng...
jeeflow 文档站 规范 02「流程定义」------后加签开关(allowAddSign / addSignMaxRounds) jeeflow-doc.mldong.com/spec/02-flo...
jeeflow 文档站 规范 06「统一门面」------tf_addSignOperators / tf_addSignHistory jeeflow-doc.mldong.com/spec/06-fac...
jeeflow 文档站 更新日志(八引擎各自代次) jeeflow-doc.mldong.com/changelog/
jeeflow Java 参考实现 github.com/mldong/jeef...
八语言演示站(在线可试;后加签夹具见各引擎仓的 19-add-sign-after.json) jeeflow-demo.mldong.com/