import asyncio import json import logging from datetime import datetime import httpx from yuxi.channel.extensions.alipay.constants import ( ALIPAY_CHARSET, ALIPAY_FORMAT, ALIPAY_RETRY_BASE_DELAY, ALIPAY_RETRY_MAX_ATTEMPTS, ALIPAY_SIGN_TYPE, ALIPAY_SUCCESS_CODE, ALIPAY_VERSION, ) from yuxi.channel.extensions.alipay.crypto import AlipayCrypto from yuxi.channel.extensions.alipay.errors import ( AlipayError, AlipayErrorCode, classify_alipay_error, ) from yuxi.channel.extensions.alipay.types import AlipayAccount logger = logging.getLogger(__name__) class AlipayGateway: def __init__(self, crypto: AlipayCrypto | None = None): self._crypto = crypto self._clients: dict[str, httpx.AsyncClient] = {} self._lock = asyncio.Lock() async def _get_client(self, account: AlipayAccount) -> httpx.AsyncClient: if account.account_id not in self._clients: self._clients[account.account_id] = httpx.AsyncClient( base_url=account.gateway_url, timeout=httpx.Timeout(10.0), limits=httpx.Limits(max_connections=20, max_keepalive_connections=10), ) return self._clients[account.account_id] async def close(self): for client in self._clients.values(): await client.aclose() self._clients.clear() async def request( self, account: AlipayAccount, method: str, biz_content: dict | None = None, ) -> dict: crypto = self._crypto or AlipayCrypto(account.app_private_key, account.alipay_public_key) params = { "app_id": account.app_id, "method": method, "format": ALIPAY_FORMAT, "charset": ALIPAY_CHARSET, "sign_type": ALIPAY_SIGN_TYPE, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": ALIPAY_VERSION, } if biz_content: params["biz_content"] = json.dumps(biz_content, ensure_ascii=False) params["sign"] = crypto.sign(params) client = await self._get_client(account) last_error = None for attempt in range(ALIPAY_RETRY_MAX_ATTEMPTS): try: resp = await client.post("", data=params) resp.raise_for_status() data = resp.json() response_key = next((k for k in data if k.endswith("_response")), None) if not response_key: raise AlipayError( code=AlipayErrorCode.UNKNOWN, message="无法识别的响应格式", ) body = data[response_key] code = body.get("code", "") if code != ALIPAY_SUCCESS_CODE: sub_code = body.get("sub_code", "") error_code, retryable = classify_alipay_error(code, sub_code) err = AlipayError( code=error_code, message=body.get("msg", ""), sub_code=sub_code, sub_msg=body.get("sub_msg", ""), retryable=retryable, ) if retryable and attempt < ALIPAY_RETRY_MAX_ATTEMPTS - 1: last_error = err delay = ALIPAY_RETRY_BASE_DELAY * (2**attempt) await asyncio.sleep(delay) continue raise err return body except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as e: if attempt < ALIPAY_RETRY_MAX_ATTEMPTS - 1: last_error = AlipayError( code=AlipayErrorCode.NETWORK_ERROR, message=str(e), retryable=True, ) delay = ALIPAY_RETRY_BASE_DELAY * (2**attempt) await asyncio.sleep(delay) continue raise AlipayError( code=AlipayErrorCode.NETWORK_ERROR, message=str(e), ) raise last_error or AlipayError( code=AlipayErrorCode.UNKNOWN, message="请求失败,已达最大重试次数", )