182 lines
6.1 KiB
Python
182 lines
6.1 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.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,
|
|
)
|