from __future__ import annotations from contextlib import asynccontextmanager from typing import AsyncGenerator from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.config import settings from app.db import close_pool, init_pool, get_pool from app.middleware.logging import LoggingMiddleware from app.services.ws_hub import hub as ws_hub # noqa: F401 from app.services.scheduler import start_scheduler, stop_scheduler @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await init_pool() pool = get_pool() # 建 users 表并初始化 admin await pool.execute(""" CREATE TABLE IF NOT EXISTS crawl.users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(100) NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) """) from app.routers.auth import ensure_admin_user await ensure_admin_user() # 清理上次进程意外退出遗留的僵死任务 cleaned = await pool.execute( """ UPDATE scrape_jobs SET status = 'failed', error = '服务重启,任务中断', updated_at = NOW() WHERE status IN ('pending', 'running') """ ) import logging logging.getLogger(__name__).info(f"[startup] 清理僵死任务: {cleaned}") await start_scheduler() yield await stop_scheduler() await close_pool() app = FastAPI(title="Sentinel Lens", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.allowed_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(LoggingMiddleware) # Router registration from app.routers import stats # noqa: E402 from app.routers import logs # noqa: E402 from app.routers import scrape # noqa: E402 from app.routers import public # noqa: E402 from app.routers import ws # noqa: E402 from app.routers import models # noqa: E402 from app.routers import schedule # noqa: E402 from app.routers import discounts # noqa: E402 from app.routers import auth # noqa: E402 from app.routers import scrape_stats # noqa: E402 app.include_router(stats.router, prefix="/api") app.include_router(logs.router, prefix="/api") app.include_router(scrape.router, prefix="/api") app.include_router(public.router, prefix="/api/public") app.include_router(models.router, prefix="/api") app.include_router(schedule.router, prefix="/api") app.include_router(discounts.router, prefix="/api") app.include_router(auth.router, prefix="/api") app.include_router(scrape_stats.router, prefix="/api") app.include_router(ws.router) @app.get("/") async def health_check() -> dict: return {"status": "ok", "service": "sentinel-lens"}