"""rssKeeper - FastAPI 入口""" import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from starlette.middleware.cors import CORSMiddleware from database import init_db, SessionLocal from scheduler import init_feed_jobs, stop_scheduler from routers import feeds, articles, dashboard, external_api import config @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" # 启动时:初始化数据库 + 注册定时任务 init_db() db = SessionLocal() try: init_feed_jobs(db) finally: db.close() yield # 关闭时:停止调度器 stop_scheduler() app = FastAPI( title="rssKeeper", description="RSS 抓取、管理与检索系统", version="1.0.0", lifespan=lifespan, ) # CORS — 仅允许同源和开发环境 app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:7329", "http://localhost:7330", "http://127.0.0.1:7329", ], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Content-Type", "Authorization", "X-API-Key"], ) # API 路由 app.include_router(feeds.router, prefix=config.API_PREFIX) app.include_router(articles.router, prefix=config.API_PREFIX) app.include_router(dashboard.router, prefix=config.API_PREFIX) app.include_router(external_api.router, prefix=config.EXTERNAL_API_PREFIX) @app.get("/api/health") def health_check(): """健康检查""" return {"status": "ok", "service": "rssKeeper"} # 静态文件服务(前端构建产物) static_dir = os.path.join(config.BASE_DIR, "static") if os.path.exists(static_dir): app.mount("/static", StaticFiles(directory=static_dir), name="static") # API 路径白名单 — 这些路径不应被 SPA 兜底 _API_PATHS = { "api", "docs", "openapi.json", "redoc", } @app.get("/{full_path:path}") async def serve_spa(full_path: str): """Vue SPA 路由回退""" # API/文档路由不走 SPA 兜底 first_seg = full_path.split("/")[0] if full_path else "" if first_seg in _API_PATHS: return {"detail": "Not found"} index_path = os.path.join(static_dir, "index.html") if os.path.exists(index_path): return FileResponse(index_path) return {"detail": "Frontend not built"}