106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
|
|
"""日志查询的请求/响应模型。
|
|||
|
|
|
|||
|
|
操作日志与登录日志字段不同(前者含模块/操作/请求详情,后者含失败原因),
|
|||
|
|
故各自定义 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
|