From 2ce8b4b17d5e1a3f972dd8bd0b9ada29502dfeb7 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Fri, 4 Sep 2026 11:53:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(ext-data):=20=E5=86=85=E7=BD=AE=E6=A6=82?= =?UTF-8?q?=E5=BF=B5/=E8=A1=8C=E4=B8=9A=20preset=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=8D=B3=E8=87=AA=E5=8A=A8=E8=81=94=E7=BD=91?= =?UTF-8?q?=E6=8B=89=E5=8F=96=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/ext_presets.py | 13 +++-- backend/tests/test_ext_presets_startup.py | 59 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 backend/tests/test_ext_presets_startup.py diff --git a/backend/app/services/ext_presets.py b/backend/app/services/ext_presets.py index 5f9ac79..7ff77e1 100644 --- a/backend/app/services/ext_presets.py +++ b/backend/app/services/ext_presets.py @@ -1,4 +1,4 @@ -"""内置扩展数据预设 — 概念/行业首次启动自动拉取。 +"""内置扩展数据预设 — 概念/行业启动时只创建配置, 等待用户手动获取 (#199)。 设计原则: - 扩展数据通用逻辑零改动 (ExtConfig / fetch_and_ingest / API / 前端均不动) @@ -55,14 +55,16 @@ def _concept_preset() -> ExtConfig: ExtField("股票简称", "string", "股票简称"), ExtField("所属概念", "string", "所属概念"), ], - description="同花顺概念分类 (首次启动自动拉取, 可在扩展数据页手动更新)", + description="同花顺概念分类 (启动仅创建配置, 在概念/行业页手动获取)", symbol_map={"type": "mapped", "col": "股票代码"}, code_map={"type": "computed", "from": "symbol", "method": "strip_exchange"}, pull=PullConfig( url=_CONCEPT_DATA_URL, method="GET", schedule_minutes=1440, - enabled=True, + # enabled=False: ensure_builtin_presets 承诺启动不拉取, PullScheduler + # 只调度 enabled 配置; 手动获取走 fetch_preset 独立路径不受影响 (#199) + enabled=False, ), ) @@ -84,14 +86,15 @@ def _industry_preset() -> ExtConfig: ExtField("股票简称", "string", "股票简称"), ExtField("所属同花顺行业", "string", "所属同花顺行业"), ], - description="同花顺行业分类 (首次启动自动拉取, 可在扩展数据页手动更新)", + description="同花顺行业分类 (启动仅创建配置, 在概念/行业页手动获取)", symbol_map={"type": "mapped", "col": "股票代码"}, code_map={"type": "computed", "from": "symbol", "method": "strip_exchange"}, pull=PullConfig( url=_INDUSTRY_DATA_URL, method="GET", schedule_minutes=1440, - enabled=True, + # 同概念 preset: 出厂禁用, 避免启动即网络拉取 (#199) + enabled=False, ), ) diff --git a/backend/tests/test_ext_presets_startup.py b/backend/tests/test_ext_presets_startup.py new file mode 100644 index 0000000..49a142a --- /dev/null +++ b/backend/tests/test_ext_presets_startup.py @@ -0,0 +1,59 @@ +"""内置概念/行业 preset 启动不得自动拉取 (#199)。 + +ensure_builtin_presets 的契约是「只创建 config.json, 不拉取数据, 等待用户手动获取」; +但 preset 出厂 PullConfig.enabled=True 会让 PullScheduler.refresh 在启动时立即调度 +_run_loop 并马上执行一次网络拉取 (启用后立即执行一次), 与契约矛盾。 + +回归断言: 内置 preset 出厂 pull.enabled 必须为 False —— scheduler 的 enabled 过滤 +(ext_pull.refresh) 会因此跳过它们; 手动获取走 fetch_preset 独立路径, 不经过本开关 +(该路径行为由 test_ext_preset_dimension_values / test_ext_pull_refresh 覆盖)。 +""" +from __future__ import annotations + +import asyncio +from pathlib import Path + +from app.services.ext_data import ExtConfigStore +from app.services.ext_presets import ( + _concept_preset, + _industry_preset, + ensure_builtin_presets, +) + +_PRESET_IDS = ("ext_gn_ths", "ext_hy_ths") + + +def test_builtin_presets_ship_disabled() -> None: + """出厂 preset 的 pull.enabled 必须为 False, 否则启动即网络拉取 (#199)。""" + for preset in (_concept_preset(), _industry_preset()): + assert preset.pull is not None + assert preset.pull.url, "禁用归禁用, 手动获取仍需 url 配方" + assert preset.pull.enabled is False, f"{preset.id} 启动即自动拉取, 违反启动契约" + + +def test_ensure_builtin_presets_writes_disabled_configs(tmp_path: Path) -> None: + """全新数据目录: 启动只落禁用的 pull 配置, scheduler 扫描后无任务可建。""" + asyncio.run(ensure_builtin_presets(tmp_path)) + + store = ExtConfigStore(tmp_path) + for cid in _PRESET_IDS: + config = store.get(cid) + assert config is not None, f"{cid} 配置未创建" + assert config.pull is not None + assert config.pull.enabled is False + + +def test_ensure_builtin_presets_keeps_existing_user_config(tmp_path: Path) -> None: + """老用户/已存在的配置一律不动: 即使保留 enabled=True 也不被覆盖。""" + asyncio.run(ensure_builtin_presets(tmp_path)) + store = ExtConfigStore(tmp_path) + config = store.get("ext_gn_ths") + assert config is not None and config.pull is not None + config.pull.enabled = True + store.upsert(config) + + asyncio.run(ensure_builtin_presets(tmp_path)) + + refreshed = ExtConfigStore(tmp_path).get("ext_gn_ths") + assert refreshed is not None and refreshed.pull is not None + assert refreshed.pull.enabled is True, "已存在配置被静默改写, 违反「绝不覆盖」原则"