fix:批量修复了一些问题
This commit is contained in:
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user