125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
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="请求失败,已达最大重试次数",
|
|
)
|