263 lines
9.1 KiB
Markdown
263 lines
9.1 KiB
Markdown
# 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` 唯一约束,防止重复支付记录 |