Code Review 第二轮修复:前端 HTTP 统一 + 数据采集可靠性
F1: 统一前端 HTTP 客户端 - 新增 frontend/src/lib/http.ts(共享客户端,统一超时/错误/401 处理) - Matches.tsx 的列表/分页/进行中/预测请求改用 http 客户端 - 公开站与 Admin 行为一致 D2: events 采集按联赛分批提交 - _run_bzzoiro 改为 per-league 独立事务,避免超长事务 - 单联赛失败不影响其他联赛 F3: 仪表盘真实计数 - admin_stats 补充 matches/stats/standings 真实聚合 - Dashboard 展示比赛总数/已完赛/统计行/积分榜 F4: LLM 连通性测试不依赖 match_id=1 - 新增 POST /admin/llm/ping 端点(只发一次 chat,不依赖比赛) - testLLMConnection 优先调用 ping,失败回退旧方式 F5: 导航改用 React Router Link - App.tsx 中 <a href> 全部替换为 <Link to> D5: 删除 away_possession 死代码 - bzzoiro.py 中移除计算与注释 D1: 管线基础设施表文档化 - RawEvent/IngestFailure/DataQualityCheck/DataLineage 添加「预留未启用」注释 Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
co-authored by
new-provider/LongCat-2.0 <
parent
675e176603
commit
b6e367a640
@@ -293,6 +293,21 @@ async def test_datasource(name: str):
|
||||
raise HTTPException(404, f"未知数据源: {name}")
|
||||
|
||||
|
||||
@router.post("/llm/ping")
|
||||
async def llm_ping():
|
||||
"""LLM 连通性测试(不依赖比赛)。只发一次 chat 请求验证配置。"""
|
||||
from src.llm.provider import get_default_provider
|
||||
p = await get_default_provider()
|
||||
resp = await p.chat(
|
||||
system="你是测试助手。",
|
||||
user="ping",
|
||||
max_tokens=10,
|
||||
)
|
||||
if resp.error:
|
||||
return {"ok": False, "message": resp.error}
|
||||
return {"ok": True, "message": "LLM 连接正常", "model": p.model}
|
||||
|
||||
|
||||
# ── 数据源健康/最近采集状态(只读,不触发采集) ──────────────────────
|
||||
|
||||
|
||||
@@ -387,9 +402,9 @@ def _last_failure_log(source: str) -> dict | None:
|
||||
|
||||
@router.get("/stats")
|
||||
async def admin_stats(db: AsyncSession = Depends(get_db_read)):
|
||||
"""管理区统计(只读):最近预测次数。轻量聚合,无 LLM 调用。"""
|
||||
"""管理区统计(只读):预测次数 + 比赛覆盖。轻量聚合,无 LLM 调用。"""
|
||||
from sqlalchemy import func, text
|
||||
from src.db.models import Prediction
|
||||
from src.db.models import Prediction, Match, MatchStats, Standing
|
||||
day_ago = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
|
||||
r = (
|
||||
@@ -401,7 +416,17 @@ async def admin_stats(db: AsyncSession = Depends(get_db_read)):
|
||||
)
|
||||
)
|
||||
).one()
|
||||
return {"predictions": {"total": r.total, "last_24h": r.last_24h, "last_7d": r.last_7d}}
|
||||
# F3 修复: 补充真实比赛计数(非 limit=100 近似)
|
||||
match_cnt = (await db.execute(select(func.count()).select_from(Match))).scalar() or 0
|
||||
finished_cnt = (await db.execute(select(func.count()).where(Match.match_status == "finished"))).scalar() or 0
|
||||
stats_cnt = (await db.execute(select(func.count()).select_from(MatchStats))).scalar() or 0
|
||||
standings_cnt = (await db.execute(select(func.count()).select_from(Standing))).scalar() or 0
|
||||
return {
|
||||
"predictions": {"total": r.total, "last_24h": r.last_24h, "last_7d": r.last_7d},
|
||||
"matches": {"total": match_cnt, "finished": finished_cnt},
|
||||
"stats": {"total": stats_cnt},
|
||||
"standings": {"total": standings_cnt},
|
||||
}
|
||||
|
||||
|
||||
# ── 数据完整性分析(可视化数据源) ────────────────────────────────
|
||||
|
||||
+13
-12
@@ -58,20 +58,21 @@ async def _run_bzzoiro(task: str, leagues: list[str], req: IngestBzzoiroRequest)
|
||||
statuses = [req.status] if req.status else ["finished", "scheduled"]
|
||||
source = get_source("bzzoiro")
|
||||
merged: dict = {"leagues": {}, "total_inserted": 0, "total_updated": 0, "errors": []}
|
||||
async with get_uow() as session:
|
||||
# D2 修复: 按联赛分批提交,避免超长事务
|
||||
for code in leagues:
|
||||
for st in statuses:
|
||||
r = await source.ingest(
|
||||
session, leagues=leagues,
|
||||
date_from=req.date_from, date_to=req.date_to, status=st,
|
||||
)
|
||||
merged["total_inserted"] += r.get("total_inserted", 0)
|
||||
merged["total_updated"] += r.get("total_updated", 0)
|
||||
merged["errors"].extend(r.get("errors", []))
|
||||
for code, stat in r.get("leagues", {}).items():
|
||||
async with get_uow() as session:
|
||||
r = await source.ingest(
|
||||
session, leagues=[code],
|
||||
date_from=req.date_from, date_to=req.date_to, status=st,
|
||||
)
|
||||
merged["total_inserted"] += r.get("total_inserted", 0)
|
||||
merged["total_updated"] += r.get("total_updated", 0)
|
||||
merged["errors"].extend(r.get("errors", []))
|
||||
acc = merged["leagues"].setdefault(code, {"inserted": 0, "updated": 0, "errors": []})
|
||||
acc["inserted"] += stat.get("inserted", 0)
|
||||
acc["updated"] += stat.get("updated", 0)
|
||||
acc["errors"].extend(stat.get("errors", []))
|
||||
acc["inserted"] += r.get("inserted", 0)
|
||||
acc["updated"] += r.get("updated", 0)
|
||||
acc["errors"].extend(r.get("errors", []))
|
||||
logger.info(
|
||||
"bzzoiro 比赛采集完成: 新增 %d, 更新 %d, 联赛 %d 个, 状态 %s",
|
||||
merged["total_inserted"], merged["total_updated"], len(merged["leagues"]), statuses,
|
||||
|
||||
@@ -541,7 +541,6 @@ def _stats_from_payload(payload: dict) -> dict:
|
||||
p = _to_float_or_none(poss)
|
||||
if p is not None:
|
||||
out["home_possession"] = p
|
||||
out["away_possession"] = round(100 - p, 1) if 0 <= p <= 100 else None
|
||||
|
||||
for src, (h_fld, a_fld) in _STATS_FIELD_MAP.items():
|
||||
if src in ("xg", "ball_possession"):
|
||||
@@ -642,7 +641,6 @@ async def ingest_bzzoiro_event_stats(
|
||||
m.stats.available_at = m.match_date + timedelta(hours=2)
|
||||
|
||||
for fld, v in fields.items():
|
||||
# away_possession 为计算字段,模型无此列,跳过
|
||||
if hasattr(m.stats, fld):
|
||||
setattr(m.stats, fld, v)
|
||||
|
||||
|
||||
+20
-4
@@ -284,7 +284,11 @@ class Schedule(Base):
|
||||
|
||||
|
||||
class RawEvent(Base):
|
||||
"""Bronze 层:采集到的原始事件存档,便于重放与审计。"""
|
||||
"""Bronze 层:采集到的原始事件存档,便于重放与审计。
|
||||
|
||||
⚠️ 预留未启用:当前 bzzoiro 管线不写入此表。
|
||||
未来接线计划:events 采集成功后写入 raw_payload,支持重放与审计。
|
||||
"""
|
||||
__tablename__ = "raw_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
@@ -301,7 +305,11 @@ class RawEvent(Base):
|
||||
|
||||
|
||||
class IngestFailure(Base):
|
||||
"""采集失败死信:记录失败原因、重试次数与下次重试时间。"""
|
||||
"""采集失败死信:记录失败原因、重试次数与下次重试时间。
|
||||
|
||||
⚠️ 预留未启用:当前 bzzoiro 管线不写入此表。
|
||||
未来接线计划:采集失败时写入,支持按 next_retry_at 自动重试。
|
||||
"""
|
||||
__tablename__ = "ingest_failures"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
@@ -327,7 +335,11 @@ class IngestFailure(Base):
|
||||
|
||||
|
||||
class DataQualityCheck(Base):
|
||||
"""数据质量监控:记录每次质量检查的结果。"""
|
||||
"""数据质量监控:记录每次质量检查的结果。
|
||||
|
||||
⚠️ 预留未启用:当前 bzzoiro 管线不写入此表。
|
||||
未来接线计划:定时检查比赛/统计/积分榜完整性,写入检查结果。
|
||||
"""
|
||||
__tablename__ = "data_quality_checks"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
@@ -348,7 +360,11 @@ class DataQualityCheck(Base):
|
||||
|
||||
|
||||
class DataLineage(Base):
|
||||
"""ETL 血缘追踪:记录从源到目标的转换过程。"""
|
||||
"""ETL 血缘追踪:记录从源到目标的转换过程。
|
||||
|
||||
⚠️ 预留未启用:当前 bzzoiro 管线不写入此表。
|
||||
未来接线计划:每次采集写入 source_record_id → target_table/id 映射。
|
||||
"""
|
||||
__tablename__ = "data_lineage"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
|
||||
Reference in New Issue
Block a user