- 操作日志审计机制:audit 上下文(成功与失败都记、独立提交、操作人标量 固化、commit 后查询前置约定),用户/角色全部写操作接入;request_data 按键名的结构化脱敏 + UTF-8 字节边界截断 - 角色管理:CRUD 与启停;admin 禁删禁停、有绑定禁删(FOR UPDATE 锁行)、 编码创建后不可改、停用角色不可再分配 - 日志查询:操作/登录两端点,列表与详情两段式(详情含已脱敏请求参数); 时间参数统一转本地 naive(修混用时区 500 与偏移丢弃) - 用户管理:补齐 reset_password 操作人、编辑空串归一 NULL、并发唯一键兜底 400 - 测试 301 例全绿(单元 223 / 集成 78) Co-Authored-By: Claude Code <noreply@anthropic.com>
55 lines
2.1 KiB
Python
55 lines
2.1 KiB
Python
"""repositories 层公共查询工具。
|
|
|
|
集中两类易错点,避免各 repository 各写一份、口径漂移:
|
|
|
|
- **模糊匹配的 LIKE 通配符转义**:直接 ``f"%{value}%"`` 拼接时,输入中的
|
|
下划线会作为单字符通配执行(本系统用户名普遍含下划线,误配是常态而非
|
|
理论),百分号则命中全表并触发全表扫描
|
|
- **两类日志共有的筛选条件**:用户名/结果/时间范围在操作日志与登录日志
|
|
之间逐字重复,抽为同一实现后「列表与计数」「操作与登录」不可能对不上
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import Select
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LogFilters:
|
|
"""两类日志(操作/登录)共有的筛选条件。"""
|
|
|
|
username: str | None = None
|
|
success: int | None = None
|
|
start_time: datetime | None = None
|
|
end_time: datetime | None = None
|
|
|
|
|
|
def fuzzy(stmt: Select, column: Any, value: str) -> Select:
|
|
"""模糊匹配(含通配符自动转义)。
|
|
|
|
``contains(autoescape=True)`` 会转义输入中的 ``%``/``_`` 并附加 ``ESCAPE``
|
|
子句,把用户输入当字面量匹配。
|
|
"""
|
|
return stmt.where(column.contains(value, autoescape=True))
|
|
|
|
|
|
def apply_log_filters(stmt: Select, model: Any, filters: LogFilters) -> Select:
|
|
"""应用两类日志共有的筛选条件。
|
|
|
|
``model`` 传 ``OperationLog`` / ``LoginLog`` 的模型类(两者字段同名同型)。
|
|
|
|
列表与计数共用本函数,避免「列表按条件过滤而总数不按条件过滤」——
|
|
漏改一侧时表现为翻到最后一页是空页或页码虚增,且没有任何报错。
|
|
"""
|
|
if filters.username:
|
|
stmt = fuzzy(stmt, model.username, filters.username)
|
|
if filters.success is not None:
|
|
stmt = stmt.where(model.success == filters.success)
|
|
if filters.start_time:
|
|
stmt = stmt.where(model.create_time >= filters.start_time)
|
|
if filters.end_time:
|
|
stmt = stmt.where(model.create_time <= filters.end_time)
|
|
return stmt
|