怎么设计MongoDB的邀请码裂变结构_父子层级与奖励统计

1次阅读

必须用referralCode双向索引并建invitedBy普通索引;三层以上邀请链需预存ancestors数组;rewardLogs须独立分片,奖励统计用原子更新totalInviteRewards字段;所有关系变更须事务强一致。

怎么设计MongoDB的邀请码裂变结构_父子层级与奖励统计

邀请码关联必须用 referralCode 字段做双向索引

用户注册时填的邀请码,不能只存进被邀请人文档里就完事。mongodb 没有外键约束,父子关系靠字段维系,一旦漏建索引或字段命名不统一,后续查“谁邀请了谁”或“某人邀请了几个人”会极慢甚至查不准。

实操建议:

  • users 集合中固定两个字段:referralCode(本用户的唯一邀请码)、invitedBy字符串,存上级的 referralCode
  • invitedBy 建普通索引:db.users.createIndex({ invitedBy: 1 });若常按邀请人查全路径,再加一个复合索引:{ invitedBy: 1, createdAt: -1 }
  • 避免用 userIdinvitedBy —— 用户注销或 ID 格式变更时,关系链直接断裂;而 referralCode 是业务层生成、稳定不变的标识
  • 注册时校验 invitedBy 是否真实存在且未禁用,否则写入空值或拒绝,别让脏数据进库

三层以内裂变用递归聚合,超三层必须预计算 ancestors 数组

MongoDB 的 $graphLookup 能查邀请链,但深度超过 3 层后性能陡降,尤其集合大了以后容易超内存或超时。真实业务里,奖励结算、排行榜、风控扫描都要求毫秒级响应,不能每次请求都现场遍历。

实操建议:

  • 新用户注册/邀请关系变更时,同步更新其文档的 ancestors 字段(字符串数组),例如:["A123", "B456", "C789"] 表示 A 邀 B、B 邀 C、C 邀当前用户
  • 这个数组长度建议硬性限制为 3 或 5(根据业务规则),避免无限蔓延;超出部分截断,不补全
  • 查“某人所有下级”时,不再用 $graphLookup,改用:{ ancestors: { $in: ["X999"] } }{ invitedBy: "X999" } 分层查,快且可控
  • 注意:ancestors 必须在事务内更新(如果用 MongoDB 4.0+),否则并发注册可能导致父链错位

rewardLogs 集合必须分片 + 写时分离,别和 users 混在一起

奖励发放是高频写操作,每笔都要记日志、扣余额、更新统计。如果把奖励记录嵌在 users 文档里,很快会触发 BSON 16MB 限制;如果全在单个集合,写入瓶颈和查询压力都会集中爆发。

实操建议:

  • 单独建 rewardLogs 集合,字段精简:必含 userIdtype(如 "invite_bonus")、amounttriggeredBy(谁触发的)、createdAt
  • userId 分片(shard key),避免热点;如果用副本集且量不大,至少加 TTL 索引自动清理半年前日志:db.rewardLogs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 * 24 * 180 })
  • 统计“某人累计获得邀请奖励”时,别实时 $sum 全表,而是维护一个 totalInviteRewards 字段在 users 里,每次发奖后原子更新:db.users.updateOne({ _id: userId }, { $inc: { totalInviteRewards: 10 } })
  • 切记:奖励金额必须用整数单位(如“分”),别存浮点型 NumberDecimal —— 多次加减后精度漂移,对账时哭都来不及

父子层级变更时,ancestorstotalInviteRewards 必须强一致性更新

用户注销、邀请码作废、关系申诉重置……这些操作会动到层级结构。如果只删 invitedBy 却不清理 ancestors,或者只清日志不回滚统计,奖励就会多发或少发,而且很难追溯。

实操建议:

  • 任何影响邀请关系的操作,必须走统一 service 方法,内部用 session.startTransaction() 包裹:先查原链,再删子树 ancestors,再递减各级 totalInviteRewards,最后删对应 rewardLogs
  • 不要依赖定时任务“事后修正”——延迟高、难调试、易漏;修复逻辑比正向逻辑更复杂,优先堵住源头
  • 上线前用脚本抽样验证:随机选 100 个三级邀请链,检查 ancestors 长度、内容、对应用户的 totalInviteRewards 是否与 rewardLogs 总和一致

层级深了之后,最麻烦的不是怎么存,而是怎么保证每次写都把所有相关字段同步改对。字段分散在不同集合、不同文档里,一个漏掉,整个链就偏了。

text=ZqhQzanResources