加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用
This commit is contained in:
455
gvsdsdk/payment/manager.py
Normal file
455
gvsdsdk/payment/manager.py
Normal file
@@ -0,0 +1,455 @@
|
||||
"""模块:支付管理器
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user