Files
FastapiAdmin/backend/app/core/exceptions.py
T
zhangtao 211fddd6e0 refactor: 完成项目大规模代码重构与依赖清理
这是一次综合性的重构更新,包含以下主要变更:
1.  升级Python版本到3.12,更新依赖配置
2.  替换旧的.j2模板为.jinja2格式,新增代码生成模板
3.  重构权限过滤策略,更新权限枚举与模型配置
4.  移除Prefect依赖,替换为自研拓扑并行执行引擎
5.  重构认证与上下文管理,拆分租户/请求上下文
6.  简化响应模型、CRUD与服务层代码
7.  清理废弃的支付网关模块,重构订单定时任务
8.  更新在线用户、监控等模块的接口与路由
9.  优化邮件模板与工具类,新增邮件模板文件
10. 修复数据库会话配置与类型提示
2026-06-21 06:02:05 +08:00

132 lines
6.1 KiB
Python

import traceback
from typing import Any
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from pydantic_validation_decorator import FieldValidationError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from app.common.constant import RET
from app.common.response import ErrorResponse
from app.core.logger import logger
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 _tb_source(exc: Exception) -> str:
"""从 traceback 提取抛出的 文件名:行号:函数名"""
if exc.__traceback__ and (tb := traceback.extract_tb(exc.__traceback__)):
f = tb[-1]
return f"{f.filename}:{f.lineno}:{f.name}"
return "unknown"
_VALIDATION_ERROR_MAP: dict[str, str] = {
"Field required": "请求失败,缺少必填项!",
"value is not a valid list": "类型错误,提交参数应该为列表!",
"value is not a valid int": "类型错误,提交参数应该为整数!",
"value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!",
"Input should be a valid list": "类型错误,输入应该是一个有效的列表!",
}
def handle_exception(app: FastAPI) -> None:
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException) -> JSONResponse:
logger.error(
"[自定义异常] %s %s | source=%s | code=%s | msg=%s | data=%s",
request.method, request.url.path,
_tb_source(exc), exc.code, exc.msg, exc.data,
)
return ErrorResponse(msg=exc.msg, code=exc.code, status_code=exc.status_code, data=exc.data)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
logger.error(
"[HTTP异常] %s %s | status_code=%s | detail=%s",
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:
raw_msg = exc.errors()[0].get("msg")
msg = _VALIDATION_ERROR_MAP.get(raw_msg, raw_msg)
if isinstance(msg, str) and msg.startswith("Value error"):
msg = msg[11:].lstrip(" ,")
logger.error(
"[参数验证异常] %s %s | msg=%s | errors=%s",
request.method, request.url.path, msg, exc.errors(),
)
return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body)
@app.exception_handler(ResponseValidationError)
async def response_validation_handler(request: Request, exc: ResponseValidationError) -> JSONResponse:
logger.error(
"[响应验证异常] %s %s | errors=%s",
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__
logger.error(
"[数据库异常] %s %s | type=%s | detail=%s",
request.method, request.url.path, exc_type, exc,
)
if isinstance(exc, IntegrityError):
detail = str(exc.orig) if exc.orig else str(exc)
if "Duplicate entry" in detail:
return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=detail)
if "foreign key constraint" in detail:
return ErrorResponse(msg="存在关联数据,无法删除", status_code=status.HTTP_409_CONFLICT, data=detail)
if "cannot be null" in detail:
return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=detail)
return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=detail)
lower = str(exc).lower()
if "connect" in lower or "connection" in lower:
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_503_SERVICE_UNAVAILABLE, data=exc_type)
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("[值异常] %s %s | msg=%s", request.method, request.url.path, exc)
return ErrorResponse(msg=str(exc), status_code=status.HTTP_400_BAD_REQUEST)
@app.exception_handler(FieldValidationError)
async def field_validation_exception_handler(request: Request, exc: FieldValidationError) -> JSONResponse:
logger.error("[字段验证异常] %s %s | msg=%s", request.method, request.url.path, exc.message)
return ErrorResponse(msg=exc.message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@app.exception_handler(Exception)
async def all_exception_handler(request: Request, exc: Exception) -> JSONResponse:
exc_type = type(exc).__name__
logger.error(
"[未捕获异常] %s %s | type=%s | detail=%s",
request.method, request.url.path, exc_type, exc,
)
return ErrorResponse(msg="服务器内部错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)