全量修复:预测系统正确性、安全性与部署问题

P0 严重问题修复:
- 修复 form_slice/stats_slice 主客身份反转(历史比赛视角错误)
- 修复 understat.py httpx 未导入导致的 NameError
- 修复 LLM 解析失败时静默产生假成功预测(0-0 平局+置信度0.5)

预测路径修复:
- multi-agent 路径增加 backtest cutoff 透传,回测防泄漏生效
- H2H 切片汇总统计改为从当前主队视角计数
- 预测唯一约束增加 mode+run_type 维度,防止回测覆盖实盘预测

伤停管线修复:
- IntegrityError 后不再整批回滚丢数据(改用逐条 flush)
- return_date 正确解析并写入
- retrieved_at 比较统一用 date() 避免当天数据不可见
- 唯一索引改为 partial unique index(排除 NULL 重复)
- HTTP 缓存 TTL 从 7 天改为 6 小时

安全与连接管理:
- /api/v1/predict 增加内存滑动窗口限流(10次/分钟/IP)
- 预测路由改用短 session 模式,LLM 调用期间不持有 DB 连接

Docker 部署修复:
- 修复 .dockerignore 排除 *.md 导致 COPY README.md 失败
- 容器内 DATABASE_URL 使用 postgres 服务名(非 localhost)
- 启动时自动执行 alembic upgrade head
- 前端改用多阶段构建(Dockerfile.frontend)

新增测试(5个文件,24+用例):
- test_p0_home_away.py: 主客身份反转回归测试
- test_p0_parse_failure.py: LLM 解析失败回归测试
- test_multi_agent_cutoff.py: multi-agent cutoff 透传测试
- test_h2h_perspective.py: H2H 视角测试
- test_injuries_pipeline.py: 伤停管线 5 项修复测试
- test_predict_protection.py: 限流+短 session 测试
- test_prediction_unique_constraint.py: 唯一约束测试

迁移:
- 0012_injuries_partial_unique_and_return_date.py
- 0013_predictions_unique_constraint_mode_run_type.py
This commit is contained in:
Profeto Agent
2026-09-19 06:43:55 +00:00
parent 11efe91ce9
commit bee330f31f
27 changed files with 1666 additions and 137 deletions
+57
View File
@@ -96,3 +96,60 @@ async def require_admin(
return
raise HTTPException(status_code=401, detail="未登录或凭证无效")
# ── 简易内存限流(按 IP,无外部依赖) ──
class _RateLimiter:
"""内存式滑动窗口限流。
设计取舍:
- 单进程内有效,多 worker 各自计数(生产前置于 Nginx 做全局限流更精确)
- 滑动窗口:记录每次请求时间戳,清理过期条目
- O(n) 清理,n = 时间窗口内请求数(通常 < 100)
"""
def __init__(self, max_requests: int = 10, window_seconds: int = 60):
self.max_requests = max_requests
self.window_seconds = window_seconds
self._hits: dict[str, list[float]] = {}
def is_allowed(self, key: str) -> bool:
"""检查 key 是否允许通过。True=允许,False=拒绝。"""
now = time.time()
window_start = now - self.window_seconds
# 获取并清理该 key 的过期记录
timestamps = self._hits.get(key, [])
timestamps = [t for t in timestamps if t > window_start]
if len(timestamps) >= self.max_requests:
self._hits[key] = timestamps # 更新清理后的列表
return False
timestamps.append(now)
self._hits[key] = timestamps
return True
# 全局限流实例: /api/v1/predict 每分钟 10 次
_predict_limiter = _RateLimiter(max_requests=10, window_seconds=60)
async def rate_limit_predict(request: Request) -> None:
"""POST /api/v1/predict 限流依赖。
基于客户端 IP(考虑 X-Forwarded-For),超过 10 次/分钟返回 429。
"""
# 获取客户端 IP(支持反向代理)
client_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "unknown")
# X-Forwarded-For 可能包含多个 IP(代理链),取第一个
if "," in client_ip:
client_ip = client_ip.split(",")[0].strip()
if not _predict_limiter.is_allowed(client_ip):
logger.warning("rate limit exceeded for %s", client_ip)
raise HTTPException(
status_code=429,
detail="请求过于频繁,请稍后再试(每分钟最多 10 次)",
)