Files
surugaya/app/main.py
T

183 lines
6.2 KiB
Python

"""应用入口: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.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.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,
)