diff --git a/exceptions.py b/exceptions.py index a79c376..5f3cb7b 100644 --- a/exceptions.py +++ b/exceptions.py @@ -25,9 +25,9 @@ class CustomError(Enum): self.cn_message = cn_message self.en_message = en_message - def as_dict(self, detail: str = None, language: str = 'zh') -> dict: + def as_dict(self, detail: str = None, lang: str = 'zh') -> dict: """转换为API响应格式,支持中英文""" - message = self.cn_message if language == 'zh' else self.en_message + message = self.cn_message if lang == 'zh' else self.en_message if detail: message += f"({detail})" return {"code": self.code, "message": message} diff --git a/main.py b/main.py index 6109730..ac2eb4c 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,7 @@ from fastapi import FastAPI from src.router import v1_router from src.utils.logger import logger -from src import middlewares -from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse -from fastapi import Request, status -from exceptions import CustomError +from src.middlewares import PrepareMiddleware, ResponseMiddleware # 0. 创建 FastAPI 应用 @@ -15,31 +11,11 @@ app = FastAPI(title="CapCut Mate API", version="1.0") app.include_router(v1_router, prefix="/openapi", tags=["capcut-mate"]) # 2. 添加中间件 -app.middleware("http")(middlewares.prepare_middleware) +app.add_middleware(PrepareMiddleware) # 注册统一响应处理中间件(注意顺序,应该在其他中间件之后注册) -app.middleware("http")(middlewares.response_middleware) +app.add_middleware(ResponseMiddleware) -# 3. 异常处理:参数校验错误 -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: - """自定义参数校验错误处理器""" - # 1. 拼接完整错误信息 - error_messages = [] - for error in exc.errors(): - field = ".".join(str(loc) for loc in error["loc"]) - error_type = error["type"] - error_msg = error["msg"] - # 组合字段+错误类型+详情信息 - error_messages.append(f"{field} ({error_type}): {error_msg}") - - # 2. 构建统一响应结构 - full_message = "; ".join(error_messages) # 用分号分隔多个错误 - return JSONResponse( - status_code=status.HTTP_200_OK, - content=CustomError.PARAM_VALIDATION_FAILED.as_dict(full_message) - ) - -# 4. 打印所有路由 +# 3. 打印所有路由 for r in app.routes: # 1. 取 HTTP 方法列表 methods = getattr(r, "methods", None) or [getattr(r, "method", "WS")] diff --git a/src/middlewares/__init__.py b/src/middlewares/__init__.py index 36cad29..21df790 100644 --- a/src/middlewares/__init__.py +++ b/src/middlewares/__init__.py @@ -1,5 +1,5 @@ # 中间件实现 -from .prepare import prepare_middleware -from .response import response_middleware +from .prepare import PrepareMiddleware +from .response import ResponseMiddleware -__all__ = ["prepare_middleware", "response_middleware"] +__all__ = ["PrepareMiddleware", "ResponseMiddleware"] diff --git a/src/middlewares/prepare.py b/src/middlewares/prepare.py index 63f6ba0..4902ced 100644 --- a/src/middlewares/prepare.py +++ b/src/middlewares/prepare.py @@ -1,14 +1,21 @@ # 环境初始化中间件 from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware import config import os -# 请求前的准备工作,如:创建草稿目录 -async def prepare_middleware(request: Request, call_next): - # 递归创建目录,如果目录存在,就直接跳过创建 - os.makedirs(config.DRAFT_DIR, exist_ok=True) +class PrepareMiddleware(BaseHTTPMiddleware): + """请求前的准备工作中间件 + 功能: + 1. 创建临时目录 + 2. 创建输出目录 + """ - # 继续处理请求 - response = await call_next(request) - return response \ No newline at end of file + async def dispatch(self, request: Request, call_next): + # 递归创建目录,如果目录存在,就直接跳过创建 + os.makedirs(config.DRAFT_DIR, exist_ok=True) + + # 继续处理请求 + response = await call_next(request) + return response \ No newline at end of file diff --git a/src/middlewares/response.py b/src/middlewares/response.py index 7c92a89..8b7a3a1 100644 --- a/src/middlewares/response.py +++ b/src/middlewares/response.py @@ -1,65 +1,141 @@ from fastapi import Request from fastapi.responses import JSONResponse from exceptions import CustomError, CustomException +from starlette.middleware.base import BaseHTTPMiddleware from src.utils.logger import logger import json -async def response_middleware(request: Request, call_next): +class ResponseMiddleware(BaseHTTPMiddleware): """统一响应处理中间件 功能: 1. 统一处理业务正常响应,添加code和message字段 2. 统一处理异常,返回标准错误格式 """ - try: - # 获取语言偏好(默认中文) - lang = request.headers.get('Accept-Language', 'zh').split(',')[0].split('-')[0] - if lang not in ['zh', 'en']: - lang = 'zh' - - # 调用下一个处理函数 - response = await call_next(request) - - # 检查是否为JSON响应 - if response.headers.get('content-type') == 'application/json': - # 读取响应内容 - body = [section async for section in response.body_iterator] - if body: - body_str = b''.join(body).decode() - try: - # 解析JSON响应 - data = json.loads(body_str) - - # 检查响应是否已经包含code和message - if 'code' not in data or 'message' not in data: - # 包装成统一响应格式(平铺业务数据) - unified_response = { - 'code': CustomError.SUCCESS.code, - 'message': CustomError.SUCCESS.as_dict(language=lang)['message'], - **data - } - - # 创建新的JSON响应 - return JSONResponse( - status_code=response.status_code, - content=unified_response - ) - except json.JSONDecodeError: - logger.warning(f"JSON decode error: {body_str}") + + async def dispatch(self, request: Request, call_next): + try: + lang = self._get_language_from_request(request) + response = await call_next(request) + + # 处理非200状态码的响应 + if response.status_code != 200: + return await self._handle_non_200_response(response, lang) + + # 处理JSON响应 + if self._is_json_response(response): + return await self._process_json_response(response, lang) + + return response - return response + except CustomException as e: + return self._handle_custom_exception(e, lang) + except Exception as e: + return self._handle_generic_exception(e, lang) + + def _get_language_from_request(self, request: Request) -> str: + """从请求头获取语言偏好""" + lang = request.headers.get('Accept-Language', 'zh').split(',')[0].split('-')[0] + return lang if lang in ['zh', 'en'] else 'zh' + + def _handle_422_error(self, body_str: str, lang: str) -> JSONResponse: + """特殊处理422参数验证错误""" + try: + # 尝试解析422错误的响应体 + error_data = json.loads(body_str) + + # 提取验证错误的详细信息 + validation_messages = [] + if "detail" in error_data: + for error in error_data["detail"]: + if "loc" in error and "msg" in error: + # 格式化错误信息 + field = ".".join(str(part) for part in error["loc"] if part != "body") + message = f"{field}: {error['msg']}" + validation_messages.append(message) + + # 构建统一的422错误响应(不包含data字段) + error_message = "; ".join(validation_messages) if validation_messages else "" + error_response = CustomError.PARAM_VALIDATION_FAILED.as_dict(detail=error_message, lang=lang) + return JSONResponse(status_code=200, content=error_response) + + except json.JSONDecodeError: + logger.warning(f"Failed to parse 422 response body: {body_str}") + + error_response = CustomError.PARAM_VALIDATION_FAILED.as_dict(detail=body_str, lang=lang) + return JSONResponse(status_code=200, content=error_response) + + async def _handle_non_200_response(self, response, lang: str) -> JSONResponse: + """处理非200状态码的响应""" + body = b"" + async for chunk in response.body_iterator: + body += chunk - except CustomException as e: - # 处理自定义业务异常 - logger.error(f"Custom exception: {e.err.code} - {e.err.cn_message}" + (f" ({e.detail})" if e.detail else "")) - return JSONResponse( - status_code=200, - content=e.err.as_dict(e.detail, language=lang) - ) - except Exception as e: - # 处理其他未捕获的异常 - logger.error(f"Internal server error: {str(e)}") - return JSONResponse( - status_code=200, - content=CustomError.INTERNAL_SERVER_ERROR.as_dict(str(e), language=lang) - ) \ No newline at end of file + body_str = body.decode() + + # 特殊处理422错误(参数验证错误) + if response.status_code == 422: + return self._handle_422_error(body_str, lang) + + # 其它情况不应该发生,每一个错误都应该在前面被处理 + logger.error(f"Non-200 response: {response.status_code} - {body_str}") + # 其他非200错误处理 + error_response = { + "code": response.status_code, + "message": f"HTTP Error {response.status_code}", + "data": {"detail": body_str} + } + + return JSONResponse(status_code=200, content=error_response) + + def _is_json_response(self, response) -> bool: + """检查是否为JSON响应""" + return response.headers.get('content-type') == 'application/json' + + async def _process_json_response(self, response, lang: str): + """处理JSON响应并统一格式""" + body = [section async for section in response.body_iterator] + if not body: + return response + + body_str = b''.join(body).decode() + + try: + data = json.loads(body_str) + + # 如果响应已经有统一格式,直接返回 + if 'code' in data and 'message' in data: + return response + + # 创建统一格式的响应(成功响应保留data字段) + unified_response = { + 'code': CustomError.SUCCESS.code, + 'message': CustomError.SUCCESS.as_dict(lang=lang)['message'], + 'data': data + } + + return JSONResponse( + status_code=response.status_code, + content=unified_response + ) + + except json.JSONDecodeError: + logger.warning(f"JSON decode error: {body_str}") + return response + + def _handle_custom_exception(self, e: CustomException, lang: str) -> JSONResponse: + """处理自定义异常(不包含data字段)""" + logger.warning(f"Custom exception: {e.err.code} - {e.err.cn_message}" + + (f" ({e.detail})" if e.detail else "")) + + # 获取错误信息 + error_response = e.err.as_dict(detail=e.detail, lang=lang) + return JSONResponse(status_code=200, content=error_response) + + def _handle_generic_exception(self, e: Exception, lang: str) -> JSONResponse: + """处理通用异常(不包含data字段)""" + logger.warning(f"Internal server error: {str(e)}") + + # 获取错误信息 + error_response = CustomError.INTERNAL_SERVER_ERROR.as_dict(detail=str(e), lang=lang) + return JSONResponse(status_code=200, content=error_response) \ No newline at end of file