"""业务异常与全局异常处理器:统一转成 {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())