Files
pay-bridge/docs/approach-design.md
2026-03-13 15:51:59 +08:00

345 lines
20 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.
# pay-bridge Approach Design
| 字段 | 内容 |
|-----|------|
| 文档版本 | v1.0 |
| 创建日期 | 2026-02-27 |
| 状态 | 草稿 |
---
## 1. 整体架构分层
pay-bridge 采用四层架构自上而下分别为API 接入层、业务逻辑层、渠道适配层、基础设施层。
```
┌──────────────────────────────────────────────────────────────┐
│ 下游业务系统 │
│ (电商 / SaaS / 多商户平台) │
└────────────────────────┬─────────────────────────────────────┘
│ REST/JSON over HTTPS
┌────────────────────────▼─────────────────────────────────────┐
│ Layer 1: API 接入层 │
│ • 鉴权中间件appId + appSecret 签名验证) │
│ • 请求路由Gin Router
│ • 参数校验binding/validator
│ • 限流中间件Redis Token Bucket
│ • 统一响应格式封装 / 错误码映射 │
├──────────────────────────────────────────────────────────────┤
│ Layer 2: 业务逻辑层 │
│ • 交易服务(统一下单、查询、退款、关单) │
│ • 通知服务(接收上游回调、通知下游、重试调度) │
│ • 分润服务(分账计算、分润触发、退款回退) │
│ • 服务费服务(费率计算、自动扣收) │
│ • 收款匹配服务(入账通知接收、三维度匹配引擎) │
│ • 对账服务(账单拉取、逐笔比对、差异报告) │
│ • 商户进件服务(进件提交、审核状态跟踪) │
│ • 微信通知服务(模板消息推送、绑定管理) │
│ • 订单编码服务(按 app_id 隔离的序列号生成) │
├──────────────────────────────────────────────────────────────┤
│ Layer 3: 渠道适配层 │
│ • 渠道适配器接口(统一 interface 定义) │
│ • 汇元支付适配器RSA/MD5 签名、RSA+3DES 加解密) │
│ • 渠道工厂(根据 channel_code 动态路由到对应适配器) │
│ • 渠道配置仓储(密钥、证书的加密读取) │
├──────────────────────────────────────────────────────────────┤
│ Layer 4: 基础设施层 │
│ • MySQL 8.0(交易持久化、配置存储) │
│ • Redis幂等 Key 缓存、分布式锁、延迟队列) │
│ • 定时任务调度器cron 对账、状态补偿) │
│ • 结构化日志zerolog / zap
└──────────────────────────────────────────────────────────────┘
│ 渠道私有协议
┌────────────────────────▼─────────────────────────────────────┐
│ 上游支付渠道 │
│ 汇元支付 │ (预留微信直连) │ (预留支付宝直连) │
└──────────────────────────────────────────────────────────────┘
```
**分层理由**
- API 接入层与业务逻辑层分离,使鉴权、限流、协议转换等横切关注点集中处理,业务层保持纯粹。
- 渠道适配层独立为一层,而非嵌入业务层,使新增渠道无需改动任何业务代码,只新增一个适配器实现。
- 基础设施层统一封装,方便后续将 Redis 替换为其他实现,或扩展消息队列。
---
## 2. 核心模块划分
| 模块 | 职责 | 关键依赖 |
|-----|-----|--------|
| 交易核心Trade | 统一下单、查询、关单,维护订单状态机 | MySQL、Redis幂等、渠道适配层 |
| 退款Refund | 退款请求、退款状态跟踪、退款查询 | MySQL、渠道适配层 |
| 异步通知Notify | 接收上游回调验签、更新状态、通知下游、重试调度 | Redis 延迟队列、MySQL |
| 分润ProfitSharing | 分润配置读取、分账调用、退款回退分润 | 渠道适配层、MySQL |
| 服务费ServiceFee | 费率配置读取、服务费计算、分账划转 | 渠道适配层、MySQL |
| 收款匹配PaymentMatch | 接收固定账户入账通知、三维度匹配引擎 | MySQL、通知模块 |
| 对账Reconciliation | T+1 账单拉取、逐笔比对、差异入库 | 渠道适配层、MySQL |
| 商户进件Merchant | 进件提交、审核跟踪、状态异常检测 | 渠道适配层、MySQL |
| 微信通知WechatNotify | 微信绑定管理、模板消息推送 | 微信公众号 API、MySQL |
| 订单编码Sequence | 按 app_id 隔离的订单号生成 | MySQL悲观锁序列或 Redis原子自增 |
| 渠道适配Channel | 统一 interface、汇元适配器实现、渠道工厂 | 无外部依赖(纯逻辑) |
| 应用鉴权Auth | appId + appSecret 签名校验、应用信息缓存 | MySQL、Redis |
---
## 3. 中间件选型及理由
### 3.1 MySQL 8.0
**理由**:交易数据要求强一致性和 ACID 事务金融场景不接受数据丢失。MySQL 在团队熟悉度、生态(如 gorm、成本自建方面综合最优。单表千万级订单通过分区表或归档策略应对不需要引入分布式数据库的复杂性。
选 MySQL 而非 PostgreSQLPRD 明确列出 MySQL 8.0团队已有运维经验MySQL 在国内支付系统中更为普遍,社区案例更多。
### 3.2 Redis
Redis 在本系统中承担三个独立职责,每个职责对应不同的数据结构:
| 职责 | 数据结构 | 说明 |
|-----|---------|-----|
| 幂等 Key 缓存 | StringSET NX EX | 下单去重、回调幂等TTL 与订单超时一致 |
| 分布式锁 | StringRedlock 语义) | 防止并发退款、分润重复触发 |
| 异步通知延迟队列 | Sorted Setscore 为下次重试时间戳) | 实现指数退避重试,无需引入重量级 MQ |
**为什么不引入 RabbitMQ 或 Kafka**MVP 阶段通知 QPS 较低(< 500Redis Sorted Set 延迟队列已足够;引入独立 MQ 会增加运维复杂度和故障点。PRD 也明确将 Redis Stream 或 RabbitMQ 列为备选Redis Sorted Set 是更轻量的起点,后续可迁移。
### 3.3 Gin 框架
**理由**Go 生态最成熟的 HTTP 框架,性能优异(零 gc 路由),中间件生态完善,团队学习成本低。符合 PRD 的 P95 < 200ms 要求。
### 3.4 GORM v2
**理由**:提供类型安全的 ORM减少 SQL 拼接错误支持软删除、Hook 钩子方便审计日志Migration 能力支持版本化数据库变更。对于金融场景中需要精确控制的批量 SQL支持原生 SQL 直接执行。
---
## 4. 模块间交互方式
pay-bridge 采用**同进程函数调用**而非微服务拆分,理由是 MVP 阶段团队规模和交易量均不需要微服务的复杂性,单体部署运维更简单,且 Go 的并发模型goroutine天然支持高并发。
模块间交互规则:
1. **同步调用链**请求主路径API Handler → Service → Repository → DB。Service 层不互相直接调用,通过 interface 注入解耦。
2. **事件驱动**(异步后处理):支付成功后,通知服务、分润服务、服务费服务通过 Go channel 或直接异步 goroutine 触发,不阻塞主请求链路。
3. **重试队列**(延迟执行):通知重试写入 Redis Sorted Set独立的 goroutine 定时扫描到期任务并执行。
4. **定时任务**:对账、商户状态检测等 cron 任务由独立 goroutine 调度,与 HTTP 服务共进程但相互隔离。
```
HTTP Request
API Handler
│ 同步
Trade Service ──── Repository ──── MySQL
│ 异步goroutine
├──► Notify Service ──► Redis Sorted Set延迟重试
├──► ProfitSharing Service ──► Channel Adapter ──► 汇元分账 API
└──► ServiceFee Service ──► Channel Adapter ──► 汇元分账 API
```
---
## 5. 渠道适配器框架设计思路
渠道适配器是整个系统可扩展性的核心。设计原则:**对外暴露统一接口,对内封装渠道差异**。
### 5.1 统一接口抽象
定义 `PaymentChannel` interface所有渠道适配器必须实现此接口
```go
type PaymentChannel interface {
// Code 返回渠道编码
Code() string
// CreateOrder 下单,返回支付凭证
CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error)
// QueryOrder 查询订单状态
QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error)
// CloseOrder 关闭订单
CloseOrder(ctx context.Context, req *CloseOrderRequest) (*CloseOrderResponse, error)
// Refund 发起退款
Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error)
// QueryRefund 查询退款状态
QueryRefund(ctx context.Context, req *QueryRefundRequest) (*QueryRefundResponse, error)
// VerifyNotify 验证上游回调签名,返回解析后的通知数据
VerifyNotify(ctx context.Context, rawBody []byte, headers map[string]string) (*NotifyData, error)
// ProfitSharing 分账(渠道不支持时返回 ErrNotSupported
ProfitSharing(ctx context.Context, req *ProfitSharingRequest) (*ProfitSharingResponse, error)
// RollbackProfitSharing 回退分账
RollbackProfitSharing(ctx context.Context, req *RollbackSharingRequest) (*RollbackSharingResponse, error)
// DownloadBill 下载对账账单
DownloadBill(ctx context.Context, req *DownloadBillRequest) (*BillData, error)
// MerchantApply 商户进件
MerchantApply(ctx context.Context, req *MerchantApplyRequest) (*MerchantApplyResponse, error)
// QueryMerchantStatus 查询商户审核状态
QueryMerchantStatus(ctx context.Context, channelMerchantID string) (*MerchantStatusResponse, error)
}
```
### 5.2 渠道工厂与注册Registry 模式)
采用注册表Registry模式避免 switch-case 的硬编码,实现插件化设计:
```go
var registry = map[string]ChannelFactory{}
func Register(channelCode string, factory ChannelFactory) {
registry[channelCode] = factory
}
func GetChannel(channelCode string, config *ChannelConfig) (PaymentChannel, error) {
factory, ok := registry[channelCode]
if !ok {
return nil, ErrChannelNotFound
}
return factory(config), nil
}
```
每个渠道适配器包在 `init()` 中调用 `Register()` 完成自注册。新增渠道只需:新建包、实现 interface、调用 Register主程序 `import _ "pay-bridge/internal/channel/newchannel"` 即可接入,无需修改任何现有代码。
### 5.3 汇元适配器特殊处理
汇元支付HePay采用 RSA/MD5 双签名和 RSA+3DES 双层加密,适配器内部封装:
- **签名层**:根据渠道配置决定使用 RSA 还是 MD5对外透明。
- **加密层**:请求体使用 3DES 加密,密钥使用 RSA 公钥加密后随请求发送;响应体同样解密处理。
- **重试安全**:所有加解密操作幂等,同一请求多次加密结果不同但均有效。
---
## 6. 异步通知 + 重试机制方案
### 6.1 整体流程
```
上游回调 → 验签 → 更新交易状态DB 事务)→ 写入通知任务 → 立即第一次推送
推送成功 → 标记完成
推送失败 → 写入 Redis Sorted Set
score = now + 重试间隔
Poller goroutine 定时扫描
到期任务 → 执行推送
成功 → 删除任务
失败 → 更新 score下次重试时间
超过 8 次 → 标记「通知异常」
```
### 6.2 重试间隔设计
共 9 次推送机会(第 1 次立即,后续 8 次重试),重试间隔:
| 第 N 次推送 | 距上次等待时长 | 累计等待时间 |
|-----------|------------|-----------|
| 1 | 立即 | 0 |
| 2 | 15s | 15s |
| 3 | 30s | 45s |
| 4 | 1m | 1m45s |
| 5 | 5m | 6m45s |
| 6 | 30m | 36m45s |
| 7 | 1h | 1h36m45s |
| 8 | 6h | 7h36m45s |
| 9最终 | 12h | 19h36m45s |
重试策略参考微信支付官方规范,前期快速重试应对网络抖动,后期拉长间隔避免无效轰炸,最终兜底是下游主动查询。
### 6.3 幂等保障
通知发送前检查 `notify_log` 中的发送记录,避免 Poller 并发触发重复推送。同一通知任务加 Redis 分布式锁lock key = `notify_lock:{trade_no}`),保证同一时刻只有一个 goroutine 执行推送。
---
## 7. 幂等方案
系统在三个层面保障幂等,形成防重复操作的纵深防御:
### 7.1 下单幂等(防重复创建订单)
- **第一层 - Redis SET NX**:下单时以 `idempotent:{app_id}:{merchant_order_no}` 为 keySET NX EXTTL = 订单超时时间 + 缓冲)。命中则直接返回已有凭证,不进入后续流程。
- **第二层 - DB 唯一索引**`trade_order` 表在 `(app_id, merchant_order_no)` 上建唯一索引,作为 Redis 失效或缓存穿透时的兜底防护。
- **处理逻辑**:若幂等 key 已存在,直接查询并返回已有订单的支付凭证,不重新调用渠道。
### 7.2 回调幂等(防重复处理上游通知)
- 上游回调到达后,先以 `(channel_trade_no, status)` 查询 DB如已处理则直接返回成功。
- 更新 `trade_order.status` 使用**乐观锁**`WHERE status = 'PAYING' AND trade_no = ?`),确保状态只正向流转,并发更新时只有一个成功,其余幂等返回。
### 7.3 分润 / 服务费幂等(防重复分账)
- `profit_sharing_order``service_fee_log``trade_no` 上建唯一索引(同一笔交易只触发一次分账)。
- 调用渠道分账 API 前,以 `sharing_lock:{trade_no}` 加 Redis **分布式锁**TTL = 30s防止并发触发。
---
## 8. 关键技术决策点
### 8.1 单体还是微服务
**决策:单体**。理由团队规模小MVP 阶段交易量 < 500 QPS单体的运维复杂度远低于微服务Go 的 goroutine 模型天然支持高并发,单体完全能满足性能目标。架构上通过分层和 interface 保持模块边界清晰,未来有需要时可以沿模块边界拆分为微服务。
### 8.2 消息队列选型
**决策Redis Sorted Set 作为延迟队列**。理由:避免引入独立 MQ 组件RabbitMQ、Kafka增加运维负担Redis 已是必选基础设施Sorted Set 的 score 天然表达「下次执行时间」,实现简单可靠。当 QPS 超过 5000 或需要多消费者时再迁移到专业 MQ。
### 8.3 订单号策略
**决策:数据库序列(行级锁 + 自增)**,不用 UUID不用雪花 ID。理由UUID 无序,频繁插入导致 B+ 树碎片化;雪花 ID 依赖机器 ID 配置,多实例部署时配置复杂;数据库序列在 MySQL 层保证 app_id 维度的唯一递增,实现最简单,天然满足 PRD 中「不同应用独立编码序列」的要求。生成格式:`{prefix}{yyMMdd}{8位序号}`,例如 `PAY26022700000001`
### 8.4 密钥存储
**决策数据库加密存储AES-256-GCM 加密**。私钥、appSecret 等敏感字段以加密后的 Base64 字符串存入 MySQL加密密钥由环境变量注入不写入代码或配置文件。读取时在渠道配置仓储层解密上层服务不感知加密细节。
### 8.5 对账执行机制
**决策:进程内 cron + 数据库任务锁**。使用 `robfig/cron` 在进程内调度,多实例部署时通过 Redis SETNX 确保同一时刻只有一个实例执行对账,避免重复。不引入外部调度平台(如 XXL-Job保持轻量。
### 8.6 数据隔离粒度
**决策:以 app_id 为核心隔离维度**。订单、分润配置、服务费配置、订单编码序列均以 app_id 隔离。不同 app 的数据物理上在同一张表(通过 app_id 字段区分),通过数据库索引和应用层鉴权确保 A 应用无法访问 B 应用的数据。MVP 阶段不做分库分表,当单 app 订单量超过 5000 万后再考虑分表。
### 8.7 固定账户收款匹配策略
**决策:规则引擎(优先级匹配)而非 ML 模型**。三维度匹配(备注解析 > 金额精确匹配 > 付款方名称相似度)通过确定性规则实现,可解释、可审计、易排查。备注解析采用正则提取订单号,名称比较采用精确相等(不用模糊匹配),避免误匹配导致的资金风险。匹配降级策略:备注匹配优先,失败时降级为金额匹配,多候选时用名称缩小范围,无法确定时标记待人工确认。
---
## 9. 部署架构
```
┌─────────────────┐
│ Nginx / LB │
└────────┬────────┘
┌───────────────┼───────────────┐
│ │ │
┌─────────▼──────┐ ┌──────▼───────┐ ┌────▼──────────┐
│ pay-bridge │ │ pay-bridge │ │ pay-bridge │
│ instance 1 │ │ instance 2 │ │ instance 3 │
└────────┬───────┘ └──────┬───────┘ └────┬──────────┘
│ │ │
┌────────▼────────────────▼──────────────▼──────────┐
│ MySQL 8.0(主从复制) │
│ RedisSentinel / Cluster 模式) │
└───────────────────────────────────────────────────┘
```
**水平扩展能力保障**
- 无本地状态:所有实例状态通过 Redis 和 MySQL 共享,任意实例可处理任意请求。
- 幂等在 Redis/DB 层:多实例并发下单、重复回调均由 Redis SET NX 和 DB 唯一索引保障。
- 分布式锁在 Redis 层:分润、服务费分账操作通过 Redis 锁防止并发执行。
- 定时任务去重cron 任务通过 Redis SETNX 保证多实例中只有一个实例执行。
**高可用方案**
- MySQL 主从复制:写操作走主库,读操作可走从库(按需);主库故障时手动或自动切换。
- Redis Sentinel3 节点 Sentinel 监控,主节点故障自动切换,客户端感知切换。
- Nginx 负载均衡upstream 配置健康检查,实例故障自动摘除。
- 日志聚合:结构化 JSON 日志接入 ELK/Loki多实例日志统一查询。