"""新手引导完成标记(按 设备 + 账号 去重)。 产品规则:同一台设备 + 同一个账号,新手引导只跑一次;卸载重装(同设备同账号) 不再触发。本地标记(SharedPreferences)卸载即丢,所以"完成"必须落后端,按 (user_id, device_id) 唯一一条。 ⚠️ device_id 这里用客户端的**硬件级稳定标识**(Android `Settings.Secure.ANDROID_ID`), 同签名 app 卸载重装不变、仅恢复出厂才重置——区别于领券/比价用的 per-install device_id (见 coupon_state,存 SP、重装会变)。换新设备 → 无此行 → 重新走引导。 """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, Integer, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class OnboardingCompletion(Base): """一台设备 + 一个账号 一条:走完新手引导即写入,登录时据此跳过引导。""" __tablename__ = "onboarding_completion" __table_args__ = ( # 同账号、同设备只一条:重复 mark / 并发提交靠它幂等。 UniqueConstraint("user_id", "device_id", name="uq_onboarding_user_device"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # 登录态用户。引导是登录后才走的,故必填(非空)、进唯一键。 user_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False) # 硬件级稳定设备标识(ANDROID_ID),卸载重装不变。 device_id: Mapped[str] = mapped_column(String(64), nullable=False) completed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) def __repr__(self) -> str: # pragma: no cover return f""