审批节点状态字段应使用原子化字符串枚举值(如"pending"),单独存储并建立复合索引;责任人存为有序ID数组配合currentApproverIndex字段;状态流转校验必须在应用层通过条件更新严格控制。

审批节点状态字段怎么建才不翻车
状态字段必须用原子化字符串,别存对象或嵌套结构。MongoDB 查询和更新都依赖字段路径的确定性,status 字段直接设为 "pending"、"approved"、"rejected"、"cancelled" 这类固定枚举值,避免用 { current: "pending", step: 3 } 这种结构。
常见错误是把状态逻辑塞进数组里,比如 workflow: [{ step: 1, status: "done" }, { step: 2, status: "pending" }]——这会让「查当前卡在哪一步」变成聚合管道噩梦,且无法利用索引高效过滤。
-
status单独成字段,加compound index(如{ status: 1, updatedAt: -1 })支撑列表页分页查询 - 如果真要记录历史,另建
approval_history子集合,用nodeId+timestamp关联主文档 - 状态变更必须用
findOneAndUpdate带upsert: false和明确filter(比如只允许从"pending"变成"approved"),防并发误刷
责任人数组怎么存、怎么查、怎么更新
责任人不是“谁审过”,而是“谁该审”——所以 approvers 字段应存待审人 ID 列表,顺序即审批顺序,比如 ["u_abc", "u_def", "u_ghi"]。不要存已审人,也不要用 Map 或对象包装。
常见错误是把责任人存在嵌套文档里:approvers: [{ id: "u_abc", role: "manager", status: "approved" }]。这导致无法用 $in 快速定位某人是否在当前待审队列中,也难以原子更新“下一个待审人”。
- 用
approvers数组 +currentApproverIndex数字字段配合使用,比维护一个“下一个ID”更可靠(避免ID重复或失效时逻辑断裂) - 更新当前节点时,用
$inc: { currentApproverIndex: 1 },再用$arrayElemAt: ["$approvers", "$currentApproverIndex"]在聚合中取下一人——注意索引越界要兜底 - 如果某人离职或被替换,直接
$set: { "approvers.1": "u_new" }即可,无需重写整个数组
状态流转校验必须在应用层做,不能靠 MongoDB 触发器
MongoDB 本身没有事务级状态机钩子,4.2+ 的变更流(change stream)延迟高、不可靠,且无法阻止非法状态写入。所有流转规则——比如“只有上一节点通过才能激活下一节点”“拒绝后不允许再批准”——必须在业务代码里用条件更新硬校验。
典型翻车场景:前端传个 { status: "approved", approvers: ["u_xyz"] } 直接调 updateOne,后端没检查当前 status 是否真是 "pending",结果跳过中间人直接终审。
- 每次更新前,先
findOne拿出旧文档,校验status、currentApproverIndex、approvers长度三者是否匹配预期流转路径 - 用
updateOne的filter把校验条件全写进去,例如:{ _id: id, status: "pending", currentApproverIndex: 0, "approvers.0": "u_current" } - 不要依赖
version字段做乐观锁——MongoDB 没内置 version,手加容易漏更新,不如用updatedAt+$lt组合判断是否被并发修改
复杂分支(会签/或签/跳过)别硬塞进单文档模型
当流程出现“三人中两人同意即通过”或“财务部不填则自动跳过”,就说明单文档数组模型已经撑不住了。强行在 approvers 里加标记字段(如 required: true)会让查询爆炸,索引失效,且无法表达动态条件。
这时候得切出去:主文档只存当前活跃节点类型(nodeType: "or_sign"),具体规则和投票状态放独立子文档或集合,用 processId 关联。
- 会签场景建
sign_votes子集合,每条记录包含processId、approverId、vote、timestamp,方便查“已有几票”“谁还没投” - 跳过逻辑由服务端根据配置实时计算,而不是在文档里预埋
skipIf: { field: "amount", lt: 10000 }这种表达式——执行权不在数据库 - 所有分支判定必须有明确的“决策点文档”,避免状态散落在多个字段里互相耦合
状态和责任人看着简单,但一旦流程变长、角色变多、规则变活,单文档嵌套模型就会在索引、查询、更新三方面同时失守。真正难的不是存,是让每一次 findOneAndUpdate 都能准确回答:“此刻,谁该干啥,以及为啥是他”。


















