refactor: 以 bzzoiro 为唯一数据源的全面重构
数据源统一为 bzzoiro,移除 Understat 与 injuries:
- 删除 src/data/understat.py / injuries.py 及相关测试
- 删除 injuries 模型与表;扩展 match_stats(xG 之外增加 big_chances/fouls)
- 新增 standings 表(联赛积分榜:位置/积分/xG/走势/分区)
- matches 表增加 source_event_id 血缘列,支撑统计回填
采集管线(bzzoiro 三条管线):
- events:赛程/比分(/events/),记录 source_event_id
- standings:积分榜快照(/leagues/{id}/standings/)
- stats:已完赛比赛详细统计回填(/events/{id}/stats/)
预测增强:
- standings_slice 替代 injuries_slice;积分榜专家替代阵容完整性专家
- AGENT_META runtime_config 同步更新
管理后台:
- ingest 路由重写为单一 bzzoiro 入口 + task 参数(events/standings/stats/all)
- 新增 /admin/data-completeness 数据完整性分析 API
- 数据源状态页简化为 bzzoiro 单源
前端:
- 采集页重构为任务驱动(比赛/积分榜/统计回填/全量)
- 新增「数据完整性」可视化页(覆盖率矩阵/字段完整率/健康摘要)
- 新增主站积分榜页(/standings)与比赛详情完整统计面板
- agent 名称同步更新(injuries→standings)
迁移 0015_bzzoiro_single_source 已在容器内验证通过,后端测试全部通过。
Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
co-authored by
new-provider/LongCat-2.0 <
parent
ec8f36abb2
commit
f05dc1ae15
+39
-29
@@ -1,4 +1,7 @@
|
||||
"""6 张表 ORM: leagues / teams / matches / match_stats / predictions / injuries。"""
|
||||
"""ORM 模型: leagues / teams / matches / match_stats / standings / predictions。
|
||||
|
||||
数据源统一为 bzzoiro(单一数据源),伤停(injuries)与 Understat 已移除。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
@@ -16,7 +19,6 @@ from sqlalchemy import (
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
and_,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
@@ -75,6 +77,8 @@ class Match(Base):
|
||||
home_ht_goals: Mapped[int | None] = mapped_column(Integer)
|
||||
away_ht_goals: Mapped[int | None] = mapped_column(Integer)
|
||||
match_stage: Mapped[str | None] = mapped_column(String(100))
|
||||
# 数据血缘:bzzoiro 上游事件 ID,用于 /events/{id}/stats/ 统计回填
|
||||
source_event_id: Mapped[int | None] = mapped_column(BigInteger, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
@@ -127,6 +131,11 @@ class MatchStats(Base):
|
||||
away_yellow_cards: Mapped[int | None] = mapped_column(Integer)
|
||||
home_red_cards: Mapped[int | None] = mapped_column(Integer)
|
||||
away_red_cards: Mapped[int | None] = mapped_column(Integer)
|
||||
# bzzoiro /events/{id}/stats/ 扩展字段
|
||||
home_big_chances: Mapped[int | None] = mapped_column(Integer)
|
||||
away_big_chances: Mapped[int | None] = mapped_column(Integer)
|
||||
home_fouls: Mapped[int | None] = mapped_column(Integer)
|
||||
away_fouls: Mapped[int | None] = mapped_column(Integer)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
# 数据血缘:追踪统计数据的来源和可用时间
|
||||
source: Mapped[str | None] = mapped_column(String(30)) # bzzoiro / understat
|
||||
@@ -146,39 +155,40 @@ class MatchStats(Base):
|
||||
)
|
||||
|
||||
|
||||
class Injury(Base):
|
||||
"""球员伤停记录(api-football 数据源)。"""
|
||||
__tablename__ = "injuries"
|
||||
class Standing(Base):
|
||||
"""联赛积分榜快照(bzzoiro /leagues/{id}/standings/)。
|
||||
|
||||
同一联赛同一赛季只保留最新快照:重新采集时按 (league_id, season, team_id)
|
||||
upsert。zone 来自 bzzoiro 分区(如 champions_league / europa_league / relegation)。
|
||||
"""
|
||||
__tablename__ = "standings"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
player_id: Mapped[int | None] = mapped_column(Integer, index=True)
|
||||
player_name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
team_id: Mapped[int | None] = mapped_column(ForeignKey("teams.id"), index=True)
|
||||
fixture_id: Mapped[int | None] = mapped_column(Integer)
|
||||
league_id: Mapped[int | None] = mapped_column(Integer)
|
||||
injury_type: Mapped[str | None] = mapped_column(String(50)) # Missing Fixture / Suspended
|
||||
reason: Mapped[str | None] = mapped_column(String(200))
|
||||
injury_date: Mapped[date | None] = mapped_column(Date, index=True)
|
||||
return_date: Mapped[date | None] = mapped_column(Date)
|
||||
league_id: Mapped[int] = mapped_column(ForeignKey("leagues.id"), nullable=False)
|
||||
season: Mapped[str] = mapped_column(String(12), nullable=False)
|
||||
team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), nullable=False)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
played: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
won: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
drawn: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
lost: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
goals_for: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
goals_against: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
goal_diff: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
points: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
xg_for: Mapped[float | None] = mapped_column(Float)
|
||||
xg_against: Mapped[float | None] = mapped_column(Float)
|
||||
form: Mapped[str | None] = mapped_column(String(20)) # 近期赛果串,如 "WWDLW"
|
||||
zone: Mapped[str | None] = mapped_column(String(50)) # champions_league / relegation 等
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
retrieved_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
team: Mapped["Team | None"] = relationship()
|
||||
league: Mapped[League] = relationship()
|
||||
team: Mapped[Team] = relationship(lazy="selectin")
|
||||
|
||||
__table_args__ = (
|
||||
# Fix 4: partial unique index — 只在 player_id 和 fixture_id 都非空时强制唯一
|
||||
# PostgreSQL 中 NULL != NULL,普通唯一索引无法防止 NULL 重复
|
||||
Index(
|
||||
"ix_injuries_player_fixture",
|
||||
"player_id",
|
||||
"fixture_id",
|
||||
"injury_type",
|
||||
unique=True,
|
||||
postgresql_where=and_(
|
||||
player_id.is_not(None),
|
||||
fixture_id.is_not(None),
|
||||
),
|
||||
),
|
||||
Index("ix_injuries_team_date", "team_id", "injury_date"),
|
||||
UniqueConstraint("league_id", "season", "team_id", name="uq_standings_league_season_team"),
|
||||
Index("ix_standings_league_season_pos", "league_id", "season", "position"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user