"""邀请指纹归因表(剪贴板归因兜底)。 数据流(对应 [[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"" )