fix:批量修复了一些问题

This commit is contained in:
shangfangjian
2026-09-19 22:51:35 +08:00
parent 835d7217d0
commit 8e6ad5394e
44 changed files with 4921 additions and 395 deletions
+122 -2
View File
@@ -7,7 +7,7 @@ from __future__ import annotations
import logging
import time
from datetime import date, datetime, timezone
from datetime import date, datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
@@ -28,7 +28,7 @@ from src.core.runtime_config import (
set_runtime_value,
)
from src.db.base import AsyncSession, get_db_read
from src.db.models import Injury, MatchStats
from src.db.models import Injury, Match, MatchStats
logger = logging.getLogger(__name__)
@@ -317,3 +317,123 @@ async def test_datasource(name: str):
"https://v3.football.api-sports.io/status",
headers={"x-apisports-key": api_key},
)
# ── 数据源健康/最近采集状态(只读,不触发采集) ──────────────────────
@router.get("/ingest/status")
async def ingest_status(db: AsyncSession = Depends(get_db_read)):
"""各数据源采集健康概览(只读,不触发任何采集)。
返回尽量可得的信息;基于现有表近似的数据会标明 approximation。
失败追踪目前依赖系统日志缓冲,无专用采集失败表。
"""
# ── bzzoiro: 落库目标是 matches 表,无专用采集时间戳 ──
# 近似:以 matches 表最大 match_date(已覆盖的最远比赛日) 与 created_at 作为参考
bzzoiro_key = await get_runtime_value("BZZOIRO_KEY")
bzzoiro_base = await get_runtime_value("BZZOIRO_BASE")
row = (
await db.execute(
select(
func.count().label("cnt"),
func.max(Match.match_date).label("latest_match_date"),
func.max(Match.created_at).label("latest_row_at"),
).where(Match.match_status == "finished")
)
).one()
bzzoiro = {
"name": "bzzoiro",
"label": "Bzzoiro",
"key_configured": bool(bzzoiro_key),
"base_url": (bzzoiro_base.rstrip("/") if bzzoiro_base else None) or settings.BZZOIRO_BASE,
"reachable": None, # 不主动探测
"last_success_at": row.latest_row_at.isoformat() if row.latest_row_at else None,
"latest_match_date": row.latest_match_date.isoformat() if row.latest_match_date else None,
"recent_count": row.cnt or 0,
"note": "approx:基于 matches.finished 表,last_success_at 为行写入时间而非精确采集完成时间",
"last_failure": _last_failure_log("bzzoiro"),
}
# ── understat: 落库到 match_stats(source=understat),有精确 retrieved_at ──
row = (
await db.execute(
select(
func.count().label("cnt"),
func.max(MatchStats.retrieved_at).label("latest_retrieved"),
).where(MatchStats.source == "understat")
)
).one()
understat = {
"name": "understat",
"label": "Understat",
"key_configured": True, # 无需 Key
"reachable": None,
"last_success_at": row.latest_retrieved.isoformat() if row.latest_retrieved else None,
"recent_count": row.cnt or 0,
"note": "基于 match_stats.source=understat 的 retrieved_at",
"last_failure": _last_failure_log("understat"),
}
# ── injuries: 落库到 injuries 表,有精确 retrieved_at;区分 Key/无数据/有数据 ──
api_key = await get_runtime_value("API_FOOTBALL_KEY")
row = (
await db.execute(
select(
func.count().label("cnt"),
func.max(Injury.retrieved_at).label("latest_retrieved"),
)
)
).one()
if not api_key:
injuries_status, injuries_note = "key_not_configured", "API_FOOTBALL_KEY 未配置"
elif not row.cnt:
injuries_status, injuries_note = "no_data", "本地无伤停数据,请先采集"
else:
injuries_status, injuries_note = "has_data", f"{row.cnt} 条伤停记录"
injuries = {
"name": "injuries",
"label": "Injuries (API-Football)",
"key_configured": bool(api_key),
"reachable": None,
"status": injuries_status,
"last_success_at": row.latest_retrieved.isoformat() if row.latest_retrieved else None,
"recent_count": row.cnt or 0,
"note": injuries_note,
"last_failure": _last_failure_log("injuries"),
}
return {"sources": [bzzoiro, understat, injuries]}
def _last_failure_log(source: str) -> dict | None:
"""从系统日志缓冲中查找某数据源的最近一次错误(仅作参考,非专用失败表)。"""
entries = get_entries(min_level="ERROR", keyword=source, limit=5)
if not entries:
return None
e = entries[0]
return {
"at": datetime.fromtimestamp(e["ts"]).isoformat(),
"logger": e["logger"],
"detail": e["message"][:200],
"note": "approx:来自内存日志缓冲,非专用采集失败表;进程重启后清零",
}
@router.get("/stats")
async def admin_stats(db: AsyncSession = Depends(get_db_read)):
"""管理区统计(只读):最近预测次数。轻量聚合,无 LLM 调用。"""
from sqlalchemy import func, text
from src.db.models import Prediction
day_ago = datetime.now(timezone.utc) - timedelta(days=1)
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
r = (
await db.execute(
select(
func.count().label("total"),
func.count().filter(Prediction.created_at >= day_ago).label("last_24h"),
func.count().filter(Prediction.created_at >= week_ago).label("last_7d"),
)
)
).one()
return {"predictions": {"total": r.total, "last_24h": r.last_24h, "last_7d": r.last_7d}}