456 lines
15 KiB
Python
456 lines
15 KiB
Python
"""模块:支付管理器
|
||
SAAS 级支付编排器——子服务通过此管理器发起支付操作。
|
||
|
||
职责:
|
||
1. 根据租户UUID + 平台查询 PaymentGatewayConfig
|
||
2. 解密密钥构建 PaymentCredentials
|
||
3. 从网关注册表获取适配器类,创建网关实例
|
||
4. 执行支付操作并记录 PaymentTransaction 流水"""
|
||
|
||
import uuid
|
||
import datetime
|
||
import logging
|
||
from decimal import Decimal
|
||
from gvsdsdk.payment.gateways.base import (
|
||
PaymentCredentials,
|
||
PaymentRequest,
|
||
PaymentResponse,
|
||
RefundRequest,
|
||
RefundResponse,
|
||
TransferRequest,
|
||
TransferResponse,
|
||
QueryResponse,
|
||
)
|
||
from gvsdsdk.payment.gateways.registry import payment_registry
|
||
from gvsdsdk.payment.models import (
|
||
PaymentGatewayConfig,
|
||
PaymentTransaction,
|
||
EncryptField,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PaymentError(Exception):
|
||
def __init__(self, message, transaction=None):
|
||
super().__init__(message)
|
||
self.Transaction = transaction
|
||
|
||
|
||
class PaymentManager:
|
||
"""支付编排器——SAAS 主服务器核心入口
|
||
|
||
用法:
|
||
manager = PaymentManager(tenant_uuid='xxx')
|
||
|
||
# 配置支付网关(SAAS 管理员操作)
|
||
manager.ConfigureGateway(
|
||
platform='wechat',
|
||
app_id='wx123',
|
||
mch_id='456',
|
||
api_key='secret',
|
||
notify_url='https://xxx.gvsds.com/api/payment/notify/wechat/',
|
||
)
|
||
|
||
# 发起支付
|
||
tx = manager.Pay(
|
||
platform='wechat',
|
||
amount=Decimal('100.00'),
|
||
subject='订单支付',
|
||
method='jsapi',
|
||
)
|
||
|
||
# 查询
|
||
manager.Query(out_trade_no='PY-xxx')
|
||
|
||
# 退款
|
||
manager.Refund(out_trade_no='PY-xxx', amount=Decimal('100.00'))
|
||
|
||
# 回调处理
|
||
manager.HandleNotify(platform='wechat', raw_data=body, headers=headers)
|
||
"""
|
||
|
||
def __init__(self, tenant_uuid):
|
||
self.TenantUUID = tenant_uuid
|
||
|
||
def ConfigureGateway(self, platform, app_id='', mch_id='',
|
||
api_key='', private_key='', public_key='',
|
||
cert_path='', cert_key_path='',
|
||
notify_url='', return_url='', extra=None, set_default=False):
|
||
"""配置支付网关——SAAS 管理员为租户配置支付渠道
|
||
|
||
Args:
|
||
platform: 支付平台 ('wechat', 'alipay', 'bank')
|
||
app_id: 应用 ID
|
||
mch_id: 商户号
|
||
api_key: API 密钥(将加密存储)
|
||
private_key: 私钥(将加密存储)
|
||
public_key: 公钥(将加密存储)
|
||
cert_path: 证书路径
|
||
cert_key_path: 证书密钥路径
|
||
notify_url: 回调通知 URL
|
||
return_url: 支付完成跳转 URL
|
||
extra: 额外配置
|
||
set_default: 是否设为默认渠道
|
||
|
||
Returns:
|
||
PaymentGatewayConfig 实例
|
||
"""
|
||
config, created = PaymentGatewayConfig.objects.update_or_create(
|
||
TenantUUID=self.TenantUUID,
|
||
Platform=platform,
|
||
defaults={
|
||
'AppID': app_id,
|
||
'MchID': mch_id,
|
||
'APIKeyEncrypted': EncryptField(api_key) if api_key else '',
|
||
'PrivateKeyEncrypted': EncryptField(private_key) if private_key else '',
|
||
'PublicKeyEncrypted': EncryptField(public_key) if public_key else '',
|
||
'CertPath': cert_path,
|
||
'CertKeyPath': cert_key_path,
|
||
'NotifyURL': notify_url,
|
||
'ReturnURL': return_url,
|
||
'ConfigExtra': extra or {},
|
||
'ConfigStatus': 1,
|
||
'UpdateTime': datetime.datetime.utcnow(),
|
||
},
|
||
)
|
||
|
||
if set_default:
|
||
PaymentGatewayConfig.objects.filter(
|
||
TenantUUID=self.TenantUUID, ConfigDefault=True,
|
||
).update(ConfigDefault=False)
|
||
config.ConfigDefault = True
|
||
config.save(update_fields=['ConfigDefault'])
|
||
|
||
logger.info(f'支付网关配置: {platform}@tenant={self.TenantUUID}')
|
||
return config
|
||
|
||
def Pay(self, platform, amount, subject='', body='',
|
||
method='jsapi', open_id='', client_ip='',
|
||
extra=None, expire_minutes=30):
|
||
"""发起支付
|
||
|
||
Args:
|
||
platform: 支付平台
|
||
amount: 金额
|
||
subject: 商品标题
|
||
body: 商品描述
|
||
method: 支付方式 (jsapi/app/h5/native/miniapp)
|
||
open_id: 用户 OpenID(JSAPI 必填)
|
||
client_ip: 客户端 IP
|
||
extra: 额外参数
|
||
expire_minutes: 过期分钟数
|
||
|
||
Returns:
|
||
PaymentTransaction 实例
|
||
|
||
Raises:
|
||
PaymentError: 支付失败
|
||
"""
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
raise PaymentError(f'租户未配置支付渠道: {platform}')
|
||
|
||
creds = self._build_credentials(config)
|
||
gateway = payment_registry.create(platform, creds)
|
||
|
||
out_trade_no = f'PY-{uuid.uuid4().hex[:16].upper()}'
|
||
req = PaymentRequest(
|
||
OutTradeNO=out_trade_no,
|
||
TotalAmount=Decimal(str(amount)),
|
||
Subject=subject,
|
||
Body=body,
|
||
Method=method,
|
||
OpenID=open_id,
|
||
ClientIP=client_ip,
|
||
ExpireMinutes=expire_minutes,
|
||
Extra=extra or {},
|
||
)
|
||
|
||
tx = PaymentTransaction(
|
||
TransactionUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
Platform=platform,
|
||
TransactionType=PaymentTransaction.TX_PAY,
|
||
OutTradeNO=out_trade_no,
|
||
TotalAmount=Decimal(str(amount)),
|
||
Subject=subject,
|
||
RawRequest={
|
||
'method': method,
|
||
'open_id': open_id,
|
||
'extra': extra or {},
|
||
},
|
||
)
|
||
tx.save()
|
||
|
||
resp = gateway.Pay(req)
|
||
tx.RawResponse = resp.RawResponse
|
||
tx.UpdateTime = datetime.datetime.utcnow()
|
||
|
||
if resp.Success:
|
||
tx.ChannelTransactionID = resp.TransactionID
|
||
tx.save(update_fields=['RawResponse', 'ChannelTransactionID', 'UpdateTime'])
|
||
tx._prepay_info = resp.PayInfo
|
||
return tx
|
||
else:
|
||
tx.MarkFailed()
|
||
raise PaymentError('支付发起失败', transaction=tx)
|
||
|
||
def Query(self, out_trade_no, platform=None):
|
||
"""查询订单状态
|
||
|
||
Args:
|
||
out_trade_no: 商户订单号
|
||
platform: 支付平台(不传则从交易记录推断)
|
||
|
||
Returns:
|
||
QueryResponse
|
||
"""
|
||
try:
|
||
tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no)
|
||
except PaymentTransaction.DoesNotExist:
|
||
raise PaymentError(f'交易记录不存在: {out_trade_no}')
|
||
|
||
platform = platform or tx.Platform
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
raise PaymentError(f'租户未配置支付渠道: {platform}')
|
||
|
||
creds = self._build_credentials(config)
|
||
gateway = payment_registry.create(platform, creds)
|
||
result = gateway.Query(out_trade_no)
|
||
|
||
if result.IsPaid and not tx.IsSuccess:
|
||
tx.MarkSuccess(
|
||
channel_transaction_id=result.TransactionID,
|
||
notify_data={'query_result': result.RawResponse},
|
||
)
|
||
elif result.IsRefunded:
|
||
tx.TransactionStatus = PaymentTransaction.TX_CLOSED
|
||
tx.RawResponse = result.RawResponse
|
||
tx.UpdateTime = datetime.datetime.utcnow()
|
||
tx.save(update_fields=['TransactionStatus', 'RawResponse', 'UpdateTime'])
|
||
|
||
return result
|
||
|
||
def Refund(self, out_trade_no, amount, reason='', platform=None, extra=None):
|
||
"""发起退款
|
||
|
||
Args:
|
||
out_trade_no: 商户订单号
|
||
amount: 退款金额
|
||
reason: 退款原因
|
||
platform: 支付平台
|
||
extra: 额外参数
|
||
|
||
Returns:
|
||
PaymentTransaction 实例
|
||
"""
|
||
try:
|
||
pay_tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no, TransactionType=PaymentTransaction.TX_PAY)
|
||
except PaymentTransaction.DoesNotExist:
|
||
raise PaymentError(f'原始交易不存在: {out_trade_no}')
|
||
|
||
if not pay_tx.IsSuccess:
|
||
raise PaymentError(f'原始交易未支付成功,无法退款: {out_trade_no}')
|
||
|
||
platform = platform or pay_tx.Platform
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
raise PaymentError(f'租户未配置支付渠道: {platform}')
|
||
|
||
creds = self._build_credentials(config)
|
||
gateway = payment_registry.create(platform, creds)
|
||
|
||
out_refund_no = f'RF-{uuid.uuid4().hex[:16].upper()}'
|
||
amount = Decimal(str(amount))
|
||
|
||
if amount > pay_tx.TotalAmount:
|
||
raise PaymentError(f'退款金额超过原始交易金额: {amount} > {pay_tx.TotalAmount}')
|
||
|
||
req = RefundRequest(
|
||
OutTradeNO=out_trade_no,
|
||
OutRefundNO=out_refund_no,
|
||
TotalAmount=pay_tx.TotalAmount,
|
||
RefundAmount=amount,
|
||
Reason=reason,
|
||
Extra=extra or {},
|
||
)
|
||
|
||
rx = PaymentTransaction(
|
||
TransactionUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
Platform=platform,
|
||
TransactionType=PaymentTransaction.TX_REFUND,
|
||
OutTradeNO=out_trade_no,
|
||
OutRefundNO=out_refund_no,
|
||
TotalAmount=amount,
|
||
RawRequest={'refund_reason': reason, 'extra': extra or {}},
|
||
)
|
||
rx.save()
|
||
|
||
resp = gateway.Refund(req)
|
||
rx.RawResponse = resp.RawResponse
|
||
|
||
if resp.Success:
|
||
rx.MarkSuccess(channel_transaction_id=resp.RefundID)
|
||
else:
|
||
rx.MarkFailed()
|
||
|
||
return rx
|
||
|
||
def Transfer(self, platform, amount, payee_account='',
|
||
payee_name='', description='', transfer_type='wallet',
|
||
extra=None):
|
||
"""企业付款到零钱/银行卡
|
||
|
||
Args:
|
||
platform: 支付平台
|
||
amount: 转账金额
|
||
payee_account: 收款账户
|
||
payee_name: 收款人姓名
|
||
description: 转账备注
|
||
transfer_type: 转账类型 (wallet/bank)
|
||
extra: 额外参数
|
||
|
||
Returns:
|
||
PaymentTransaction 实例
|
||
"""
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
raise PaymentError(f'租户未配置支付渠道: {platform}')
|
||
|
||
creds = self._build_credentials(config)
|
||
gateway = payment_registry.create(platform, creds)
|
||
|
||
out_transfer_no = f'TF-{uuid.uuid4().hex[:16].upper()}'
|
||
amount = Decimal(str(amount))
|
||
|
||
req = TransferRequest(
|
||
OutTransferNO=out_transfer_no,
|
||
Amount=amount,
|
||
PayeeAccount=payee_account,
|
||
PayeeName=payee_name,
|
||
Description=description,
|
||
TransferType=transfer_type,
|
||
Extra=extra or {},
|
||
)
|
||
|
||
tx = PaymentTransaction(
|
||
TransactionUUID=uuid.uuid4().bytes,
|
||
TenantUUID=self.TenantUUID,
|
||
Platform=platform,
|
||
TransactionType=PaymentTransaction.TX_TRANSFER,
|
||
OutTradeNO=out_transfer_no,
|
||
TotalAmount=amount,
|
||
Subject=description,
|
||
RawRequest={
|
||
'payee_account': payee_account,
|
||
'payee_name': payee_name,
|
||
'transfer_type': transfer_type,
|
||
'extra': extra or {},
|
||
},
|
||
)
|
||
tx.save()
|
||
|
||
try:
|
||
resp = gateway.Transfer(req)
|
||
except NotImplementedError as e:
|
||
tx.MarkFailed(raw_response={'error': str(e)})
|
||
raise PaymentError(str(e), transaction=tx)
|
||
|
||
tx.RawResponse = resp.RawResponse
|
||
if resp.Success:
|
||
tx.MarkSuccess(channel_transaction_id=resp.TransferID)
|
||
else:
|
||
tx.MarkFailed()
|
||
|
||
return tx
|
||
|
||
def HandleNotify(self, platform, raw_data, headers=None):
|
||
"""处理支付回调通知
|
||
|
||
Args:
|
||
platform: 支付平台
|
||
raw_data: 原始回调数据 (bytes)
|
||
headers: 请求头字典
|
||
|
||
Returns:
|
||
PaymentTransaction 实例或 None
|
||
"""
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
logger.error(f'租户未配置支付渠道: {platform}')
|
||
return None
|
||
|
||
creds = self._build_credentials(config)
|
||
gateway = payment_registry.create(platform, creds)
|
||
|
||
if not gateway.VerifyNotify(raw_data, headers or {}):
|
||
logger.warning(f'支付回调签名验证失败: {platform}')
|
||
return None
|
||
|
||
notify_data = gateway.ParseNotify(raw_data)
|
||
out_trade_no = notify_data.get('out_trade_no', '')
|
||
if not out_trade_no:
|
||
logger.warning('回调缺少 out_trade_no')
|
||
return None
|
||
|
||
try:
|
||
tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no)
|
||
except PaymentTransaction.DoesNotExist:
|
||
logger.warning(f'回调对应交易不存在: {out_trade_no}')
|
||
return None
|
||
|
||
trade_state = notify_data.get('trade_state', '')
|
||
transaction_id = notify_data.get('transaction_id', '')
|
||
|
||
if trade_state in ('SUCCESS', 'TRADE_SUCCESS', 'TRADE_FINISHED'):
|
||
tx.MarkSuccess(
|
||
channel_transaction_id=transaction_id,
|
||
notify_data=notify_data,
|
||
)
|
||
elif trade_state in ('CLOSED', 'REVOKED', 'TRADE_CLOSED'):
|
||
tx.MarkClosed()
|
||
elif trade_state in ('REFUND', 'TRADE_REFUND'):
|
||
tx.TransactionStatus = PaymentTransaction.TX_CLOSED
|
||
tx.NotifyData = notify_data
|
||
tx.UpdateTime = datetime.datetime.utcnow()
|
||
tx.save(update_fields=['TransactionStatus', 'NotifyData', 'UpdateTime'])
|
||
|
||
return tx
|
||
|
||
def GetGatewayInstance(self, platform):
|
||
"""直接获取网关实例——供子服务自定义操作
|
||
|
||
Args:
|
||
platform: 支付平台
|
||
|
||
Returns:
|
||
AbstractPaymentGateway 实例
|
||
|
||
Raises:
|
||
PaymentError: 租户未配置支付渠道
|
||
"""
|
||
config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform)
|
||
if config is None:
|
||
raise PaymentError(f'租户未配置支付渠道: {platform}')
|
||
|
||
creds = self._build_credentials(config)
|
||
return payment_registry.create(platform, creds)
|
||
|
||
def _build_credentials(self, config):
|
||
"""从数据库配置构建 PaymentCredentials"""
|
||
return PaymentCredentials(
|
||
AppID=config.AppID,
|
||
MchID=config.MchID,
|
||
APIKey=config.DecryptAPIKey(),
|
||
PrivateKey=config.DecryptPrivateKey(),
|
||
PublicKey=config.DecryptPublicKey(),
|
||
CertPath=config.CertPath,
|
||
CertKeyPath=config.CertKeyPath,
|
||
NotifyURL=config.NotifyURL,
|
||
ReturnURL=config.ReturnURL,
|
||
Extra=config.ConfigExtra,
|
||
)
|