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:
shangfangjian
2026-09-22 00:27:53 +08:00
parent 4b0d6ee58a
commit e15b554ba3
10 changed files with 290 additions and 15 deletions
+35
View File
@@ -117,6 +117,19 @@ class IngestBzzoiroRequest(BaseModel):
season: str | None = Field(None, description="standings 赛季,如 '2026-2027';空 = 当前赛季")
class TeamAliasIn(BaseModel):
"""POST /api/v1/admin/teams/aliases 请求体:为已有 Team 添加别名。"""
alias: str = Field(..., min_length=1, max_length=120, description="球队别名(原始写法)")
team_id: int = Field(..., gt=0, description="归一后的目标 teams.id")
class TeamAliasOut(BaseModel):
alias_normalized: str
team_id: int
original_alias: str
class IngestResponse(BaseModel):
leagues: dict
total_inserted: int
@@ -124,6 +137,28 @@ class IngestResponse(BaseModel):
errors: list[str] = []
class IngestBzzoiroResponse(BaseModel):
"""POST /api/v1/ingest/bzzoiro 响应:兼容原 message 字段,新增 job_id 供轮询。"""
ok: bool = True
job_id: str = Field(..., description="采集任务 ID(GET /api/v1/admin/ingest/jobs/{job_id} 轮询)")
message: str = ""
class IngestJobOut(BaseModel):
"""采集任务状态详情。"""
id: str
task: str
params: dict
status: str # pending | running | success | failed
result: dict | None = None
error: str | None = None
created_at: datetime | None = None
started_at: datetime | None = None
finished_at: datetime | None = None
class ScheduleIn(BaseModel):
id: str = Field(..., description="任务唯一标识,如 'daily-events'")
task: str = Field(..., description="events / standings / stats / all")