全量修复:预测系统正确性、安全性与部署问题
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:
@@ -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 次)",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user