# 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,多实例日志统一查询。