"""FastAPI 应用工厂。""" from __future__ import annotations import logging from collections.abc import AsyncIterator from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from src.core.config import settings logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: from src.db.base import init_db from src.core.http_client import close_client from src.core.runtime_config import ( ensure_admin_password_hashed, migrate_plaintext_sensitive_settings, ) from src.core.security_check import assert_security_on_startup await init_db() # 验证连接,不建表 await migrate_plaintext_sensitive_settings() # 明文敏感配置 → 加密(幂等) await ensure_admin_password_hashed() # .env 明文密码 → scrypt 哈希(幂等) await assert_security_on_startup() # 启动安全校验(生产拒绝/开发警告) yield await close_client() def create_app() -> FastAPI: from src.core.log_buffer import setup_memory_logging setup_memory_logging(settings.LOG_LEVEL) # 生产环境不暴露 OpenAPI 文档(避免向访客泄露接口结构) openapi_url = "/openapi.json" if settings.APP_ENV != "production" else None app = FastAPI( title="Profeto API", description="足球数据 + LLM 预测服务", version="0.1.0", lifespan=lifespan, openapi_url=openapi_url, ) origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()] methods = [m.strip() for m in settings.CORS_METHODS.split(",") if m.strip()] headers = [h.strip() for h in settings.CORS_HEADERS.split(",") if h.strip()] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=methods, allow_headers=headers, ) from src.api.routes.matches import router as matches_router from src.api.routes.predict import router as predict_router from src.api.routes.ingest import router as ingest_router from src.api.routes.eval import router as eval_router from src.api.routes.backtest import router as backtest_router from src.api.routes.auth import router as auth_router from src.api.routes.admin_settings import router as admin_settings_router app.include_router(matches_router) app.include_router(predict_router) app.include_router(ingest_router) app.include_router(eval_router) app.include_router(backtest_router) app.include_router(auth_router) app.include_router(admin_settings_router) @app.get("/health") async def health(): return {"status": "healthy", "service": "profeto"} @app.get("/health/ready") async def health_ready(): """就绪检查:验证数据库连接。 数据库不可达时返回 HTTP 503,而非 200 + not_ready —— 这样 K8s/Compose 的 readinessProbe 才能正确判定「未就绪」并停止流量。 """ from src.db.base import engine from fastapi.responses import JSONResponse try: async with engine.begin() as conn: await conn.run_sync(lambda conn: None) return {"status": "ready"} except Exception as e: logger.warning("就绪检查失败(数据库不可达): %s", e) return JSONResponse( status_code=503, content={"status": "not_ready", "reason": "database_unreachable"}, ) return app app = create_app()