Files
meijiaka-zy/python-api/app/core/exceptions.py
T

234 lines
7.2 KiB
Python

"""
自定义异常类
============
分层异常体系:
- 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