Files
Profeto/src/api/app.py
T
shangfangjianandnew-provider/LongCat-2.0 < ae89d0f04f feat: 数据采集管线基础设施接线 + 数据管线管理页
后端:
- bzzoiro 采集成功后写入 RawEvent(Bronze 层原始事件存档)
- 采集失败写入 IngestFailure(死信队列,支持重试)
- 写入 DataLineage(ETL 血缘追踪)
- 新增 DataQualityScheduler(每小时自动质量检查)
- 新增 /api/v1/admin/data-quality 端点(质量检查 + 手动触发)
- 新增 /api/v1/admin/ingest-failures 端点(失败记录 + 重试)
- 新增 /admin/llm/ping 端点(LLM 连通性测试,不依赖比赛)
- lifespan 启动 quality_scheduler

前端:
- 新增「数据管线」管理页(/admin/data-pipeline)
- 采集失败记录列表(状态/重试次数/错误类型)
- 数据质量检查结果(通过/未通过/严重度)
- 手动触发质量检查按钮
- 失败记录重试按钮
- 预测历史:比赛信息内嵌(日期/队名/主客徽标/赛果自动填充)
- 导航统一为 React Router Link
- AdminStats 类型扩展(matches/stats/standings 真实计数)

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 10:04:26 +08:00

139 lines
5.2 KiB
Python

"""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
from src.core.scheduler import scheduler, quality_scheduler
from src.api.routes.schedules import _run_scheduled_task
from src.data.config import BZZOIRO_LEAGUE_IDS
await init_db() # 验证连接,不建表
await migrate_plaintext_sensitive_settings() # 明文敏感配置 → 加密(幂等)
await ensure_admin_password_hashed() # .env 明文密码 → scrypt 哈希(幂等)
await assert_security_on_startup() # 启动安全校验(生产拒绝/开发警告)
# 注册默认定时任务(如果数据库中没有)
from src.db.base import AsyncSessionLocal
from sqlalchemy import select
from src.db.models import Schedule
async with AsyncSessionLocal() as session:
existing = (await session.execute(select(Schedule.id))).scalars().all()
if "daily-events" not in existing:
session.add(Schedule(id="daily-events", task="events", cron="0 8 * * *", enabled=False))
if "daily-standings" not in existing:
session.add(Schedule(id="daily-standings", task="standings", cron="0 9 * * *", enabled=False))
if "daily-stats" not in existing:
session.add(Schedule(id="daily-stats", task="stats", cron="*/30 * * * *", enabled=False))
await session.commit()
# 从数据库加载所有启用的定时任务
async with AsyncSessionLocal() as session:
schedules = (await session.execute(select(Schedule).where(Schedule.enabled))).scalars().all()
for s in schedules:
leagues = s.leagues.split(",") if s.leagues else list(BZZOIRO_LEAGUE_IDS.keys())
scheduler.register(
s.id, s.task,
lambda sid=s.id: _run_scheduled_task(sid),
enabled=s.enabled,
)
await scheduler.start()
await quality_scheduler.start()
logger.info("应用启动完成")
yield
await scheduler.stop()
await quality_scheduler.stop()
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
from src.api.routes.schedules import router as schedules_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.include_router(schedules_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()