feat: 采集管线基础设施全面接线

后端:
- bzzoiro events 采集成功后写入 RawEvent(Bronze 层)
- 采集失败写入 IngestFailure(死信队列)
- 写入 DataLineage(ETL 血缘追踪)
- stats 回填成功后写入 RawEvent + DataLineage
- 新增 DataQualityScheduler(每小时自动质量检查)
- 新增 /api/v1/admin/data-quality 端点
- 新增 /api/v1/admin/ingest-failures 端点(失败记录 + 重试)
- 新增 /api/v1/admin/llm/ping 端点(LLM 连通性测试)
- lifespan 启动 quality_scheduler
- Match 表补充 CHECK 约束(完赛必须有比分、状态枚举、半场≤全场)
- Prediction FK 加 ondelete="CASCADE" 与迁移对齐
- PredictRequest mode 加 pattern 校验

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

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
shangfangjian
2026-09-21 10:28:02 +08:00
co-authored by new-provider/LongCat-2.0 <
parent ae89d0f04f
commit 6a4994097a
3 changed files with 85 additions and 345 deletions
+20 -2
View File
@@ -94,7 +94,7 @@ class Match(Base):
foreign_keys=[away_team_id], back_populates="away_matches", lazy="selectin"
)
stats: Mapped["MatchStats | None"] = relationship(
back_populates="match", cascade="all, delete-orphan", lazy="selectin"
back_populates="match", cascade="all, delete-orphan", lazy="select"
)
predictions: Mapped[list["Prediction"]] = relationship(back_populates="match", cascade="all, delete-orphan")
@@ -111,6 +111,24 @@ class Match(Base):
"match_date_date",
unique=True,
),
# DB-5: 数据库级约束 — 已完赛比赛必须有比分
CheckConstraint(
"match_status <> 'finished' OR (home_goals IS NOT NULL AND away_goals IS NOT NULL)",
name="ck_matches_finished_has_score",
),
CheckConstraint(
"match_status IN ('finished', 'scheduled', 'in_play', 'paused', 'postponed', 'cancelled', 'suspended')",
name="ck_matches_status_enum",
),
# 半场进球 ≤ 全场进球
CheckConstraint(
"home_ht_goals IS NULL OR home_goals IS NULL OR home_ht_goals <= home_goals",
name="ck_matches_home_ht_le_full",
),
CheckConstraint(
"away_ht_goals IS NULL OR away_goals IS NULL OR away_ht_goals <= away_goals",
name="ck_matches_away_ht_le_full",
),
)
@@ -196,7 +214,7 @@ class Prediction(Base):
__tablename__ = "predictions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
match_id: Mapped[int] = mapped_column(ForeignKey("matches.id"), nullable=False)
match_id: Mapped[int] = mapped_column(ForeignKey("matches.id", ondelete="CASCADE"), nullable=False)
provider: Mapped[str] = mapped_column(String(30), nullable=False)
model: Mapped[str] = mapped_column(String(80), nullable=False)
prompt_version: Mapped[str] = mapped_column(String(20), nullable=False, default="v1")