From f05dc1ae152d827572ac791b0787be78941e7fa2 Mon Sep 17 00:00:00 2001 From: shangfangjian Date: Sun, 20 Sep 2026 19:13:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A5=20bzzoiro=20=E4=B8=BA?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=95=B0=E6=8D=AE=E6=BA=90=E7=9A=84=E5=85=A8?= =?UTF-8?q?=E9=9D=A2=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据源统一为 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 <> --- .../versions/0015_bzzoiro_single_source.py | 102 +++++ frontend/src/App.tsx | 51 +++ frontend/src/admin/AdminLayout.tsx | 1 + frontend/src/admin/components.tsx | 17 +- frontend/src/admin/dal.ts | 94 +++-- frontend/src/admin/pages/Collection.tsx | 203 ++++----- frontend/src/admin/pages/Dashboard.tsx | 6 +- frontend/src/admin/pages/DataCompleteness.tsx | 183 ++++++++ frontend/src/admin/pages/Predictions.tsx | 2 +- frontend/src/admin/routes.tsx | 2 + frontend/src/admin/types.ts | 27 +- frontend/src/pages/Matches.tsx | 84 +++- frontend/src/pages/Standings.tsx | 174 ++++++++ src/api/routes/admin_settings.py | 253 +++++++----- src/api/routes/ingest.py | 147 +++---- src/api/routes/matches.py | 98 ++++- src/api/schemas.py | 22 +- src/core/http_client.py | 4 +- src/core/runtime_config.py | 2 +- src/data/bzzoiro.py | 390 +++++++++++++++--- src/data/config.py | 11 +- src/data/injuries.py | 347 ---------------- src/data/normalize.py | 32 -- src/data/sources.py | 4 +- src/data/understat.py | 223 ---------- src/db/models.py | 68 +-- src/db/repositories.py | 2 +- src/llm/agents/base.py | 2 +- src/llm/agents/orchestrator.py | 12 +- src/llm/context_builder.py | 114 ++--- src/llm/prompts/agents/injuries_v1.md | 21 - src/llm/prompts/agents/standings_v1.md | 23 ++ tests/test_agent_weights_persist.py | 2 +- tests/test_agents.py | 10 +- tests/test_available_at.py | 22 +- tests/test_injuries_inserted_count.py | 166 -------- tests/test_injuries_integrity_rollback.py | 172 -------- tests/test_injuries_no_local_data.py | 75 ---- tests/test_injuries_pipeline.py | 204 --------- tests/test_injuries_slice_semantic.py | 110 ----- tests/test_multi_agent_degraded.py | 6 +- 41 files changed, 1603 insertions(+), 1885 deletions(-) create mode 100644 alembic/versions/0015_bzzoiro_single_source.py create mode 100644 frontend/src/admin/pages/DataCompleteness.tsx create mode 100644 frontend/src/pages/Standings.tsx delete mode 100644 src/data/injuries.py delete mode 100644 src/data/understat.py delete mode 100644 src/llm/prompts/agents/injuries_v1.md create mode 100644 src/llm/prompts/agents/standings_v1.md delete mode 100644 tests/test_injuries_inserted_count.py delete mode 100644 tests/test_injuries_integrity_rollback.py delete mode 100644 tests/test_injuries_no_local_data.py delete mode 100644 tests/test_injuries_pipeline.py delete mode 100644 tests/test_injuries_slice_semantic.py diff --git a/alembic/versions/0015_bzzoiro_single_source.py b/alembic/versions/0015_bzzoiro_single_source.py new file mode 100644 index 0000000..41d6241 --- /dev/null +++ b/alembic/versions/0015_bzzoiro_single_source.py @@ -0,0 +1,102 @@ +"""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') diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0179c57..66bc50b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { ErrorBoundary } from './components/ErrorBoundary' import Matches from './pages/Matches' +import Standings from './pages/Standings' import { adminRoutes } from './admin/routes' /** 报眉日期行 */ @@ -26,6 +27,52 @@ function dateLine(): string { }) } +function StandingsLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+
+

+ 先知 + + Profeto + +

+

+ 联赛积分榜 · 积分 / 净胜 / xG差 / 近期走势 +

+
+ +
+
+ +
+ {children} +
+ +
+
+ 数据由 bzzoiro 提供 · 仅供研究参考 +
+
+
+ ) +} + function HomePage() { return (
@@ -46,6 +93,9 @@ function HomePage() {
{dateLine()}