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:
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user