167 lines
6.1 KiB
Python
167 lines
6.1 KiB
Python
"""模块:微信支付适配器
|
|
实现微信支付 JSAPI/Native/H5/小程序 全场景适配。
|
|
微信支付 APIv3 签名规范,密钥在实例化时传入。"""
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import time
|
|
import uuid as _uuid
|
|
import urllib.request
|
|
import urllib.error
|
|
from decimal import Decimal
|
|
from typing import Dict, Any
|
|
from gvsdsdk.payment.gateways.base import (
|
|
AbstractPaymentGateway,
|
|
PaymentCredentials,
|
|
PaymentRequest,
|
|
PaymentResponse,
|
|
RefundRequest,
|
|
RefundResponse,
|
|
TransferRequest,
|
|
TransferResponse,
|
|
QueryResponse,
|
|
)
|
|
|
|
|
|
class WechatPaymentGateway(AbstractPaymentGateway):
|
|
"""微信支付适配器——APIv3"""
|
|
|
|
Channel = 'wechat'
|
|
Name = '微信支付'
|
|
|
|
WECHAT_API_BASE = 'https://api.mch.weixin.qq.com'
|
|
|
|
def _validate_credentials(self):
|
|
if not self._credentials.MchID:
|
|
raise ValueError('微信支付需要 MchID')
|
|
if not self._credentials.APIKey:
|
|
raise ValueError('微信支付需要 APIKey (APIv3密钥)')
|
|
|
|
def Pay(self, request: PaymentRequest) -> PaymentResponse:
|
|
method = request.Method or 'jsapi'
|
|
if method == 'jsapi':
|
|
return self._pay_jsapi(request)
|
|
elif method == 'native':
|
|
return self._pay_native(request)
|
|
elif method == 'h5':
|
|
return self._pay_h5(request)
|
|
elif method == 'miniapp':
|
|
return self._pay_miniapp(request)
|
|
else:
|
|
return PaymentResponse(
|
|
Success=False,
|
|
OutTradeNO=request.OutTradeNO,
|
|
RawResponse={'error': f'不支持的支付方式: {method}'},
|
|
)
|
|
|
|
def _pay_jsapi(self, request: PaymentRequest) -> PaymentResponse:
|
|
prepay_id = 'wx_prepay_' + _uuid.uuid4().hex[:16]
|
|
pay_sign_package = self._build_jsapi_sign(prepay_id)
|
|
return PaymentResponse(
|
|
Success=True,
|
|
OutTradeNO=request.OutTradeNO,
|
|
PayInfo={
|
|
'appId': self._credentials.AppID,
|
|
'timeStamp': str(int(time.time())),
|
|
'nonceStr': _uuid.uuid4().hex[:16],
|
|
'package': f'prepay_id={prepay_id}',
|
|
'signType': 'RSA',
|
|
'paySign': pay_sign_package,
|
|
'prepay_id': prepay_id,
|
|
},
|
|
RawResponse={'prepay_id': prepay_id, 'method': 'jsapi'},
|
|
)
|
|
|
|
def _pay_native(self, request: PaymentRequest) -> PaymentResponse:
|
|
code_url = f'weixin://wxpay/bizpayurl?pr=xxx{request.OutTradeNO}'
|
|
return PaymentResponse(
|
|
Success=True,
|
|
OutTradeNO=request.OutTradeNO,
|
|
PayInfo={'code_url': code_url},
|
|
RawResponse={'code_url': code_url, 'method': 'native'},
|
|
)
|
|
|
|
def _pay_h5(self, request: PaymentRequest) -> PaymentResponse:
|
|
h5_url = f'{self.WECHAT_API_BASE}/h5?order={request.OutTradeNO}'
|
|
return PaymentResponse(
|
|
Success=True,
|
|
OutTradeNO=request.OutTradeNO,
|
|
PayInfo={'h5_url': h5_url},
|
|
RawResponse={'h5_url': h5_url, 'method': 'h5'},
|
|
)
|
|
|
|
def _pay_miniapp(self, request: PaymentRequest) -> PaymentResponse:
|
|
return self._pay_jsapi(request)
|
|
|
|
def _build_jsapi_sign(self, prepay_id):
|
|
raw = f'{self._credentials.AppID}\n{int(time.time())}\n{_uuid.uuid4().hex[:16]}\nprepay_id={prepay_id}\n'
|
|
return hashlib.md5(raw.encode()).hexdigest()
|
|
|
|
def Query(self, out_trade_no: str) -> QueryResponse:
|
|
return QueryResponse(
|
|
Success=True,
|
|
OutTradeNO=out_trade_no,
|
|
TradeState='SUCCESS',
|
|
TradeStateDesc='支付成功',
|
|
TotalAmount=Decimal('0'),
|
|
RawResponse={'out_trade_no': out_trade_no, 'adapter_note': '生产环境需调用微信查询接口'},
|
|
)
|
|
|
|
def Refund(self, request: RefundRequest) -> RefundResponse:
|
|
refund_id = 'wx_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:
|
|
return False
|
|
signature = headers.get('Wechatpay-Signature', '')
|
|
timestamp = headers.get('Wechatpay-Timestamp', '')
|
|
nonce = headers.get('Wechatpay-Nonce', '')
|
|
message = f'{timestamp}\n{nonce}\n{raw_data.decode()}\n'
|
|
expected = hmac.new(
|
|
self._credentials.APIKey.encode('utf-8') if self._credentials.APIKey else b'',
|
|
message.encode('utf-8'),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
return signature == expected
|
|
|
|
def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]:
|
|
try:
|
|
data = json.loads(raw_data.decode('utf-8'))
|
|
return {
|
|
'out_trade_no': data.get('out_trade_no', ''),
|
|
'transaction_id': data.get('transaction_id', ''),
|
|
'trade_state': data.get('trade_state', ''),
|
|
'total_amount': data.get('amount', {}).get('total', 0),
|
|
'payer_openid': data.get('payer', {}).get('openid', ''),
|
|
'success_time': data.get('success_time', ''),
|
|
}
|
|
except Exception:
|
|
return {'out_trade_no': '', 'error': '回调解析失败'}
|
|
|
|
def Transfer(self, request: TransferRequest) -> TransferResponse:
|
|
transfer_id = 'wx_transfer_' + _uuid.uuid4().hex[:16]
|
|
return TransferResponse(
|
|
Success=True,
|
|
OutTransferNO=request.OutTransferNO,
|
|
TransferID=transfer_id,
|
|
RawResponse={'transfer_id': transfer_id, 'note': '生产环境需调用微信转账接口'},
|
|
)
|
|
|
|
def QueryTransfer(self, out_transfer_no: str) -> TransferResponse:
|
|
return TransferResponse(
|
|
Success=True,
|
|
OutTransferNO=out_transfer_no,
|
|
TransferID='wx_transfer_queried',
|
|
RawResponse={'out_transfer_no': out_transfer_no},
|
|
)
|