Bakkac's Blog
本文目录

上级:后端学习 from zero

场景:拉新。如果一个用户邀请新用户达到一定数量,可以获得勋章。

最朴素的设计是:

用户注册时提交邀请码
-> 查询邀请码对应的用户
-> 给邀请人邀新数量 +1
-> 达到条件后展示勋章

这能跑通最短主流程,但直接落地很容易在重复计数、作弊、回滚、并发一致性上出问题。

主流程驱动 + 风险校正

很多结构不是凭空设计出来的,而是从朴素方案暴露的问题中长出来的:

  • 因为担心重复加 1,所以引入幂等。
  • 因为担心以后查不清,所以引入关系记录。
  • 因为担心规则变更,所以区分事实和派生值。
  • 因为担心主链路太重,所以拆异步。
  • 因为担心误发,所以把「展示勋章」改成「授予记录」。

判断风险是否值得处理,可以看两个维度:

  1. 发生概率高不高。
  2. 一旦发生损失大不大。

优先处理高概率高损失,以及低概率高损失。

风险审查角度

常用的几个角度:

1. 正确性风险
2. 一致性风险
3. 幂等 / 重复风险
4. 性能 / 容量风险
5. 可演进性风险

第一轮修正

邀请关系不能只存一个累计数。否则回答不了:

  • 这几个人分别是谁。
  • 哪天拉来的。
  • 哪几个后来被风控判无效。
  • 哪几个是同设备刷的。
  • 哪几个已经计入勋章。
  • 运营申诉时怎么查。

勋章也不应该只是「达到后显示一下」,而应该有明确的授予记录。

更清楚的流程:

  1. 邀请人拥有一个绑定自己的邀请码。
  2. 新用户注册时提交邀请码。
  3. 注册接口校验邀请码,并建立邀请关系。
  4. 邀请关系初始状态记为 pending 或 valid。
  5. 满足有效邀新规则后,把关系置为 valid。
  6. 系统统计邀请人的有效邀新数。
  7. 如果达到门槛,写入勋章授予记录。
  8. 前端读取勋章记录进行展示。

关键变化是:邀请账号和邀请关系本身都可能存在无效状态,因此需要可追溯的关系表和状态字段。

邀请码是否应该独立成表

如果把邀请码当作用户表上的一个字段,会遗漏这些业务语义:

  • 用户想换邀请码。
  • 邀请码需要停用。
  • 邀请码需要过期。
  • 需要知道某个码本身的使用情况。

如果邀请码只是 users 表上的字段,换码就等于覆盖旧值,旧码不可管理;停用、过期等状态也会污染用户表职责。

判断一个字段是否应该成为实体,可以看它自己有没有状态、属性、生命周期。如果有,通常适合单独成表。

数据模型

核心表:

invite_code(
  id,
  code,
  owner_user_id,
  status,
  expires_at,
  created_at,
  updated_at
)

invite_relation(
  id,
  inviter_user_id,
  invitee_user_id,
  invite_code_id,
  source_type,
  status,
  bind_at,
  effective_at,
  invalid_reason,
  created_at,
  updated_at
)

外键放在哪一侧的通用原则:

外键放在从属方、可选方、生命周期依赖另一方的一侧。

所以邀请码和用户之间,邀请码应持有用户 ID。

索引建议

  • invite_code: UNIQUE(code), UNIQUE(owner_user_id)(如果确定一个用户只有一个有效码)。
  • invite_relation: UNIQUE(invitee_user_id), INDEX(inviter_user_id, status, effective_at), INDEX(invite_code_id)。

一般优先级是:索引优先于冗余。

原因:

  • 索引是低风险优化,只改查询路径,不改数据语义。
  • 冗余会引入一致性维护成本。

UNIQUE 在语义上是唯一性约束,数据库通常通过唯一索引实现它。常见类型约束可以按业务规模选择:ID 使用 BIGINT,邀请码使用 VARCHAR(32),状态使用枚举或小整数,时间使用带毫秒的时间类型。

如果以后需要汇总优化,可以增加按用户维护的 pending_invite_count、valid_invite_count 和 invalid_invite_count,但它们是派生值,必须允许从关系表重算;不要让汇总表成为唯一事实来源。

业务逻辑

被邀请方:

  1. 用户注册时提交邀请码。
  2. 校验 invite_code 是否存在、可用、未过期、非自邀。
  3. 写入 invite_relation。
  4. 判定这条关系是否立即 valid,否则先留 pending。
  5. 如果变成 valid,统计邀请人的有效邀请数。

邀请方:

  1. 读取当前邀请码。
  2. 刷新邀请码:失效旧码,生成新码。
  3. 失效邀请码:修改当前邀请码状态。

邀请链接不一定要落库。链接是邀请码的派生字段,可以在 response 中根据注册地址和 code 动态生成。

后端 Checklist

对邀请码场景,优先级最高的是:

  1. 唯一约束和幂等。
  2. 状态流转。
  3. 事务一致性。
  4. 勋章发放防重复。
  5. 基础反作弊。

更通用的检查清单:

  1. 会不会重复生效。
  2. 会不会并发错发。
  3. 状态迁移是否受控。
  4. 关系归因是否准确。
  5. 事务失败会不会留下半成品。
  6. 运营能不能查清和修正。
  7. 是否容易被作弊。
  8. 规则变化后能不能重算。

并发与接口设计

注册绑定邀请码时,后端至少要在事务中完成:校验邀请码状态、校验非自邀、插入邀请关系。invitee_user_id 唯一约束保证同一个被邀请用户只能绑定一次;勋章表使用 (user_id, badge_type) 唯一约束,发放时“尝试插入,插入成功才算首次授予”。

状态迁移也要带旧状态条件,避免已经 invalid 的关系被重复改回 valid:

update invite_relation
set status = 'valid', effective_at = current_timestamp
where id = ? and status = 'pending';

刷新邀请码可以合并为一个接口:先找到当前用户的 active 码并置为 disabled,再插入新的 active 码。邀请链接是邀请码的派生字段,直接在 response 中根据注册地址和 code 生成,不必单独落库。

消息或异步任务重跑时,不能直接“再加一次计数”。应保存事件 ID、关系状态变更和勋章授予记录,并对关键写入建立唯一约束;发现作弊或误绑时用 invalid 和撤销记录修正,不要硬删历史关系。