安全加固 + 数据管线修复 + 质量改进(第2批)

安全加固:
- /api/v1/predict 限流改用 get_client_ip 防 X-Forwarded-For 伪造
- 新增 TRUST_PROXY_HEADERS/REQUIRE_ADMIN_AUTH 配置,默认 fail-closed
- 生产环境管理接口未配置鉴权时拒绝(503),不再放行

数据管线修复:
- injuries IntegrityError 改用 begin_nested(SAVEPOINT)隔离批次
- injuries 空名单 vs 未配置语义区分(has_data 精确标记)
- bzzoiro 统计字段映射(shots/possession/cards) + 入库条件放宽
- available_at 语义收紧(回测防泄漏)
- team_names NFKD 去变音双重查找 + 补齐变体键

预测系统改进:
- multi-agent 全专家失败时 status=degraded 跳过终裁
- agent_weights 独立持久化到 predictions 表

新增迁移:
- 0014_predictions_agent_weights.py

新增测试(8个文件):
- test_ip_spoofing.py: 限流防伪造
- test_require_admin_fail_closed.py: 生产 fail-closed
- test_injuries_integrity_rollback.py: SAVEPOINT 隔离
- test_injuries_slice_semantic.py: 空名单 vs 未配置
- test_available_at.py: 回测防泄漏
- test_bzzoirot_stats.py: 统计字段映射
- test_team_names_normalize.py: NFKD 变体
- test_multi_agent_degraded.py: 全失败 degraded
- test_agent_weights_persist.py: 权重持久化
This commit is contained in:
Profeto Agent
2026-09-19 08:02:57 +00:00
parent bee330f31f
commit 6c24672e87
21 changed files with 1580 additions and 87 deletions
+28 -2
View File
@@ -1,6 +1,9 @@
"""队名归一化:各源队名 → 统一规范名。
迁移自旧项目 app/data/team_names.py。
NFKD 归一化会剥离变音符号(ü→u),导致「Bayern München」与「Bayern Munich」
映射到不同规范名。修复:双重查找(原始名 + NFKD 名) + 补齐常见变体键。
"""
from __future__ import annotations
@@ -61,10 +64,15 @@ NORMALIZE_MAP = {
"Barcelona": "Barcelona",
# ---- 德甲 ----
"Bayern Munich": "Bayern München",
"Bayern Munchen": "Bayern München", # NFKD stripped variant
"Bayern München": "Bayern München", # canonical with umlaut (direct hit)
"FC Koln": "FC Köln",
"FC Köln": "FC Köln",
"RB Leipzig": "RB Leipzig",
"Borussia Dortmund": "Borussia Dortmund",
"Borussia M'gladbach": "Borussia Mönchengladbach",
"Borussia Monchengladbach": "Borussia Mönchengladbach", # NFKD stripped
"Borussia Mönchengladbach": "Borussia Mönchengladbach", # canonical
"Bayer Leverkusen": "Bayer Leverkusen",
"Eintracht Frankfurt": "Eintracht Frankfurt",
"VfB Stuttgart": "VfB Stuttgart",
@@ -121,6 +129,9 @@ NORMALIZE_MAP = {
"Le Havre": "Le Havre AC",
"Lorient": "FC Lorient",
"Saint-Etienne": "AS Saint-Étienne",
"Saint-Etienne": "AS Saint-Étienne", # NFKD stripped (ê → e)
"AS Saint-Étienne": "AS Saint-Étienne", # canonical prefix
"AS Saint-Etienne": "AS Saint-Étienne", # NFKD stripped with prefix
"Angers": "Angers SCO",
"Auxerre": "AJ Auxerre",
"Leganes": "Leganés",
@@ -128,10 +139,25 @@ NORMALIZE_MAP = {
def normalize(name: str) -> str:
"""队名归一化:变音符号变体 → 统一规范名。
双重查找策略:
1. 原始名(带变音)直接查表
2. NFKD 去变音后再查表
确保「Bayern München」「Bayern Munchen」「Bayern Munich」
都映射到同一规范名「Bayern München」。
"""
if not name:
return ""
# unicode 归一(重音)
n = unicodedata.normalize("NFKD", name)
# 1. 原始名直接查表(保留变音变体)
stripped = name.strip()
if stripped in NORMALIZE_MAP:
return NORMALIZE_MAP[stripped]
# 2. NFKD 去变音后查表
n = unicodedata.normalize("NFKD", stripped)
n = "".join(c for c in n if not unicodedata.combining(c))
n = n.strip()
return NORMALIZE_MAP.get(n, n)