加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用

This commit is contained in:
2026-06-16 00:13:10 +08:00
parent 9d9cfa53a4
commit 63e0f4edfc
68 changed files with 10256 additions and 1143 deletions

455
gvsdsdk/payment/manager.py Normal file
View 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: 用户 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,
)