feat: standings Bronze 血缘补齐 + 队名归一 MVP

standings 成功 upsert 后写入 RawEvent(source_record_id=standings:{league}:{season})
+ DataLineage(target_table=standings),与 events/stats 管线对称。

队名归一化收敛到 TeamRepository.get_or_create 唯一咽喉点,
创建新 Team 时 info 日志打出原始名与归一后名;
Admin 新增 GET /api/v1/admin/team-name-duplicates 只读接口,
启发式列出近似重名候选(大小写变体/子串/前缀碰撞),不做自动合并。
This commit is contained in:
shangfangjian
2026-09-21 23:28:46 +08:00
parent 317a5e338a
commit 66f0844798
5 changed files with 685 additions and 5 deletions
+19 -3
View File
@@ -5,6 +5,10 @@ Repository 只负责查询,不负责事务提交。
"""
from __future__ import annotations
import logging
logger = logging.getLogger(__name__)
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -108,10 +112,22 @@ class TeamRepository:
return (await self._session.execute(stmt)).scalar_one_or_none()
async def get_or_create(self, name: str, *, name_zh: str | None = None) -> Team:
"""按名获取球队,不存在则创建(name_zh 供 bzzoiro 管线写中文名)。"""
team = await self.get_by_name(name)
"""按名获取球队,不存在则创建(name_zh 供 bzzoiro 管线写中文名)。
归一化咽喉:所有入库 Team.name 必须经过 team_names.normalize,
此处统一收敛,避免各调用点散落归一化逻辑导致重复 Team。
创建新 Team 时 info 打出原始名与归一后的规范名,便于排查重名。
"""
from src.data.team_names import normalize as normalize_name
normalized = normalize_name(name) or name.strip()
team = await self.get_by_name(normalized)
if team is None:
team = Team(name=name, name_zh=name_zh)
logger.info(
"创建新 Team: %s -> %s",
name, normalized,
)
team = Team(name=normalized, name_zh=name_zh)
self._session.add(team)
await self._session.flush()
return team