"""日志查询的请求/响应模型。 操作日志与登录日志字段不同(前者含模块/操作/请求详情,后者含失败原因), 故各自定义 Query 与 Out,仅共用「用户名 + 结果 + 时间范围」的筛选基类。 """ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from app.core.response import PageQuery class LogQueryBase(PageQuery): """日志公共查询条件:模糊用户名 + 结果 + 时间范围。""" username: str | None = Field(default=None, max_length=64, description="用户名,模糊匹配") success: int | None = Field(default=None, ge=0, le=1, description="1成功 0失败") start_time: datetime | None = Field(default=None, description="起始时间(含)") end_time: datetime | None = Field(default=None, description="结束时间(含)") @field_validator("start_time", "end_time") @classmethod def _to_local_naive(cls, value: datetime | None) -> datetime | None: """统一为本地时区的 naive 时间。 库列存本地时间(容器 TZ=Asia/Shanghai),而 API 接受 ISO 8601 (可带 ``Z`` 等时区后缀)。不归一时有两种故障:aware 与 naive 混用比较抛 TypeError(穿透成 500);aware 直接透传给驱动会 **静默丢弃偏移**(查询窗口整体偏 8 小时且无任何提示)。 aware 先转系统本地时区、再摘除 tzinfo 是唯一正确姿势; naive 视为本地时间原样保留。 """ if value is not None and value.tzinfo is not None: return value.astimezone().replace(tzinfo=None) return value @model_validator(mode="after") def _check_time_range(self) -> "LogQueryBase": if self.start_time and self.end_time and self.start_time > self.end_time: raise ValueError("起始时间不能晚于结束时间") return self class OperationLogQuery(LogQueryBase): """操作日志查询条件。""" module: str | None = Field(default=None, max_length=64, description="所属模块,精确匹配") action: str | None = Field(default=None, max_length=32, description="操作类型,精确匹配") class OperationLogOut(BaseModel): """操作日志列表项。 **不含 request_data**(TEXT,单行最大 60000 字节):列表页默认不展示它, 随列表返回会让翻页负担随日志表增长线性放大;展开详情走 `GET /operation-logs/{id}`(`OperationLogDetailOut`),前端按需加载。 """ model_config = ConfigDict(from_attributes=True) id: int user_id: int | None = None username: str module: str action: str summary: str method: str | None = None path: str | None = None ip: str | None = None user_agent: str | None = None success: int = 0 error_message: str | None = None duration_ms: int | None = None create_time: datetime | None = None class OperationLogDetailOut(OperationLogOut): """操作日志详情:列表字段 + 请求参数。 `request_data` 入库时已经结构化 + 文本两层脱敏,可直接展示。 """ request_data: str | None = None class LoginLogQuery(LogQueryBase): """登录日志查询条件。""" ip: str | None = Field(default=None, max_length=45, description="客户端IP,精确匹配") class LoginLogOut(BaseModel): """登录日志响应。""" model_config = ConfigDict(from_attributes=True) id: int user_id: int | None = None username: str ip: str | None = None user_agent: str | None = None success: int = 0 fail_reason: str | None = None create_time: datetime | None = None