@@ -183,47 +183,120 @@ def mark_subscription(account_id: int, request: MarkSubscriptionRequest):
183183_countries_cache : dict = {} # {"data": [...], "expires_at": float}
184184
185185
186+ def _get_fallback_countries ():
187+ """内置 fallback 国家/货币列表"""
188+ return [
189+ {"country_code" : "AU" , "currency" : "AUD" , "country_name" : "AU" },
190+ {"country_code" : "BR" , "currency" : "BRL" , "country_name" : "BR" },
191+ {"country_code" : "CA" , "currency" : "CAD" , "country_name" : "CA" },
192+ {"country_code" : "GB" , "currency" : "GBP" , "country_name" : "GB" },
193+ {"country_code" : "HK" , "currency" : "HKD" , "country_name" : "HK" },
194+ {"country_code" : "IN" , "currency" : "INR" , "country_name" : "IN" },
195+ {"country_code" : "JP" , "currency" : "JPY" , "country_name" : "JP" },
196+ {"country_code" : "MX" , "currency" : "MXN" , "country_name" : "MX" },
197+ {"country_code" : "SG" , "currency" : "SGD" , "country_name" : "SG" },
198+ {"country_code" : "TR" , "currency" : "TRY" , "country_name" : "TR" },
199+ {"country_code" : "US" , "currency" : "USD" , "country_name" : "US" },
200+ ]
201+
202+
186203@router .get ("/countries" )
187204def get_checkout_countries ():
188- """从 ChatGPT checkout 接口获取支持的国家/货币列表(缓存 1 小时 )"""
205+ """从 ChatGPT checkout 接口获取支持的国家/货币列表(优先读 DB 缓存,成功后回写 )"""
189206 import time
207+ import json
190208 import curl_cffi .requests as cffi_requests
209+ from concurrent .futures import ThreadPoolExecutor , as_completed
191210
211+ _DB_CACHE_KEY = "cache.checkout_countries"
192212 now = time .time ()
213+
214+ # 1. 内存缓存命中
193215 if _countries_cache .get ("expires_at" , 0 ) > now :
194216 return {"success" : True , "countries" : _countries_cache ["data" ]}
195217
218+ # 2. 读取 DB 缓存
196219 with get_db () as db :
197220 proxy = get_settings ().get_proxy_url (db = db )
221+ db_setting = crud .get_setting (db , _DB_CACHE_KEY )
222+ if db_setting and db_setting .value :
223+ try :
224+ cached = json .loads (db_setting .value )
225+ if cached .get ("expires_at" , 0 ) > now :
226+ _countries_cache .update (cached )
227+ return {"success" : True , "countries" : cached ["data" ]}
228+ except Exception :
229+ pass
230+
231+ proxies = {"http" : proxy , "https" : proxy } if proxy else None
198232
233+ # 3. 请求 ChatGPT API 获取国家代码列表
199234 try :
200235 resp = cffi_requests .get (
201236 "https://chatgpt.com/backend-api/checkout_pricing_config/countries" ,
202- proxies = { "http" : proxy , "https" : proxy } if proxy else None ,
237+ proxies = proxies ,
203238 timeout = 15 ,
204239 impersonate = "chrome110" ,
205240 )
206241 resp .raise_for_status ()
207- data = resp .json ()
208- countries = data if isinstance (data , list ) else data .get ("countries" , [])
209- _countries_cache ["data" ] = countries
210- _countries_cache ["expires_at" ] = now + 3600
211- return {"success" : True , "countries" : countries }
242+ raw = resp .json ()
243+ country_codes = raw .get ("countries" , []) if isinstance (raw , dict ) else raw
244+ if not isinstance (country_codes , list ) or not country_codes :
245+ raise ValueError (f"国家列表为空或格式异常: { str (raw )[:200 ]} " )
246+ except Exception as e :
247+ logger .warning (f"获取国家代码列表失败: { e } " )
248+ return {"success" : False , "countries" : _get_fallback_countries (), "error" : str (e )}
249+
250+ # 4. 并发请求各国 configs,提取 symbol_code
251+ def fetch_config (code : str ):
252+ try :
253+ r = cffi_requests .get (
254+ f"https://chatgpt.com/backend-api/checkout_pricing_config/configs/{ code } " ,
255+ proxies = proxies ,
256+ timeout = 10 ,
257+ impersonate = "chrome110" ,
258+ )
259+ if r .status_code == 200 :
260+ data = r .json ()
261+ cfg = data .get ("currency_config" , {})
262+ currency = cfg .get ("symbol_code" ) or cfg .get ("symbol" ) or ""
263+ if currency :
264+ return {"country_code" : code , "currency" : currency , "country_name" : code }
265+ except Exception :
266+ pass
267+ return None
268+
269+ countries = []
270+ with ThreadPoolExecutor (max_workers = 10 ) as executor :
271+ futures = {executor .submit (fetch_config , code ): code for code in country_codes }
272+ for future in as_completed (futures ):
273+ result = future .result ()
274+ if result :
275+ countries .append (result )
276+
277+ countries .sort (key = lambda c : c ["country_code" ])
278+
279+ if not countries :
280+ logger .warning ("所有国家 configs 请求均失败,使用 fallback" )
281+ return {"success" : False , "countries" : _get_fallback_countries (), "error" : "所有 configs 请求失败" }
282+
283+ # 5. 写入内存缓存 + DB 缓存(缓存 7 天)
284+ expires_at = now + 86400 * 7
285+ cache_payload = {"data" : countries , "expires_at" : expires_at }
286+ _countries_cache .update (cache_payload )
287+
288+ try :
289+ with get_db () as db :
290+ crud .set_setting (
291+ db ,
292+ key = _DB_CACHE_KEY ,
293+ value = json .dumps (cache_payload , ensure_ascii = False ),
294+ description = "checkout 国家/货币列表缓存" ,
295+ category = "cache" ,
296+ )
212297 except Exception as e :
213- logger .warning (f"获取国家列表失败: { e } " )
214- fallback = [
215- {"country_code" : "SG" , "currency" : "SGD" , "country_name" : "Singapore" },
216- {"country_code" : "US" , "currency" : "USD" , "country_name" : "United States" },
217- {"country_code" : "TR" , "currency" : "TRY" , "country_name" : "Turkey" },
218- {"country_code" : "JP" , "currency" : "JPY" , "country_name" : "Japan" },
219- {"country_code" : "HK" , "currency" : "HKD" , "country_name" : "Hong Kong" },
220- {"country_code" : "GB" , "currency" : "GBP" , "country_name" : "United Kingdom" },
221- {"country_code" : "AU" , "currency" : "AUD" , "country_name" : "Australia" },
222- {"country_code" : "CA" , "currency" : "CAD" , "country_name" : "Canada" },
223- {"country_code" : "IN" , "currency" : "INR" , "country_name" : "India" },
224- {"country_code" : "BR" , "currency" : "BRL" , "country_name" : "Brazil" },
225- {"country_code" : "MX" , "currency" : "MXN" , "country_name" : "Mexico" },
226- ]
227- return {"success" : False , "countries" : fallback , "error" : str (e )}
298+ logger .warning (f"写入 DB 缓存失败(不影响返回结果): { e } " )
299+
300+ return {"success" : True , "countries" : countries }
228301
229302
0 commit comments