Files
shaguabijia-app-server/docs/database/invite_relation.md
T
2026-07-09 14:24:46 +08:00

6.5 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)的邀请码完成绑定后写入,绑定关系注册即生效,但绑定本身不发奖(#113 起):发奖后置到 B 完成比价并实际下单(POST /order/reporttry_reward_on_compare),给 A 发邀请奖励金(入独立账本 invite_cash_transaction,非金币),本表 compare_reward_granted 做幂等闸。数据来自 bind(),触发它的归因来源有三种(channel)。这是邀请功能的结果表 / 账本;邀请码本身存在 user.invite_code,不在这张表。

用在哪 / 增删改查

  • C(插入):POST /api/v1/invite/bind(bind_invite)→ invite_repo.bind()。过四道防线后建一行 status='effective'#113 起绑定只建关系、不发奖(inviter_coin/invitee_coin 新行恒 0)。
    • 幂等键 = 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(发奖,#113):B 比价后下单 POST /order/reporttry_reward_on_compare(invitee_user_id):compare_reward_granted=False 才发 → 置 True + 记 compare_reward_cents/compare_rewarded_at,同事务 wallet.grant_invite_cash 给 A 入邀请奖励金 → "置闸 + 入账"原子,一个 B 只发一次。
  • D:无。
  • R:
    • GET /api/v1/invite/me(my_invite)→ get_stats(inviter_id):count(*) 得已邀人数、sum(inviter_coin) 得累计金币(历史口径);另调 get_reward_stats 出邀请奖励金余额/累计已提(读 coin_account.invite_cash_balance_cents + withdraw_order(source=invite_cash))。
    • 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(绑定关系注册即生效)。「发奖后置」没有走 status,而是用下面的 compare_reward_granted 闸表达
inviter_coin Integer NOT NULL, default 0 历史留痕(#113 前"绑定即发金币"时代给 A 发的金币,当时=10000);#113 起新绑定恒 0
invitee_coin Integer NOT NULL, default 0 同上,给 B 发的金币历史留痕;新绑定恒 0
compare_reward_granted Boolean NOT NULL, default false 发奖幂等闸(#113):B 首次「比价并下单」后置 true,一个 B 只给 A 发一次邀请奖励金
compare_reward_cents Integer NOT NULL, default 0 实发的邀请奖励金(分,留痕;改常量不影响历史行)
compare_rewarded_at DateTime(tz) nullable 发奖时间
created_at DateTime(tz) server_default now(), index 绑定时间(= 列表倒序键)

关系 / Join Key

  • inviter_user_iduser.id(硬 FK,多对一):一个 A 可邀多个 B。
  • invitee_user_iduser.id(硬 FK,一对一,唯一约束):一个 B 至多一行。
  • 流水关联:#113 前的绑定发金币落 coin_transaction(biz_type='invite_inviter'/'invite_invitee',ref_id 互指对方)——现为历史类型,新绑定不再产生;#113 起发奖落 invite_cash_transaction(biz_type='invite_reward',ref_id=被邀请人 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(绑定关系维度);「发奖后置」由 compare_reward_granted 闸表达,没有引入 pending 状态。
  • 金币留痕字段已冻结:inviter_coin/invitee_coin 只反映 #113 前旧口径的历史发放额,便于对账;新奖励额看 compare_reward_cents
  • channel 三种取值:clipboard(deferred-deeplink 主路径,落地页写剪贴板、首启读回)/ manual(用户在邀请页手输)/ fingerprint(剪贴板被覆盖时走指纹兜底,见 invite_fingerprint);三者都汇入同一个 bind(),只 channel 不同。
  • 风控演进:#24 时代的缺口是"注册即发 1 万可提现金币、接码批量刷"——#113 把发奖后置到真实比价+下单(且发的是邀请奖励金独立账本),批量注册空号不再直接得利,刷奖成本显著抬高;inviter 总数上限等进一步风控仍待补。
  • alembic 多 head:本表迁移 invite_code_and_relation(down_revision=11a1d08c6f55)与 coupon_state_tables 是同一父的兄弟迁移,合 main 前需建 merge 迁移,否则 prod alembic upgrade headMultiple head revisions(#24 设计文档已警示)。