"""邀请相关 API 收发模型。""" from __future__ import annotations from datetime import datetime from pydantic import BaseModel, Field class InviteInfoOut(BaseModel): invite_code: str # 我的邀请码 share_url: str # 落地页链接(含 ?ref=),前端据此生成二维码 + 复制分享 invited_count: int # 已成功邀请人数 coins_earned: int # 累计从邀请获得的金币(v1 口径;v2 邀请人改发奖励金) reward_balance_cents: int = 0 # v2 可提现邀请奖励金(分) reward_withdrawn_cents: int = 0 # v2 累计提现成功的邀请奖励金(分) countdown_days_left: int = 7 # v2 本轮剩余天数(7 天 1 轮) countdown_is_fresh_round: bool = False # 是否刚进入新一轮(非首轮第1天) countdown_text: str = "" # 倒计时展示文案(前端直接显示,新轮含换行) class LandingTrackIn(BaseModel): """落地页 dl.html 访问时上报的指纹(用于剪贴板归因失败时兜底)。""" ref: str = Field(..., min_length=4, max_length=16, description="邀请码(落地页 ?ref=)") screen: str = Field("", max_length=32, description="屏幕分辨率,如 1080x2400") # IP / UA 服务端从 HTTP 头自动拿,无需 JS 上报 class LandingTrackOut(BaseModel): status: str # ok / invalid_code / no_ip class FingerprintIn(BaseModel): """客户端登录后剪贴板归因失败时上报的设备指纹。 跨端匹配字段:浏览器落地页存的同名字段 == 客户端 Build.MODEL / DisplayMetrics 算的值。 IP 服务端从 HTTP 头拿,不在这里。 """ screen: str = Field("", max_length=32) device_model: str = Field("", max_length=64) class BindInviteIn(BaseModel): # invite_code: 可选 — 走指纹兜底时为空(channel=fingerprint) invite_code: str | None = Field( None, max_length=16, description="邀请码;走指纹兜底时为空" ) channel: str = Field( "clipboard", max_length=16, description="归因来源:clipboard(首启读剪贴板自动) / manual(手动填) / fingerprint(指纹兜底)", ) # 当 invite_code 为空、channel=fingerprint 时,后端用这组指纹反查 fingerprint: FingerprintIn | None = None class BindInviteOut(BaseModel): status: str # success / already_bound / invalid_code / self_invite / not_eligible / fp_not_found coins_awarded: int # 本次给当前用户(被邀请人)发的金币 message: str # 给前端直接展示的文案 class InviteeItem(BaseModel): """被邀请人列表的一条(邀请页小窗 / 完整列表页共用)。 display_name / avatar_url 的"降级兜底"在后端算好(见 repositories/invite.get_invitees): 名字 = 昵称 → 微信昵称 → 脱敏手机号(前端拿不到完整号,必须后端脱敏); 头像 = 用户头像 → 微信头像 → null(前端拿到 null 画默认色块)。 """ display_name: str # 已兜底好的显示名(真名/脱敏号) avatar_url: str | None = None # 头像 URL;null = 前端画默认色块 coins: int # 这次邀请给我(邀请人)发的金币 invited_at: datetime # 邀请绑定时间(前端转"今天/3天前") is_compared: bool = False # 该好友是否已完成过一次比价(好友列表分"去提醒/邀请成功";在途列表只取 False) class InviteeListOut(BaseModel): """GET /invite/invitees 响应:被邀请人列表 + 分页。""" items: list[InviteeItem] total: int # 我邀请的总人数(用于"共 N 人") has_more: bool # 还有下一页吗(完整列表页滚到底加载更多)