Files
autoclip/backend/core/error_middleware.py
Kris Ka dc0cf66159 完成项目问题修复和优化
主要改进:
1. 清理重复文件和冗余代码
   - 删除重复的API文件 (upload_backup.py, upload_new.py等)
   - 删除旧版本的B站服务文件 (bilibili_upload_v2.py到v6.py)
   - 统一B站服务接口

2. 优化数据存储架构
   - 创建OptimizedStorageService解决双重存储问题
   - 数据库只存储元数据,文件系统存储实际文件
   - 提供数据迁移脚本和一致性检查工具

3. 统一错误处理机制
   - 创建统一错误处理中间件
   - 提供错误处理装饰器和上下文管理器
   - 统一错误响应格式

4. 完善配置管理
   - 创建UnifiedConfig统一配置系统
   - 整合所有配置源(环境变量、配置文件、默认值)
   - 提供配置迁移脚本

5. 增强进度系统
   - 创建EnhancedProgressService
   - 支持Redis缓存、数据库持久化和内存缓存
   - 提供完整的进度跟踪和状态管理

新增文件:
- backend/services/optimized_storage_service.py
- backend/core/error_middleware.py
- backend/core/unified_config.py
- backend/services/enhanced_progress_service.py
- scripts/migrate_to_optimized_storage.py
- scripts/check_data_consistency.py
- scripts/migrate_config.py
- docs/ERROR_HANDLING_GUIDE.md
- docs/PROGRESS_SYSTEM_GUIDE.md

修复文件:
- backend/services/bilibili_service.py (统一上传接口)
- backend/main.py (使用新的错误处理中间件)
2025-09-15 12:07:09 +08:00

284 lines
8.7 KiB
Python

"""
统一错误处理中间件
为FastAPI应用提供统一的错误处理机制
"""
import logging
import traceback
import time
import asyncio
import functools
from typing import Union
from contextlib import contextmanager
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from ..utils.error_handler import AutoClipsException, ErrorCategory, ErrorLevel
from ..services.exceptions import ServiceError
logger = logging.getLogger(__name__)
class ErrorResponse:
"""统一错误响应格式"""
def __init__(self,
error_code: str,
message: str,
details: dict = None,
request_id: str = None):
self.error_code = error_code
self.message = message
self.details = details or {}
self.request_id = request_id
self.timestamp = None
def to_dict(self) -> dict:
return {
"error": {
"code": self.error_code,
"message": self.message,
"details": self.details,
"request_id": self.request_id,
"timestamp": self.timestamp
}
}
def create_error_response(
status_code: int,
error_code: str,
message: str,
details: dict = None,
request_id: str = None
) -> JSONResponse:
"""创建统一格式的错误响应"""
response = ErrorResponse(error_code, message, details, request_id)
response.timestamp = time.time()
return JSONResponse(
status_code=status_code,
content=response.to_dict()
)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""全局异常处理器"""
request_id = getattr(request.state, 'request_id', None)
# 记录异常详情
logger.error(
f"未处理的异常: {type(exc).__name__}: {str(exc)}",
extra={
"request_id": request_id,
"path": request.url.path,
"method": request.method,
"traceback": traceback.format_exc()
}
)
# 根据异常类型返回不同的错误响应
if isinstance(exc, AutoClipsException):
return handle_autoclips_exception(exc, request_id)
elif isinstance(exc, ServiceError):
return handle_service_error(exc, request_id)
elif isinstance(exc, HTTPException):
return handle_http_exception(exc, request_id)
elif isinstance(exc, RequestValidationError):
return handle_validation_error(exc, request_id)
elif isinstance(exc, StarletteHTTPException):
return handle_starlette_http_exception(exc, request_id)
else:
return handle_generic_exception(exc, request_id)
def handle_autoclips_exception(exc: AutoClipsException, request_id: str = None) -> JSONResponse:
"""处理AutoClipsException"""
status_code = get_status_code_for_category(exc.category)
return create_error_response(
status_code=status_code,
error_code=f"AUTOCLIPS_{exc.category.value}",
message=exc.message,
details=exc.details,
request_id=request_id
)
def handle_service_error(exc: ServiceError, request_id: str = None) -> JSONResponse:
"""处理ServiceError"""
status_code = get_status_code_for_service_error(exc.error_code)
return create_error_response(
status_code=status_code,
error_code=exc.error_code.value,
message=exc.message,
details=exc.details,
request_id=request_id
)
def handle_http_exception(exc: HTTPException, request_id: str = None) -> JSONResponse:
"""处理HTTPException"""
return create_error_response(
status_code=exc.status_code,
error_code=f"HTTP_{exc.status_code}",
message=exc.detail,
request_id=request_id
)
def handle_validation_error(exc: RequestValidationError, request_id: str = None) -> JSONResponse:
"""处理请求验证错误"""
errors = []
for error in exc.errors():
errors.append({
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
"type": error["type"]
})
return create_error_response(
status_code=422,
error_code="VALIDATION_ERROR",
message="请求参数验证失败",
details={"errors": errors},
request_id=request_id
)
def handle_starlette_http_exception(exc: StarletteHTTPException, request_id: str = None) -> JSONResponse:
"""处理StarletteHTTPException"""
return create_error_response(
status_code=exc.status_code,
error_code=f"STARLETTE_{exc.status_code}",
message=str(exc.detail),
request_id=request_id
)
def handle_generic_exception(exc: Exception, request_id: str = None) -> JSONResponse:
"""处理通用异常"""
return create_error_response(
status_code=500,
error_code="INTERNAL_SERVER_ERROR",
message="服务器内部错误",
details={"exception_type": type(exc).__name__},
request_id=request_id
)
def get_status_code_for_category(category: ErrorCategory) -> int:
"""根据错误分类获取HTTP状态码"""
status_mapping = {
ErrorCategory.CONFIGURATION: 500,
ErrorCategory.NETWORK: 503,
ErrorCategory.API: 502,
ErrorCategory.FILE_IO: 500,
ErrorCategory.PROCESSING: 500,
ErrorCategory.VALIDATION: 400,
ErrorCategory.SYSTEM: 500
}
return status_mapping.get(category, 500)
def get_status_code_for_service_error(error_code) -> int:
"""根据服务错误代码获取HTTP状态码"""
status_mapping = {
"CONFIG_NOT_FOUND": 500,
"CONFIG_INVALID": 500,
"CONFIG_MISSING_REQUIRED": 500,
"FILE_NOT_FOUND": 404,
"FILE_PERMISSION_DENIED": 403,
"FILE_CORRUPTED": 500,
"PROCESSING_FAILED": 500,
"STEP_EXECUTION_FAILED": 500,
"PIPELINE_VALIDATION_FAILED": 400,
"TASK_NOT_FOUND": 404,
"TASK_ALREADY_RUNNING": 409,
"TASK_CANCELLED": 410,
"PROJECT_NOT_FOUND": 404,
"PROJECT_ALREADY_EXISTS": 409,
"SYSTEM_ERROR": 500,
"NETWORK_ERROR": 503,
"TIMEOUT_ERROR": 504,
"CONCURRENT_ACCESS": 409,
"LOCK_ACQUISITION_FAILED": 423,
"UNKNOWN_ERROR": 500
}
return status_mapping.get(error_code.value, 500)
# 装饰器:自动错误处理
def handle_errors(error_category: ErrorCategory = ErrorCategory.SYSTEM):
"""错误处理装饰器"""
def decorator(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except AutoClipsException:
# 重新抛出AutoClipsException
raise
except ServiceError:
# 重新抛出ServiceError
raise
except Exception as e:
# 转换为AutoClipsException
raise AutoClipsException(
message=str(e),
category=error_category,
original_exception=e
)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except AutoClipsException:
# 重新抛出AutoClipsException
raise
except ServiceError:
# 重新抛出ServiceError
raise
except Exception as e:
# 转换为AutoClipsException
raise AutoClipsException(
message=str(e),
category=error_category,
original_exception=e
)
# 根据函数类型返回对应的包装器
if asyncio.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
return decorator
# 上下文管理器:错误上下文
@contextmanager
def error_context(category: ErrorCategory, context_info: dict = None):
"""错误上下文管理器"""
try:
yield
except Exception as e:
if isinstance(e, AutoClipsException):
# 已经是自定义异常,直接抛出
raise
else:
# 转换为自定义异常
details = context_info or {}
details["original_exception_type"] = type(e).__name__
raise AutoClipsException(
message=str(e),
category=category,
details=details,
original_exception=e
)