""" 自定义异常类 ============ 分层异常体系: - AppException: 业务层错误(参数校验、权限、资源不存在等) - PlatformError: 第三方平台调用错误(网络、限流、认证、服务异常等) Router 层只处理这两类异常,其余全部兜底为 500。 """ from fastapi import HTTPException, status # ═══════════════════════════════════════════════════════════════ # 业务层异常(AppException 体系) # ═══════════════════════════════════════════════════════════════ class AppException(HTTPException): """应用基础异常""" def __init__( self, status_code: int, message: str = "操作失败", detail: dict | None = None, *, error_code: str | None = None, ): body = detail or {} body["message"] = message if error_code: body["error_code"] = error_code super().__init__(status_code=status_code, detail=body) self.message = message self.error_code = error_code class NotFoundException(AppException): """资源不存在""" def __init__(self, message: str = "资源不存在"): super().__init__( status_code=status.HTTP_404_NOT_FOUND, message=message, ) class ValidationException(AppException): """参数验证失败""" def __init__(self, message: str = "参数验证失败"): super().__init__( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, message=message, ) class UnauthorizedException(AppException): """未授权""" def __init__(self, message: str = "未授权,请先登录"): super().__init__( status_code=status.HTTP_401_UNAUTHORIZED, message=message, ) class ForbiddenException(AppException): """禁止访问""" def __init__(self, message: str = "无权访问该资源"): super().__init__( status_code=status.HTTP_403_FORBIDDEN, message=message, ) class BusinessException(AppException): """业务逻辑错误""" def __init__(self, message: str = "业务操作失败"): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, message=message, ) class InsufficientPointsException(AppException): """积分不足""" def __init__(self, message: str = "积分不足"): super().__init__( status_code=status.HTTP_402_PAYMENT_REQUIRED, message=message, error_code="insufficient_points", ) class ModelUnavailableException(AppException): """AI 模型不可用""" def __init__(self, message: str = "AI 模型服务暂时不可用"): super().__init__( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, message=message, ) class TaskFailedException(AppException): """异步任务执行失败""" def __init__(self, message: str = "任务执行失败"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=message, ) class PromptNotFoundException(AppException): """提示词文件不存在""" def __init__(self, message: str = "未找到提示词"): super().__init__( status_code=status.HTTP_404_NOT_FOUND, message=message, error_code="prompt_not_found", ) class AIEmptyResponseException(AppException): """AI 返回内容为空""" def __init__(self, message: str = "AI 返回内容为空"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=message, error_code="empty_result", ) class AIParseErrorException(AppException): """AI 返回内容解析失败""" def __init__(self, message: str = "AI 返回格式解析失败"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=message, error_code="parse_error", ) class AITimeoutException(AppException): """AI 调用超时""" def __init__(self, message: str = "AI 请求超时,请稍后重试"): super().__init__( status_code=status.HTTP_504_GATEWAY_TIMEOUT, message=message, error_code="timeout", ) # ═══════════════════════════════════════════════════════════════ # 第三方平台异常(PlatformError 体系) # ═══════════════════════════════════════════════════════════════ class PlatformErrorType: """第三方错误类型标准枚举 所有 Adapter 必须将供应商异常映射为以下标准类型之一, 确保前端和网关能够统一处理。 """ RATE_LIMIT = "rate_limit" # 429,可重试 AUTH_FAILED = "auth_failed" # 401/403,不可重试 TIMEOUT = "timeout" # 连接/读取超时,可重试 SERVER_ERROR = "server_error" # 第三方 5xx,可重试 BAD_REQUEST = "bad_request" # 参数错误,不可重试 QUOTA_EXHAUSTED = "quota_exhausted" # 额度用完,不可重试(或延迟重试) NOT_FOUND = "not_found" # 资源不存在,不可重试 CONTENT_VIOLATION = "content_violation" # 内容安全/审核不通过,不可重试 UNKNOWN = "unknown" # 兜底 class PlatformError(Exception): """第三方平台调用失败的唯一异常类 Router 层只需 except PlatformError,即可返回标准 HTTP 状态码。 所有 app/services/ 和 app/ai/ 下的代码,对外抛出的异常必须是此类。 Example: raise PlatformError( "Vidu 限流", platform="vidu", retryable=True, error_type=PlatformErrorType.RATE_LIMIT, status_code=429, ) """ def __init__( self, message: str, *, platform: str, retryable: bool = False, error_type: str = PlatformErrorType.UNKNOWN, status_code: int | None = None, raw_code: str | None = None, ): super().__init__(message) self.platform = platform self.retryable = retryable self.error_type = error_type self.status_code = status_code self.raw_code = raw_code def to_http_status(self) -> int: """根据 error_type 和 retryable 返回标准 HTTP 状态码""" mapping = { PlatformErrorType.RATE_LIMIT: 429, PlatformErrorType.QUOTA_EXHAUSTED: 429, PlatformErrorType.TIMEOUT: 504, PlatformErrorType.AUTH_FAILED: 401, PlatformErrorType.BAD_REQUEST: 400, PlatformErrorType.NOT_FOUND: 404, PlatformErrorType.CONTENT_VIOLATION: 400, } if self.error_type in mapping: return mapping[self.error_type] return 502 if self.retryable else 400