fix(critical): 恢复 bzzoiro events 采集管线,消除数据源注册表静默失效

C2 数据丢失修复。重构提交 6a49940 删除了 src/data/bzzoiro.py 中的
fetch_bzzoiro_events 与 BzzoiroSource 后未做替换,同时 sources.py 用
`try/except Exception: pass` 吞掉了 ImportError,导致:

  - _SOURCES 注册表恒为空
  - get_source("bzzoiro") 恒抛 ValueError("未知数据源: bzzoiro")
  - src/api/routes/ingest.py 与 schedules.py 运行时全线失效,
    且日志中无任何导入错误痕迹

修复内容:
1. 从 f05dc1a 恢复并移植 events 管线:
   - fetch_bzzoiro_events():分页抓取 /events/(纯异步,复用现有 _fetch_json_async)
   - @register class BzzoiroSource + ingest():保留原签名
     (db, *, leagues, date_from, date_to, status),保持「不 commit,
     事务由调用方 UnitOfWork 控制」的契约,并保留单联赛抓取失败的
     错误隔离(记录 error 后 continue)
   - 复用现有 _to_date / _to_int_or_none / _match_key,未重复定义
   - 血缘字段继续使用与 nm 配对的 raw(避免 P0-3 回归)
2. sources.py:导入失败改为 logger.exception 记录,新增 _loaded 标记
   保证 _load_sources() 至多执行一次;失败时不置位以便后续重试。
   get_source() 对真正未知的名字仍抛 ValueError。

验证:
   - 新增 tests/test_bzzoiro_source_registry.py(8 项):注册表非空、
     get_source/list_sources 可用、ingest 签名契约、不再静默吞异常
   - test_regressions.py:3 failed -> 2 failed
   - 全量:14 failed -> 13 failed,通过数 190 -> 199

注意事项:未改动任何 caller(routes/ingest.py、routes/schedules.py 原样
可用,证明签名保持正确)。
This commit is contained in:
shangfangjian
2026-09-21 17:10:17 +08:00
parent 8c87b9d024
commit 9ccda4bf51
3 changed files with 410 additions and 13 deletions
+27 -11
View File
@@ -8,10 +8,13 @@
"""
from __future__ import annotations
import logging
from typing import Protocol
from src.db.base import AsyncSession
logger = logging.getLogger(__name__)
class DataSource(Protocol):
"""比赛数据源契约:抓取 → 规范化 → 入库。"""
@@ -29,6 +32,9 @@ class DataSource(Protocol):
# ── 注册表 ──
_SOURCES: dict[str, DataSource] = {}
# 延迟加载标记:保证 _load_sources() 最多执行一次,重复调用廉价
_loaded = False
def register(source):
"""装饰器:将数据源注册到全局注册表。
@@ -42,8 +48,7 @@ def register(source):
def get_source(name: str) -> DataSource:
"""按名获取数据源。"""
if not _SOURCES:
_load_sources()
_load_sources()
if name not in _SOURCES:
raise ValueError(f"未知数据源: {name}")
return _SOURCES[name]
@@ -51,18 +56,29 @@ def get_source(name: str) -> DataSource:
def list_sources() -> list[str]:
"""列出所有已注册数据源名。"""
if not _SOURCES:
_load_sources()
_load_sources()
return list(_SOURCES.keys())
def _load_sources() -> None:
"""延迟导入数据源触发 @register(避免循环导入)。"""
from src.data.bzzoiro import BzzoiroSource # noqa: F811
"""延迟导入数据源触发 @register(避免循环导入)。
导入失败必须留下痕迹:静默吞掉 ImportError 会让注册表恒为空,
导致 get_source() 对所有数据源都报「未知数据源」,把导入错误
伪装成「不存在的名字」—— 这类静默失效极难定位,故在此显式记日志。
"""
global _loaded
if _loaded:
return
try:
from src.data.bzzoiro import BzzoiroSource # noqa: F401
except Exception:
logger.exception(
"数据源模块导入失败,注册表将为空 —— get_source() 会对所有名字报「未知数据源」"
)
return # 保持 _loaded=False,下次调用可重试
_loaded = True
# 保持向后兼容:模块加载时尝试加载(但不再强制)
try:
_load_sources()
except Exception:
pass
# 模块加载时预热(失败会记录日志,不再静默)
_load_sources()