refactor: Sprint 3 - 引入 UnitOfWork + Repository 架构
新增: - src/db/unit_of_work.py: UnitOfWork 事务封装 - src/db/repositories.py: Match/Team/League/Prediction Repository 重构: - 删除 src/data/match_lookup.py(由 Repository 替代) - 数据源(bzzoiro/understat/injuries)不再自行 commit - API 路由(ingest)改用 UnitOfWork - LLM 服务(predict/orchestrator/eval/backtest)改用 UnitOfWork 事务边界统一由调用方控制,数据层不再自行决定 commit。
This commit is contained in:
+24
-7
@@ -1,6 +1,7 @@
|
||||
"""Understat xG 数据源。
|
||||
|
||||
迁移自旧项目 app/data/sources/understat.py,改成 async。
|
||||
使用 Repository 模式进行数据访问,不直接控制事务。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,12 +11,13 @@ import logging
|
||||
import random
|
||||
import re
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from src.core.http_client import get_client
|
||||
from src.data.config import FDCO_TO_UNDERSTAT, LEAGUE_NAMES
|
||||
from src.data.match_lookup import find_existing_match
|
||||
from src.data.normalize import normalize_understat
|
||||
from src.data.sources import register
|
||||
from src.db.models import League, Match, MatchStats
|
||||
from src.db.models import League, Match, MatchStats, Team
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,9 +82,10 @@ class UnderstatSource:
|
||||
name = "understat"
|
||||
|
||||
async def ingest(self, db, *, league: str, season: int) -> dict:
|
||||
"""采集 understat xG → 回填到现有 Match。只回填 xG 字段,不创建新 Match。"""
|
||||
from sqlalchemy import select
|
||||
"""采集 understat xG → 回填到现有 Match。只回填 xG 字段,不创建新 Match。
|
||||
|
||||
注意: 本方法不控制事务(commit/rollback),由调用方通过 UnitOfWork 控制。
|
||||
"""
|
||||
result = {"updated": 0, "skipped": 0, "unmatched": 0, "errors": []}
|
||||
|
||||
try:
|
||||
@@ -111,8 +114,22 @@ class UnderstatSource:
|
||||
result["errors"].append(f"normalize: {e}")
|
||||
continue
|
||||
|
||||
# 匹配已有 Match(天级)
|
||||
existing = await find_existing_match(db, league_obj.id, nm.home_team, nm.away_team, nm.date)
|
||||
# 匹配已有 Match(天级) - 直接查询
|
||||
home_team = (await db.execute(select(Team).where(Team.name == nm.home_team))).scalar_one_or_none()
|
||||
away_team = (await db.execute(select(Team).where(Team.name == nm.away_team))).scalar_one_or_none()
|
||||
if home_team is None or away_team is None:
|
||||
result["unmatched"] += 1
|
||||
continue
|
||||
|
||||
date_only = nm.date.date() if hasattr(nm.date, "date") else nm.date
|
||||
stmt = (
|
||||
select(Match)
|
||||
.where(Match.league_id == league_obj.id)
|
||||
.where(Match.home_team_id == home_team.id)
|
||||
.where(Match.away_team_id == away_team.id)
|
||||
.where(func.date(Match.match_date) == date_only)
|
||||
)
|
||||
existing = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if existing is None:
|
||||
result["unmatched"] += 1
|
||||
continue
|
||||
@@ -129,5 +146,5 @@ class UnderstatSource:
|
||||
if existing.stats.away_xg is None and nm.away_xg is not None:
|
||||
existing.stats.away_xg = nm.away_xg
|
||||
|
||||
await db.commit()
|
||||
# 注意: 不在此处 commit,由调用方 UnitOfWork 控制事务
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user