120 lines
3.0 KiB
Python
120 lines
3.0 KiB
Python
"""
|
|
用户模型
|
|
========
|
|
|
|
采用"手机号 + 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"<User(id={self.id}, mobile={self.mobile}, status={self.status})>"
|
|
|
|
@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:]}"
|