Files
Django/gvsdsdk/payment/gateways/base.py

224 lines
6.3 KiB
Python

"""模块:抽象支付网关
SAAS 支付网关抽象层——不绑定具体业务,只定义支付操作的通用契约。
架构设计:
子服务(如阿龙电竞)
├── 注入租户密钥 → PaymentGatewayConfig
└── 调用 PaymentManager.pay(...)
├── 根据 platform 类型获取对应的 GatewayAdapter
├── 网关实例由密钥创建(非来自数据库)
└── 执行 Pay / Query / Refund / Close
设计原则:
1. 网关是无状态的,凭据由调用方在构造时传入
2. 不关心""在付款(那是子服务的事)
3. 只关心"通过哪个渠道"付了"多少钱"
4. SAAS 主服务器只记录交易流水,不记录业务上下文"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, Literal
from decimal import Decimal
PaymentChannel = Literal['wechat', 'alipay', 'bank', 'unionpay', 'custom']
PaymentMethod = Literal['jsapi', 'app', 'h5', 'native', 'miniapp', 'scan', 'transfer']
@dataclass
class PaymentCredentials:
"""支付凭证——网关实例创建时传入,不持久化到数据库"""
AppID: str = ''
MchID: str = ''
APIKey: str = ''
PrivateKey: str = ''
PublicKey: str = ''
CertPath: str = ''
CertKeyPath: str = ''
NotifyURL: str = ''
ReturnURL: str = ''
Extra: Dict[str, str] = field(default_factory=dict)
def AsDict(self):
return {
'app_id': self.AppID,
'mch_id': self.MchID,
'api_key': '***',
'notify_url': self.NotifyURL,
'return_url': self.ReturnURL,
'extra_keys': list(self.Extra.keys()),
}
@dataclass
class PaymentRequest:
"""支付请求——标准化入参"""
OutTradeNO: str
TotalAmount: Decimal
Currency: str = 'CNY'
Subject: str = ''
Body: str = ''
Method: PaymentMethod = 'jsapi'
OpenID: str = ''
ClientIP: str = ''
ExpireMinutes: int = 30
Extra: Dict[str, Any] = field(default_factory=dict)
@dataclass
class PaymentResponse:
"""支付响应——标准化出参"""
Success: bool
OutTradeNO: str
TransactionID: str = ''
PayInfo: Dict[str, Any] = field(default_factory=dict)
RawResponse: Dict[str, Any] = field(default_factory=dict)
@property
def PrepayID(self):
return self.PayInfo.get('prepay_id', self.PayInfo.get('prepayid', ''))
@dataclass
class RefundRequest:
"""退款请求"""
OutTradeNO: str
OutRefundNO: str
TotalAmount: Decimal
RefundAmount: Decimal
Reason: str = ''
Extra: Dict[str, Any] = field(default_factory=dict)
@dataclass
class RefundResponse:
"""退款响应"""
Success: bool
OutRefundNO: str
RefundID: str = ''
RawResponse: Dict[str, Any] = field(default_factory=dict)
@dataclass
class TransferRequest:
"""转账请求(企业付款到零钱/银行卡)"""
OutTransferNO: str
Amount: Decimal
PayeeAccount: str = ''
PayeeName: str = ''
Description: str = ''
TransferType: Literal['wallet', 'bank'] = 'wallet'
Extra: Dict[str, Any] = field(default_factory=dict)
@dataclass
class TransferResponse:
"""转账响应"""
Success: bool
OutTransferNO: str
TransferID: str = ''
RawResponse: Dict[str, Any] = field(default_factory=dict)
@dataclass
class QueryResponse:
"""订单查询响应"""
Success: bool
OutTradeNO: str
TransactionID: str = ''
TradeState: str = ''
TradeStateDesc: str = ''
TotalAmount: Decimal = Decimal('0')
RawResponse: Dict[str, Any] = field(default_factory=dict)
@property
def IsPaid(self):
return self.TradeState in ('SUCCESS', 'TRADE_SUCCESS', 'TRADE_FINISHED')
@property
def IsRefunded(self):
return self.TradeState in ('REFUND', 'TRADE_REFUND')
class AbstractPaymentGateway(ABC):
"""抽象支付网关基类
所有支付渠道的适配器必须实现此接口。
每个实例绑定一组支付凭证,不同租户/不同平台使用不同实例。
用法:
creds = PaymentCredentials(AppID='wx...', MchID='123', APIKey='...')
gw = WechatPaymentGateway(creds)
resp = gw.Pay(PaymentRequest(OutTradeNO='X', TotalAmount=Decimal('1.00')))
"""
Channel: PaymentChannel = 'custom'
Name: str = 'Abstract'
def __init__(self, credentials: PaymentCredentials):
self._credentials = credentials
self._validate_credentials()
def _validate_credentials(self):
pass
@property
def Credentials(self):
return self._credentials
# ─── 核心支付接口 ──────────────────────────────────────
@abstractmethod
def Pay(self, request: PaymentRequest) -> PaymentResponse:
"""发起支付——返回前端调起支付所需参数"""
...
@abstractmethod
def Query(self, out_trade_no: str) -> QueryResponse:
"""查询订单状态"""
...
@abstractmethod
def Refund(self, request: RefundRequest) -> RefundResponse:
"""发起退款"""
...
@abstractmethod
def Close(self, out_trade_no: str) -> bool:
"""关闭未支付的订单"""
...
# ─── 回调处理 ─────────────────────────────────────────
@abstractmethod
def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool:
"""验证支付回调签名的合法性"""
...
@abstractmethod
def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]:
"""解析支付回调数据为标准化字典"""
...
# ─── 可选接口 ─────────────────────────────────────────
def Transfer(self, request: TransferRequest) -> TransferResponse:
"""企业付款到零钱/银行卡(部分渠道不支持)"""
raise NotImplementedError(f'{self.Name} 不支持转账功能')
def QueryTransfer(self, out_transfer_no: str) -> TransferResponse:
"""查询转账状态"""
raise NotImplementedError(f'{self.Name} 不支持转账查询')