Become a sponsor

操作日志
操作日志中间件自动拦截写请求(POST/PUT/DELETE/PATCH)并记录操作日志,无需手动埋点。日志落库通过 response.background 异步执行,不阻塞响应。源码位于 src/middleware/operation_log.py。
1. 请求进入 → operation_log_middleware 判断是否需要记录
2. 仅拦截写请求(POST/PUT/DELETE/PATCH),排除登录/验证码等路径
3. 记录请求参数、响应结果、耗时、操作类型
4. 日志落库挂到 response.background,响应返回后异步执行
5. 不阻塞主请求,对性能影响极小以下路径不记录操作日志:
_EXCLUDE_PATHS = (
'/api/v1/login',
'/api/v1/logout',
'/api/v1/captcha',
'/api/v1/upload',
'/api/v1/loginlog',
'/api/v1/operationlog',
'/api/v1/index/menu',
'/api/v1/index/user',
)中间件根据 URL 路径末段自动推断操作类型,映射表定义在 _TYPE_MAP 中:
_TYPE_MAP = {
'add': 1, # 新增
'update': 2, # 修改
'delete': 3, # 删除
'batchdelete': 4, # 批量删除
'status': 5, # 状态设置
'resetpwd': 6, # 重置密码
'updatepassword': 7, # 修改密码
'import': 8, # 导入
'export': 9, # 导出
'kickout': 10, # 强退
'generate': 11, # 生成代码
'clear': 12, # 清空数据
'auth': 13, # 授权
'refresh': 14, # 缓存
'openaccount': 15, # 开账号
}
def _infer_type(request: Request) -> int:
"""从 URL 路径末段推断操作类型,匹配不到返回 0(其它)"""
path = request.url.path.rstrip('/')
last_segment = path.split('/')[-1].lower()
# 跳过纯数字路径参数(如 /delete/123 → 取 delete)
if last_segment.isdigit():
parts = path.split('/')
last_segment = parts[-2].lower() if len(parts) >= 2 else ''
return _TYPE_MAP.get(last_segment, 0)| 路径末段 | 操作类型 | 类型ID |
|---|---|---|
add | 新增 | 1 |
update | 修改 | 2 |
delete | 删除 | 3 |
batchdelete | 批量删除 | 4 |
status | 状态设置 | 5 |
resetpwd | 重置密码 | 6 |
updatepassword | 修改密码 | 7 |
import | 导入 | 8 |
export | 导出 | 9 |
kickout | 强退 | 10 |
generate | 生成代码 | 11 |
clear | 清空数据 | 12 |
auth | 授权 | 13 |
refresh | 缓存 | 14 |
openaccount | 开账号 | 15 |
| 其它 | 其它 | 0 |
| 字段 | 说明 |
|---|---|
title | 操作标题(从路由 summary 获取) |
type | 操作类型(自动推断) |
method | 路由方法名(如 v1.level.add) |
url | 请求路径 |
param | 请求参数(截取前2000字符) |
result | 响应结果(截取前2000字符) |
consume_time | 耗时(毫秒) |
status | 状态:0-成功 1-失败 |
error | 错误信息 |
create_user | 操作人 |
GET /api/v1/oper/log/list?pageNo=1&pageSize=10
GET /api/v1/oper/log/detail/{id}1. 后台任务:日志落库通过 response.background 异步执行,不阻塞响应
2. 前缀解析:仅解析响应体前 8KB(_READ_LIMIT),避免大响应体全量解析
3. 长度截断:参数和结果截取前 2000 字符(_RESULT_MAX_LEN),防止撑爆日志表
4. 路径排除:登录/验证码等高频路径不记录
5. 路径段边界匹配:排除前缀按 path == prefix 或 prefix + '/' 匹配,避免子串误伤日志落库的后台任务挂载逻辑:
def _attach_background(response, task: BackgroundTask) -> None:
"""把日志落库挂到响应后台任务;若响应已有后台任务则链式追加,避免覆盖"""
prev = getattr(response, 'background', None)
if prev is None:
response.background = task
return
async def _combined():
await prev()
await task()
response.background = BackgroundTask(_combined)背景任务
Starlette 在发送响应体后自动运行 response.background 中的任务。如果响应已有后台任务(如其他中间件添加的),操作日志会链式追加,不会覆盖。
操作日志模块具备以下特点:
1. 自动记录:中间件拦截写请求,无需手动埋点
2. 类型推断:根据 URL 路径自动推断操作类型
3. 异步落库:后台任务执行,不阻塞主请求
4. 性能友好:前缀解析 + 长度截断 + 路径排除
5. 完整信息:记录参数、结果、耗时、操作人等