Files
InsightRadar/backend/app/main.py
T
2026-04-02 18:36:34 +08:00

139 lines
4.4 KiB
Python

# app/main.py
import logging
import os
from pathlib import Path
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
import httpx
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request, staticfiles
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
# 统一配置日志格式和级别,确保 delivery_service 等的 INFO 日志可见
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 降低 APScheduler 运行心跳日志,避免每分钟刷屏
logging.getLogger("apscheduler").setLevel(logging.WARNING)
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.services.fetcher_service import fetch_and_save_trending_data
from app.services.summary_service import generate_unified_summaries
from app.services.delivery_service import check_and_deliver
from app.database import engine
from app.models.models import Base
from app.initialize import init
# 路由总线
from app.api.router import api_router
CRAWL_INTERVAL = int(os.getenv("CRAWL_INTERVAL_MINUTES", 10))
SUMMARY_INTERVAL = int(os.getenv("SUMMARY_INTERVAL_MINUTES", 30))
scheduler = AsyncIOScheduler()
# ==========================================
# 1. 生命周期管理:App 启动时自动建表 & 启动调度器
# ==========================================
@asynccontextmanager
async def lifespan(app: FastAPI):
# 1. 数据库建表
logging.info("正在初始化数据库表...")
Base.metadata.create_all(bind=engine)
logging.info("数据库表初始化完成!")
logging.info("初始化订阅源")
init()
logging.info("订阅源初始化完毕")
# 2. 配置并启动定时任务
scheduler.add_job(
fetch_and_save_trending_data,
'interval',
minutes=CRAWL_INTERVAL,
id='trending_fetch_job',
replace_existing=True
)
# 平台摘要
scheduler.add_job(
generate_unified_summaries,
'interval',
minutes=SUMMARY_INTERVAL,
id='ai_summary_job',
replace_existing=True
)
# 推送调度:每分钟检查是否有用户需要接收邮件推送
scheduler.add_job(
check_and_deliver,
'interval',
minutes=1,
id='delivery_check_job',
replace_existing=True,
)
scheduler.start()
logging.info(f"定时抓取任务已启动,每 {CRAWL_INTERVAL} 分钟执行一次")
logging.info(f"AI 摘要生成任务已启动,每 {SUMMARY_INTERVAL} 分钟执行一次")
logging.info("邮件推送调度已启动,每分钟检查一次")
# 为了测试方便,启动时立即执行一次
# await fetch_and_save_trending_data()
# await generate_unified_summaries()
yield # 此时 FastAPI 开始接受请求
# 优雅关闭
scheduler.shutdown()
logging.info("定时任务已安全关闭")
# 初始化 FastAPI
app = FastAPI(title="AI 新闻聚合引擎 API", lifespan=lifespan)
# ==========================================
# 2. CORS 中间件:允许前端开发服务器跨域请求
# ==========================================
app.add_middleware(
CORSMiddleware,
# allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_origins=["*"],
# allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==========================================
# 3. 挂载路由总线
# ==========================================
# 版本控制
app.include_router(api_router, prefix="/api/v1")
# 只需要保留API的优先匹配,catch_all可以简化成这样
@app.get("/api/{full_path:path}")
async def api_not_found(full_path: str):
return {"detail": "API Not Found"}
staticPath = staticfiles.StaticFiles(directory="app/static", html=True)
# 把目录改成static对应我们放dist内容的路径就可以
app.mount("/", staticPath, name="static")
INDEX_HTML = Path("app/static/index.html").read_text(encoding="utf-8")
@app.exception_handler(404)
async def not_found_handler(request: Request, exc: HTTPException):
# 如果是API路径才返回404,前端路径走catch-all不会进这里
if request.url.path.startswith("/api/"):
return JSONResponse({"detail": "Not Found"}, status_code=404)
return HTMLResponse(INDEX_HTML)
# 健康检查
@app.get("/", tags=["健康检查"])
async def root():
return {"message": "Welcome to AI News Aggregator API", "status": "ok"}