"""模块:支付管理器 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, )