本文目录
上级:后端学习 from zero
场景:拉新。如果一个用户邀请新用户达到一定数量,可以获得勋章。
最朴素的设计是:
用户注册时提交邀请码
-> 查询邀请码对应的用户
-> 给邀请人邀新数量 +1
-> 达到条件后展示勋章
这能跑通最短主流程,但直接落地很容易在重复计数、作弊、回滚、并发一致性上出问题。
主流程驱动 + 风险校正
很多结构不是凭空设计出来的,而是从朴素方案暴露的问题中长出来的:
- 因为担心重复加 1,所以引入幂等。
- 因为担心以后查不清,所以引入关系记录。
- 因为担心规则变更,所以区分事实和派生值。
- 因为担心主链路太重,所以拆异步。
- 因为担心误发,所以把「展示勋章」改成「授予记录」。
判断风险是否值得处理,可以看两个维度:
- 发生概率高不高。
- 一旦发生损失大不大。
优先处理高概率高损失,以及低概率高损失。
风险审查角度
常用的几个角度:
1. 正确性风险
2. 一致性风险
3. 幂等 / 重复风险
4. 性能 / 容量风险
5. 可演进性风险
第一轮修正
邀请关系不能只存一个累计数。否则回答不了:
- 这几个人分别是谁。
- 哪天拉来的。
- 哪几个后来被风控判无效。
- 哪几个是同设备刷的。
- 哪几个已经计入勋章。
- 运营申诉时怎么查。
勋章也不应该只是「达到后显示一下」,而应该有明确的授予记录。
更清楚的流程:
- 邀请人拥有一个绑定自己的邀请码。
- 新用户注册时提交邀请码。
- 注册接口校验邀请码,并建立邀请关系。
- 邀请关系初始状态记为
pending或valid。 - 满足有效邀新规则后,把关系置为
valid。 - 系统统计邀请人的有效邀新数。
- 如果达到门槛,写入勋章授予记录。
- 前端读取勋章记录进行展示。
关键变化是:邀请账号和邀请关系本身都可能存在无效状态,因此需要可追溯的关系表和状态字段。
邀请码是否应该独立成表
如果把邀请码当作用户表上的一个字段,会遗漏这些业务语义:
- 用户想换邀请码。
- 邀请码需要停用。
- 邀请码需要过期。
- 需要知道某个码本身的使用情况。
如果邀请码只是 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,但它们是派生值,必须允许从关系表重算;不要让汇总表成为唯一事实来源。
业务逻辑
被邀请方:
- 用户注册时提交邀请码。
- 校验
invite_code是否存在、可用、未过期、非自邀。 - 写入
invite_relation。 - 判定这条关系是否立即
valid,否则先留pending。 - 如果变成
valid,统计邀请人的有效邀请数。
邀请方:
- 读取当前邀请码。
- 刷新邀请码:失效旧码,生成新码。
- 失效邀请码:修改当前邀请码状态。
邀请链接不一定要落库。链接是邀请码的派生字段,可以在 response 中根据注册地址和 code 动态生成。
后端 Checklist
对邀请码场景,优先级最高的是:
- 唯一约束和幂等。
- 状态流转。
- 事务一致性。
- 勋章发放防重复。
- 基础反作弊。
更通用的检查清单:
- 会不会重复生效。
- 会不会并发错发。
- 状态迁移是否受控。
- 关系归因是否准确。
- 事务失败会不会留下半成品。
- 运营能不能查清和修正。
- 是否容易被作弊。
- 规则变化后能不能重算。
并发与接口设计
注册绑定邀请码时,后端至少要在事务中完成:校验邀请码状态、校验非自邀、插入邀请关系。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 和撤销记录修正,不要硬删历史关系。