feat: 球队实体一致性 — 归一化咽喉 + team_aliases 别名机制
events/standings 创建 Team 前均经 team_names.normalize(已有,确认), TeamRepository.get_or_create 收敛为归一化唯一咽喉 + info 日志。 新增 team_aliases 表(NFKD 归一别名 → teams.id FK CASCADE), 定位三步链:normalize(name) → teams.name → team_aliases → insert。 不自动合并历史重复队;提供 POST /api/v1/admin/teams/aliases 显式添加。 迁移 0020_team_aliases + Admin 别名管理端点(admin_teams.py)。 全量测试 270 通过。
This commit is contained in:
@@ -20,7 +20,7 @@ from datetime import date, datetime, timezone
|
||||
import pytest
|
||||
|
||||
import src.data.bzzoiro as bz
|
||||
from src.db.models import DataLineage, League, Match, RawEvent, Team
|
||||
from src.db.models import DataLineage, League, Match, RawEvent, Team, TeamAlias
|
||||
|
||||
|
||||
def _event(eid=1001, status="finished", home="Arsenal", away="Chelsea", hs=2, as_=1):
|
||||
@@ -71,11 +71,20 @@ class _FakeDB:
|
||||
League: list(leagues),
|
||||
RawEvent: list(raw_events),
|
||||
}
|
||||
self._next_id = 0
|
||||
self._teams_by_id: dict[int, Team] = {t.id: t for t in teams if getattr(t, "id", None)}
|
||||
self._aliases: dict[str, TeamAlias] = {}
|
||||
self._next_id = max((t.id for t in teams if getattr(t, "id", None)), default=0)
|
||||
|
||||
def add(self, obj):
|
||||
self.added.append(obj)
|
||||
|
||||
async def get(self, cls, key):
|
||||
if cls is Team:
|
||||
return self._teams_by_id.get(key)
|
||||
if cls is TeamAlias:
|
||||
return self._aliases.get(key)
|
||||
return None
|
||||
|
||||
async def execute(self, stmt):
|
||||
entities = set()
|
||||
for d in (stmt.column_descriptions or []):
|
||||
@@ -90,6 +99,8 @@ class _FakeDB:
|
||||
if getattr(obj, "id", None) is None:
|
||||
self._next_id += 1
|
||||
obj.id = self._next_id
|
||||
if isinstance(obj, Team) and obj.id is not None:
|
||||
self._teams_by_id[obj.id] = obj
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -17,6 +17,7 @@ import re
|
||||
|
||||
import pytest
|
||||
|
||||
from src.db.models import Team, TeamAlias
|
||||
from src.data.key_ring import _mask
|
||||
from src.llm import backtest as bt_mod
|
||||
from src.llm.agents import orchestrator as orch_mod
|
||||
@@ -105,6 +106,15 @@ class _FakeDb:
|
||||
self.added: list = []
|
||||
self.flush_count = 0
|
||||
self._next_id = 1000
|
||||
self._teams_by_id: dict[int, Team] = {}
|
||||
self._aliases: dict[str, TeamAlias] = {}
|
||||
|
||||
async def get(self, cls, key):
|
||||
if cls is Team:
|
||||
return self._teams_by_id.get(key)
|
||||
if cls is TeamAlias:
|
||||
return self._aliases.get(key)
|
||||
return None
|
||||
|
||||
async def execute(self, _stmt):
|
||||
if self._results:
|
||||
@@ -120,6 +130,10 @@ class _FakeDb:
|
||||
if getattr(obj, "id", None) is None:
|
||||
self._next_id += 1
|
||||
obj.id = self._next_id
|
||||
if isinstance(obj, Team) and getattr(obj, "id", None) is not None:
|
||||
self._teams_by_id[obj.id] = obj
|
||||
if isinstance(obj, TeamAlias):
|
||||
self._aliases[obj.alias_normalized] = obj
|
||||
|
||||
|
||||
async def test_r2_standings_actually_upserts(monkeypatch):
|
||||
|
||||
@@ -18,7 +18,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
import src.data.bzzoiro as bz
|
||||
from src.db.models import DataLineage, IngestFailure, League, RawEvent, Standing, Team
|
||||
from src.db.models import DataLineage, IngestFailure, League, RawEvent, Standing, Team, TeamAlias
|
||||
|
||||
|
||||
def _payload():
|
||||
@@ -78,11 +78,21 @@ class _FakeDB:
|
||||
Standing: list(standings),
|
||||
RawEvent: list(raw_events),
|
||||
}
|
||||
self._next_id = 0
|
||||
# session.get 查找表(Team/TeamAlias)
|
||||
self._teams_by_id: dict[int, Team] = {t.id: t for t in teams if getattr(t, "id", None)}
|
||||
self._aliases: dict[str, TeamAlias] = {}
|
||||
self._next_id = max((t.id for t in teams if getattr(t, "id", None)), default=0)
|
||||
|
||||
def add(self, obj):
|
||||
self.added.append(obj)
|
||||
|
||||
async def get(self, cls, key):
|
||||
if cls is Team:
|
||||
return self._teams_by_id.get(key)
|
||||
if cls is TeamAlias:
|
||||
return self._aliases.get(key)
|
||||
return None
|
||||
|
||||
async def execute(self, stmt):
|
||||
entities = set()
|
||||
for d in (stmt.column_descriptions or []):
|
||||
@@ -110,6 +120,9 @@ class _FakeDB:
|
||||
if getattr(obj, "id", None) is None:
|
||||
self._next_id += 1
|
||||
obj.id = self._next_id
|
||||
# 同步 session.get 可查到新建 Team
|
||||
if isinstance(obj, Team) and obj.id is not None:
|
||||
self._teams_by_id[obj.id] = obj
|
||||
|
||||
|
||||
def _preset_league():
|
||||
|
||||
Reference in New Issue
Block a user