Files
Profeto/alembic/versions/0015_bzzoiro_single_source.py
shangfangjianandnew-provider/LongCat-2.0 < f05dc1ae15 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>>
2026-09-20 19:13:07 +08:00

103 lines
4.5 KiB
Python

"""bzzoiro 单一数据源重构:新增 standings、扩展 match_stats、删除 injuries
Revision ID: 0015_bzzoiro_single_source
Revises: 0014_predictions_agent_weights
Create Date: 2026-09-20
变更内容:
1. 新增 standings 表(联赛积分榜快照,来源 bzzoiro /leagues/{id}/standings/)
2. match_stats 新增 bzzoiro /events/{id}/stats/ 扩展字段:
home/away_big_chances, home/away_fouls
3. 删除 injuries 表(数据源已下线,不再采集伤停)
4. Understat 无独立表(xG 写入 match_stats),无需删表;
历史 source='understat' 数据保留,不再新增。
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0015_bzzoiro_single_source'
down_revision: Union[str, None] = '0014_predictions_agent_weights'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 0. matches 增加数据血缘列(bzzoiro 上游事件 ID)
op.add_column('matches', sa.Column('source_event_id', sa.BigInteger()))
op.create_index('ix_matches_source_event_id', 'matches', ['source_event_id'])
# 1. standings 表
op.create_table(
'standings',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('league_id', sa.Integer(), sa.ForeignKey('leagues.id'), nullable=False),
sa.Column('season', sa.String(12), nullable=False),
sa.Column('team_id', sa.Integer(), sa.ForeignKey('teams.id'), nullable=False),
sa.Column('position', sa.Integer(), nullable=False),
sa.Column('played', sa.Integer(), nullable=False, server_default='0'),
sa.Column('won', sa.Integer(), nullable=False, server_default='0'),
sa.Column('drawn', sa.Integer(), nullable=False, server_default='0'),
sa.Column('lost', sa.Integer(), nullable=False, server_default='0'),
sa.Column('goals_for', sa.Integer(), nullable=False, server_default='0'),
sa.Column('goals_against', sa.Integer(), nullable=False, server_default='0'),
sa.Column('goal_diff', sa.Integer(), nullable=False, server_default='0'),
sa.Column('points', sa.Integer(), nullable=False, server_default='0'),
sa.Column('xg_for', sa.Float()),
sa.Column('xg_against', sa.Float()),
sa.Column('form', sa.String(20)),
sa.Column('zone', sa.String(50)),
sa.Column('updated_at', sa.DateTime(timezone=True)),
sa.Column('retrieved_at', sa.DateTime(timezone=True)),
sa.UniqueConstraint('league_id', 'season', 'team_id', name='uq_standings_league_season_team'),
)
op.create_index(
'ix_standings_league_season_pos', 'standings',
['league_id', 'season', 'position'],
)
# 2. match_stats 扩展字段
op.add_column('match_stats', sa.Column('home_big_chances', sa.Integer()))
op.add_column('match_stats', sa.Column('away_big_chances', sa.Integer()))
op.add_column('match_stats', sa.Column('home_fouls', sa.Integer()))
op.add_column('match_stats', sa.Column('away_fouls', sa.Integer()))
# 3. 删除 injuries 表
op.drop_index('ix_injuries_player_fixture', table_name='injuries')
op.drop_index('ix_injuries_team_date', table_name='injuries')
op.drop_table('injuries')
def downgrade() -> None:
# 恢复 injuries 表(不含数据)
op.create_table(
'injuries',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('player_id', sa.Integer()),
sa.Column('player_name', sa.String(120), nullable=False),
sa.Column('team_id', sa.Integer(), sa.ForeignKey('teams.id')),
sa.Column('fixture_id', sa.Integer()),
sa.Column('league_id', sa.Integer()),
sa.Column('injury_type', sa.String(50)),
sa.Column('reason', sa.String(200)),
sa.Column('injury_date', sa.Date()),
sa.Column('return_date', sa.Date()),
sa.Column('retrieved_at', sa.DateTime(timezone=True)),
)
op.create_index('ix_injuries_player_fixture', 'injuries', ['player_id', 'fixture_id', 'injury_type'])
op.create_index('ix_injuries_team_date', 'injuries', ['team_id', 'injury_date'])
op.drop_index('ix_matches_source_event_id', table_name='matches')
op.drop_column('matches', 'source_event_id')
op.drop_column('match_stats', 'away_fouls')
op.drop_column('match_stats', 'home_fouls')
op.drop_column('match_stats', 'away_big_chances')
op.drop_column('match_stats', 'home_big_chances')
op.drop_index('ix_standings_league_season_pos', table_name='standings')
op.drop_table('standings')