9.1 KiB
9.1 KiB
SMANAGE 抽象支付网关 — 设计文档
一、设计目标
SMANAGE 是 SAAS 系统,不绑定具体业务。支付网关抽象层提供多平台支付能力的统一接入,子服务只需要关心"付了多少钱"和"通过哪个渠道付",SAAS 负责"怎么付"。
核心原则
| 原则 | 说明 |
|---|---|
| 适配器模式 | 每个支付平台是一个适配器,新增平台只需实现接口 + 注册一行 |
| 密钥走构造传参 | PaymentCredentials 在网关实例创建时注入,网关层不持有状态 |
| 租户隔离 | 所有操作通过 X-Tenant-UUID 头识别租户,配置和交易记录完全分离 |
| 加密存储 | 密钥使用 XOR + Base64 加密,密钥通过 settings.PAYMENT_ENCRYPTION_KEY 管理 |
| 回调不认证 | 回调端点关闭 DRF 认证,由网关自身 VerifyNotify() 签名验证保证安全 |
二、架构分层
子服务(如阿龙电竞)
│
├── 注入租户密钥 → PaymentGatewayConfig(SAAS配置)
│
└── 调用 PaymentManager.pay(platform='wechat', amount=100)
│
├── 根据 platform 从 GatewayRegistry 获取适配器类
├── 网关实例由密钥创建(构造时传参,非来自数据库)
└── 执行 Pay / Query / Refund / Transfer / Close
三、文件清单(12 个文件)
| 层级 | 文件 | 职责 |
|---|---|---|
| 抽象层 | gateways/base.py |
AbstractPaymentGateway 抽象基类 + 7 个数据模型 |
| 注册表 | gateways/registry.py |
GatewayRegistry 渠道注册工厂 + payment_registry 全局单例 |
| 适配器 | gateways/wechat.py |
WechatPaymentGateway — 微信支付(JSAPI/Native/H5/小程序 + 转账 + 回调验签) |
| 适配器 | gateways/alipay.py |
AlipayPaymentGateway — 支付宝(APP/H5/Native + 转账 + 回调验签) |
| 适配器 | gateways/bank.py |
BankPaymentGateway — 银行卡(银联/网银 + 代付到银行卡) |
| 模型 | models.py |
PaymentGatewayConfig(密钥加密存储)+ PaymentTransaction(交易流水) |
| 编排器 | manager.py |
PaymentManager — 配置/支付/查询/退款/转账/回调六合一 |
| API | views.py |
7 个 REST 端点(含无需认证的 NotifyView) |
| 序列化 | serializers.py |
5 个 Serializer |
| 路由 | urls.py |
7 个 URL 路由 |
| 配置 | apps.py |
AppConfig,ready() 时自动注册三个渠道 |
| 管理 | admin.py |
Django Admin 注册 |
四、核心数据模型
4.1 PaymentCredentials(非持久化)
网关实例创建时传入,不存储到数据库:
@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)
4.2 PaymentGatewayConfig(数据库持久化—加密)
每个租户每个渠道一条配置,密钥字段加密存储:
| 字段 | 类型 | 说明 |
|---|---|---|
| ConfigUUID | UUID | 主键 |
| TenantUUID | UUID | 租户标识 |
| Platform | CharField(32) | wechat / alipay / bank |
| AppID | CharField(64) | 应用ID |
| MchID | CharField(64) | 商户号 |
| APIKeyEncrypted | CharField(512) | 加密的 API 密钥 |
| PrivateKeyEncrypted | TextField | 加密的私钥 |
| NotifyURL | CharField(512) | 回调地址 |
| ConfigStatus | SmallIntegerField | 1=启用 0=禁用 |
4.3 PaymentTransaction(数据库持久化)
每笔支付/退款/转账操作的统一流水记录:
| 字段 | 类型 | 说明 |
|---|---|---|
| TransactionUUID | UUID | 主键 |
| TenantUUID | UUID | 租户 |
| Platform | CharField(32) | 支付平台 |
| TransactionType | CharField(16) | pay / refund / transfer |
| OutTradeNO | CharField(64) | 商户订单号(唯一) |
| TotalAmount | DecimalField | 金额(最大18位,4位小数) |
| TransactionStatus | SmallIntegerField | 0=处理中 1=成功 2=失败 3=已关闭 |
4.4 请求/响应数据模型(dataclass,非持久化)
| 数据类 | 用途 |
|---|---|
PaymentRequest |
支付请求标准化入参 |
PaymentResponse |
支付响应标准化出参(含 PrepayID 属性) |
RefundRequest |
退款请求 |
RefundResponse |
退款响应 |
TransferRequest |
转账请求(企业付款到零钱/银行卡) |
TransferResponse |
转账响应 |
QueryResponse |
订单查询响应(含 IsPaid/IsRefunded 属性) |
五、抽象网关接口
class AbstractPaymentGateway(ABC):
Channel: PaymentChannel = 'custom'
Name: str = 'Abstract'
def __init__(self, credentials: PaymentCredentials):
self._credentials = credentials
self._validate_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} 不支持转账功能')
已实现适配器
| 渠道 | Channel | 类名 | 支持的方法 | Transfer |
|---|---|---|---|---|
| 微信支付 | wechat |
WechatPaymentGateway |
jsapi / native / h5 / miniapp | 支持 |
| 支付宝 | alipay |
AlipayPaymentGateway |
app / h5 / native | 支持 |
| 银行卡 | bank |
BankPaymentGateway |
app / h5 / native | 仅 bank 类型 |
新增支付渠道只需三步
# 1. 实现适配器
class FooPaymentGateway(AbstractPaymentGateway):
Channel = 'foo'
Name = '某支付'
def Pay(self, request: PaymentRequest) -> PaymentResponse:
...
# 2. 在 apps.py 注册
payment_registry.register('foo', FooPaymentGateway)
# 3. 子服务配置密钥后即可使用
manager = PaymentManager(tenant_uuid)
manager.Pay(platform='foo', amount=100)
六、PaymentManager 编排器
class PaymentManager:
def __init__(self, tenant_uuid)
def ConfigureGateway(platform, app_id, mch_id, api_key, ...) # 配置支付渠道
def Pay(platform, amount, subject, method, ...) # 发起支付
def Query(out_trade_no, platform=None) # 查询订单
def Refund(out_trade_no, amount, reason, ...) # 发起退款
def Transfer(platform, amount, payee_account, ...) # 企业付款
def HandleNotify(platform, raw_data, headers=None) # 处理回调
def GetGatewayInstance(platform) # 获取网关实例(供子服务自定义)
七、API 端点
POST /api/payment/config/ # 配置支付渠道(SAAS管理员)
GET /api/payment/config/ # 查看已配置渠道列表
DELETE /api/payment/config/ # 禁用渠道
POST /api/payment/pay/ # 发起支付
GET /api/payment/query/<out_trade_no>/ # 查询订单
POST /api/payment/refund/ # 发起退款
POST /api/payment/transfer/ # 企业付款
POST /api/payment/notify/<platform>/ # 支付回调(微信/支付宝等)
GET /api/payment/channels/ # 列出所有可用渠道
八、子服务集成示例
from gvsds.apps.payment.manager import PaymentManager, PaymentError
manager = PaymentManager(tenant_uuid='al_long_001')
# 配置微信支付
manager.ConfigureGateway(
platform='wechat',
app_id='wx1234567890',
mch_id='1234567890',
api_key='your_api_key',
notify_url='https://al_long.gvsds.com/api/payment/notify/wechat/',
)
# 发起支付
tx = manager.Pay(
platform='wechat',
amount=Decimal('100.00'),
subject='订单支付',
method='jsapi',
open_id='user_openid',
)
# tx._prepay_info 包含前端调起支付所需的参数
return {'prepay_info': tx._prepay_info}
# 查询订单
result = manager.Query(out_trade_no='PY-ABC123')
if result.IsPaid:
# 更新子服务订单状态
...
# 退款
tx = manager.Refund(out_trade_no='PY-ABC123', amount=Decimal('100.00'))
九、安全设计
- 密钥加密存储:
APIKey、PrivateKey、PublicKey使用 XOR + Base64 加密后入库 - 加密密钥外置:通过
settings.PAYMENT_ENCRYPTION_KEY(从 secrets 文件读取)管理 - 回调签名验证:每个网关适配器自行实现
VerifyNotify()验证回调签名的合法性 - 回调端点不认证:
NotifyView关闭 DRF 的认证和权限类,但网关层签名验证保证安全 - 密钥不输出:
PaymentCredentials.AsDict()方法中api_key显示为*** - 幂等保护:
OutTradeNO唯一约束,防止重复支付记录