☰
一个审批按钮远远不够:领取、转派、会签、退回与规则版本
2026/9/28 20:34:07 网站建设 项目流程

一个审批按钮远远不够:领取、转派、会签、退回与规则版本

《企业级 Workflow 实战:从审批流到 AI Agent》· 第 09 篇 / 共 24 篇
贯穿项目:星河设备 AcmeFlow 客户开通中心。
本篇交付:可领取与转派的人工作业、两人会签、权限撤销、资料退回后的新任务、规则版本冻结,以及一个可运行的 SQLite 实验。
实验边界:人员身份由本地样例表模拟,没有接企业身份提供方或生产任务界面;第 03 篇 FastAPI + PostgreSQL 应用的接入落点在文末说明。

运营主管打开一条客户开通申请,看到页面上写着“审核通过”。她问了三个问题:“谁通过的?他当时有没有权限?他看的是客户哪一版资料?”开发者只找到了approved=true和一个更新时间。财务随后发现,昨天运营看过的附件今天已经被销售替换,后台却仍把旧审批结论当成有效。另一边,两位审核员先后点“通过”,却没人能说清楚系统要求的是一人通过、两人会签,还是先后两级审批。

这不是审批页面缺少几个按钮的问题。人工任务需要自己的生命周期、处理身份、对象版本和完成规则。把人放进工作流,意味着系统要知道“现在轮到谁处理什么、他凭什么可以处理、处理的是哪一份资料、该决定什么时候才算完成”。从第 08 篇开始,我们已经能控制一条审核任务何时失效;本篇继续解决任务在有效期内由谁完成,以及多人判断怎样汇合。

图 1:两个审核席位属于同一份申请资料。完成一个席位,只能说明一位审核员作了决定;按本例 v1 规则,两位不同审核员通过后才推进申请。

一、从“某人点过通过”到“某项任务被正确完成”

一个按钮通常隐藏了多个判断。操作人是否登录?他是否属于候选审核组?任务有没有被别人领取?领取后是否被转派?他的权限此刻是否仍有效?任务绑定的材料版本与当前申请是否一致?这次操作属于第几个会签席位?如果前一位要求补充资料,其他席位还应不应该继续?

这些问题不应该分散到前端按钮是否可点击、后端路由参数和数据库触发器里,由三处给出相互矛盾的答案。本例把命令集中到待办处理路径。页面展示待办时可以先筛选候选任务,但真正领取、转派、完成时必须重新检查权限和任务状态。用户停留在页面十分钟,这期间权限可能被撤销,材料可能改版,另一位审核员可能已经领取。页面上的旧快照不能替代提交时的判断。

我们先给人工作业一个清楚的词汇。候选人是当前有资格领取任务的人,不等于已经负责;领取人是目前持有任务的人;处理决定是领取人提交的业务结果;会签席位是规则要求的一个独立判断位置。一个申请可以有多个任务,一项任务也可能经过领取、释放、转派和完成多次状态变化。申请的整体状态与任务的状态不同:两个任务中一项已完成时,申请仍可能停在REVIEWING。

Camunda 的任务授权文档也将读取、更新、领取和完成区分为不同操作权限,并允许结合任务属性上的候选用户、候选组与受理人控制访问。本篇不是复刻 Camunda 产品,而是借此说明:真实的人工作业不能只靠一个“可以看到列表”的布尔值判断全部操作。参考:Camunda User Task Authorization

二、先画出任务生命周期,再设计 API

本例任务从OPEN开始。具有运营审核权限且不是申请人的用户可以尝试领取,领取成功后进入CLAIMED,保存claimed_by。当前领取人可以转派给另一个有资格的人;转派不会重建任务,也不抹掉先前领取者,而是在审计里记下责任转移。当前领取人作出APPROVE或CHANGES决定以后,任务进入COMPLETED。如果其他席位仍在等待,申请保持REVIEWING;如果所有席位按本版规则通过,申请成为APPROVED。若某位审核员要求补充材料,申请进入NEEDS_INFO,其他未完成任务被取消,本轮审核结束。

图 2:领取和完成是不同命令。被撤销审核资格的人,即使曾领取任务,也不能在后续完成它。

这里没有使用“退回以后把任务重新设成 OPEN”的捷径。退回意味着当前资料需要改变,而审批对象即将成为另一个版本。若重用同一任务,数据库很难说明decision针对旧资料还是新资料。本例保留旧任务及其审计,申请人补交资料时增加material_version,并创建新一轮任务。这样,页面可以显示完整历史:v1 因材料缺失退回,v2 等待重新审核。它也让审计人员能够解释一次批准到底依据哪份材料。

同理,会签规则不能只写成“谁最后点按钮谁改申请状态”。每个席位先形成独立决定,汇合时在事务中读取当前版本全部席位。如果所需席位都已由不同的合格人员完成且决定为通过,才推进申请。若任一席位退回,应停止这一轮并明确下一步由谁补充。后续如果增加“多数通过”“财务必须通过”“法务有否决权”等规则,可以扩展为不同任务类型与决策表,但不能让一个没有说明的计数器代替业务政策。

三、任务、权限、规则、审计分别存什么?

第 03 篇的主库已经有申请、流程实例、审核任务和迁移历史。本篇在同一语义上增加了四类信息:申请保存当前材料版本与绑定的规则版本;任务保存席位、对象版本、状态、领取人和决定;权限目录说明谁现在可以审核;审计保存每次领取、转派、撤权、退回和汇合发生的上下文。教学脚本把这些放在独立 SQLite 中,是为了可离线复现,不表示已经把新字段迁进 PostgreSQL 主线。实验中形如application_id:v2:review-1的任务标识是便于观察的复合字符串;接入主库时继续使用第 03 篇的 UUIDtask_id,另外用申请 ID、资料版本与席位做业务唯一键。

图 3:application_id贯穿四类记录;material_version和rule_version决定一个审批结果能否用于当前申请。

为什么权限目录不直接复制到任务里?候选组可以写入任务,但“某用户目前是否属于该组”会随组织调动而变化。若系统只在创建任务时复制人员名单,昨天被撤权的用户今天仍可能拿着旧任务完成审批。反过来,如果完全不保存任务当时采用的候选规则,也无法说明为什么某人曾经可以领取。合适的做法是同时保留当时的规则版本与当前权限事实:前者用于解释策略,后者用于执行实时授权。

审计也不能只靠应用日志。日志可能按保留期轮转,也可能只记录一次 HTTP 200,不记录审批看的是资料 v1 还是 v2。至少需要保存申请 ID、任务 ID、资料版本、规则版本、动作、行为人和结果。时间戳在生产系统里同样重要;本篇的简化脚本为了聚焦逻辑,只保存动作顺序与细节,不宣称已经提供满足监管要求的不可篡改审计。接入第 03 篇主线时,应沿用transition_history的occurred_at和actor_id,新增任务事件记录或在业务历史中保存任务 ID 与版本。

流程状态也不应直接等同于审核票数。REVIEWING表示这份申请还在本轮审核;一张任务的COMPLETED只说明一个席位完成。APPROVED是在满足当前规则时形成的申请结论。后面的签署、到账、ERP、权益仍是另一段流程;第 01 篇已经建立的READY ≠ ACTIVE约束并未因为两人会签而消失。把人工作业与申请状态分开,才能继续说明“审核通过但还未到账”的正常等待。

四、会签必须防止同一人占两个席位

本篇示例规定 v1 规则需要两位不同运营审核员;v2 规则用于新申请,需要三位。真实业务可能需要不同角色,而不只是不同人员。本篇先用最小规则展示问题:如果创建两个任务,却允许 bob 领取并批准两次,系统虽然看见“两张通过票”,却没有得到“两个人的独立判断”。因此领取和完成时都要检查同一申请、同一资料版本下是否已有该用户的完成记录。

图 4:申请 A 的 v1 两席已通过;申请 B 即使补交到资料 v2,仍使用创建时绑定的规则 v1;新申请 C 才采用三席规则 v2。

规则版本是长期运行流程的基本功。运营今天宣布以后新客户需要三人会签,并不自动决定昨天已经交给两人审核的申请怎么处理。可以把旧实例留在旧规则下完成,也可以经过明确迁移,把在途申请改为新规则并补建任务,但这需要业务批准和数据核对。我们在教学实例里选择最清楚的方案:创建申请时绑定rule_version,该申请的材料更新只改变资料版本,不暗中改变会签人数;新申请可使用新版本。这样处理不是所有企业的默认答案,而是一个可执行、可审计的选择。

注意“规则版本”与“资料版本”独立。资料 v2 可能只是补了一份营业执照,仍遵守旧会签政策;规则 v2 可能要求多一位审核人,却不改变客户已经提交的材料。若把两者混成一个version字段,开发者迟早会遇到“版本 2 到底代表什么”的争议。本篇数据模型将它们明确分开,并把两者都写到任务上,完成时验证当前申请仍匹配。

Camunda 的多实例文档说明一个活动可以以并行或顺序形式产生多个实例,完成条件也需要明确。课程中的“双人会签”正是一个业务完成条件,而不是画两条平行线就自动得到的效果;将来第 20 篇做 BPMN 对照实验时,仍要检查变量汇合与未完成实例的处理。参考:Camunda Multi-instance

五、撤销权限与转派,为什么都要重新核对?

一个常见漏洞发生在任务列表与完成动作之间。alice 早上是运营组成员,领取了任务;下午调离岗位,管理员撤销了她的审核资格。晚上她在旧浏览器页面点击“通过”。如果后端只检查claimed_by == alice,这个操作就会成功。我们需要同时检查任务仍归她、任务未完成、资料版本仍有效、她此刻仍具有审核资格。撤权操作还应释放她持有的未完成任务,使其他人能重新领取。

图 5:本篇实验通过先领取、后撤权、再完成的顺序,证明“曾经有权限”不能代替提交时的授权。

转派也不是简单覆盖一个人名。bob 把任务转给 dave 以后,bob 再提交原页面应被拒绝;dave 要在当前规则下仍有资格,且不能已经占用同一会签中的另一个已完成席位。审计里要同时留下操作者和接收者。若组织要求只有主管能转派,需再加入“转派权限”判断;本篇教学规则采用当前领取人可以转给另一位合格审核员,规则简单,但动作依然明确。

实验用一张memberships表模拟实时权限。生产环境若从 SSO 或目录服务获取角色,还要决定撤权同步延迟、缓存有效期、失败时是否拒绝操作、以及身份服务不可用时如何处理。那些是身份系统与工作流系统的共同约束,不能只在 UI 上隐藏按钮。这里使用字符串bob、alice作为样例身份;它们不是可用于生产的身份认证机制,代码没有会话、令牌或外部目录验证。

还有职责分离:本例销售提交人不能审核自己发起的申请。这个限制在candidate()中执行,不依赖前端是否显示任务。它同样需要基于可信身份;如果客户端可以随意传user_id字符串,所谓职责分离只是一段可绕过的演示规则。未来接入第 03 篇 FastAPI 时,处理人要从认证上下文取得,而不是从请求体里无条件信任。

六、退回材料后,旧审批为什么必须失效?

客户开通申请在审核中被指出附件不清晰。销售重新上传资料,运营决定的对象就变了。即使公司名称、套餐和款项不变,旧资料上的审批结论也不能自动覆盖新资料。系统应明确本轮是否允许补充,补充后生成哪类新任务,已完成的旧任务如何展示。我们选择保留所有旧记录,但禁止它们推进新的资料版本。

图 6:退回结束 v1 审核轮次;补充资料生成 v2 的新席位。规则版本可以保持 v1,说明业务政策和材料对象是两条不同的版本线。

实验里,dave 在 v1 席位提交CHANGES,申请进入NEEDS_INFO;其他未完成席位设为CANCELED。销售提交补充资料后,material_version从 1 增加到 2,程序根据申请绑定的rule_version=1再建立两个新任务。旧任务 ID 包含v1,提交旧任务会因状态或版本不符被拒绝。已经完成的退回决定仍保留在审计里,不能被删除,否则后续无法解释为什么又多了一轮审批。

本教学例子没有处理“材料变了但不影响审批”的细粒度差异。真实项目可能把材料分成身份、合同、资质、设备清单等部分,某一项变更只需要指定角色重审,也可能要求整轮重新开始。关键原则是:审批结果应绑定它所审核的对象,变更以后依据规则判定是否仍可用。不宜先假定所有变更都失效,也不宜默认所有变更都有效;业务规则需要对字段和证据的作用范围做出说明。

签署与到账事实也有类似但不相同的关系。合同签署往往绑定合同版本;只改证明材料,是否需要重签要由合同政策决定。到账事实通常关联付款和业务申请,不能因为资料重新上传就凭空消失。第 01 篇的模拟已经区分这些事实。本篇围绕审批对象版本,不把财务和合同规则强塞进任务表。

七、运行代码:看四组具名场景怎样落库

在本篇目录执行python code/demo.py,不需要安装第三方依赖。脚本创建临时 SQLite 文件、写入申请与任务、执行一组命令,并用断言检查结果。执行结束临时目录自动清理。代码与预期输出分别见 code/demo.py 和 code/expected-output.txt。

claim(db,a1,"alice")revoke(db,"alice")denied(lambda:complete(db,a1,"alice","APPROVE"))claim(db,a1,"bob")claim(db,a2,"carol")assertcomplete(db,a1,"bob","APPROVE")=="REVIEWING"assertcomplete(db,a2,"carol","APPROVE")=="APPROVED"

09-A 展示撤权和会签。09-B 展示转派、退回、补交资料以及旧任务失效。09-C 使用两个独立 SQLite 连接和两个线程同时领取同一席位;只应有一个成功,另一个得到冲突。09-D 再打开数据库文件,证明待办、规则绑定与审计仍在。这些场景覆盖了最容易在演示时被略过的状态变化;代码没有复杂框架,读者可以直接修改样例人名和规则人数继续试验。

09-C 需要说明实验边界:SQLite 的BEGIN IMMEDIATE把写事务串行化,因此在单个本地数据库文件里能给这两次领取确定结果。它没有验证 PostgreSQL 主库里的隔离级别、Web 请求重试、分布式身份目录,也不能证明“所有并发审批都安全”。接入主线时,要用主库的唯一约束、条件更新或行锁和冲突响应重新完成同样验收。SQLite 官方事务文档明确说明BEGIN IMMEDIATE会在已有写事务时受到竞争,读者可以据此理解本地实验的约束。参考:SQLite Transaction

建议自行再做四个改动实验。第一,把申请人sales-1加入审核组后尝试领取自己的任务,仍应因职责分离被拒绝。第二,bob 完成第一席后再尝试占用第二席,应被拒绝,即使他仍是运营组成员。第三,把rule_version=2的新申请设为三席,其中两人通过以后申请仍应保持REVIEWING。第四,在补交 v2 后拿着 v1 的任务 ID 提交,旧结果不得影响 v2。每一个预期都能由数据状态和审计记录验证,不只是看终端有没有抛异常。

八、如何接续第 03 篇主应用?

第 03 篇的applications已有application_id、tenant_id、material_version;workflow_instances有state、rule_version、revision;tasks有 UUID 任务 ID、申请版本与完成信息。本篇接续它时,应在同一套 ID 和租户范围内演化,而不是在前端再造一套“审批表”。具体需要增加:候选组或候选资格、claimed_by、任务状态CLAIMED和CANCELED、会签slot、决定与完成时间、规则版本绑定,以及任务事件审计。可对(instance_id, application_version, slot)加业务唯一约束;不应把实验中方便阅读的复合字符串强行塞进 UUID 主键。第 03 篇已经完成的简单运营审核任务可以视为单席规则 v0;迁移旧数据时要给它们清楚的版本解释。

下一步的 API 可以是领取任务、转派任务、完成任务、申请补交材料,以及管理员撤销审核资格。路由只是入口;真正的检查必须在事务中的命令处理函数里。任务完成时先检查租户与可信身份,再检查申请状态、资料版本、任务状态和当前权限;满足条件后写任务决定,汇总同一轮会签结果,并写迁移历史。对于任务领取,多个请求同抢一个席位,要返回明确冲突,而不是让后提交的人覆盖领取人。

身份系统与待办查询也需要关联。用户在列表中看不到不属于自己的任务,是界面体验与保密要求;用户即使知道任务 ID 也不能调用完成接口,是服务端授权要求。两者缺一不可。若采用缓存的组成员信息,需要定义撤权生效时间与缓存失效路径。对高风险审批,提交时重新验证权限通常比只相信进入页面时的一次检查更合适。审计应能说明实际使用了哪个身份和哪个权限判断结果。

规则版本最好是不可变的定义。发布 v2 时创建新规则,而不是修改数据库里 v1 的“需要两人”为三人,否则旧申请会在不知情下改变验收条件。需要将旧实例迁到新规则时,应提供显式迁移命令,说明旧票怎样处理、是否补建任务、是否要求重新审批,并把操作者与理由写入历史。第 12 篇会专门讨论长期运行流程的版本与升级,本篇先给人工作业一个不会被静默覆盖的规则绑定。

领取、指定与转派不该混用一个赋值接口

候选任务由有资格的审核员主动领取,适合团队共享待办池;指定任务由主管或规则直接分派给具体人员;转派是已有责任人或授权管理者把任务交给另一人。这三种操作最终都可能改变claimed_by,但业务含义和授权主体不同。如果系统只暴露“修改 assignee”接口,任何拿到任务 ID 的调用者都可能把任务指派给自己,甚至把它转给没有资格的人。接入主应用时,命令名应反映动作:领取检查候选资格,指定检查分派权限,转派检查当前持有与接收人资格。

任务领取还要处理占用时间。有的人点了领取却请假,任务可能长时间停在个人名下。业务可以允许主动释放、主管收回、领取租约到期或超时升级。每一种都有不同的审计:释放表示本人归还,收回表示授权管理动作,租约到期表示系统政策。不能用“隔一段时间把所有 CLAIMED 改回 OPEN”替代这些政策,否则审核员在提交表单时可能突然失去任务,却不知道为什么。第 08 篇的期限技术能提供触发时刻,但决定如何重新分配仍属于人工任务规则。

本篇 SQLite 脚本故意只实现领取、转派、权限撤销和完成,让每一种决定容易看见。生产待办中心还应考虑代理审批与请假授权、岗位变更、跨部门任务、组织层级、服务账号误用和管理员紧急接管。这些能力不用一开始全建,但项目至少要写出“谁有权改变任务责任”的清单。待办系统若只会把任务发给人,却没有办法解释任务为什么在某人手上,迟早会回到群聊催办。

会签的否定结果要写成业务政策

很多教程只演示两个人都点通过。真实流程更常遇到一人通过、一人要求补充材料,或者一人长时间没有响应。本例采用“任一席位要求补充,结束本轮并取消其余未完任务”的政策,理由是材料不完整时继续收集赞成票意义不大。另一些场景可能要求所有人完成后再汇总意见,或由主管在意见冲突时仲裁。技术上都能实现,重要的是不要让 SQL 查询行数决定了业务政策。

如果一位审核员已经通过,另一位退回,补交 v2 后第一位是否需要重新看?本例要求重新审核,因为对象版本改变,旧通过仍保留在历史但不计入新轮次。业务也可能允许某些与变更无关的审批继续有效,例如财务只核对付款而材料只更换设备照片。要采用这种细粒度复用,系统必须保存每个决定覆盖的字段或证据范围,以及变化是否触及该范围。没有这些信息,就不能仅凭“第一位已经同意过”自动复用票数。

若会签期间审核人离职或权限撤销,已完成的决定是否追溯失效,也需要业务明确。本篇规则只阻止撤权以后再完成任务,并释放未完成任务;已经合法提交的历史决定不因以后调岗自动删除。若发现当时身份被冒用,或者审批资格在决定发生前就应撤销,则应走异常复核或显式作废流程。审计记录必须保留原决定与后续纠正,否则团队无法回答为什么同一申请曾被标成通过又重新审核。

一个容易忽视的审计问题是“被拒绝的命令”是否需要记录。权限被撤销后 alice 再尝试完成任务,申请状态当然不能改变;但高风险系统通常仍要在安全日志中记录这次尝试的身份、目标任务、失败原因与请求来源,供事后排查。业务审计记录有效的审批决定,安全审计记录异常访问与拒绝,两者服务的目的不同。本篇脚本为了保持最小,只把有效任务动作写入audit,用抛错和测试断言验证拒绝;正式接入服务时应补充拒绝事件记录,并注意不要把完整敏感材料写进日志。

还要处理“领取时有权限,完成事务中权限刚被撤销”的竞争。若撤权与完成请求落在同一数据库事务边界内,二者应按明确顺序生效;若权限来自外部目录,主库事务无法自动锁住远端目录的状态。这时要定义授权信息的生效口径,例如短期有效的权限版本、提交时同步查询,或管理员撤权后让已有任务强制失效。任何口径都需要与安全团队一起验证,不能宣称一次数据库锁便解决了跨服务权限竞争。本篇 SQLite 样例只有本地成员表,因此它演示的是本地顺序,不是完整企业身份治理。

九、验收与 Workflow Thinking

本篇的验收不是“页面上有领取、转派按钮”,而是四种可观察行为:撤权后旧领取人完成请求被拒绝且任务可再分配;同一人不能形成两位审核员的会签结果;资料 v1 的任务不能批准 v2;旧申请继续遵守旧规则,新申请按新规则生成任务。运行输出已经覆盖这些路径,正式集成时还要补充身份提供方、租户边界、HTTP 冲突码和数据库迁移验证。

Workflow Thinking:为什么会签结果不能只用approve_count += 1?因为一个数字不知道每一票是谁、针对哪一份材料、依据哪个规则,也不知道某位审核员撤权或退回后哪些票仍有效。多个任务的决定与版本记录看上去比计数器多,但它们保留了业务意义。只有先保存这些事实,系统才可能在政策变化或争议出现时重新解释结论。

下一篇把已经审核、签署和到账的申请送往 ERP 与服务平台。那里不存在一个能同时提交两个系统的本地事务:ERP 建档成功而权益开通失败时,任务不再只是“请人看一眼”,而需要选择继续、补偿或交给授权人员对账。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询