From bf4f571f0eeeb162e25761646128d11bf17c9677 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sat, 4 Jul 2026 11:15:17 +0800 Subject: [PATCH] fix(cache): remove redundant filtering and avoid mutating StorageManager results --- app/services/cache.py | 13 +++++--- tests/test_cache_storage_integration.py | 41 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/app/services/cache.py b/app/services/cache.py index 3cd9d6e..4aa67e0 100644 --- a/app/services/cache.py +++ b/app/services/cache.py @@ -1,6 +1,7 @@ """ 缓存服务 - SQLite 数据库操作 """ +import copy import json import logging from datetime import datetime, timedelta @@ -295,6 +296,8 @@ def save_market_data(db: Session, symbol: str, data: Dict) -> Optional[MarketDat try: if storage.save_market_data(symbol, data): storage.save_symbol_timestamp(symbol, data_type, now) + # 注意:StorageManager 命中时返回的是内存构造的代表性 MarketData, + # 不代表 SQLite 持久化记录,因此没有 SQLite 自增 id。 return _representative_market_data(symbol, data, now) else: raise Exception("save_market_data returned False") @@ -391,6 +394,8 @@ def get_latest_cached( candle_count=len(candles), ) ) + # 注意:StorageManager 命中时返回的是内存构造的 MarketData 列表, + # 不代表 SQLite 持久化记录,因此没有 SQLite 自增 id。 return records except Exception as e: logger.warning(f"StorageManager 最新缓存读取失败,降级到 SQLite: {e}") @@ -431,11 +436,9 @@ def get_cached_data( symbol, data_type, periods, end_time=end_time, max_candles=max_candles ) if merged and merged.get("timeframes"): - for period, candles in merged["timeframes"].items(): - merged["timeframes"][period] = _filter_candles( - candles, end_time, max_candles - ) - return merged + # StorageManager 已负责 end_time / max_candles 过滤, + # 这里深拷贝后返回,避免原地修改 StorageManager 内部对象。 + return copy.deepcopy(merged) except Exception as e: logger.warning(f"StorageManager 读取失败,降级到 SQLite: {e}") diff --git a/tests/test_cache_storage_integration.py b/tests/test_cache_storage_integration.py index fffd73c..f354752 100644 --- a/tests/test_cache_storage_integration.py +++ b/tests/test_cache_storage_integration.py @@ -246,8 +246,8 @@ class TestGetCachedData: "AG2606", "futures", ["5min"], end_time=end_time, max_candles=50 ) - def test_applies_end_time_and_max_candles_to_storage_results(self, db, storage_manager_mock): - """StorageManager 返回的数据会按 end_time / max_candles 过滤截断。""" + def test_does_not_refilter_storage_manager_results(self, db, storage_manager_mock): + """cache.py 不再对 StorageManager 返回的数据做二次过滤,直接原样返回。""" fetched_at = datetime.now() def mock_get_market_data(symbol, data_type, periods, end_time=None, max_candles=100): @@ -280,9 +280,40 @@ class TestGetCachedData: assert result is not None candles = result["timeframes"]["5min"] - # 过滤掉 10:05 之后保留 09:50/09:55,再截断为最新 1 条 - assert len(candles) == 1 - assert candles[0]["datetime"] == "2026-07-04T09:55:00" + # StorageManager 未做过滤,cache.py 也不应再次过滤 + assert len(candles) == 3 + + def test_does_not_mutate_storage_manager_result(self, db, storage_manager_mock): + """get_cached_data 返回 StorageManager 结果时不会原地修改原对象。""" + fetched_at = datetime.now() + original = { + "symbol": "AG2606", + "type": "futures", + "current_price": 123.45, + "timestamp": fetched_at.isoformat(), + "fetched_at": fetched_at.isoformat(), + "timeframes": { + "5min": [ + {"datetime": "2026-07-04T09:55:00", "open": 100, "close": 110} + ] + }, + "is_fresh": True, + } + storage_manager_mock.get_market_data.return_value = original + + with patch("app.services.cache.get_storage_manager", return_value=storage_manager_mock): + from app.services import cache + + result = cache.get_cached_data(db, "AG2606", "futures", ["5min"]) + + assert result is not None + assert result is not original + assert result["timeframes"]["5min"] == original["timeframes"]["5min"] + # 修改返回结果不应影响 StorageManager 返回的原对象 + result["timeframes"]["5min"].append( + {"datetime": "2026-07-04T10:00:00", "open": 111, "close": 112} + ) + assert len(original["timeframes"]["5min"]) == 1 def test_queries_all_periods_when_periods_is_none(self, db, storage_manager_mock): """periods=None 时优先走 StorageManager 查询全部周期。"""