b2f5a53dd8
- GET /invitees: 分页 + 名字降级兜底(昵称→微信昵称→脱敏手机号) - 指纹归因: 落地页采集 + (IP,设备型号)反查撞库 + 时间窗口闸 - test_invite: +5 个列表测试(脱敏/倒序/昵称优先/分页/空), 修指纹测试跨用例串味 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Reviewed-on: #31 Co-authored-by: xiebing <xiebing@wonderable.ai> Co-committed-by: xiebing <xiebing@wonderable.ai>
47 lines
2.2 KiB
Python
47 lines
2.2 KiB
Python
"""邀请指纹归因表(剪贴板归因兜底)。
|
||
|
||
数据流(对应 [[invite-three-tasks]] 任务 3):
|
||
1. B 浏览器打开落地页 dl.html?ref=邀请码 → JS POST /api/v1/invite/landing-track
|
||
2. 后端从 HTTP 头拿 IP/UA,解析 UA 得 device_model,跟 JS 上报的 screen 一起入库
|
||
3. B 装包首启 → 登录后,若剪贴板没拿到邀请码(被覆盖),客户端再算一次 screen+Build.MODEL
|
||
上报 → 后端用 (ip, screen, device_model) 反查本表 7 天内最近匹配 → 拿出 inviter_user_id
|
||
→ 走原 bind 路径(channel=fingerprint)
|
||
|
||
不要索引(IP, created_at) 单独,组合索引 (ip, screen, device_model, created_at desc) 反查更省。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.db.base import Base
|
||
|
||
|
||
class InviteFingerprint(Base):
|
||
__tablename__ = "invite_fingerprint"
|
||
|
||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||
inviter_user_id: Mapped[int] = mapped_column(
|
||
Integer, ForeignKey("user.id"), index=True, nullable=False
|
||
)
|
||
# 落地页访问者 IP(服务端从 HTTP 头拿,X-Forwarded-For 由部署侧 nginx 透传)
|
||
ip: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
# 客户端 Build.MODEL / 浏览器 UA 解析出来的型号(如 "PJF110"、"23046PNC9C")
|
||
device_model: Mapped[str] = mapped_column(String(64), nullable=False, default="")
|
||
# 屏幕分辨率 "1080x2400"(浏览器 screen.width × height,客户端 DisplayMetrics)
|
||
screen: Mapped[str] = mapped_column(String(32), nullable=False, default="")
|
||
# 完整 UA 字符串(留 debug / 排查"匹配不上"用,不直接参与匹配)
|
||
user_agent: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
|
||
)
|
||
|
||
def __repr__(self) -> str: # pragma: no cover
|
||
return (
|
||
f"<InviteFingerprint inviter={self.inviter_user_id} "
|
||
f"ip={self.ip} model={self.device_model} screen={self.screen}>"
|
||
)
|