mongodb 的 auditlog 和 oplog 均不能直接用作工单操作轨迹:auditlog 仅记录命令类型且无业务语义与工单 id 关联;oplog 是秒级精度的底层物理日志,缺少用户、动作描述、前后 diff 及事务上下文,change stream 也无法区分业务意图或还原审批路径。

不能靠 MongoDB 自带功能自动记录工单操作轨迹,必须由应用层控制写入时机、内容和关联关系。
为什么 auditLog 和 oplog 都不能直接用作工单操作轨迹
auditLog 只记录命令入口(如 update、find),不携带业务语义,也不关联工单 ID 或审批节点;oplog 虽含变更细节,但它是底层物理操作日志,ts 时间戳精度为秒级,o 字段里没有用户、动作描述、前后 diff,且事务内多条操作在 oplog 中是离散条目,无法自动拼成“张三在 14:02:33 批准了节点 A”这样的可读轨迹。
常见错误是试图监听 change stream 并过滤 ns: "mydb.tickets" ——它能捕获文档更新,但无法区分“这是状态推进”还是“这是客服补填备注”,更无法还原出完整审批路径。
- auditLog 中
"ns": "mydb.tickets"的日志不含user字段,除非你已开启authorization并配置了auditAuthorizationSuccess - oplog 条目中
"o"是原始 BSON 更新器(如{"$set": {"status": "approved"}}),没有上下文字段如operatorId或comment - change stream 的
fullDocumentBeforeChange选项仅在 6.0+ 启用且需额外配置,且不保证每一步都触发(比如只改数组末尾元素可能被压缩)
推荐结构:独立审计集合 + 应用层原子写入
建一个 ticket_audit 集合,每条记录对应一次**有意义的业务操作**,不是每次数据库 update 都写。关键字段包括:ticketId、traceId(UUID v4)、action(如 "approve"、"reject"、"reassign")、operatorId、before 和 after(精简后的文档快照,只含变化字段)、comment、createdAt。
写入必须与主表更新在同一个事务内完成:
- 使用
session.startTransaction()开启会话 - 先
tickets.updateOne()更新工单状态或字段 - 再
ticket_audit.insertOne()写入审计记录,traceId与上一步传入一致 - 最后
session.commitTransaction()
这样即使中间出错,两条记录要么全写入,要么全不写,避免轨迹断层。
如何避免历史记录膨胀和查询变慢
ticket_audit 很容易成为大集合,但别急着加 TTL 索引删旧数据——很多合规场景要求保留 6 个月以上。更实际的做法是分层存储:
- 热数据(最近 30 天)保留在
ticket_audit,建复合索引{ ticketId: 1, createdAt: -1 } - 冷数据按月归档到
ticket_audit_202608这类集合,用moveChunk或mongodump/mongorestore拆分 - 前端查某工单轨迹时,先查主集合,再查审计集合;不要用
$lookup一次性连表——它会拖慢主表查询,且无法利用ticketId索引做高效跳转
另外,before 和 after 字段建议只存 diff 后的键值对(比如只记 {"status": ["pending", "approved"], "approvedAt": [null, "2026-09-07T14:02:33Z"]}),而不是整个文档快照,省空间也便于前端渲染变更高亮。
真正难的不是“怎么记”,而是“哪些操作算一次有效轨迹”。比如客服后台批量修改 100 个工单的优先级,要不要为每个生成一条审计?答案是否定的——应该记一条 action: "bulk_update_priority",并在 details 字段里放数量和条件,否则审计集合会瞬间膨胀十倍。这个边界,得由业务规则定义清楚,不能交给数据库自动猜。











