89 lines
3.2 KiB
Python
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()) |