diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 9be844c..a78ed30 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -932,6 +932,16 @@ def update_realtime_quotes(req: RealtimeQuotesPrefs, request: Request) -> dict: qs.disable() _sync_depth_polling(False) return {"realtime_quotes_enabled": False, "realtime_allowed": False} + if req.realtime_quotes_enabled: + # 首用门禁: 本地完全无数据 (日K/enriched 均空, 与看板首用弹窗同口径) 时 + # 禁止开启 — 实时行情的展示 (enriched 底座 + 快照覆盖)、监控、连板梯队 + # 全部依赖本地数据, 空数据下轮询只是空转耗配额, 且无 instruments 维表。 + repo = getattr(request.app.state, "repo", None) + if repo is not None and repo.latest_daily_date() is None and repo.latest_enriched_date() is None: + raise HTTPException( + status_code=409, + detail="本地暂无行情数据,请先在「数据」页同步后再开启实时行情", + ) if req.realtime_quotes_enabled and qs and qs.is_paused(): # 管道/数据修正运行期间禁止开启实时行情 — 防止写盘竞态 raise HTTPException(status_code=409, detail="数据同步运行中,实时行情已临时暂停,请稍后再开启") diff --git a/backend/tests/test_data_integrity.py b/backend/tests/test_data_integrity.py index 74821ca..f9e43c0 100644 --- a/backend/tests/test_data_integrity.py +++ b/backend/tests/test_data_integrity.py @@ -227,14 +227,19 @@ def test_describe_and_issue_dataclass(): # ── 开实时行情门禁 (钩子2) ────────────────────────────────────────── -def _gate_state(tmp_path, quote_service, repo): +def _gate_state(tmp_path, quote_service, repo, *, has_data=True): from types import SimpleNamespace return SimpleNamespace( app=SimpleNamespace(state=SimpleNamespace( quote_service=quote_service, depth_service=None, - repo=SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path)), + repo=SimpleNamespace( + store=SimpleNamespace(data_dir=tmp_path), + # 首用门禁判据: 日K/enriched 最近日期 (None = 本地无数据) + latest_daily_date=lambda: date(2026, 8, 28) if has_data else None, + latest_enriched_date=lambda: None, + ), capabilities=None, )) ) @@ -298,6 +303,30 @@ def test_realtime_gate_blocks_on_snapshot_and_launches_repair(tmp_path, monkeypa assert saved == {} +def test_realtime_gate_blocks_when_no_local_data(tmp_path, monkeypatch): + """首用门禁: 日K/enriched 均无数据时禁止开启实时行情 (409 + 同步指引), + 且不落偏好 (开关不生效)。""" + from fastapi import HTTPException + + from app.api import settings as settings_api + + saved = {} + monkeypatch.setattr( + "app.services.preferences.save", lambda payload: saved.update(payload), + ) + + qs = _QuoteServiceStub() + request = _gate_state(tmp_path, qs, repo=None, has_data=False) + req = settings_api.RealtimeQuotesPrefs(realtime_quotes_enabled=True) + + with pytest.raises(HTTPException) as exc_info: + settings_api.update_realtime_quotes(req, request) + + assert exc_info.value.status_code == 409 + assert "同步" in exc_info.value.detail + assert saved == {} # 未开启 + + def test_realtime_gate_allows_clean_data(tmp_path, monkeypatch): from app.api import settings as settings_api