"""登录日志数据访问。""" from sqlalchemy import Select, func, select from sqlalchemy.orm import Session from app.models import LoginLog from app.repositories.query_utils import LogFilters, apply_log_filters # user_agent 列宽,超长需截断否则插入报错 _USER_AGENT_MAX_LEN = 255 def add( session: Session, *, user_id: int | None, username: str, ip: str | None, user_agent: str | None, success: bool, fail_reason: str | None = None, ) -> None: """写入一条登录日志。只 flush 不 commit。 **绝不记录密码** —— 明文与哈希都不写。日志是最容易泄漏凭据的地方。 :param user_id: 账号不存在时传 None(列本身可空) :param username: 用户名原样记录,便于账号不存在时也能追溯 """ session.add( LoginLog( user_id=user_id, username=username, ip=ip, user_agent=user_agent[:_USER_AGENT_MAX_LEN] if user_agent else None, success=1 if success else 0, fail_reason=fail_reason, ) ) session.flush() def list_logs( session: Session, *, offset: int, limit: int, filters: LogFilters, ip: str | None = None, ) -> list[LoginLog]: """分页查询登录日志。 **按 id 倒序而非 create_time**:后者是秒级精度,同一秒内的多条记录排序 不确定,翻页会重复或遗漏(与用户列表同一理由)。 """ stmt = _apply_filters(select(LoginLog), filters=filters, ip=ip) stmt = stmt.order_by(LoginLog.id.desc()).offset(offset).limit(limit) return list(session.execute(stmt).scalars()) def count_logs( session: Session, *, filters: LogFilters, ip: str | None = None, ) -> int: """统计符合条件的登录日志总数,供分页使用。""" stmt = _apply_filters(select(func.count(LoginLog.id)), filters=filters, ip=ip) return int(session.execute(stmt).scalar_one()) def _apply_filters(stmt: Select, *, filters: LogFilters, ip: str | None) -> Select: """应用筛选条件(公共部分复用 query_utils,与操作日志同口径)。""" stmt = apply_log_filters(stmt, LoginLog, filters) if ip: stmt = stmt.where(LoginLog.ip == ip) return stmt