Files
2026-09-08 19:17:35 +08:00

89 lines
3.2 KiB
Python

"""业务异常与全局异常处理器:统一转成 {code, message, data, trace_id}。"""
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from utils.logger import get_logger
from utils.response import Code, fail
logger = get_logger("api")
class ApiError(Exception):
"""业务异常基类。code 见 utils.response.Code。"""
def __init__(self, code: int, message: str, data=None):
self.code = code
self.message = message
self.data = data
super().__init__(message)
class ParamError(ApiError):
def __init__(self, message: str = "参数错误"):
super().__init__(Code.PARAM_ERROR, message)
class AuthError(ApiError):
def __init__(self, message: str = "未登录或登录已过期"):
super().__init__(Code.UNAUTHORIZED, message)
class ForbiddenError(ApiError):
def __init__(self, message: str = "无权限执行该操作"):
super().__init__(Code.FORBIDDEN, message)
class NotFoundError(ApiError):
def __init__(self, message: str = "资源不存在"):
super().__init__(Code.NOT_FOUND, message)
class LLMFailError(ApiError):
"""LLM 调用失败(重试耗尽,§1001)。"""
def __init__(self, message: str = "LLM 调用失败,请稍后再试"):
super().__init__(Code.LLM_FAIL, message)
class RiskTriggeredError(ApiError):
"""交易被风控拦截(§1004)。"""
def __init__(self, message: str = "交易被风控拦截,请在订单列表查看处理进度", data=None):
super().__init__(Code.RISK_TRIGGERED, message, data)
class NotSuitableError(ApiError):
"""适当性不匹配(§1005)。"""
def __init__(self, message: str = "客户风险等级与产品风险等级不匹配", data=None):
super().__init__(Code.NOT_SUITABLE, message, data)
def _http_status(code: int) -> int:
"""业务码若为标准 HTTP 状态码则原样返回,自定义业务码(1001+)统一返回 200。"""
return code if 400 <= code < 600 else 200
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(StarletteHTTPException)
async def http_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(status_code=exc.status_code, content=fail(exc.status_code, str(exc.detail)).model_dump())
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content=fail(Code.PARAM_ERROR, "参数校验失败", exc.errors()).model_dump(),
)
@app.exception_handler(ApiError)
async def api_error_handler(request: Request, exc: ApiError):
status = _http_status(exc.code)
return JSONResponse(status_code=status, content=fail(exc.code, exc.message, exc.data).model_dump())
@app.exception_handler(Exception)
async def unhandled_handler(request: Request, exc: Exception):
logger.exception("unhandled error on %s", request.url.path)
return JSONResponse(status_code=500, content=fail(Code.SERVER_ERROR, "服务内部错误").model_dump())