Files
DjangoAdmin_Flask_EleVue/application/middleware/op_log.py
T
DjangoAdmin DevandClaude 658ee6d596 refactor: P2 core+common 建设(应用工厂/扩展/工具层迁入 src/)
【结构】
- src/core/:database/response/jwt/password/security/redis_client/mail/scheduler
- src/common/:base_model/base_db/forms(base_form+fields)/constants/utils
- src/models_registry.py:模型聚合器(Flask-Migrate 全量表识别)

【绞杀者 shim】extensions/、utils/、application/constants|forms|models 打兼容转发
【兼容修复】应用工厂抽取至 application/factory.py(P2 前置,P1 已完成)

【基线修复】application/models(25) + middleware(7) 原被嵌套 .gitignore(*)
误排除出版本库,已补入(base_model/base_db 现为 shim,原版在 src/common/)

【登记既有缺陷】
1. flask db CLI 因 .env FLASK_VERSION 与 --version 冲突报错 → Makefile 已加 FLASK_SKIP_DOTENV=1
2. 本机 Redis 未设密码但 REDIS_AUTH=True,/captcha 报 AUTH 错误(环境问题)

【验收】22 模型表全量识别;src/ flake8 零告警;pytest 5 passed;应用启动;
/login 表单校验、/user/page 鉴权拦截冒烟正常

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 13:20:46 +08:00

221 lines
7.2 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.
"""
操作日志自动记录中间件
=============================================================
通过 after_request 钩子自动记录写操作(POST/PUT/DELETE/PATCH)日志。
无需在每个视图函数中手动调用 record_operation_log。
记录规则:
- 只记录写操作(POST/PUT/DELETE/PATCH),GET/OPTIONS/HEAD 不记录
- 只记录成功操作(HTTP 2xx 且业务 code=0)
- 内部调用(定时任务等)不记录
- 登录/退出/验证码等路径不记录(由 login_log 单独记录)
- 日志记录失败不影响主流程
"""
import logging
import time
from flask import g, request
from application.models.operation_log import OperationLog
logger = logging.getLogger(__name__)
# 不记录操作日志的路径前缀(登录/退出/验证码等由 login_log 单独记录)
_EXCLUDED_PREFIXES = ('/login', '/captcha', '/logout')
# HTTP 方法 → 操作日志类型 映射
# 类型值参考 application/constants/dict.py 的 OPERATION_LOG_TYPE_LIST
_METHOD_LOG_TYPE = {
'POST': 3, # 新增
'PUT': 4, # 修改
'DELETE': 5, # 删除
'PATCH': 4, # 修改
}
# 路径后缀 → 操作日志类型 覆盖映射(更精确)
_PATH_ACTION_TYPE = {
'/add': 3, # 新增
'/update': 4, # 修改
'/delete': 5, # 删除
'/status': 4, # 状态变更→修改
'/batchdelete': 5, # 批量删除
'/reset': 4, # 重置→修改
'/save': 4, # 保存→修改
'/sort': 4, # 排序→修改
}
def register_operation_log(app):
"""
注册操作日志自动记录中间件
在 after_request 中自动记录写操作日志。
Args:
app: Flask 应用实例
"""
@app.after_request
def record_op_log(response):
# 只记录写操作
if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'):
return response
# 排除登录/退出/验证码等路径
path = request.path
if any(path.startswith(prefix) for prefix in _EXCLUDED_PREFIXES):
return response
# 只记录成功操作(HTTP 2xx 且业务 code=0)
if not (200 <= response.status_code < 300):
return response
try:
data = response.get_json(silent=True)
if data and data.get('code', 0) != 0:
return response
except Exception:
pass
# 内部调用不记录
if getattr(g, '_internal_call', False):
return response
# 异步记录,不阻塞响应
try:
_write_log(response)
except Exception as e:
logger.warning(f"记录操作日志失败: {str(e)}")
return response
logger.info("操作日志中间件注册成功")
def _write_log(response):
"""写入操作日志"""
from application.services import operation_log
from utils.security import get_username
from utils.ip import get_client_ip
from utils.ip2region import get_ip_location
from utils.user_agent import parse_user_agent
username = get_username() or 'system'
client_ip = get_client_ip()
ua_info = parse_user_agent()
# 确定操作类型
log_type = _infer_log_type(request.method, request.path)
# 构建标题:从路径提取模块名,如 /user/add → "用户管理-新增"
title = _build_title(request.path)
# 请求参数(限制长度,避免存入过大的数据)
param = _get_request_param()
# 返回结果(限制长度)
result = _get_response_result(response)
# 请求耗时
start_time = getattr(g, 'request_start_time', None)
consume_time = int((time.time() - start_time) * 1000) if start_time else 0
operation_log.record_operation_log(
title=title,
log_type=log_type,
source=OperationLog.Source.ADMIN,
status=OperationLog.Status.NORMAL,
method=request.method,
request_method=request.endpoint or '',
url=request.path,
param=param,
result=result,
ip=client_ip,
location=get_ip_location(client_ip),
os=ua_info.get('os', '未知'),
browser=ua_info.get('browser', '未知'),
consume_time=consume_time,
error='',
username=username,
)
def _infer_log_type(method, path):
"""根据请求方法和路径推断操作日志类型"""
# 优先根据路径后缀匹配
path_lower = path.lower()
for suffix, log_type in _PATH_ACTION_TYPE.items():
if path_lower.endswith(suffix):
return log_type
# 回退到 HTTP 方法映射
return _METHOD_LOG_TYPE.get(method, 18) # 18=其他
def _get_request_param():
"""获取请求参数"""
try:
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
# 优先解析 JSON body(batchDelete 等 DELETE 请求也带 body)
data = request.get_json(silent=True)
if data:
import json
return json.dumps(data, ensure_ascii=False)
# 回退到 URL 查询参数
if request.args:
import json
from collections import OrderedDict
return json.dumps(OrderedDict(request.args), ensure_ascii=False)
except Exception:
pass
return ''
def _get_response_result(response):
"""获取响应结果中的data字段"""
try:
data = response.get_json(silent=True)
if data and isinstance(data, dict):
result_data = data.get('data')
if result_data is not None:
import json
return json.dumps(result_data, ensure_ascii=False)
# data字段为空时取msg
return data.get('msg', '')
except Exception:
pass
return ''
def _build_title(path):
"""从请求路径构建操作标题"""
# 路径格式: /user/add, /role/update, /menu/delete 等
parts = path.strip('/').split('/')
if len(parts) >= 2:
module = parts[0]
# 模块名中文化映射
module_names = {
'user': '用户管理', 'role': '角色管理', 'menu': '菜单管理',
'dept': '部门管理', 'position': '岗位管理', 'level': '职级管理',
'param': '参数配置', 'dict': '字典管理', 'dictitem': '字典项管理',
'config': '系统配置', 'configitem': '配置项管理',
'category': '分类管理', 'article': '文章管理',
'notice': '通知管理', 'link': '友情链接',
'job': '定时任务', 'login': '登录管理',
'loginlog': '登录日志', 'operationlog': '操作日志',
'upload': '文件上传',
}
module_name = module_names.get(module.lower(), module)
action = parts[-1] if len(parts) > 1 else ''
action_names = {
'add': '新增', 'update': '修改', 'delete': '删除',
'status': '状态变更', 'batchdelete': '批量删除',
'reset': '重置', 'save': '保存', 'sort': '排序',
'page': '查询', 'detail': '查询', 'list': '查询',
}
action_name = action_names.get(action.lower(), action)
return f"{module_name}-{action_name}" if action_name else module_name
return path