fix(HEL-494): 盘中双免费源故障切换并禁止问天假0覆盖

主源东财失败后自动改走腾讯行情,成功结果写入缓存;两源都失败时返回最近真实快照并标明延迟,不再显示假0。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 17:38:38 +08:00
co-authored by Cursor multica-agent
parent b5d65ecb41
commit ef13d6feb5
13 changed files with 588 additions and 92 deletions
@@ -232,9 +232,57 @@ class MarketQuotesTests(unittest.TestCase):
self.assertEqual(mocked.return_value.fetch_quotes.call_count, 2)
self.assertEqual(len(payload["data"]), 70)
def test_named_quotes_fail_over_to_tencent(self) -> None:
named = [
{
"ts_code": "000737.SZ",
"name": "北方铜业",
"close": 12.3,
"pre_close": 11.2,
}
]
with patch("datahub.realtime_serve.EastmoneyAdapter") as eastmoney, patch(
"datahub.realtime_serve.TencentAdapter"
) as tencent:
eastmoney.return_value.fetch_quotes.side_effect = AdapterError("HTTP 503")
tencent.return_value.fetch_quotes.return_value = named
payload = self.api.handle("/v1/quotes/latest", {"codes": ["000737.SZ"]})
self.assertEqual(payload["data"][0]["ts_code"], "000737.SZ")
self.assertEqual(payload["meta"]["source"], "tencent:qt")
self.assertTrue(payload["meta"]["failover"])
self.assertFalse(payload["meta"]["stale"])
def test_both_quote_sources_return_last_known_good(self) -> None:
from datahub.realtime_serve import _envelope, _write_cache
cache_key = "quotes:placeholder:1"
payload = _envelope(
[{"ts_code": "000737.SZ", "close": 12.3, "pre_close": 11.2}],
{"source": "eastmoney:ulist", "stale": False, "trade_date": "20260908"},
)
_write_cache(self.db, cache_key, payload, 60, "eastmoney:ulist")
self.db.execute(
"UPDATE rt_cache SET expires_at = ? WHERE cache_key = ?",
("2000-01-01T00:00:00+08:00", cache_key),
)
with patch("datahub.realtime_serve.EastmoneyAdapter") as eastmoney, patch(
"datahub.realtime_serve.TencentAdapter"
) as tencent, patch("datahub.realtime_serve.hashlib.sha1") as sha1:
eastmoney.return_value.fetch_quotes.side_effect = AdapterError("HTTP 503")
tencent.return_value.fetch_quotes.side_effect = AdapterError("timeout")
sha1.return_value.hexdigest.return_value = "placeholder"
recovered = self.api.handle("/v1/quotes/latest", {"codes": ["000737.SZ"]})
self.assertTrue(recovered["meta"]["stale"])
self.assertIn("真实快照", recovered["meta"]["delay_notice"])
self.assertEqual(recovered["data"][0]["close"], 12.3)
self.assertNotEqual(recovered["data"][0]["close"], 0)
def test_market_unavailable_stays_source_error(self) -> None:
with patch("datahub.realtime_serve.EastmoneyAdapter") as mocked:
mocked.return_value.fetch_market_quotes.side_effect = AdapterError("too small")
with patch("datahub.realtime_serve.EastmoneyAdapter") as eastmoney, patch(
"datahub.realtime_serve.TencentAdapter"
) as tencent:
eastmoney.return_value.fetch_market_quotes.side_effect = AdapterError("too small")
tencent.return_value.fetch_quotes.side_effect = AdapterError("empty master")
with self.assertRaises(ApiError) as ctx:
self.api.handle("/v1/quotes/latest", {})
self.assertEqual(ctx.exception.code, "SOURCE_UNAVAILABLE")