状态字段必须是原子化字符串,责任人存为有序id数组,流转校验全部压在应用层的findoneandupdate条件更新里——别信“mongodb能自动管状态机”这种话。

直接说结论:状态字段必须是原子化字符串,责任人存为有序 ID 数组,流转校验全部压在应用层的 findOneAndUpdate 条件更新里——别信“MongoDB 能自动管状态机”这种话。
状态字段为什么只能用字符串,不能用对象或数字
常见错误是把状态塞进嵌套结构里,比如 { status: { current: "pending", step: 2 } } 或 { status: 1 }。前者让 find({ "status.current": "pending" }) 无法走索引;后者导致前后端对齐崩溃(前端传字符串 "1",后端查数字 1,匹配失败)。更糟的是运维手动改状态时输个 4,没人知道它代表啥。
正确做法是:状态字段直接设为 "pending"、"approved"、"rejected"、"cancelled" 这类固定枚举值。所有值统一定义在常量对象里,写入前用 Object.values(STATES) 白名单校验。
- 查询快:
find({ status: "pending" })可命中复合索引{ status: 1, updatedAt: -1 } - 日志可读:运维看日志一眼就知道当前卡在哪
- 聚合简单:不用先
$convert再比,少一层出错可能
责任人数组怎么存才支持“取下一个审批人”和“中途换人”
责任人不是“谁审过”,而是“谁该审”。所以 approvers 字段必须是纯 ID 数组,如 ["u_abc", "u_def", "u_ghi"],顺序即审批顺序。别用 approvers: [{ id: "u_abc", status: "approved" }] 这种嵌套结构——它既没法用 $in 快速判断某人是否在待审队列中,也难以原子更新“下一个待审人”。
配合一个 currentApproverIndex 数字字段使用:
- 取当前审批人:
$arrayElemAt: ["$approvers", "$currentApproverIndex"] - 推进到下一步:
$inc: { currentApproverIndex: 1 } - 换人时直接
$set: { "approvers.1": "u_new" },不重写整个数组 - 注意兜底:聚合中加
$cond判断索引是否越界,避免取到null
状态流转为什么必须用 findOneAndUpdate 带条件 filter
典型翻车场景:前端传个 { status: "approved", approvers: ["u_xyz"] } 直接调 updateOne,后端没检查当前 status 是否真是 "pending",结果跳过中间节点直接批准。
MongoDB 没有事务级状态机钩子,变更流(change stream)延迟高、不可靠,且无法阻止非法写入。所有规则必须在业务代码里硬控:
- 更新语句必须带守门条件:
{ _id: id, status: "pending" },而不是只写{ _id: id } - 用
findOneAndUpdate并检查返回的matchedCount和modifiedCount:为 0 说明状态已变,需提示用户“已被他人处理” - 拒绝后不允许再批准、上一节点未通过不能激活下一节点——这些逻辑全在应用层 if/else 里写死,别幻想数据库替你拦
历史记录要不要内嵌?内嵌多少条才安全
可以内嵌,但只嵌最近 5–10 条,且每条只保留必要字段:by(用户 ID)、status(字符串)、at(ISODate)、comment(截断到 200 字以内)。别把完整 user 对象或长文本评论全塞进去——三个月后文档大小飙到 12MB,find({ _id }) 都开始不稳定。
用 $push + $slice: -10 自动截断:
{ $push: { history: { $each: [newEntry], $slice: -10 } } }
需要查全部历史?单独建 content_audit_log 集合,用 content_id 索引。主文档膨胀是 MongoDB 生产中最隐蔽也最致命的问题之一,它不会报错,只会让查询越来越慢、复制越来越卡、备份越来越不敢动。











