Files
FastapiAdmin/backend/app/core/router_class.py
T
zhangtao cf88ab8897 refactor: 整合仪表盘功能到监控模块,清理冗余代码
- 移除原监控仪表盘独立模块,将相关功能合并到在线监控模块
- 重构租户配置字段名,统一使用logo_url和name替代tenant_logo/tenant_name
- 优化搜索工具函数,移除重复导入
- 调整参数配置模型字段长度限制,移除config_value的max_length约束
- 清理冗余的常量定义和导入语句
- 修复批量状态设置接口的redis依赖注入
- 增强OAuth登录安全性,添加租户默认归属和state一次性消费
- 优化资源目录缓存逻辑,减少重复计算
- 新增API Token模块基础框架
- 完善用户token版本管理,支持主动失效JWT
- 调整AI模型配置缓存过期时间
- 修复菜单类型字段索引,提升查询性能
- 简化前端刷新token调用逻辑
- 新增滑块验证完成接口和忘记密码验证码校验
- 调整系统配置默认值,添加操作日志保留天数和接口白名单配置
- 限制Mock支付回调仅在开发环境可用
- 重构websocket认证方式,支持更安全的subprotocol传参
2026-07-13 01:14:20 +08:00

107 lines
4.3 KiB
Python

import json
import time
from collections.abc import Callable, Coroutine
from typing import Any
from fastapi import Request, Response
from fastapi.routing import APIRoute
from starlette.background import BackgroundTask
from app.config.setting import settings
from app.core.logger import logger
from app.utils.ip_local_util import get_client_ip
_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
# (通常在登录前调用,没有 JWT token)
_PUBLIC_WRITE_PATHS: set[str] = {
"/auth/login",
"/auth/token/refresh",
"/auth/captcha/slider/complete",
"/auth/tenant/register",
"/auth/user/register",
}
async def _write_operation_log_async(log_data: dict) -> None:
"""直接写入操作日志(函数体内导入避免循环依赖)。"""
try:
from app.api.v1.module_system.log.crud import OperationLogCRUD
from app.api.v1.module_system.log.schema import OperationLogCreateSchema
from app.core.base_schema import AuthSchema
from app.core.database import async_db_session
async with async_db_session() as _session, _session.begin():
auth = AuthSchema(check_data_scope=False)
await OperationLogCRUD(auth, _session).create(data=OperationLogCreateSchema(**log_data))
except Exception:
logger.exception("操作日志写入失败: path={}", log_data.get("request_path"))
class OperationLogRoute(APIRoute):
"""操作日志路由 — 自动记录请求/响应并后台异步写入。
根据 HTTP 方法判断:
- 写方法 (POST/PUT/DELETE/PATCH):注入租户写权限检查
- 读方法 (GET/HEAD/OPTIONS):不注入
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
methods = getattr(self, "methods", set())
if methods & _WRITE_METHODS and self.path not in _PUBLIC_WRITE_PATHS:
if self.dependencies is None:
self.dependencies = []
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
start = time.time()
response: Response = await original_route_handler(request)
if request.method not in settings.OPERATION_RECORD_METHOD:
return response
route: APIRoute = request.scope.get("route", None)
try:
oper_param: dict[str, Any] = {}
content_type = request.headers.get("Content-Type", "")
if content_type.startswith(("multipart/form-data", "application/x-www-form-urlencoded")):
form_data = await request.form()
oper_param["form"] = dict(form_data.items())
else:
payload = await request.body()
if payload:
try:
oper_param["body"] = json.loads(payload.decode())
except (json.JSONDecodeError, UnicodeDecodeError):
oper_param["body"] = payload.decode("utf-8", errors="ignore")
if request.path_params:
oper_param["path_params"] = dict(request.path_params)
log_payload = json.dumps(oper_param, ensure_ascii=False)
if len(log_payload) > 2000:
log_payload = "请求参数过长"
is_json = "application/json" in response.headers.get("Content-Type", "")
response_data = response.body if is_json else b"{}"
log_data: dict[str, Any] = {
"request_path": request.url.path,
"request_method": request.method,
"request_payload": log_payload,
"response_code": response.status_code,
"response_json": bytes(response_data).decode(),
"process_time": f"{(time.time() - start):.2f}s",
"description": route.summary if route else "",
"request_ip": get_client_ip(request),
}
response.background = BackgroundTask(_write_operation_log_async, log_data)
except Exception:
logger.warning("操作日志采集异常: {}", request.url.path, exc_info=True)
return response
return custom_route_handler