ForcePilot/backend/package/yuxi/channel/extensions/alipay/gateway.py
Kris 71364ea579 feat(alipay): 新增支付宝渠道插件完整实现
实现了支付宝生活号渠道的完整功能,包括消息接收回调、发送、安全验证、去重、配对绑定、流式回复支持等完整能力。
2026-05-21 10:39:23 +08:00

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