Files
shaguabijia-app-server/docs/database/invite_relation.md
T
2026-06-17 23:32:36 +08:00

5.4 KiB
Raw Blame History

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;② 无效码 / 邀请人非 activeinvalid_code;③ 自邀(inviter==invitee)→ self_invite;④ 新人闸 _is_new_user(B 的 created_atrewards.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):join user 出被邀请人列表(倒序分页),名字降级兜底 nickname → wechat_nickname → 脱敏手机号,coinsinviter_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_iduser.id(硬 FK,多对一):一个 A 可邀多个 B。
  • invitee_user_iduser.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_inviteesInviteRelation JOIN user ON user.id = invitee_user_id 取被邀请人资料;total 单独 counthas_more

索引与约束

  • PK id
  • ix_invite_relation_inviter_user_id(inviter_user_id):get_stats / get_invitees 按邀请人查。
  • ix_invite_relation_invitee_user_id(invitee_user_idUNIQUE):幂等键 + 加速 _relation_of_invitee 反查。
  • ix_invite_relation_created_at(created_at):列表倒序。

注意

  • 防重复发奖三道(repositories/invite.py docstring):① 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 迁移,否则 prod alembic upgrade headMultiple head revisions(#24 设计文档已警示)。