# 错误处理器重构说明 ## 📦 重构目标 将错误处理逻辑从 `main.py` 中抽离,提升代码的: - **模块化**:职责分离,每个模块做好自己的事 - **可维护性**:错误处理逻辑集中管理 - **可测试性**:独立的模块更容易编写单元测试 - **可读性**:`main.py` 更简洁清晰 ## 🔄 重构前后对比 ### ❌ 重构前 `main.py` 文件有 **238 行**,包含大量错误处理逻辑: ```python # main.py (238 行) """ FastAPI 应用入口 """ from fastapi import FastAPI, Request, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from datetime import datetime, timezone # ... 其他导入 ... # ==================== 全局异常处理器 ==================== @app.exception_handler(404) async def not_found_handler(request: Request, exc): """处理 404 错误""" return JSONResponse(...) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): """处理 HTTPException""" return JSONResponse(...) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """处理请求参数验证错误""" # 100+ 行的复杂逻辑 friendly_errors = [] for error in exc.errors(): # ... 错误转换逻辑 ... return JSONResponse(...) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): """处理未捕获的异常""" return JSONResponse(...) ``` **问题**: - 🔴 `main.py` 过于臃肿,职责不清 - 🔴 错误处理逻辑混在应用启动代码中 - 🔴 难以单独测试错误处理逻辑 - 🔴 不符合单一职责原则 --- ### ✅ 重构后 #### 1. 新增 `app/core/error_handlers.py` (250 行) ```python """ 全局错误处理器 统一处理应用中的各种异常,转换为友好的 API 响应格式。 """ from datetime import datetime, timezone from typing import Dict, List, Any from fastapi import Request, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from app.core.config import get_settings from app.core.logging import get_logger # 错误消息映射 ERROR_TYPE_MESSAGES = { "uuid_parsing": "必须是有效的 UUID 格式,或者不传此字段", "string_too_short": "长度不能少于 {min_length} 个字符", # ... 更多映射 ... } def get_friendly_error_message(error: Dict[str, Any]) -> str: """将 Pydantic 验证错误转换为用户友好的消息""" # ... 转换逻辑 ... def extract_field_path(loc: List[Any]) -> str: """从错误位置中提取字段路径""" # ... 提取逻辑 ... def create_error_response(...) -> JSONResponse: """创建统一格式的错误响应""" # ... 创建响应 ... # 各类异常处理器 async def not_found_handler(request: Request, exc: Exception) -> JSONResponse: """处理 404 错误""" # ... async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: """处理 HTTPException""" # ... async def validation_exception_handler(...) -> JSONResponse: """处理请求参数验证错误""" # ... async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: """处理未捕获的异常""" # ... def register_exception_handlers(app) -> None: """注册所有异常处理器到 FastAPI 应用""" app.add_exception_handler(404, not_found_handler) app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(Exception, general_exception_handler) logger.info("✅ 异常处理器注册完成") ``` #### 2. 简化后的 `main.py` (107 行) ```python """ FastAPI 应用入口 """ from contextlib import asynccontextmanager from fastapi import FastAPI from app.core.config import get_settings from app.core.logging import setup_logging, get_logger from app.core.database import init_db from app.core.error_handlers import register_exception_handlers # ✅ 导入注册函数 from app.api.v1 import api_router from app.middleware.request_id import RequestIdMiddleware from app.middleware.logging import LoggingMiddleware from app.middleware.cors import setup_cors # 初始化配置和日志 settings = get_settings() setup_logging() logger = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" logger.info("🚀 Starting Jointo API...") await init_db() yield logger.info("👋 Shutting down Jointo API...") # 创建 FastAPI 应用 app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, description="AI 驱动的视频创作平台后端 API", docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", lifespan=lifespan, ) # 配置 CORS setup_cors(app) # 添加中间件 app.add_middleware(LoggingMiddleware) app.add_middleware(RequestIdMiddleware) # ✅ 注册全局异常处理器(一行搞定!) register_exception_handlers(app) # 注册路由 app.include_router(api_router, prefix="/api/v1") # 健康检查路由 @app.get("/health") async def health_check(): return {"status": "healthy"} ``` **改进**: - ✅ `main.py` 从 238 行减少到 107 行,减少了 **55%** - ✅ 职责清晰:`main.py` 只负责应用配置和启动 - ✅ 错误处理逻辑完全独立,易于维护 - ✅ 一行代码注册所有异常处理器 - ✅ 更容易编写单元测试 ## 📁 新的项目结构 ``` server/app/ ├── main.py # 应用入口(107 行)✨ 简化 ├── core/ │ ├── config.py # 配置管理 │ ├── logging.py # 日志配置 │ ├── database.py # 数据库连接 │ └── error_handlers.py # 错误处理器(新增)✨ ├── middleware/ │ ├── request_id.py # 请求 ID 中间件 │ ├── logging.py # 日志中间件 │ └── cors.py # CORS 中间件 ├── api/ │ └── v1/ │ ├── projects.py # 项目路由 │ └── ... └── schemas/ ├── response.py # 响应格式 └── ... ``` ## 🔧 使用方式 ### 在 main.py 中注册 ```python from app.core.error_handlers import register_exception_handlers app = FastAPI(...) # 注册所有异常处理器 register_exception_handlers(app) ``` ### 测试错误处理器 ```python # tests/test_error_handlers.py import pytest from fastapi import Request from fastapi.exceptions import RequestValidationError from app.core.error_handlers import ( validation_exception_handler, get_friendly_error_message, extract_field_path ) def test_extract_field_path(): """测试字段路径提取""" assert extract_field_path(["body", "folderId"]) == "folderId" assert extract_field_path(["body", "user", "name"]) == "user -> name" def test_get_friendly_error_message(): """测试友好错误消息生成""" error = { "type": "uuid_parsing", "msg": "Invalid UUID" } message = get_friendly_error_message(error) assert "UUID" in message assert "不传此字段" in message @pytest.mark.asyncio async def test_validation_exception_handler(): """测试验证错误处理器""" # 构造模拟的 RequestValidationError # 验证返回的响应格式 # ... ``` ## 🎯 优势总结 | 维度 | 重构前 | 重构后 | |------|--------|--------| | **main.py 行数** | 238 行 | 107 行 (-55%) | | **职责分离** | ❌ 混杂 | ✅ 清晰 | | **可维护性** | ⚠️ 困难 | ✅ 简单 | | **可测试性** | ⚠️ 难测试 | ✅ 易测试 | | **代码复用** | ❌ 无法复用 | ✅ 可复用 | | **扩展性** | ⚠️ 修改 main.py | ✅ 独立扩展 | ## 📝 最佳实践 ### 1. 添加新的错误类型映射 在 `error_handlers.py` 的 `ERROR_TYPE_MESSAGES` 字典中添加: ```python ERROR_TYPE_MESSAGES = { "uuid_parsing": "必须是有效的 UUID 格式,或者不传此字段", "my_custom_error": "自定义错误消息", # ✅ 新增 } ``` ### 2. 自定义异常处理器 ```python # app/core/error_handlers.py async def custom_exception_handler(request: Request, exc: CustomException): """处理自定义异常""" return create_error_response( status_code=400, message=str(exc) ) def register_exception_handlers(app): """注册异常处理器""" # ... 其他处理器 ... app.add_exception_handler(CustomException, custom_exception_handler) # ✅ ``` ### 3. 环境特定的错误处理 ```python async def validation_exception_handler(request: Request, exc: RequestValidationError): """处理验证错误""" # ... # 开发环境:记录详细日志 if settings.DEBUG: logger.warning(f"详细错误: {exc.errors()}") # 生产环境:只返回友好消息 return create_error_response(...) ``` ## 🚀 后续优化建议 1. **添加错误码映射** ```python ERROR_CODES = { "uuid_parsing": "INVALID_UUID", "missing": "REQUIRED_FIELD", } ``` 2. **国际化支持** ```python def get_friendly_error_message(error: Dict, language: str = "zh"): messages = { "zh": {"uuid_parsing": "必须是有效的 UUID 格式"}, "en": {"uuid_parsing": "Must be a valid UUID format"} } return messages[language].get(error["type"]) ``` 3. **错误统计和监控** ```python from prometheus_client import Counter error_counter = Counter('api_errors', 'API errors', ['error_type']) async def validation_exception_handler(...): for error in exc.errors(): error_counter.labels(error_type=error["type"]).inc() # ... ``` ## ✅ 总结 通过这次重构: - ✅ **代码更简洁**:`main.py` 减少了 131 行代码 - ✅ **结构更清晰**:错误处理逻辑独立成模块 - ✅ **维护更容易**:修改错误处理不影响主文件 - ✅ **测试更方便**:可以独立测试错误处理器 - ✅ **扩展更灵活**:添加新的错误类型很简单 这是一个典型的 **关注点分离(Separation of Concerns)** 的最佳实践!