"""应用入口:FastAPI 应用创建与生命周期管理 职责: - 构建服务容器(依赖注入) - 管理应用生命周期(启动/关闭浏览器池和会话存储) - 注册路由和全局异常处理器 """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import ValidationError from starlette.exceptions import HTTPException as StarletteHTTPException from app.api.routes.health import router as health_router from app.api.routes.scrape import router as scrape_router from app.api.routes.news import router as news_router from app.core.config import get_settings from app.core.container import ServiceContainer from app.core.errors import AppError from app.core.logging_setup import configure_logging from app.models.scrape import ApiResponse from app.services.browser_pool import BrowserPool from app.services.cloudflare_session import CloudflareSessionManager from app.services.session_store import SessionStore from app.services.surugaya_client import SurugayaClient from app.services.cargo_service import CargoService logger = logging.getLogger(__name__) def _format_validation_msg(errors: list[dict]) -> str: """将校验错误整理为便于前端展示的消息。""" if not errors: return "Validation error" messages: list[str] = [] for error in errors: loc = ".".join(str(part) for part in error.get("loc", []) if part != "body") msg = str(error.get("msg", "Validation error")) messages.append(f"{loc}: {msg}" if loc else msg) return "; ".join(messages) def build_container() -> ServiceContainer: """构建服务容器,组装所有依赖""" settings = get_settings() session_store = SessionStore(settings) browser_pool = BrowserPool(settings) session_manager = CloudflareSessionManager(settings, browser_pool, session_store) surugaya_client = SurugayaClient(settings, browser_pool, session_manager, session_store) cargo_service = CargoService(settings) return ServiceContainer( settings=settings, session_store=session_store, browser_pool=browser_pool, session_manager=session_manager, surugaya_client=surugaya_client, cargo_service=cargo_service, ) @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理:启动时初始化各服务,关闭时释放资源""" container = build_container() app.state.container = container configure_logging(container.settings) logger.info("应用启动:%s:%s", container.settings.app_host, container.settings.app_port) logger.info("日志级别:%s", container.settings.log_level) logger.info("当前环境:%s", container.settings.app_env) await container.session_store.start() await container.browser_pool.start() try: yield finally: await container.session_manager.close() await container.browser_pool.close() await container.session_store.close() def create_app() -> FastAPI: """创建 FastAPI 应用实例,注册路由和异常处理器""" app = FastAPI(title="Surugaya Scraper Service", lifespan=lifespan) app.include_router(health_router) app.include_router(scrape_router) app.include_router(news_router) @app.exception_handler(AppError) async def app_error_handler(_: Request, exc: AppError) -> JSONResponse: """业务异常处理器:返回结构化的错误响应""" return JSONResponse( status_code=exc.status_code, content=ApiResponse[None]( success=False, code=exc.err_code, msg=exc.message, data=None, ).model_dump(), headers=exc.headers, ) @app.exception_handler(RequestValidationError) async def validation_error_handler(_: Request, exc: RequestValidationError) -> JSONResponse: """请求参数校验异常处理器""" errors = exc.errors() return JSONResponse( status_code=422, content=ApiResponse[object]( success=False, msg=_format_validation_msg(errors), data=errors, code=1002, ).model_dump(), ) @app.exception_handler(ValidationError) async def pydantic_validation_error_handler(_: Request, exc: ValidationError) -> JSONResponse: """Pydantic 模型校验异常处理器。""" errors = exc.errors() return JSONResponse( status_code=422, content=ApiResponse[object]( success=False, msg=_format_validation_msg(errors), data=errors, code=1002, ).model_dump(), ) @app.exception_handler(StarletteHTTPException) async def http_exception_handler(_: Request, exc: StarletteHTTPException) -> JSONResponse: """HTTP 异常处理器(404、500 等)""" status_code = int(getattr(exc, "status_code", 500) or 500) err_code = 1404 if status_code == 404 else 1500 return JSONResponse( status_code=status_code, content=ApiResponse[None]( success=False, msg=str(getattr(exc, "detail", "HTTP error")), data=None, code=err_code, ).model_dump(), headers=getattr(exc, "headers", None), ) @app.exception_handler(Exception) async def unhandled_exception_handler(_: Request, __: Exception) -> JSONResponse: """兜底异常处理器:捕获所有未处理的异常""" return JSONResponse( status_code=500, content=ApiResponse[None]( success=False, msg="Internal server error", data=None, code=1500, ).model_dump(), ) return app app = create_app() if __name__ == "__main__": import uvicorn settings = get_settings() configure_logging(settings) uvicorn.run( "app.main:app", host=settings.app_host, port=settings.app_port, log_config=None, timeout_keep_alive=120, )