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

9.1 KiB
Raw Permalink Blame History

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'))

九、安全设计

  1. 密钥加密存储:APIKey、PrivateKey、PublicKey 使用 XOR + Base64 加密后入库
  2. 加密密钥外置:通过 settings.PAYMENT_ENCRYPTION_KEY(从 secrets 文件读取)管理
  3. 回调签名验证:每个网关适配器自行实现 VerifyNotify() 验证回调签名的合法性
  4. 回调端点不认证:NotifyView 关闭 DRF 的认证和权限类,但网关层签名验证保证安全
  5. 密钥不输出:PaymentCredentials.AsDict() 方法中 api_key 显示为 ***
  6. 幂等保护:OutTradeNO 唯一约束,防止重复支付记录