feat(payment): 优化国家货币列表获取逻辑

- 添加内置 fallback 国家/货币列表作为备用方案
- 实现多层缓存策略:内存缓存 -> DB 缓存 -> API 请求
- 改进 API 请求逻辑:先获取国家代码列表,再并发请求各国配置
- 使用线程池并发获取各国货币配置信息
- 增加详细的错误处理和日志记录
- 将缓存时间延长至 7 天并添加缓存回写功能
- 优化响应数据结构和字段提取逻辑
This commit is contained in:
cnlimiter
2026-03-27 19:08:01 +08:00
parent f458f2159c
commit 808d4529f8

View File

@@ -183,47 +183,120 @@ def mark_subscription(account_id: int, request: MarkSubscriptionRequest):
_countries_cache: dict = {} # {"data": [...], "expires_at": float}
def _get_fallback_countries():
"""内置 fallback 国家/货币列表"""
return [
{"country_code": "AU", "currency": "AUD", "country_name": "AU"},
{"country_code": "BR", "currency": "BRL", "country_name": "BR"},
{"country_code": "CA", "currency": "CAD", "country_name": "CA"},
{"country_code": "GB", "currency": "GBP", "country_name": "GB"},
{"country_code": "HK", "currency": "HKD", "country_name": "HK"},
{"country_code": "IN", "currency": "INR", "country_name": "IN"},
{"country_code": "JP", "currency": "JPY", "country_name": "JP"},
{"country_code": "MX", "currency": "MXN", "country_name": "MX"},
{"country_code": "SG", "currency": "SGD", "country_name": "SG"},
{"country_code": "TR", "currency": "TRY", "country_name": "TR"},
{"country_code": "US", "currency": "USD", "country_name": "US"},
]
@router.get("/countries")
def get_checkout_countries():
"""从 ChatGPT checkout 接口获取支持的国家/货币列表(缓存 1 小时"""
"""从 ChatGPT checkout 接口获取支持的国家/货币列表(优先读 DB 缓存,成功后回写"""
import time
import json
import curl_cffi.requests as cffi_requests
from concurrent.futures import ThreadPoolExecutor, as_completed
_DB_CACHE_KEY = "cache.checkout_countries"
now = time.time()
# 1. 内存缓存命中
if _countries_cache.get("expires_at", 0) > now:
return {"success": True, "countries": _countries_cache["data"]}
# 2. 读取 DB 缓存
with get_db() as db:
proxy = get_settings().get_proxy_url(db=db)
db_setting = crud.get_setting(db, _DB_CACHE_KEY)
if db_setting and db_setting.value:
try:
cached = json.loads(db_setting.value)
if cached.get("expires_at", 0) > now:
_countries_cache.update(cached)
return {"success": True, "countries": cached["data"]}
except Exception:
pass
proxies = {"http": proxy, "https": proxy} if proxy else None
# 3. 请求 ChatGPT API 获取国家代码列表
try:
resp = cffi_requests.get(
"https://chatgpt.com/backend-api/checkout_pricing_config/countries",
proxies={"http": proxy, "https": proxy} if proxy else None,
proxies=proxies,
timeout=15,
impersonate="chrome110",
)
resp.raise_for_status()
data = resp.json()
countries = data if isinstance(data, list) else data.get("countries", [])
_countries_cache["data"] = countries
_countries_cache["expires_at"] = now + 3600
return {"success": True, "countries": countries}
raw = resp.json()
country_codes = raw.get("countries", []) if isinstance(raw, dict) else raw
if not isinstance(country_codes, list) or not country_codes:
raise ValueError(f"国家列表为空或格式异常: {str(raw)[:200]}")
except Exception as e:
logger.warning(f"获取国家列表失败: {e}")
fallback = [
{"country_code": "SG", "currency": "SGD", "country_name": "Singapore"},
{"country_code": "US", "currency": "USD", "country_name": "United States"},
{"country_code": "TR", "currency": "TRY", "country_name": "Turkey"},
{"country_code": "JP", "currency": "JPY", "country_name": "Japan"},
{"country_code": "HK", "currency": "HKD", "country_name": "Hong Kong"},
{"country_code": "GB", "currency": "GBP", "country_name": "United Kingdom"},
{"country_code": "AU", "currency": "AUD", "country_name": "Australia"},
{"country_code": "CA", "currency": "CAD", "country_name": "Canada"},
{"country_code": "IN", "currency": "INR", "country_name": "India"},
{"country_code": "BR", "currency": "BRL", "country_name": "Brazil"},
{"country_code": "MX", "currency": "MXN", "country_name": "Mexico"},
]
return {"success": False, "countries": fallback, "error": str(e)}
logger.warning(f"获取国家代码列表失败: {e}")
return {"success": False, "countries": _get_fallback_countries(), "error": str(e)}
# 4. 并发请求各国 configs提取 symbol_code
def fetch_config(code: str):
try:
r = cffi_requests.get(
f"https://chatgpt.com/backend-api/checkout_pricing_config/configs/{code}",
proxies=proxies,
timeout=10,
impersonate="chrome110",
)
if r.status_code == 200:
data = r.json()
cfg = data.get("currency_config", {})
currency = cfg.get("symbol_code") or cfg.get("symbol") or ""
if currency:
return {"country_code": code, "currency": currency, "country_name": code}
except Exception:
pass
return None
countries = []
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(fetch_config, code): code for code in country_codes}
for future in as_completed(futures):
result = future.result()
if result:
countries.append(result)
countries.sort(key=lambda c: c["country_code"])
if not countries:
logger.warning("所有国家 configs 请求均失败,使用 fallback")
return {"success": False, "countries": _get_fallback_countries(), "error": "所有 configs 请求失败"}
# 5. 写入内存缓存 + DB 缓存(缓存 7 天)
expires_at = now + 86400 * 7
cache_payload = {"data": countries, "expires_at": expires_at}
_countries_cache.update(cache_payload)
try:
with get_db() as db:
crud.set_setting(
db,
key=_DB_CACHE_KEY,
value=json.dumps(cache_payload, ensure_ascii=False),
description="checkout 国家/货币列表缓存",
category="cache",
)
except Exception as e:
logger.warning(f"写入 DB 缓存失败(不影响返回结果): {e}")
return {"success": True, "countries": countries}