Files
FastapiAdmin/backend/app/core/exceptions.py
T
zhangtao 158c14a46d refactor: 项目代码整理与依赖优化
主要变更:
1.  替换默认部门名称为系统部门
2.  简化SQLAlchemy异步导入语句
3.  移除request_context.py与相关依赖,重构请求上下文逻辑
4.  替换旧的日志链路ID实现,将代码整合到logger.py
5.  简化WebSocket限流逻辑,移除冗余import
6.  删除多个模块的测试文件:common/ai/example/generator/monitor/task
7.  移除cron_util.py,改用croniter库实现Cron表达式校验
8.  优化系统参数Redis缓存键格式,移除多余的:1:前缀
9.  简化初始化脚本逻辑,移除日期自动转换逻辑
10. 简化动态路由发现代码,移除热重载相关逻辑
11. 简化认证逻辑,移除request.state.ctx相关操作
12. 修复定时任务调度器调用方式,移除await
13. 新增修改定时任务接口
14. 新增croniter依赖并更新相关配置
15. 优化数据库异常日志格式化方式
16. 简化base_schema.py空行格式
17. 移除initialize.py中的冗余依赖导入
2026-07-17 23:19:37 +08:00

158 lines
6.6 KiB
Python

from functools import wraps
from math import ceil
from typing import Any
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from slowapi.errors import RateLimitExceeded
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from app.common.enums import RET, EnvironmentEnum
from app.common.response import ErrorResponse
from app.config.setting import settings
from app.core.logger import logger
def require_superadmin(func):
"""装饰器:仅超级管理员可调用 Service 方法。
自动校验 ``self.auth.user.is_superuser`` 属性,非超管直接抛出 403。
适用于实例方法(``Service(auth).xxx(...)``),由 ``self.auth`` 取认证上下文。
用法:
class XxxService:
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
@require_superadmin
async def create(self, data: ...) -> ...:
...
"""
@wraps(func)
async def wrapper(self, *args, **kwargs):
if not self.auth.user or not self.auth.user.is_superuser:
raise CustomException(msg="仅平台管理员可操作")
return await func(self, *args, **kwargs)
return wrapper
class CustomException(Exception):
def __init__(
self,
msg: str = RET.EXCEPTION.msg,
code: int = RET.EXCEPTION.code,
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
data: Any | None = None,
success: bool = False,
) -> None:
super().__init__(msg)
self.status_code = status_code
self.code = code
self.msg = msg
self.data = data
self.success = success
def __str__(self) -> str:
return self.msg
def handle_exception(app: FastAPI) -> None:
@app.exception_handler(RateLimitExceeded)
async def rate_limit_exception_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
return ErrorResponse(
msg="请求过于频繁,请稍后重试!",
code=429,
status_code=429,
data={"Retry-After": str(ceil(getattr(exc, "retry_after", 60)))},
)
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException) -> JSONResponse:
logger.error(
"[自定义异常] {} {} | code={} | msg={} | data={}",
request.method,
request.url.path,
exc.code,
exc.msg,
exc.data,
)
# 生产环境不外泄 data(可能含 SQL 字段、约束名等内部细节)
expose_data = exc.data if settings.ENVIRONMENT != EnvironmentEnum.PROD else None
return ErrorResponse(msg=exc.msg, code=exc.code, status_code=exc.status_code, data=expose_data)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
logger.error(
"[HTTP异常] {} {} | status_code={} | detail={}",
request.method,
request.url.path,
exc.status_code,
exc.detail,
)
return ErrorResponse(msg=exc.detail, status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
errors = exc.errors()
msg = errors[0].get("msg", str(errors[0])) if errors else "请求参数验证失败"
if msg.startswith("Value error"):
msg = msg[11:].lstrip(" ,")
logger.error(
"[参数验证异常] {} {} | errors={}",
request.method,
request.url.path,
errors,
)
return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=errors)
@app.exception_handler(ResponseValidationError)
async def response_validation_handler(request: Request, exc: ResponseValidationError) -> JSONResponse:
logger.error(
"[响应验证异常] {} {} | errors={}",
request.method,
request.url.path,
exc.errors(),
)
return ErrorResponse(msg="服务器响应格式错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc.body)
@app.exception_handler(SQLAlchemyError)
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) -> JSONResponse:
exc_type = type(exc).__name__
if isinstance(exc, IntegrityError):
detail = str(exc.orig) if exc.orig else str(exc)
expose_detail = detail if settings.ENVIRONMENT != EnvironmentEnum.PROD else None
if "connect" in detail or "connection" in detail:
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_403_SERVICE_UNAVAILABLE, data=expose_detail)
if "Duplicate entry" in detail:
return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
if "foreign key constraint" in detail:
return ErrorResponse(msg="存在关联数据,无法删除", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
if "cannot be null" in detail:
return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
logger.error("[数据库异常] {} {} | type={} | detail={}", request.method, request.url.path, exc_type, exc)
return ErrorResponse(msg=f"数据库操作失败: {exc_type}", status_code=status.HTTP_400_BAD_REQUEST, data=str(exc))
@app.exception_handler(ValueError)
async def value_exception_handler(request: Request, exc: ValueError) -> JSONResponse:
logger.error("[值异常] {} {} | msg={}", request.method, request.url.path, exc)
return ErrorResponse(msg=str(exc), status_code=status.HTTP_400_BAD_REQUEST)
@app.exception_handler(Exception)
async def all_exception_handler(request: Request, exc: Exception) -> JSONResponse:
exc_type = type(exc).__name__
logger.error(
"[未捕获异常] {} {} | type={} | detail={}",
request.method,
request.url.path,
exc_type,
exc,
)
return ErrorResponse(msg="服务器内部错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)