Files
meijiaka-zy/python-api/app/models/user.py
T
小鱼开发 b597d715c8 fix: 认证流程修复 + alembic 迁移补全 + 前端僵尸代码清理
后端:
- 修复 get_current_user 未校验 is_active,被封禁用户仍可用旧 Token
- auth.py 捕获 ValueError 转 HTTPException(验证码错误、账号被封、Token 无效等不再返回 500)
- 修正 SMS 每日上限注释(3次 → 10次)
- 修复迁移脚本外键引用错误:users.id → mjk_users.id
- 新建积分系统 4 张表的迁移(mjk_user_points/batches/transactions/recharge_orders)
- pyproject.toml 补充 alembic + psycopg2-binary 依赖
- ruff 格式修复(import 排序等)

前端:
- 修复 doRefreshToken 成功后不持久化新 Token 的严重 bug
- 修复应用重启后 SSE 不自动重连(收不到踢人通知)
- 修复 App.tsx handleLogout 未 await
- client.ts 统一从 utils/env 导入 isTauri,默认 base URL 兜底 localhost:8000
- 清理 ~20 个未使用的 hooks/utils/api 模块/组件导出
- 修复所有 ESLint 警告(206 → 0)和 TSC 错误
- 测试通过(5/5)

其他:
- 更新 requirements.lock 和 uv.lock
2026-05-08 11:10:48 +08:00

123 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(str, enum.Enum):
"""用户状态"""
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:]}"