fix: reject malformed market snapshot containers

This commit is contained in:
ZhuLinsen
2026-08-29 21:53:39 +08:00
parent 80e06ee062
commit 8adb598c14
3 changed files with 25 additions and 1 deletions

View File

@@ -29,7 +29,7 @@
- red/yellow/green 状态变化输出 `market.<region>.status` - red/yellow/green 状态变化输出 `market.<region>.status`
- change item 的 quality 取 current/previous 两份快照中较差的一侧;任一侧 partial 会降低整个 what_changed 块,任一侧 unavailable 不生成可靠变化项。 - change item 的 quality 取 current/previous 两份快照中较差的一侧;任一侧 partial 会降低整个 what_changed 块,任一侧 unavailable 不生成可靠变化项。
- 没有第二份有效快照时,返回 `previous_completed_snapshot_unavailable`;部分 region 缺基线时整个块标记为 `partial`,并追加 `previous_completed_snapshot_unavailable:<region>`,避免把“缺少基线”误解为“没有变化”。不会临时拉行情或生成一份“当前”快照冒充基线。 - 没有第二份有效快照时,返回 `previous_completed_snapshot_unavailable`;部分 region 缺基线时整个块标记为 `partial`,并追加 `previous_completed_snapshot_unavailable:<region>`,避免把“缺少基线”误解为“没有变化”。不会临时拉行情或生成一份“当前”快照冒充基线。
- 历史详情读取失败,或投影出的 `context_snapshot` 不是 JSON object例如损坏的 JSON 字符串)时,不再把其余更旧记录提升为 current/latest对应快照和变化对比保持不可用并返回 limitation。 - 历史详情读取失败投影出的 `context_snapshot` 不是 JSON object或其中已存在的 `market_light_snapshots` 不是 object例如损坏的 JSON 字符串/数组)时,不再把其余更旧记录提升为 current/latest对应快照和变化对比保持不可用并返回 limitation。
- 快照校验失败时仍保留其 `trade_date` 的目标位置:最新日期无有效快照时不回退到更旧 current最近更早日期无有效快照时不越过它继续寻找更旧 previous。同一目标日期存在其他有效重跑快照时仍可使用该日期的有效版本。 - 快照校验失败时仍保留其 `trade_date` 的目标位置:最新日期无有效快照时不回退到更旧 current最近更早日期无有效快照时不越过它继续寻找更旧 previous。同一目标日期存在其他有效重跑快照时仍可使用该日期的有效版本。
- 外层市场键必须与快照内部 `region` 一致;例如 `cn` 键下的 `region=us` 快照会按无效快照处理,不能进入 A 股的 current/previous 或变化项。 - 外层市场键必须与快照内部 `region` 一致;例如 `cn` 键下的 `region=us` 快照会按无效快照处理,不能进入 A 股的 current/previous 或变化项。
- 历史扫描被截断等 market limitations 会同步进入 what_changed剩余快照不能在来源历史不完整时被标记为 fresh。 - 历史扫描被截断等 market limitations 会同步进入 what_changed剩余快照不能在来源历史不完整时被标记为 fresh。

View File

@@ -88,6 +88,10 @@ class DashboardOverviewService:
if not isinstance(context_snapshot, dict): if not isinstance(context_snapshot, dict):
detail_failure_count += 1 detail_failure_count += 1
continue continue
snapshot_container = context_snapshot.get("market_light_snapshots")
if snapshot_container is not None and not isinstance(snapshot_container, dict):
detail_failure_count += 1
continue
raw_snapshots = self._extract_snapshots(context_snapshot) raw_snapshots = self._extract_snapshots(context_snapshot)
for region, raw_snapshot in raw_snapshots.items(): for region, raw_snapshot in raw_snapshots.items():
raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip() raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip()

View File

@@ -296,6 +296,26 @@ def test_malformed_projected_context_does_not_promote_older_snapshots() -> None:
assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"] assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"]
def test_malformed_nested_snapshot_container_does_not_promote_older_snapshots() -> None:
dependencies = _dependencies()
original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect
def detail_with_malformed_container(record_id: int) -> dict:
if record_id == 1:
return {"context_snapshot": {"market_light_snapshots": "{invalid-json"}}
return original_detail(record_id)
dependencies["history_service"].get_history_detail_by_id.side_effect = detail_with_malformed_container
payload = DashboardOverviewService(**dependencies).get_overview()
assert payload["market"]["data"]["latest_snapshots"] == {}
assert payload["what_changed"]["data"]["current_trade_dates"] == {}
assert payload["what_changed"]["data"]["items"] == []
assert "market_review_detail_partial" in payload["market"]["meta"]["limitations"]
assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"]
def test_invalid_latest_snapshot_does_not_promote_an_older_trade_date() -> None: def test_invalid_latest_snapshot_does_not_promote_an_older_trade_date() -> None:
dependencies = _dependencies() dependencies = _dependencies()
reviews = [ reviews = [