fix: 代码审查问题修复

- unit_of_work.py: 简化为纯 session 上下文管理器,消除 double-close 风险
- validation.py: 移除未使用的 import,简化 _score_to_1x2 逻辑
- repositories.py: 将 func 导入移到模块顶层
- 更新所有 get_uow() 调用点使用新接口(yield session 而非 uow 对象)
This commit is contained in:
shangfangjian
2026-09-15 01:18:35 +08:00
parent 74586aa5b7
commit 53e602b4f6
8 changed files with 40 additions and 86 deletions
+6 -9
View File
@@ -16,15 +16,14 @@ async def ingest_bzzoiro_route(req: IngestBzzoiroRequest):
"""触发 bzzoiro 采集。""" """触发 bzzoiro 采集。"""
source = get_source("bzzoiro") source = get_source("bzzoiro")
try: try:
async with get_uow() as uow: async with get_uow() as session:
result = await source.ingest( result = await source.ingest(
uow.session, session,
leagues=req.leagues, leagues=req.leagues,
date_from=req.date_from, date_from=req.date_from,
date_to=req.date_to, date_to=req.date_to,
status=req.status, status=req.status,
) )
await uow.commit()
return IngestResponse(**result) return IngestResponse(**result)
except Exception as e: except Exception as e:
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@@ -35,9 +34,8 @@ async def ingest_understat_route(req: IngestUnderstatRequest):
"""触发 understat xG 回填。""" """触发 understat xG 回填。"""
source = get_source("understat") source = get_source("understat")
try: try:
async with get_uow() as uow: async with get_uow() as session:
result = await source.ingest(uow.session, league=req.league, season=req.season) result = await source.ingest(session, league=req.league, season=req.season)
await uow.commit()
return IngestSimpleResponse(**result) return IngestSimpleResponse(**result)
except Exception as e: except Exception as e:
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@@ -47,9 +45,8 @@ async def ingest_understat_route(req: IngestUnderstatRequest):
async def ingest_injuries_route(req: IngestInjuriesRequest): async def ingest_injuries_route(req: IngestInjuriesRequest):
"""触发伤停采集。""" """触发伤停采集。"""
try: try:
async with get_uow() as uow: async with get_uow() as session:
result = await ingest_injuries(uow.session, date=req.date) result = await ingest_injuries(session, date=req.date)
await uow.commit()
return IngestSimpleResponse(**result) return IngestSimpleResponse(**result)
except Exception as e: except Exception as e:
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
+1 -3
View File
@@ -5,7 +5,7 @@ Repository 只负责查询,不负责事务提交。
""" """
from __future__ import annotations from __future__ import annotations
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -38,8 +38,6 @@ class MatchRepository:
self, league_id: int, home_team_id: int, away_team_id: int, date self, league_id: int, home_team_id: int, away_team_id: int, date
) -> Match | None: ) -> Match | None:
"""按联赛+主队+客队+日期查找比赛(天级匹配)。""" """按联赛+主队+客队+日期查找比赛(天级匹配)。"""
from sqlalchemy import func
if hasattr(date, "date"): if hasattr(date, "date"):
date = date.date() date = date.date()
+15 -45
View File
@@ -1,63 +1,33 @@
"""工作单元(Unit of Work):统一事务边界。 """工作单元(Unit of Work):统一事务边界。
使用方式: 使用方式:
async with UnitOfWork(db) as uow: async with get_uow() as uow:
await uow.matches.get_by_id(1) await uow.session.get(Match, 1)
await uow.matches.add(new_match) await uow.commit()
# 退出时自动 commit,异常时 rollback
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from src.db.base import AsyncSessionLocal from src.db.base import AsyncSessionLocal
class UnitOfWork:
"""工作单元:封装事务边界。"""
def __init__(self, session: AsyncSession) -> None:
self._session = session
self.committed = False
@property
def session(self) -> AsyncSession:
return self._session
async def commit(self) -> None:
await self._session.commit()
self.committed = True
async def rollback(self) -> None:
await self._session.rollback()
async def close(self) -> None:
await self._session.close()
async def __aenter__(self) -> "UnitOfWork":
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
if exc_type is not None:
await self.rollback()
await self.close()
@asynccontextmanager @asynccontextmanager
async def get_uow() -> AsyncGenerator[UnitOfWork, None]: async def get_uow() -> AsyncIterator[AsyncSessionLocal]:
"""创建新的工作单元(用于非路由上下文)。""" """创建新的工作单元(用于非路由上下文)。
用法:
async with get_uow() as session:
await session.get(...)
# 退出时自动 commit(无异常) 或 rollback(有异常)
"""
session = AsyncSessionLocal() session = AsyncSessionLocal()
uow = UnitOfWork(session)
try: try:
yield uow yield session
if not uow.committed: await session.commit()
await uow.commit()
except Exception: except Exception:
await uow.rollback() await session.rollback()
raise raise
finally: finally:
await uow.close() await session.close()
+4 -6
View File
@@ -186,9 +186,8 @@ async def predict_match_multi(
).hexdigest() ).hexdigest()
# 4. 存库(使用 UnitOfWork) # 4. 存库(使用 UnitOfWork)
async with get_uow() as uow: async with get_uow() as session:
db = uow.session m = await session.get(Match, match_id)
m = await db.get(Match, match_id)
if m is None: if m is None:
raise ValueError(f"match {match_id} not found") raise ValueError(f"match {match_id} not found")
@@ -219,9 +218,8 @@ async def predict_match_multi(
cutoff_at=cutoff_at, cutoff_at=cutoff_at,
input_hash=input_hash, input_hash=input_hash,
) )
db.add(pred) session.add(pred)
await uow.commit() await session.refresh(pred)
await db.refresh(pred)
return MultiPredictResult( return MultiPredictResult(
prediction_id=pred.id, prediction_id=pred.id,
+2 -2
View File
@@ -108,9 +108,9 @@ async def run_backtest(
Returns: Returns:
BacktestSummary 含逐场结果 + 汇总统计 BacktestSummary 含逐场结果 + 汇总统计
""" """
async with get_uow() as uow: async with get_uow() as session:
matches = await _get_historical_matches( matches = await _get_historical_matches(
uow.session, league_id=league_id, date_from=date_from, date_to=date_to, limit=limit session, league_id=league_id, date_from=date_from, date_to=date_to, limit=limit
) )
summary = BacktestSummary(total=len(matches), scored=0) summary = BacktestSummary(total=len(matches), scored=0)
+4 -5
View File
@@ -13,14 +13,13 @@ logger = logging.getLogger(__name__)
async def settle_prediction(prediction_id: int, home_goals: int, away_goals: int) -> Prediction: async def settle_prediction(prediction_id: int, home_goals: int, away_goals: int) -> Prediction:
"""回填实际结果。""" """回填实际结果。"""
async with get_uow() as uow: async with get_uow() as session:
pred = await uow.session.get(Prediction, prediction_id) pred = await session.get(Prediction, prediction_id)
if pred is None: if pred is None:
raise ValueError(f"prediction {prediction_id} not found") raise ValueError(f"prediction {prediction_id} not found")
pred.actual_home_goals = home_goals pred.actual_home_goals = home_goals
pred.actual_away_goals = away_goals pred.actual_away_goals = away_goals
pred.settled = True pred.settled = True
await uow.commit()
return pred return pred
@@ -35,12 +34,12 @@ def _actual_1x2(home: int, away: int) -> str:
async def get_eval_summary() -> dict: async def get_eval_summary() -> dict:
"""按 provider × 模型聚合评估。""" """按 provider × 模型聚合评估。"""
async with get_uow() as uow: async with get_uow() as session:
stmt = ( stmt = (
select(Prediction) select(Prediction)
.where(Prediction.settled == True) .where(Prediction.settled == True)
) )
result = await uow.session.execute(stmt) result = await session.execute(stmt)
rows = list(result.scalars().all()) rows = list(result.scalars().all())
from collections import defaultdict from collections import defaultdict
+4 -7
View File
@@ -145,10 +145,9 @@ async def _predict_single(
raise RuntimeError(f"LLM 输出校验失败: {e}") raise RuntimeError(f"LLM 输出校验失败: {e}")
# 4. 存预测(使用 UnitOfWork 统一事务) # 4. 存预测(使用 UnitOfWork 统一事务)
async with get_uow() as uow: async with get_uow() as session:
db = uow.session
# 验证 match 存在 # 验证 match 存在
m = await db.get(Match, match_id) m = await session.get(Match, match_id)
if m is None: if m is None:
raise ValueError(f"match {match_id} not found") raise ValueError(f"match {match_id} not found")
@@ -169,9 +168,8 @@ async def _predict_single(
cutoff_at=cutoff_at, cutoff_at=cutoff_at,
input_hash=input_hash, input_hash=input_hash,
) )
db.add(pred) session.add(pred)
await db.commit() await session.refresh(pred)
await db.refresh(pred)
result = PredictResult( result = PredictResult(
prediction_id=pred.id, prediction_id=pred.id,
@@ -190,5 +188,4 @@ async def _predict_single(
# 5. 写入缓存 # 5. 写入缓存
_set_cached(match_id, settings.LLM_PROVIDER, provider.model, version, result) _set_cached(match_id, settings.LLM_PROVIDER, provider.model, version, result)
await uow.commit()
return result return result
+4 -9
View File
@@ -6,8 +6,6 @@ from __future__ import annotations
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, field_validator, model_validator
from src.db.models import Prediction
class AgentReportSchema(BaseModel): class AgentReportSchema(BaseModel):
"""单个专家 Agent 输出的校验 schema。""" """单个专家 Agent 输出的校验 schema。"""
@@ -62,23 +60,20 @@ class PredictionOutputSchema(BaseModel):
@model_validator(mode="after") @model_validator(mode="after")
def check_consistency(self) -> "PredictionOutputSchema": def check_consistency(self) -> "PredictionOutputSchema":
"""验证比分与胜平负一致。""" """验证比分与胜平负一致,不一致则自动修正"""
expected = _score_to_1x2(self.pred_home_goals, self.pred_away_goals) expected = _score_to_1x2(self.pred_home_goals, self.pred_away_goals)
if expected and self.pred_1x2 != expected: if self.pred_1x2 != expected:
# 自动修正而非拒绝(LLM 常见小错误)
self.pred_1x2 = expected self.pred_1x2 = expected
return self return self
def _score_to_1x2(home: float, away: float) -> str | None: def _score_to_1x2(home: float, away: float) -> str:
"""从比分推导胜平负。""" """从比分推导胜平负。"""
if home > away: if home > away:
return "1" return "1"
if home == away:
return "X"
if home < away: if home < away:
return "2" return "2"
return None return "X"
def validate_agent_output(raw: dict) -> AgentReportSchema: def validate_agent_output(raw: dict) -> AgentReportSchema: