2026-06-07 21:51:01 +08:00
|
|
|
"""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
|
|
|
|
|
|
2026-06-08 14:24:23 +08:00
|
|
|
from app.api import admin, admin_llm, articles, auth, bookmarks, me, sources, subscriptions
|
2026-06-07 21:51:01 +08:00
|
|
|
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)
|
2026-06-08 14:24:23 +08:00
|
|
|
app.include_router(admin_llm.router, prefix=API_PREFIX)
|
2026-06-07 21:51:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 健康检查 ===
|
2026-06-07 23:15:33 +08:00
|
|
|
@app.get(f"{API_PREFIX}/healthz", include_in_schema=False)
|
2026-06-07 21:51:01 +08:00
|
|
|
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"}
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 23:15:33 +08:00
|
|
|
@app.get(f"{API_PREFIX}/", include_in_schema=False)
|
2026-06-07 21:51:01 +08:00
|
|
|
async def root():
|
2026-06-07 23:15:33 +08:00
|
|
|
return {"name": "diary-news", "version": app.version, "docs": f"{API_PREFIX}/docs"}
|