Files
SMANAGE/docs/payment_gateway.md
T
2026-06-16 11:55:14 +08:00

263 lines
9.1 KiB
Markdown
Raw 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.
# 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(非持久化)
网关实例创建时传入,不存储到数据库:
```python
@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` 属性) |
---
## 五、抽象网关接口
```python
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 类型 |
### 新增支付渠道只需三步
```python
# 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 编排器
```python
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/ # 列出所有可用渠道
```
---
## 八、子服务集成示例
```python
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'))
```
---
## 九、安全设计
1. **密钥加密存储**:`APIKey`、`PrivateKey`、`PublicKey` 使用 XOR + Base64 加密后入库
2. **加密密钥外置**:通过 `settings.PAYMENT_ENCRYPTION_KEY`(从 secrets 文件读取)管理
3. **回调签名验证**:每个网关适配器自行实现 `VerifyNotify()` 验证回调签名的合法性
4. **回调端点不认证**:`NotifyView` 关闭 DRF 的认证和权限类,但网关层签名验证保证安全
5. **密钥不输出**:`PaymentCredentials.AsDict()` 方法中 `api_key` 显示为 `***`
6. **幂等保护**:`OutTradeNO` 唯一约束,防止重复支付记录