# -*- coding: utf-8 -*- import time import json from typing import Any, Callable, Coroutine from fastapi import Request, Response from fastapi.routing import APIRoute from user_agents import parse from app.core.database import async_db_session from app.config.setting import settings from app.utils.ip_local_util import IpLocalUtil from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.log.schema import OperationLogCreateSchema from app.api.v1.module_system.log.service import OperationLogService """ 在 FastAPI 中,route_class 参数用于自定义路由的行为。 通过设置 route_class,你可以定义一个自定义的路由类,从而在每个路由处理之前或之后执行特定的操作。 这对于日志记录、权限验证、性能监控等场景非常有用。 """ class OperationLogRoute(APIRoute): """操作日志路由装饰器""" def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: """ 自定义路由处理程序,在每个路由处理之前或之后执行特定的操作。 参数: - request (Request): FastAPI请求对象。 返回: - Response: FastAPI响应对象。 """ original_route_handler = super().get_route_handler() async def custom_route_handler(request: Request) -> Response: """ 自定义路由处理程序,在每个路由处理之前或之后执行特定的操作。 参数: - request (Request): FastAPI请求对象。 描述: - 该方法在每个路由处理之前被调用,用于记录操作日志。 返回: - Response: FastAPI响应对象。 """ start_time = time.time() # 请求前的处理 response: Response = await original_route_handler(request) # 请求后的处理 if not settings.OPERATION_LOG_RECORD: return response if request.method not in settings.OPERATION_RECORD_METHOD: return response route: APIRoute = request.scope.get("route", None) if route.name in settings.IGNORE_OPERATION_FUNCTION: return response user_agent = parse(request.headers.get("user-agent")) payload = b"{}" req_content_type = request.headers.get("Content-Type", "") if req_content_type and ( req_content_type.startswith('multipart/form-data') or req_content_type.startswith('application/x-www-form-urlencoded') ): form_data = await request.form() oper_param = '\n'.join([f'{k}: {v}' for k, v in form_data.items()]) payload = oper_param # 直接使用字符串格式的参数 else: payload = await request.body() path_params = request.path_params oper_param = {} # 处理请求体数据 if payload: try: oper_param['body'] = json.loads(payload.decode()) except (json.JSONDecodeError, UnicodeDecodeError): oper_param['body'] = payload.decode('utf-8', errors='ignore') # 处理路径参数 if path_params: oper_param['path_params'] = dict(path_params) payload = json.dumps(oper_param, ensure_ascii=False) # 日志表请求参数字段长度最大为2000,因此在此处判断长度 if len(payload) > 2000: payload = '请求参数过长' response_data = response.body if "application/json" in response.headers.get("Content-Type", "") else b"{}" process_time = f"{(time.time() - start_time):.2f}s" # 获取当前用户ID,如果是登录接口则为空 log_type = 1 # 1:登录日志 2:操作日志 current_user_id = None # 优化:只在操作日志场景下获取current_user_id if "user_id" in request.scope: current_user_id = request.scope.get("user_id") log_type = 2 request_ip = None x_forwarded_for = request.headers.get('X-Forwarded-For') if x_forwarded_for: # 取第一个 IP 地址,通常为客户端真实 IP request_ip = x_forwarded_for.split(',')[0].strip() else: # 若没有 X-Forwarded-For 头,则使用 request.client.host if request.client: request_ip = request.client.host login_location = await IpLocalUtil.get_ip_location(request_ip) if request_ip else None # 判断请求是否来自api文档 referer = request.headers.get('referer') request_from_swagger = referer and referer.endswith('docs') request_from_redoc = referer and referer.endswith('redoc') if request_from_swagger or request_from_redoc: # 如果请求来自api文档,则不记录日志 pass else: async with async_db_session() as session: async with session.begin(): auth = AuthSchema(db=session) await OperationLogService.create_log_service(data=OperationLogCreateSchema( type = log_type, request_path = request.url.path, request_method = request.method, request_payload = payload, request_ip = request_ip, login_location=login_location, request_os = user_agent.os.family, request_browser = user_agent.browser.family, response_code = response.status_code, response_json = response_data.decode() if isinstance(response_data, (bytes, bytearray)) else str(response_data), process_time = process_time, description = route.summary, created_id = current_user_id, updated_id = current_user_id, ), auth = auth) return response return custom_route_handler