fix(P1-G/H/I/K): 运行时配置解密/Redis JSON/eval summary/UPSERT
P1-G runtime_config:DB 故障回落 env;解密失败在生产环境抛出(ValueError), 非生产回落;DB 异常与解密异常不再共用 except Exception。 P1-H Redis 预测缓存:去掉 pickle 改用 JSON + dataclasses.asdict, datetime 字段 ISO 序列化。 P1-I eval get_eval_summary:默认 limit=None(不冒充全集);新增 run_type(默认 live) 与 season 过滤;_build_filters 同步扩展。 P1-K Team/League get_or_create:INSERT 改为 PG UPSERT(ON CONFLICT DO NOTHING) 防并发重复插入。 测试 test_p1_g_runtime_config(4/4) + test_p1_k_upsert(4/4);全量绿。
This commit is contained in:
+20
-4
@@ -44,9 +44,17 @@ def _build_filters(
|
||||
prompt_version: str | None = None,
|
||||
mode: str | None = None,
|
||||
league_code: str | None = None,
|
||||
run_type: str | None = "live",
|
||||
season: str | None = None,
|
||||
) -> list:
|
||||
"""构建评估筛选条件(参数化列明,防拼接注入)。"""
|
||||
"""构建评估筛选条件(参数化列明,防拼接注入)。
|
||||
|
||||
P1-I: 默认仅 run_type=live(排除回测污染),可显式改 "backtest"/None。
|
||||
"""
|
||||
filters = [Prediction.settled == True]
|
||||
# P1-I: 默认仅统计实盘预测(排除回测),除非显式指定
|
||||
if run_type is not None:
|
||||
filters.append(Prediction.run_type == run_type)
|
||||
if provider:
|
||||
filters.append(Prediction.provider == provider)
|
||||
if model:
|
||||
@@ -55,6 +63,10 @@ def _build_filters(
|
||||
filters.append(Prediction.prompt_version == prompt_version)
|
||||
if mode:
|
||||
filters.append(Prediction.mode == mode)
|
||||
# P1-I: 赛季过滤
|
||||
if season:
|
||||
season_subq = select(Match.id).where(Match.season == season).scalar_subquery()
|
||||
filters.append(Prediction.match_id.in_(season_subq))
|
||||
if league_code:
|
||||
league_subq = select(League.id).where(League.code == league_code).scalar_subquery()
|
||||
filters.append(Prediction.match_id.in_(
|
||||
@@ -64,17 +76,21 @@ def _build_filters(
|
||||
|
||||
|
||||
async def get_eval_summary(
|
||||
limit: int = 1000,
|
||||
limit: int | None = None,
|
||||
*,
|
||||
provider: str | None = None,
|
||||
model: str | None = None,
|
||||
prompt_version: str | None = None,
|
||||
mode: str | None = None,
|
||||
league_code: str | None = None,
|
||||
run_type: str | None = "live",
|
||||
season: str | None = None,
|
||||
) -> dict:
|
||||
"""按 provider × 模型聚合评估。
|
||||
|
||||
P3-4: 默认限制评估最近 1000 条已结算预测,避免全表加载导致内存压力。
|
||||
P1-I: 默认 limit=None(返回全集,不冒充 1000 条为全集);调用方可显式 limit 采样。
|
||||
默认仅 run_type=live(排除回测污染),可按需改 "backtest"。
|
||||
可选 season 过滤。
|
||||
|
||||
只统计有效预测:
|
||||
- settled == True
|
||||
@@ -82,7 +98,7 @@ async def get_eval_summary(
|
||||
- 预测比分字段齐全
|
||||
degraded 或无比分的预测不计入准确率。
|
||||
"""
|
||||
filters = _build_filters(provider, model, prompt_version, mode, league_code)
|
||||
filters = _build_filters(provider, model, prompt_version, mode, league_code, run_type=run_type, season=season)
|
||||
|
||||
async with get_uow() as session:
|
||||
total_settled = (await session.execute(
|
||||
|
||||
+12
-5
@@ -126,12 +126,14 @@ class _RedisCache(_CacheBackend):
|
||||
if not await self._ensure_conn():
|
||||
return self._memory_fallback.get(key)
|
||||
try:
|
||||
import pickle
|
||||
import json
|
||||
|
||||
raw = await self._redis.get(key) # type: ignore[union-attr]
|
||||
if raw is None:
|
||||
return None
|
||||
return pickle.loads(raw.encode("latin-1")) if isinstance(raw, str) else pickle.loads(raw)
|
||||
# P1-H: JSON 序列化(替代 pickle,跨语言安全 + 可人工阅读)
|
||||
data = json.loads(raw)
|
||||
return PredictResult(**data)
|
||||
except Exception as e:
|
||||
logger.warning("predict cache: Redis GET 失败(%s),跳过缓存", e)
|
||||
return None
|
||||
@@ -141,10 +143,15 @@ class _RedisCache(_CacheBackend):
|
||||
await self._memory_fallback.set(key, result, ttl)
|
||||
return
|
||||
try:
|
||||
import pickle
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
|
||||
payload = pickle.dumps(result).decode("latin-1")
|
||||
await self._redis.set(key, payload, ex=ttl) # type: ignore[union-attr]
|
||||
payload = asdict(result)
|
||||
# JSON 序列化;处理 datetime → ISO 字符串
|
||||
for k, v in payload.items():
|
||||
if hasattr(v, "isoformat"):
|
||||
payload[k] = v.isoformat()
|
||||
await self._redis.set(key, json.dumps(payload, default=str), ex=ttl) # type: ignore[union-attr]
|
||||
except Exception as e:
|
||||
logger.warning("predict cache: Redis SET 失败(%s),降级内存写入", e)
|
||||
await self._memory_fallback.set(key, result, ttl)
|
||||
|
||||
Reference in New Issue
Block a user