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