状态字段应直接存字符串如"pending",避免数字或枚举类;审核历史内嵌但限最近10条且精简字段;并发更新须用原子条件操作;状态机逻辑放应用层,聚合仅用于查询。

状态字段用字符串还是枚举值?
直接存 "pending"、"approved"、"rejected" 这类字符串最稳妥。别用数字 1/2/3 或自定义枚举类——MongoDB 没类型约束,但人读日志、查数据、写聚合时,"approved" 比 2 少一半理解成本,也避免前后端对齐错位。
常见错误是把状态存在 status: { type: Number, enum: [1,2,3] },结果前端传 "2"(字符串)过来,查询失效;或者运维手动改状态时输错数字,没人知道 4 代表啥。
- 所有状态值统一定义在常量对象里,比如
STATES = { PENDING: "pending", APPROVED: "approved", ... } - 写入前必须校验,用
Object.values(STATES)做白名单判断,别只靠 schema - 聚合查询时直接匹配字符串:
{ status: "approved" },别绕路转成数字再比
审核历史该不该内嵌进主文档?
内嵌,但只嵌最近 5–10 条,且每条只存必要字段。别把整个 user 对象或长文本评论全塞进去——主文档膨胀后,单次读取变慢,复制集同步压力大,甚至可能超 16MB 限制。
典型翻车场景:每次审核都 push 一个完整 { user: { _id, name, email }, comment: "...", timestamp: ..., status: ... },三个月后某条内容有 87 条记录,文档大小飙到 12MB,find({ _id }) 开始不稳定。
- 历史项只保留:
by(用户 ID)、status(字符串)、at(ISODate)、comment(截断到 200 字以内) - 用
$push+$slice: -10自动截断:{ $push: { history: { $each: [newEntry], $slice: -10 } } } - 需要查全部历史?单独建
content_audit_log集合,用content_id索引,不强求内嵌
怎么防止并发审核导致状态错乱?
靠 MongoDB 的原子操作 + 条件更新,不是靠应用层锁。用 findOneAndUpdate 加 filter 锁定合法前序状态,比如“只有当前是 "pending" 才能改成 "approved"”。否则两个编辑同时点通过,可能一人覆盖另一人的拒绝操作。
错误做法是先 find 出来,判断 status === "pending",再 updateOne —— 中间有窗口期,第二个请求进来时看到的还是旧状态。
- 更新语句必须带状态守门:
{ _id: id, status: "pending" },而不是只写{ _id: id } - 检查返回结果的
matchedCount和modifiedCount:为 0 说明状态已变,需提示用户“内容已被他人处理” - 不要依赖
version字段做乐观锁——MongoDB 没内置版本控制,自己维护容易漏
状态机逻辑该放哪?应用层还是聚合管道?
核心流转规则(比如“驳回后不能再次提交”“草稿只能由作者修改”)必须在应用层校验并执行。聚合管道适合查,不适合控——它没法阻止非法写入,也不能抛业务异常。
有人试图用 $cond 在聚合里模拟状态跳转,结果发现:管道跑完只是返回新字段,原始文档没变;而且复杂条件一多,可读性归零,调试全靠猜。
- 状态迁移入口统一收口到一个函数,比如
transitionContent(contentId, action, userId, comment) - 函数内先查当前状态,再根据
action("approve"/"reject"/"resubmit")查规则表,最后调用原子更新 - 聚合只用于报表场景,例如:
db.contents.aggregate([ { $match: { "history.at": { $gt: ISODate("...") } } }, ... ])
真正麻烦的是跨系统协同——比如审核通过后要触发通知、生成静态页、更新搜索索引。这些动作别塞在状态更新事务里,用变更流(change stream)监听 status 字段变化更可靠,也解耦。

















