审批流程
把记录路由给人签核 —— 同时不让自动化悄悄绕过行级安全。
**审批就是带审批节点的流程:流程暂停,直到有人批准或拒绝,然后沿匹配的分支继续。**没有需要另学的独立审批引擎 —— 触发器、分支、错误处理都与任何其他流程完全一致。本页新增的内容是审批节点本身,以及两个与路由同样重要的访问决策。
谁做什么
三个角色,刻意分离:
| 角色 | 能做 | 需要 |
|---|---|---|
| 构建者 | 编写和编辑审批流程 | manage_metadata(通常是 Studio 用户) |
| 提交人 | 提交进入流程的记录 | 正常的记录访问 —— 不需要任何自动化配置权限 |
| 审批人 | 处理审批请求(批准 / 拒绝) | 由其权限集授予的能力 |
最终用户提交记录、处理请求;他们从不编辑自动化。把自动化配置界面挡在消费者应用之外。
以谁的身份运行 —— 安全决策
流程通过 runAs 声明运行身份,对审批而言,正是这个决策防止流程悄悄绕过行级安全:
runAs | 数据操作以谁运行 | 何时使用 |
|---|---|---|
'user'(默认) | 提交人,遵守其 RLS | 流程只触碰提交人本来就能看到的记录 |
'system' | 提权 —— 绕过 RLS | 流程必须读写提交人看不到的记录(写入总账、通知一个拥有提交人不可见行的审批人) |
显式声明提权,让它可见而非意外。默认的 'user' 意味着审批流程无法悄悄给提交人跨租户或跨所有者的触达 —— 提权是自愿开启且可审计的。
**提示:**由定时触发的升级流程没有触发用户 —— 它必须
runAs: 'system'才能行动。这是提权的正当场景;"到处system好让它能跑"才是反模式。
审批节点
节点声明谁来审批以及多个决策如何聚合:
{
id: 'manager_approval',
type: 'approval',
label: 'Manager Approval',
config: {
approvers: [{ type: 'field', value: 'owner_manager_id' }],
behavior: 'unanimous',
approvalStatusField: 'approval_status',
lockRecord: true,
},
}| 配置 | 作用 |
|---|---|
approvers | 谁必须决策 —— 具名用户、岗位,或从记录字段解析出的用户(如提交人的经理) |
behavior | 多个审批人如何聚合:'first_response'(默认)、'unanimous'、'quorum'、'per_group' |
minApprovals | 所需批准数 —— quorum 为总数(N 中取 M),per_group 为每组所需数(默认 1) |
approvalStatusField | 可选的记录字段,插件把请求状态镜像进去 |
lockRecord | 请求待定期间锁定记录、禁止编辑 |
警告 ——
position与成员层级。{ type: 'position', value: 'finance_manager' }路由给某个岗位的持有者(sys_user_position)。组织成员层级(sys_member.role:owner/admin/member)自 16.0 起写作type: 'org_membership_level';旧写法role是保留一个版本的弃用别名 —— 仍能加载并按相同方式解析,但会给出警告,下个大版本移除。把岗位名写成成员层级类型谁也匹配不上,请求就会卡住。os lint对两种情况都会标记(approval-approver-not-membership-tier、approval-approver-type-deprecated);如果值是岗位名,正确写法是type: 'position'。
法定人数与会签(16.0)
在 first_response 和 unanimous 之外,16.0 新增两种聚合模式。**Quorum(法定人数)**在达到 N 中取 M 个批准时定案:
config: {
approvers: [
{ type: 'user', value: 'director_a' },
{ type: 'user', value: 'director_b' },
{ type: 'user', value: 'director_c' },
],
behavior: 'quorum',
minApprovals: 2, // 3 人中任意 2 人批准
}**Per-group(会签)**要求每个带标签的组都签核 —— 用 group 给审批人打标签,只有当每个组都达到 minApprovals 个批准(默认每组 1 个)时节点才会前进:
config: {
approvers: [
{ type: 'position', value: 'legal_counsel', group: 'legal' },
{ type: 'position', value: 'finance_manager', group: 'finance' },
],
behavior: 'per_group', // 法务一个签核,且财务一个签核
}对所有模式都成立的语义:
- 审批人集合按开启时快照统计,并带休假(OOO)替补 —— 谁必须响应在请求开启时就已确定,休假的审批人会被替补而不是卡住请求。
- 单个拒绝仍然是一票否决:在任何模式下它都把节点定案为
rejected。 - 阈值在运行时**收拢(clamp)**到可解析的审批人数量,因此配置错误的
minApprovals永远不会让请求死锁。 - 没有
group标签的审批人各自成组,所以普通的审批人列表在per_group下依然行为合理。
一个完整的审批流程
把大额提案路由给负责人的经理,再按决策分支:
export const opportunityApproval = defineFlow({
name: 'opportunity_approval',
label: 'Opportunity Approval',
type: 'record_change',
status: 'active',
runAs: 'user', // 用提交人的 RLS,除非某一步确实需要更多权限
nodes: [
{
id: 'start',
type: 'start',
config: {
triggerType: 'record-after-update',
objectName: 'opportunity',
condition: "record.amount >= 50000 && record.stage == 'proposal'",
},
},
{
id: 'manager_approval',
type: 'approval',
label: 'Manager Approval',
config: {
approvers: [{ type: 'field', value: 'owner_manager_id' }],
behavior: 'unanimous',
approvalStatusField: 'approval_status',
lockRecord: true,
},
},
{ id: 'mark_approved', type: 'update_record', label: 'Mark Approved' },
{ id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' },
{ id: 'end', type: 'end' },
],
edges: [
{ id: 'e1', source: 'start', target: 'manager_approval' },
{ id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' },
{ id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' },
{ id: 'e4', source: 'mark_approved', target: 'end' },
{ id: 'e5', source: 'mark_rejected', target: 'end' },
],
});注意带标签的边:approve 和 reject 命名了决策之后的分支。永远要建模拒绝路径 —— 只有批准分支的流程会让被拒绝的记录搁浅。
**多级审批:**串联多个 approval 节点。**并行审批:**见流程中的聚合节点模式。
审批人体验到什么
@objectstack/plugin-approvals 包持有持久化的审批状态。请求待定期间:
- 插件持久化请求(
sys_approval_request)以及针对它的每个决策(sys_approval_action)—— 你的审批历史是可查询的数据。 - 设置
lockRecord: true时,记录被锁定、禁止编辑,直到决策落地。 - 如果设置了
approvalStatusField,记录自身的字段会镜像请求状态,视图和报表就能按它筛选。 - 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。自 16.0 起,决策可以携带文件附件(持久化在
sys_approval_action.attachments,贯通决策/评论路由与客户端 SDK)—— 签署的 PDF 和证据材料与决策存放在一起。 - 请求暴露服务端计算的
decision_progress——unanimous/quorum为已获批准数 vs. 所需数,per_group为按组明细 —— 收件箱和你自己的 UI 直接渲染进度,无需重新实现计票。
**决策即声明式操作(16.0)。**完整的决策集合 —— 批准、拒绝、转办、退回修改(/revise)、补充材料、催办、撤回、重新提交 —— 以 type: 'api' 操作的形式声明在 sys_approval_request 上,带类型化参数、仅待定时可见的门控,以及提交人 vs 审批人谓词(如 record.submitter_id == ctx.user.id)。Console 审批收件箱渲染的是这些声明的操作,而不是手写按钮,因此新的决策能力以元数据形式发布 —— 无需客户端发版。
批准本身也是被门控的操作。把"可以批准"建模为一个能力(如 approve_invoice),由审批人的权限集授予,并把批准操作的 requiredPermissions 建立在它之上 —— 这样门控就在 UI 和服务端两侧同时强制,而不只是从屏幕上藏起来。
最佳实践
| 应该 | 不应该 |
|---|---|
| 定义清晰的进入条件 | 设置过多的审批环节 |
| 设置合理的超时时间 | 把审批做得过于复杂 |
| 在合适的场景允许撤回 | 忘记拒绝路径 |
| 通知所有相关方 | 把审批人写死 |
| 追踪审批历史 | 只在 UI 层门控"批准" |
默认 runAs: 'user',一步一步地提权 | 到处设置 runAs: 'system' "好让它能跑" |