Files
Django/gvsdsdk/payment/manager.py

456 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块:支付管理器
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: 用户 OpenIDJSAPI 必填)
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,
)