draft
This commit is contained in:
344
docs/approach-design.md
Normal file
344
docs/approach-design.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# 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 而非 PostgreSQL:PRD 明确列出 MySQL 8.0,团队已有运维经验;MySQL 在国内支付系统中更为普遍,社区案例更多。
|
||||
|
||||
### 3.2 Redis
|
||||
|
||||
Redis 在本系统中承担三个独立职责,每个职责对应不同的数据结构:
|
||||
|
||||
| 职责 | 数据结构 | 说明 |
|
||||
|-----|---------|-----|
|
||||
| 幂等 Key 缓存 | String(SET NX EX) | 下单去重、回调幂等,TTL 与订单超时一致 |
|
||||
| 分布式锁 | String(Redlock 语义) | 防止并发退款、分润重复触发 |
|
||||
| 异步通知延迟队列 | Sorted Set(score 为下次重试时间戳) | 实现指数退避重试,无需引入重量级 MQ |
|
||||
|
||||
**为什么不引入 RabbitMQ 或 Kafka**:MVP 阶段通知 QPS 较低(< 500),Redis 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}` 为 key,SET NX EX(TTL = 订单超时时间 + 缓冲)。命中则直接返回已有凭证,不进入后续流程。
|
||||
- **第二层 - 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(主从复制) │
|
||||
│ Redis(Sentinel / Cluster 模式) │
|
||||
└───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**水平扩展能力保障**:
|
||||
|
||||
- 无本地状态:所有实例状态通过 Redis 和 MySQL 共享,任意实例可处理任意请求。
|
||||
- 幂等在 Redis/DB 层:多实例并发下单、重复回调均由 Redis SET NX 和 DB 唯一索引保障。
|
||||
- 分布式锁在 Redis 层:分润、服务费分账操作通过 Redis 锁防止并发执行。
|
||||
- 定时任务去重:cron 任务通过 Redis SETNX 保证多实例中只有一个实例执行。
|
||||
|
||||
**高可用方案**:
|
||||
|
||||
- MySQL 主从复制:写操作走主库,读操作可走从库(按需);主库故障时手动或自动切换。
|
||||
- Redis Sentinel:3 节点 Sentinel 监控,主节点故障自动切换,客户端感知切换。
|
||||
- Nginx 负载均衡:upstream 配置健康检查,实例故障自动摘除。
|
||||
- 日志聚合:结构化 JSON 日志接入 ELK/Loki,多实例日志统一查询。
|
||||
Reference in New Issue
Block a user