""" 用户模型 ======== 采用"手机号 + JWT"的认证方案,支持单设备登录约束。 """ import enum from datetime import datetime from sqlalchemy import DateTime, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from app.models.base import BaseModel class UserStatus(enum.StrEnum): """用户状态""" ACTIVE = "active" # 正常 SUSPENDED = "suspended" # 封禁(可解封) PENDING_DELETE = "pending_delete" # 申请注销,30 天后自动删除 DELETED = "deleted" # 已删除(逻辑删除,数据保留 90 天) class User(BaseModel): """用户表""" __tablename__ = "mjk_users" # ========== 认证标识 ========== mobile: Mapped[str] = mapped_column( String(20), unique=True, nullable=False, comment="手机号,登录账号", ) # ========== 安全相关 ========== password_hash: Mapped[str | None] = mapped_column( String(255), nullable=True, comment="密码哈希(bcrypt),预留字段", ) status: Mapped[str] = mapped_column( String(20), default=UserStatus.ACTIVE.value, nullable=False, comment="账号状态", ) # ========== 用户画像 ========== nickname: Mapped[str | None] = mapped_column( String(64), nullable=True, comment="用户昵称", ) avatar_url: Mapped[str | None] = mapped_column( Text, nullable=True, comment="头像 URL", ) # ========== 业务追踪 ========== source: Mapped[str] = mapped_column( String(32), default="unknown", nullable=False, comment="注册来源", ) invited_by: Mapped[str | None] = mapped_column( String(36), nullable=True, comment="邀请人 user_id", ) # ========== 时间戳(补充)========== last_login_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, comment="最后登录时间", ) last_login_ip: Mapped[str | None] = mapped_column( String(45), nullable=True, comment="最后登录 IP(IPv6 最大 45 字符)", ) deleted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, comment="注销时间(软删除标记)", ) # ========== 冗余字段(备用)========== extra: Mapped[dict] = mapped_column( JSONB, default=dict, nullable=False, comment="冗余字段,备用", ) def __repr__(self) -> str: return f"" @property def is_active(self) -> bool: """是否有效用户""" return self.status == UserStatus.ACTIVE.value @property def display_name(self) -> str: """对外展示的名称""" return self.nickname or f"用户_{self.mobile[-4:]}"