必须严格分离公共字段与渠道字段,核心公共字段仅限_id、order_no、amount、currency、status、created_at、updated_at、user_id、merchant_id;channel为枚举字符串,渠道特有字段统一收进channel_data嵌套结构并按channel值命名子文档,对账查询需联合channel和channel_data路径条件,事务操作须用snapshot读级别并带状态前置条件。

主集合只存公共字段,用 channel 字段做语义路由
聚合支付网关的交易文档如果把微信 prepay_id、支付宝 trade_no、银联 tn 全部平铺在顶层,很快会变成稀疏、难索引、无法聚合的“字段沼泽”。必须严格分离公共字段与渠道字段。
核心公共字段仅限:_id、order_no、amount、currency、status(如 "success" / "failed" / "pending")、created_at、updated_at、user_id、merchant_id。所有这些字段含义必须跨渠道一致。
channel 必须是枚举字符串,例如 "wechat"、"alipay"、"unionpay"、"stripe",不能是自由输入或数字编码;也不允许留空或设为 null。
错误示例:{"order_no":"ORD123","alipay_trade_no":"20240512...","wechat_prepay_id":"wx123...","status":"success"}
——这会让后续查“所有成功交易”时,find({status:"success"}) 返回大量字段不全、结构混乱的文档。
渠道特有字段统一收进 channel_data 嵌套结构
所有渠道专属字段必须塞进 channel_data 这个标准字段,且子结构按 channel 值命名,例如 channel_data.wechat、channel_data.alipay。不能拆成多个并列字段(如 wechat_data + alipay_data),否则聚合、索引、权限控制都会碎片化。
各渠道内部字段名建议加前缀(如 wx_prepay_id、ali_trade_no),避免不同渠道字段同名冲突(比如都叫 out_trade_no 但语义不同)。
写入时必须校验:channel 值与 channel_data 的键名严格匹配。例如 channel: "alipay" 时,channel_data 中必须存在 alipay 子文档,且不能多出 wechat 字段。
推荐写法:{"channel":"alipay","channel_data":{"alipay":{"ali_trade_no":"20240512...","ali_pay_url":"https://..."}}}
对账查询必须联合 channel 和 channel_data 路径条件
单纯用 find({channel:"wechat"}) 查微信订单,可能捞出早期未写入 channel_data 的脏数据,或被其他渠道误写入的空对象。对账逻辑要求精确命中渠道上下文。
高频对账场景应显式检查子字段存在性与有效性:
- 查已回调的微信支付订单:
find({ channel: "wechat", "channel_data.wechat.wx_result_code": "SUCCESS" }) - 查待对账的支付宝交易(含真实金额和时间戳):
find({ channel: "alipay", "channel_data.alipay.ali_trade_no": { $exists: true }, "channel_data.alipay.ali_gmt_payment": { $exists: true } }) - 排除测试环境订单:
find({ channel: "wechat", "channel_data.wechat.wx_env": "prod" })
注意:MongoDB 对嵌套字段路径的索引需显式创建,例如:db.transactions.createIndex({ "channel": 1, "channel_data.wechat.wx_unionid": 1 })
事务内更新必须带状态条件,且读取用 snapshot 级别
财务对账不是简单标记状态,而是“查旧账 → 比差异 → 写结果 → 更新流水状态”四步原子操作。任何一步失败都必须回滚,否则出现资金错账。
关键点:
- 所有 DB 操作(
findOne、updateOne、insertOne)在withTransaction()回调中必须await,漏掉await就等于跳出事务 - 读取必须指定
readConcern: { level: "snapshot" },否则可能读到其他事务未提交的中间态(比如另一笔对账刚插入但未提交的临时记录) - 更新流水状态必须带前置条件,例如:
updateOne({ _id: id, status: "pending" }, { $set: { status: "reconciled", reconciled_at: new Date() } }, { session }),防止并发重复处理 - 禁止在事务里调外部 API(如调支付平台查单接口),它无法回滚;应提前在事务外完成核验,只把确定结果传入事务
最容易被忽略的是:本地开发时单节点 MongoDB 默认不支持事务,必须启动副本集(mongod --replSet rs0)并初始化(rs.initiate()),连接字符串还要带 ?replicaSet=rs0,否则 withTransaction() 直接报错。


















