""" 操作日志自动记录中间件 ============================================================= 通过 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