"""FastAPI 入口。 - 注册路由 - 启动 / 关闭事件:连接池、调度器 - CORS - 全局异常处理 """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException from app.api import admin, admin_llm, articles, auth, bookmarks, me, sources, subscriptions from app.config import settings from app.database import engine from app.redis_client import close_redis, get_redis logger = logging.getLogger("news.api") logging.basicConfig( level=settings.log_level, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) @asynccontextmanager async def lifespan(app: FastAPI): # 启动 logger.info("api starting, tz=%s", settings.tz) # 触发 redis 连接 await get_redis().ping() yield # 关闭 logger.info("api shutting down") await close_redis() await engine.dispose() app = FastAPI( title="Diary News", description="Private news aggregator", version="0.1.0", default_response_class=JSONResponse, lifespan=lifespan, docs_url="/api/docs" if settings.log_level == "DEBUG" else None, redoc_url=None, ) # CORS:网页 + Android,简单放开(私有) app.add_middleware( CORSMiddleware, allow_origins=["*"], # MVP 放开,生产收紧 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # === 全局异常处理(RFC 7807) === @app.exception_handler(StarletteHTTPException) async def http_exc_handler(request: Request, exc: StarletteHTTPException): return JSONResponse( status_code=exc.status_code, content={ "type": "about:blank", "title": exc.detail if isinstance(exc.detail, str) else "Error", "status": exc.status_code, "instance": str(request.url), }, headers=exc.headers or None, ) @app.exception_handler(RequestValidationError) async def validation_exc_handler(request: Request, exc: RequestValidationError): return JSONResponse( status_code=422, content={ "type": "about:blank", "title": "Validation Error", "status": 422, "errors": exc.errors(), "instance": str(request.url), }, ) # === 路由 === API_PREFIX = "/api/v1" app.include_router(auth.router, prefix=API_PREFIX) app.include_router(me.router, prefix=API_PREFIX) app.include_router(articles.router, prefix=API_PREFIX) app.include_router(sources.router, prefix=API_PREFIX) app.include_router(bookmarks.router, prefix=API_PREFIX) app.include_router(subscriptions.router, prefix=API_PREFIX) app.include_router(admin.router, prefix=API_PREFIX) app.include_router(admin_llm.router, prefix=API_PREFIX) # === 健康检查 === @app.get(f"{API_PREFIX}/healthz", include_in_schema=False) async def healthz(): try: await get_redis().ping() except Exception as e: return JSONResponse({"status": "degraded", "redis": str(e)}, status_code=503) return {"status": "ok"} @app.get(f"{API_PREFIX}/", include_in_schema=False) async def root(): return {"name": "diary-news", "version": app.version, "docs": f"{API_PREFIX}/docs"}