PayrollManagementSystem/backend/app/repositories/login_log_repository.py
liuwenxin 5ece9f6dab refactor(backend): 仓储层散参数收敛为查询 DTO
仓储层的 list_*/count_* 此前以 keyword/status/offset/limit 四五个散参数
传参,调用点重复铺开、易漏传且不可扩展;按项目「参数 >4 封装 DTO」的
约定统一收敛为各模块的 Query 对象(如 RoleQuery、BusinessQuery),
服务层与测试调用同步调整,行为不变。

- repositories 层查询方法签名改为 (session, query),分页与筛选条件
  由 Query 承载,_apply_filters 同步收口
- schemas 层补齐各模块 Query 模型(approval/business_role/report 等)
- services 层调用点同步改为传 Query
- tests 随之调整;新增用例:待办角色流转(财务通过后领导出现待办)、
  参数预览顺序稳定

验证:全量 pytest 900 passed(11:08);单元 526 passed;ruff check 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 19:43:20 +08:00

85 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""登录日志数据访问。"""
from dataclasses import dataclass
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
@dataclass(frozen=True, slots=True)
class LoginLogEntry:
"""一条待写入的登录日志(全字段 DTO)。
与 ``OperationLogEntry`` 同款做法:写入字段平铺传递时,任何一处漏传
都只表现为日志字段缺失、不报错(success 默认 0 的语义尤其隐蔽)。
"""
user_id: int | None
username: str
success: bool
ip: str | None = None
user_agent: str | None = None
fail_reason: str | None = None
@dataclass(frozen=True, slots=True)
class LoginLogListQuery:
"""登录日志列表的查询条件(筛选 + 分页)。"""
filters: LogFilters
offset: int = 0
limit: int = 20
ip: str | None = None
def add(session: Session, entry: LoginLogEntry) -> None:
"""写入一条登录日志。只 flush 不 commit。
**绝不记录密码** —— 明文与哈希都不写。日志是最容易泄漏凭据的地方。
:param entry.user_id: 账号不存在时为 None(列本身可空)
:param entry.username: 用户名原样记录,便于账号不存在时也能追溯
"""
session.add(
LoginLog(
user_id=entry.user_id,
username=entry.username,
ip=entry.ip,
user_agent=entry.user_agent[:_USER_AGENT_MAX_LEN] if entry.user_agent else None,
success=1 if entry.success else 0,
fail_reason=entry.fail_reason,
)
)
session.flush()
def list_logs(session: Session, query: LoginLogListQuery) -> list[LoginLog]:
"""分页查询登录日志。
**按 id 倒序而非 create_time**:后者是秒级精度,同一秒内的多条记录排序
不确定,翻页会重复或遗漏(与用户列表同一理由)。
"""
stmt = _apply_filters(select(LoginLog), query)
stmt = stmt.order_by(LoginLog.id.desc()).offset(query.offset).limit(query.limit)
return list(session.execute(stmt).scalars())
def count_logs(session: Session, query: LoginLogListQuery) -> int:
"""统计符合条件的登录日志总数,供分页使用。"""
stmt = _apply_filters(select(func.count(LoginLog.id)), query)
return int(session.execute(stmt).scalar_one())
def _apply_filters(stmt: Select, query: LoginLogListQuery) -> Select:
"""应用筛选条件(公共部分复用 query_utils,与操作日志同口径)。"""
stmt = apply_log_filters(stmt, LoginLog, query.filters)
if query.ip:
stmt = stmt.where(LoginLog.ip == query.ip)
return stmt