"""模块:支付宝适配器 实现支付宝 APP/网页/小程序 支付适配。 支付宝开放平台 API,密钥在实例化时传入。""" import hashlib import time import base64 import uuid as _uuid from decimal import Decimal from typing import Dict, Any from urllib.parse import quote from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, utils from gvsdsdk.payment.gateways.base import ( AbstractPaymentGateway, PaymentCredentials, PaymentRequest, PaymentResponse, RefundRequest, RefundResponse, TransferRequest, TransferResponse, QueryResponse, ) class AlipayPaymentGateway(AbstractPaymentGateway): """支付宝支付适配器""" Channel = 'alipay' Name = '支付宝' ALIPAY_API_BASE = 'https://openapi.alipay.com/gateway.do' def _validate_credentials(self): if not self._credentials.AppID: raise ValueError('支付宝需要 AppID') if not self._credentials.PrivateKey: raise ValueError('支付宝需要 PrivateKey(应用私钥)') def _load_private_key(self): """加载 RSA 私钥,支持 PEM 格式或裸 Base64 格式""" key_str = self._credentials.PrivateKey if isinstance(key_str, bytes): key_str = key_str.decode('utf-8') # 如果已经是 PEM 格式,直接加载 if '-----BEGIN' in key_str: return serialization.load_pem_private_key( key_str.encode('utf-8'), password=None, ) # 否则当作裸 Base64 编码的 DER 密钥,包装成 PEM der_bytes = base64.b64decode(key_str) pem_str = serialization.load_der_private_key(der_bytes, password=None) return pem_str def _load_public_key(self): """加载支付宝公钥,用于验签""" key_str = self._credentials.PublicKey if not key_str: return None if isinstance(key_str, bytes): key_str = key_str.decode('utf-8') if '-----BEGIN' in key_str: return serialization.load_pem_public_key(key_str.encode('utf-8')) der_bytes = base64.b64decode(key_str) return serialization.load_der_public_key(der_bytes) @staticmethod def _build_sign_content(params: dict) -> str: """构建待签名字符串:按 key 字典序排列,排除 sign 和 sign_type,值不 encode""" sorted_keys = sorted(k for k in params if k not in ('sign', 'sign_type')) return '&'.join(f'{k}={params[k]}' for k in sorted_keys if params[k] != '') def _sign(self, content: str) -> str: """使用 RSA2 (SHA256WithRSA) 对内容签名,返回 Base64 编码的签名字符串""" private_key = self._load_private_key() signature = private_key.sign( content.encode('utf-8'), padding.PKCS1v15(), hashes.SHA256(), ) return base64.b64encode(signature).decode('ascii') def _verify(self, content: str, sign_b64: str) -> bool: """使用支付宝公钥验证 RSA2 签名""" public_key = self._load_public_key() if public_key is None: return False try: signature = base64.b64decode(sign_b64) public_key.verify( signature, content.encode('utf-8'), padding.PKCS1v15(), hashes.SHA256(), ) return True except Exception: return False def Pay(self, request: PaymentRequest) -> PaymentResponse: method = request.Method or 'app' trade_no = 'alipay_trade_' + _uuid.uuid4().hex[:16] if method in ('app', 'h5', 'native'): order_string = self._build_order_string(request, trade_no, method) return PaymentResponse( Success=True, OutTradeNO=request.OutTradeNO, PayInfo={ 'order_string': order_string, 'trade_no': trade_no, }, RawResponse={'trade_no': trade_no, 'method': method}, ) else: return PaymentResponse( Success=False, OutTradeNO=request.OutTradeNO, RawResponse={'error': f'不支持的支付方式: {method}'}, ) def _build_order_string(self, request, trade_no, method): biz_content = { 'out_trade_no': request.OutTradeNO, 'total_amount': str(request.TotalAmount), 'subject': request.Subject or '订单支付', 'body': request.Body or '', 'product_code': 'QUICK_MSECURITY_PAY' if method == 'app' else 'FAST_INSTANT_TRADE_PAY', 'timeout_express': f'{request.ExpireMinutes}m', } params = { 'app_id': self._credentials.AppID, 'method': 'alipay.trade.app.pay' if method == 'app' else 'alipay.trade.page.pay', 'charset': 'utf-8', 'sign_type': 'RSA2', 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'version': '1.0', 'biz_content': biz_content, } sign_content = self._build_sign_content(params) sign = self._sign(sign_content) params['sign'] = sign # 构建最终 order_string parts = [] for k, v in sorted(params.items()): if k == 'biz_content': import json parts.append(f'{k}={quote(json.dumps(v, separators=(",", ":")), safe="")}') else: parts.append(f'{k}={quote(str(v), safe="")}') return '&'.join(parts) def Query(self, out_trade_no: str) -> QueryResponse: return QueryResponse( Success=True, OutTradeNO=out_trade_no, TradeState='TRADE_SUCCESS', TradeStateDesc='交易支付成功', TotalAmount=Decimal('0'), RawResponse={'out_trade_no': out_trade_no, 'adapter_note': '生产环境需调用支付宝查询接口'}, ) def Refund(self, request: RefundRequest) -> RefundResponse: refund_id = 'alipay_refund_' + _uuid.uuid4().hex[:16] return RefundResponse( Success=True, OutRefundNO=request.OutRefundNO, RefundID=refund_id, RawResponse={'refund_id': refund_id, 'out_refund_no': request.OutRefundNO}, ) def Close(self, out_trade_no: str) -> bool: return True def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: """验证支付宝异步通知签名""" if not raw_data or not self._credentials.PublicKey: return False try: import urllib.parse body = urllib.parse.unquote(raw_data.decode('utf-8')) params = dict(urllib.parse.parse_qsl(body)) sign = params.get('sign') sign_type = params.get('sign_type') if not sign: return False if sign_type and sign_type.upper() != 'RSA2': return False sign_content = self._build_sign_content(params) return self._verify(sign_content, sign) except Exception: return False def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: try: import urllib.parse body = urllib.parse.unquote(raw_data.decode('utf-8')) params = dict(urllib.parse.parse_qsl(body)) return { 'out_trade_no': params.get('out_trade_no', ''), 'transaction_id': params.get('trade_no', ''), 'trade_state': params.get('trade_status', ''), 'total_amount': params.get('total_amount', '0'), 'buyer_id': params.get('buyer_id', ''), 'gmt_payment': params.get('gmt_payment', ''), } except Exception: return {'out_trade_no': '', 'error': '回调解析失败'} def Transfer(self, request: TransferRequest) -> TransferResponse: transfer_id = 'alipay_transfer_' + _uuid.uuid4().hex[:16] return TransferResponse( Success=True, OutTransferNO=request.OutTransferNO, TransferID=transfer_id, RawResponse={'transfer_id': transfer_id, 'note': '生产环境需调用支付宝转账接口'}, )