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:
shangfangjian
2026-09-15 00:42:05 +08:00
parent cb36dc3ef9
commit 483cb956ba
11 changed files with 281 additions and 117 deletions
+16 -24
View File
@@ -1,6 +1,7 @@
"""Bzzoiro 数据源:抓取 + 入库。
迁移自旧项目 app/data/sources/bzzoiro/,改成 async + 简化入库。
使用 Repository 模式进行数据访问,不直接控制事务。
"""
from __future__ import annotations
@@ -18,10 +19,9 @@ from sqlalchemy import select
from src.core.config import settings
from src.data.config import BZZOIRO_LEAGUE_IDS, LEAGUE_COUNTRIES, LEAGUE_NAMES, REQUEST_INTERVAL
from src.data.match_lookup import find_existing_match, get_or_create_team
from src.data.normalize import normalize_bzzoiro
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__)
@@ -125,7 +125,10 @@ class BzzoiroSource:
date_to: str | None = None,
status: str = "finished",
) -> dict:
"""采集 bzzoiro → 入库。返回统计。"""
"""采集 bzzoiro → 入库。返回统计。
注意: 本方法不控制事务(commit/rollback),由调用方通过 UnitOfWork 控制。
"""
result: dict = {"leagues": {}, "total_inserted": 0, "total_updated": 0, "errors": []}
for code in leagues:
@@ -159,29 +162,19 @@ class BzzoiroSource:
all_team_names.add(nm.away_team)
if all_team_names:
from sqlalchemy import select
from src.db.models import Team
stmt = select(Team).where(Team.name.in_(all_team_names))
teams = (await db.execute(stmt)).scalars().all()
team_name_to_id = {t.name: t.id for t in teams}
# 预加载已有比赛 (league_id + home_id + away_id + date)
# 需要先获取球队 ID,所以分批处理
date_strs = set()
for raw in raw_events:
nm = normalize_bzzoiro(raw, code)
if nm and nm.date:
date_strs.add(nm.date.date().isoformat() if hasattr(nm.date, "date") else str(nm.date))
if date_strs:
from sqlalchemy import func
stmt = (
select(Match.home_team_id, Match.away_team_id, func.date(Match.match_date).label("d"))
.where(Match.league_id == league.id)
)
rows = (await db.execute(stmt)).all()
for row in rows:
existing_match_keys.add((row.home_team_id, row.away_team_id, str(row.d)))
# 预加载已有比赛
from sqlalchemy import func
stmt = (
select(Match.home_team_id, Match.away_team_id, func.date(Match.match_date).label("d"))
.where(Match.league_id == league.id)
)
rows = (await db.execute(stmt)).all()
for row in rows:
existing_match_keys.add((row.home_team_id, row.away_team_id, str(row.d)))
for raw in raw_events:
try:
@@ -255,7 +248,6 @@ class BzzoiroSource:
league_r["inserted"] += 1
else:
# 已有比赛: 需要查询对象来更新
# 注意: 这里为了简化仍查询一次,但只在"已有"时触发
from sqlalchemy import func
stmt = (
select(Match)
@@ -297,7 +289,7 @@ class BzzoiroSource:
if changed:
league_r["updated"] += 1
await db.commit()
# 注意: 不在此处 commit,由调用方 UnitOfWork 控制事务
result["leagues"][code] = league_r
result["total_inserted"] += league_r["inserted"]
result["total_updated"] += league_r["updated"]