"""汇率服务:CNY→RUB。数据源三级降级(FloatRates → 俄央行 → 兜底),服务端缓存。""" from __future__ import annotations import time import httpx _FALLBACK_RATE = 11.5 _MIN, _MAX = 5.0, 25.0 _CACHE_TTL = 3600 # 秒 _cache: dict = {"rate": None, "source": "", "ts": 0.0} def _valid(rate: float) -> bool: return _MIN <= rate <= _MAX async def _fetch_floatrates() -> float | None: async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: resp = await client.get("https://www.floatrates.com/daily/cny.json") resp.raise_for_status() rub = resp.json().get("rub", {}) rate = rub.get("rate") return float(rate) if rate else None async def _fetch_cbr() -> float | None: async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: resp = await client.get("https://www.cbr-xml-daily.ru/daily_json.js") resp.raise_for_status() cny = resp.json().get("Valute", {}).get("CNY", {}) value = cny.get("Value") return float(value) if value else None async def get_fx_rate() -> dict: """返回 {rate, source, updated_at}。带 1 小时内存缓存。""" now = time.time() if _cache["rate"] and (now - _cache["ts"]) < _CACHE_TTL: return dict(_cache) rate = None source = "" for name, fn in (("floatrates", _fetch_floatrates), ("cbr", _fetch_cbr)): try: r = await fn() if r is not None and _valid(r): rate, source = r, name break except Exception: # noqa: BLE001 - 数据源失败降级 continue if rate is None: rate, source = _FALLBACK_RATE, "fallback" _cache.update({"rate": rate, "source": source, "ts": now}) return dict(_cache)