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:
@@ -15,11 +15,15 @@ from fastapi import APIRouter
|
||||
|
||||
from src.api.routes.admin_config import router as admin_config_router
|
||||
from src.api.routes.admin_datasources import router as admin_datasources_router
|
||||
from src.api.routes.admin_ingest_jobs import router as admin_ingest_jobs_router
|
||||
from src.api.routes.admin_llm import router as admin_llm_router
|
||||
from src.api.routes.admin_quality import router as admin_quality_router
|
||||
from src.api.routes.admin_teams import router as admin_teams_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(admin_datasources_router)
|
||||
router.include_router(admin_config_router)
|
||||
router.include_router(admin_ingest_jobs_router)
|
||||
router.include_router(admin_llm_router)
|
||||
router.include_router(admin_quality_router)
|
||||
router.include_router(admin_teams_router)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""后台管理:球队别名管理(只读列表 + 添加别名)。
|
||||
|
||||
归一名(teams.name)是球队唯一键;别名(team_aliases)是同一球队的不同写法
|
||||
(大小写/译名/缩写)到归一后 teams.id 的映射。入库时 normalize(name) 依次查
|
||||
teams.name 与 team_aliases,命中即复用,避免重复 Team。
|
||||
|
||||
不自动合并历史重复队;需显式添加别名(或先 SQL/再经由此接口)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import desc, select
|
||||
|
||||
from src.api.deps import require_admin
|
||||
from src.api.schemas import TeamAliasIn, TeamAliasOut
|
||||
from src.db.base import AsyncSession, get_db_read
|
||||
from src.db.models import Team, TeamAlias
|
||||
from src.db.repositories import TeamRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
@router.get("/teams/aliases", response_model=list[TeamAliasOut])
|
||||
async def list_team_aliases(db: AsyncSession = Depends(get_db_read)):
|
||||
"""列出所有球队别名(最新在前)。"""
|
||||
rows = (await db.execute(select(TeamAlias).order_by(desc(TeamAlias.created_at)).limit(200))).scalars().all()
|
||||
return [
|
||||
TeamAliasOut(
|
||||
alias_normalized=r.alias_normalized,
|
||||
team_id=r.team_id,
|
||||
original_alias=r.original_alias,
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post("/teams/aliases", response_model=TeamAliasOut, status_code=201)
|
||||
async def add_team_alias(req: TeamAliasIn, db: AsyncSession = Depends(get_db_read)):
|
||||
"""为已有 Team 添加别名(幂等:重复添加会更新指向)。
|
||||
|
||||
不自动合并历史重复队。若需合并 A→B:先为 A 的归一名添加别名指向 B,
|
||||
再人工确认 A 是否仍有独立引用。
|
||||
"""
|
||||
# 校验目标 Team 存在
|
||||
team = await db.get(Team, req.team_id)
|
||||
if team is None:
|
||||
raise HTTPException(404, f"目标 Team 不存在: id={req.team_id}")
|
||||
|
||||
repo = TeamRepository(db)
|
||||
row = await repo.add_alias(req.alias, req.team_id)
|
||||
logger.info("添加 Team 别名: %s -> team_id=%s", req.alias, req.team_id)
|
||||
return TeamAliasOut(
|
||||
alias_normalized=row.alias_normalized,
|
||||
team_id=row.team_id,
|
||||
original_alias=row.original_alias,
|
||||
)
|
||||
Reference in New Issue
Block a user