498da05995
(工作区既有改动, 非本次会话产出)
5.4 KiB
5.4 KiB
invite_relation — 好友邀请绑定关系(注册即生效)
模型
app/models/invite.py(InviteRelation) · 仓库app/repositories/invite.py· 接口app/api/v1/invite.py:POST /api/v1/invite/bind(写)、GET /api/v1/invite/me(战绩)、GET /api/v1/invite/invitees(列表) · ← 索引 · 总览
一行 = 一次成功的邀请绑定。被邀请人(B)用邀请人(A)的邀请码完成绑定后写入,注册即生效:同一事务里给 A、B 各发 1 万金币(= 1 元,可提现)。数据来自 bind(),触发它的归因来源有三种(channel)。这是邀请功能的结果表 / 账本;邀请码本身存在 user.invite_code,不在这张表。
用在哪 / 增删改查
- C(插入):
POST /api/v1/invite/bind(bind_invite)→invite_repo.bind()。过四道防线后建一行status='effective',同事务复用wallet.grant_coins给双方发金币(grant_coins只 flush,由bind()统一 commit)→ "建关系 + 双方加金币"原子。- 幂等键 =
invitee_user_id唯一:一个 B 只能被绑一次,重复请求返回already_bound,不重复发奖(仿ad_reward_record.trans_id思路)。并发下两请求同时插同一 invitee → 后者撞唯一约束IntegrityError,bind()rollback 后改判already_bound兜底。 - 四道防线(
bind()内):① invitee 已绑 →already_bound;② 无效码 / 邀请人非active→invalid_code;③ 自邀(inviter==invitee)→self_invite;④ 新人闸_is_new_user(B 的created_at在rewards.INVITE_NEW_USER_WINDOW_HOURS=72h 内)→ 否则not_eligible。只有全过才插行 + 发奖。
- 幂等键 =
- U / D:无。这张表只增不改不删(纯账本)。
- R:
GET /api/v1/invite/me(my_invite)→get_stats(inviter_id):count(*)得已邀人数、sum(inviter_coin)得累计金币。GET /api/v1/invite/invitees(my_invitees)→get_invitees(inviter_id, limit, offset):joinuser出被邀请人列表(倒序分页),名字降级兜底nickname → wechat_nickname → 脱敏手机号,coins取inviter_coin。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | |
inviter_user_id |
Integer | FK→user.id, index, NOT NULL | 邀请人(A) |
invitee_user_id |
Integer | FK→user.id, UNIQUE, index, NOT NULL | 被邀请人(B)。唯一 = 幂等键,一个 B 只能被归因一次 |
channel |
String(16) | NOT NULL, default clipboard |
归因来源:clipboard(剪贴板自动)/ manual(手动填码)/ fingerprint(指纹反查兜底)。入库前 [:16] 截断 |
status |
String(16) | NOT NULL, default effective |
当前只有 effective(注册即生效)。预留 pending/effective:将来若改"完成首单才生效"时启用 |
inviter_coin |
Integer | NOT NULL, default 0 | 本次给 A 发的金币(记账留痕,=rewards.INVITE_INVITER_COINS=10000) |
invitee_coin |
Integer | NOT NULL, default 0 | 本次给 B 发的金币(=rewards.INVITE_INVITEE_COINS=10000) |
created_at |
DateTime(tz) | server_default now(), index | 绑定时间(= 列表倒序键) |
关系 / Join Key
inviter_user_id→user.id(硬 FK,多对一):一个 A 可邀多个 B。invitee_user_id→user.id(硬 FK,一对一,唯一约束):一个 B 至多一行。- 发金币时落
coin_transaction:biz_type='invite_inviter'(给 A,ref_id=invitee.id)/biz_type='invite_invitee'(给 B,ref_id=inviter.id),双方ref_id互指对方便于对账。 get_invitees用InviteRelation JOIN user ON user.id = invitee_user_id取被邀请人资料;total单独count算has_more。
索引与约束
- PK
id。 ix_invite_relation_inviter_user_id(inviter_user_id):get_stats/get_invitees按邀请人查。ix_invite_relation_invitee_user_id(invitee_user_id,UNIQUE):幂等键 + 加速_relation_of_invitee反查。ix_invite_relation_created_at(created_at):列表倒序。
注意
- 防重复发奖三道(
repositories/invite.pydocstring):①invitee_user_id唯一(应用层_relation_of_invitee先查 + DB 唯一约束并发兜底);② 自邀屏蔽;③ 手机号天然唯一(每个 B = 一个真实手机号账号)= 限制刷量规模。 status当前恒为effective:产品取"注册即生效"而非"完成首单才生效",pending取值是为后者预留、目前不写入。inviter_coin/invitee_coin是留痕字段:写死当时发的金币值,即便日后改奖励常量,历史行仍保留发奖时的额度,便于对账。channel三种取值:clipboard(deferred-deeplink 主路径,落地页写剪贴板、首启读回)/manual(用户在邀请页手输)/fingerprint(剪贴板被覆盖时走指纹兜底,见 invite_fingerprint);三者都汇入同一个bind(),只channel不同。- 风控缺口(#24 设计文档「上线前还差什么」标注):1 万金币可提现且邀请人无总数上限,接码平台批量注册新号绑同码即可刷;上线前需加 inviter 上限 + 基础风控。当前 MVP 仅靠"手机号唯一 + 72h 新人闸"挡。
- alembic 多 head:本表迁移
invite_code_and_relation(down_revision=11a1d08c6f55)与coupon_state_tables是同一父的兄弟迁移,合 main 前需建 merge 迁移,否则 prodalembic upgrade head撞Multiple head revisions(#24 设计文档已警示)。