后端 FastAPI 六层结构(api/services/repositories/models/schemas/core), 统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存 db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、 登出黑名单(TTL=令牌剩余有效期)、登录日志。 前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。 工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。 测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。 Co-Authored-By: Claude Code <noreply@anthropic.com>
54 lines
2.1 KiB
Python
54 lines
2.1 KiB
Python
"""登录日志模型。"""
|
||
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import DateTime, ForeignKey, Index, String, text
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, mysql_table_args
|
||
|
||
|
||
class LoginLog(Base):
|
||
"""登录日志。需求 2.5.1 要求记录用户登录行为。
|
||
|
||
不继承 TimestampMixin:日志是追加写、不可变的,没有"更新时间"语义,
|
||
也不使用 update_time 的 ON UPDATE 子句。
|
||
|
||
记录原则:
|
||
- **只记登录尝试,绝不记录密码**(明文或哈希都不记)
|
||
- 用户名原样记录而非仅存 user_id —— 账号不存在时也要能追溯
|
||
- 写入使用独立事务,失败不阻断登录(审计可丢,业务不可断)
|
||
"""
|
||
|
||
__tablename__ = "login_logs"
|
||
__table_args__ = mysql_table_args(
|
||
"登录日志表",
|
||
Index("idx_login_logs_username", "username"),
|
||
Index("idx_login_logs_create_time", "create_time"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||
)
|
||
user_id: Mapped[int | None] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("users.id", ondelete="SET NULL"),
|
||
comment="用户ID(账号不存在时为空)",
|
||
)
|
||
username: Mapped[str] = mapped_column(
|
||
String(64), nullable=False, comment="登录用户名(原样记录)"
|
||
)
|
||
ip: Mapped[str | None] = mapped_column(String(45), comment="客户端IP(45位兼容IPv6)")
|
||
user_agent: Mapped[str | None] = mapped_column(
|
||
String(255), comment="客户端User-Agent(截断存储)"
|
||
)
|
||
success: Mapped[int] = mapped_column(
|
||
TINYINT_TYPE, nullable=False, server_default="0", comment="是否成功:1成功 0失败"
|
||
)
|
||
fail_reason: Mapped[str | None] = mapped_column(
|
||
String(64), comment="失败原因:验证码错误/用户名或密码错误/账号锁定/账号禁用"
|
||
)
|
||
create_time: Mapped[datetime] = mapped_column(
|
||
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
|
||
)
|