全量修复:预测系统正确性、安全性与部署问题
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:
@@ -0,0 +1,65 @@
|
||||
"""修复 injuries 唯一索引允许 NULL 重复 + 添加 return_date 字段
|
||||
|
||||
Revision ID: 0012_injuries_partial_unique_and_return_date
|
||||
Revises: 0011_prediction_alt_scores
|
||||
Create Date: 2026-09-20
|
||||
|
||||
Fix 4: 唯一索引 (player_id, fixture_id, injury_type) 三列均可 NULL,
|
||||
PostgreSQL 允许多条 NULL 重复。改为 partial unique index:
|
||||
WHERE player_id IS NOT NULL AND fixture_id IS NOT NULL
|
||||
|
||||
Fix 2: return_date 字段已在 ORM 声明,确保数据库列存在。
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used Alembic.
|
||||
revision: str = '0012_injuries_partial_unique_and_return_date'
|
||||
down_revision: Union[str, None] = '0011_prediction_alt_scores'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
# 检查现有索引
|
||||
indexes = {i["name"]: i for i in inspector.get_indexes("injuries")}
|
||||
|
||||
# Fix 4: 删除旧的全局唯一索引(允许 NULL 重复)
|
||||
if "ix_injuries_player_fixture" in indexes:
|
||||
op.drop_index("ix_injuries_player_fixture", table_name="injuries")
|
||||
|
||||
# 创建 partial unique index: 只在 player_id 和 fixture_id 都非空时强制唯一
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX ix_injuries_player_fixture
|
||||
ON injuries (player_id, fixture_id, injury_type)
|
||||
WHERE player_id IS NOT NULL AND fixture_id IS NOT NULL
|
||||
"""
|
||||
)
|
||||
|
||||
# Fix 2: 确保 return_date 列存在(ORM 已声明,但早期迁移可能缺失)
|
||||
columns = [c["name"] for c in inspector.get_columns("injuries")]
|
||||
if "return_date" not in columns:
|
||||
op.add_column(
|
||||
"injuries",
|
||||
sa.Column("return_date", sa.Date, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 删除 partial unique index
|
||||
op.drop_index("ix_injuries_player_fixture", table_name="injuries")
|
||||
|
||||
# 恢复旧的全局唯一索引
|
||||
op.create_index(
|
||||
"ix_injuries_player_fixture",
|
||||
"injuries",
|
||||
["player_id", "fixture_id", "injury_type"],
|
||||
unique=True,
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""修复预测唯一约束过粗,增加 mode + run_type 维度
|
||||
|
||||
Revision ID: 0013_predictions_unique_constraint_mode_run_type
|
||||
Revises: 0012_injuries_partial_unique_and_return_date
|
||||
Create Date: 2026-09-20
|
||||
|
||||
背景:
|
||||
原唯一约束 (match_id, provider, model) 过粗,回测写入会覆盖未结算的实盘预测,
|
||||
后续 settle 会污染评估数据。
|
||||
|
||||
修复:
|
||||
1. 新增 run_type 列(默认 'live'),区分实盘与回测
|
||||
2. 唯一约束改为 (match_id, provider, model, mode, run_type)
|
||||
3. 已有数据 run_type 回填为 'live'
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0013_predictions_unique_constraint_mode_run_type'
|
||||
down_revision: Union[str, None] = '0012_injuries_partial_unique_and_return_date'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. 新增 run_type 列(先 nullable,回填后再改 NOT NULL)
|
||||
op.add_column(
|
||||
"predictions",
|
||||
sa.Column("run_type", sa.String(10), nullable=True),
|
||||
)
|
||||
|
||||
# 2. 回填已有数据:全部标记为 'live'
|
||||
op.execute("UPDATE predictions SET run_type = 'live' WHERE run_type IS NULL")
|
||||
|
||||
# 3. 改为 NOT NULL
|
||||
op.alter_column("predictions", "run_type", nullable=False)
|
||||
|
||||
# 4. 删除旧唯一约束
|
||||
op.drop_constraint("uq_predictions_match_provider_model", "predictions", type_="unique")
|
||||
|
||||
# 5. 创建新唯一约束(包含 mode + run_type)
|
||||
op.create_unique_constraint(
|
||||
"uq_predictions_match_provider_model_mode_run_type",
|
||||
"predictions",
|
||||
["match_id", "provider", "model", "mode", "run_type"],
|
||||
)
|
||||
|
||||
# 6. 添加 check constraint
|
||||
op.create_check_constraint(
|
||||
"ck_run_type_enum",
|
||||
"predictions",
|
||||
"run_type IN ('live', 'backtest')",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 1. 删除 check constraint
|
||||
op.drop_constraint("ck_run_type_enum", "predictions", type_="check")
|
||||
|
||||
# 2. 删除新唯一约束
|
||||
op.drop_constraint("uq_predictions_match_provider_model_mode_run_type", "predictions", type_="unique")
|
||||
|
||||
# 3. 恢复旧唯一约束
|
||||
op.create_unique_constraint(
|
||||
"uq_predictions_match_provider_model",
|
||||
"predictions",
|
||||
["match_id", "provider", "model"],
|
||||
)
|
||||
|
||||
# 4. 删除 run_type 列
|
||||
op.drop_column("predictions", "run_type")
|
||||
Reference in New Issue
Block a user