Go 实现生产级支付中心:模板方法+策略+工厂模式(优化版)

一、设计背景

支付中心是业务系统的核心模块,需要满足:

  1. 支持多支付方式(微信/支付宝/余额等)灵活扩展;
  2. 支付流程标准化,同时允许个性化定制;
  3. 生产级稳定性(熔断、幂等、异步);
  4. 可配置化,支持热更新。

本文基于 Go 语言,整合模板方法+策略+工厂模式,并落地熔断、幂等、异步、可配置等生产级优化点,最终实现一套高可用、高扩展的支付中心架构。

二、核心架构设计

核心模式说明

模式 作用 落地方式
模板方法 固定支付流程,开放自定义步骤 抽象基类定义流程,子类实现细节
策略模式 封装不同支付方式,支持动态切换 支付策略接口 + 具体实现类
简单工厂 统一创建支付实例,解耦创建逻辑 可配置工厂 + 懒加载 + 缓存

关键优化点说明

1. 幂等性校验

核心作用

  • 防止重复支付:支付请求因网络抖动、前端重复提交、回调重试等场景导致同一笔订单被多次扣款,是支付系统的核心容灾能力;
  • 保证数据一致性:通过唯一标识(订单号+支付方式)锁定支付请求,确保同一笔订单在支付流程中仅能被处理一次;
  • 降低资损风险:避免用户因重复支付产生资金损失,同时减少商户与支付平台的对账成本。

实现逻辑
基于 Redis 的 SET NX EX 指令实现(不存在则设置值并指定过期时间),以「订单号+支付方式编码」作为唯一 Key,支付请求处理前先校验该 Key 是否存在:

  • 存在:说明该订单已提交过支付请求,直接返回“请勿重复提交”;
  • 不存在:设置 Key 并赋予 5 分钟过期时间(覆盖单次支付的最长处理周期),允许继续处理支付流程;
  • 过期时间设计:避免因系统异常导致 Key 永久占用,同时保证正常支付流程有足够时间完成。

2. 动态配置

核心作用

  • 支持热更新:支付方式的新增/下线、限额调整、手续费率修改等无需重启服务,直接通过配置中心(Nacos/Apollo/数据库)修改后生效,提升系统可用性;
  • 降低维护成本:无需修改代码、重新编译部署,减少发布风险,尤其适合多环境(测试/预发/生产)的配置差异化管理;
  • 适配业务灵活调整:可根据业务需求动态调整支付方式的支持场景(如关闭H5端微信支付)、单笔限额(如节假日临时提高支付宝限额)等,无需侵入核心代码。

实现逻辑
将支付方式的核心配置(编码、名称、限额、手续费率等)从硬编码改为配置化存储,工厂类通过监听配置中心变更事件,动态更新本地配置缓存和支付实例缓存,实现“配置变更-实时生效”的闭环。

三、代码实现(完整可运行)

1. 基础依赖准备

先安装核心依赖(熔断、Redis、异步等):

# 熔断:sentinel-go
go get github.com/alibaba/sentinel-golang@latest
# Redis(幂等)
go get github.com/go-redis/redis/v8@latest
# 配置管理(动态配置)
go get github.com/spf13/viper@latest

2. 定义核心结构体与接口

2.1 支付请求/响应结构体

package payment

import (
"context"
"time"

"github.com/go-redis/redis/v8"
)

// PayRequest 支付请求参数
type PayRequest struct {
OrderID string // 订单号(幂等核心)
PayCode string // 支付方式编码(WECHAT/ALIPAY)
Amount float64 // 支付金额
UserID string // 用户ID
Scene string // 支付场景(APP/H5/PC)
ExtParams map[string]string // 扩展参数
}

// PayResponse 支付响应
type PayResponse struct {
Success bool // 是否成功
TradeNo string // 支付平台交易号
Msg string // 提示信息
PayTime time.Time // 支付时间
}

// PayMeta 支付方式元信息(动态配置载体)
type PayMeta struct {
PayCode string // 支付编码
Name string // 支付名称
MaxAmount float64 // 单笔限额(动态配置)
FeeRate float64 // 手续费率(动态配置)
NeedRealName bool // 是否需要实名(动态配置)
SupportScenes []string // 支持场景(动态配置)
}

// PayStrategy 支付策略接口(策略模式核心)
type PayStrategy interface {
// 模板方法:固定支付流程(禁止子类修改)
Pay(ctx context.Context, req *PayRequest) (*PayResponse, error)
// 自定义参数校验(子类实现)
ValidateCustomParam(ctx context.Context, req *PayRequest) error
// 实际支付接口调用(子类实现)
CallPayAPI(ctx context.Context, req *PayRequest) (*PayResponse, error)
// 结果通知(子类实现)
NotifyResult(ctx context.Context, req *PayRequest, resp *PayResponse) error
// 获取支付元信息
GetMeta() *PayMeta
}

2.2 模板方法抽象基类

package payment

import (
"context"
"errors"
"fmt"
"sync"
"time"

"github.com/alibaba/sentinel-golang/api"
"github.com/alibaba/sentinel-golang/core/base"
)

// BasePayTemplate 支付模板抽象基类(模板方法核心)
type BasePayTemplate struct {
meta *PayMeta // 支付元信息(动态配置)
redisClient *redis.Client // Redis客户端(幂等)
asyncPool *sync.WaitGroup // 异步池
}

// NewBasePayTemplate 初始化模板
func NewBasePayTemplate(meta *PayMeta, redisClient *redis.Client) *BasePayTemplate {
return &BasePayTemplate{
meta: meta,
redisClient: redisClient,
asyncPool: &sync.WaitGroup{},
}
}

// Pay 模板方法:固定支付流程(final,Go通过不暴露子类实现保证)
func (t *BasePayTemplate) Pay(ctx context.Context, req *PayRequest) (*PayResponse, error) {
// 1. 前置钩子(子类可选覆盖)
if err := t.BeforePay(ctx, req); err != nil {
return nil, fmt.Errorf("前置处理失败:%w", err)
}

// 2. 幂等性校验(核心容灾逻辑)
if err := t.CheckIdempotent(ctx, req); err != nil {
return nil, fmt.Errorf("幂等校验失败:%w", err)
}

// 3. 通用参数校验
if err := t.ValidateCommonParam(ctx, req); err != nil {
return nil, fmt.Errorf("通用参数校验失败:%w", err)
}

// 4. 自定义参数校验(子类实现)
if err := t.ValidateCustomParam(ctx, req); err != nil {
return nil, fmt.Errorf("自定义参数校验失败:%w", err)
}

// 5. 熔断保护下调用支付接口
resp, err := t.CallPayAPIWithSentinel(ctx, req)
if err != nil {
return nil, fmt.Errorf("支付接口调用失败:%w", err)
}

// 6. 可选的结果通知
if t.NeedNotifyResult(ctx, req) {
if err := t.NotifyResult(ctx, req, resp); err != nil {
// 通知失败不阻塞主流程,异步重试
t.asyncPool.Add(1)
go func() {
defer t.asyncPool.Done()
t.RetryNotify(ctx, req, resp)
}()
}
}

// 7. 异步记录日志
t.RecordLogAsync(ctx, req, resp)

// 8. 后置钩子(子类可选覆盖)
if err := t.AfterPay(ctx, req, resp); err != nil {
return resp, fmt.Errorf("后置处理失败:%w", err)
}

return resp, nil
}

// ------------------------------ 固定通用逻辑 ------------------------------
// ValidateCommonParam 通用参数校验
func (t *BasePayTemplate) ValidateCommonParam(ctx context.Context, req *PayRequest) error {
if req.OrderID == "" {
return errors.New("订单号不能为空")
}
if req.Amount <= 0 {
return errors.New("支付金额必须大于0")
}
if req.Amount > t.meta.MaxAmount {
return fmt.Errorf("超出%s单笔限额%.2f", t.meta.Name, t.meta.MaxAmount)
}
// 校验支持的场景(动态配置)
sceneValid := false
for _, s := range t.meta.SupportScenes {
if s == req.Scene {
sceneValid = true
break
}
}
if !sceneValid {
return fmt.Errorf("%s不支持%s场景", t.meta.Name, req.Scene)
}
return nil
}

// CheckIdempotent 幂等性校验(基于Redis)
func (t *BasePayTemplate) CheckIdempotent(ctx context.Context, req *PayRequest) error {
key := fmt.Sprintf("pay:idempotent:%s:%s", req.OrderID, req.PayCode)
// SET NX EX:不存在则设置,过期时间5分钟(防止Key永久占用)
ok, err := t.redisClient.SetNX(ctx, key, "1", 5*time.Minute).Result()
if err != nil {
return fmt.Errorf("Redis操作失败:%w", err)
}
if !ok {
return errors.New("请勿重复提交支付请求")
}
return nil
}

// CallPayAPIWithSentinel 熔断保护的支付接口调用
func (t *BasePayTemplate) CallPayAPIWithSentinel(ctx context.Context, req *PayRequest) (*PayResponse, error) {
// Sentinel资源名:支付编码+接口
resourceName := fmt.Sprintf("%s_pay_api", t.meta.PayCode)
// 熔断规则:失败率>50%,熔断5秒(需提前初始化Sentinel)
entry, err := api.Entry(resourceName, api.WithTrafficType(base.Inbound))
if err != nil {
return nil, errors.New("当前支付方式暂时不可用,请更换支付方式")
}
defer entry.Exit()

// 执行实际支付接口调用
return t.CallPayAPI(ctx, req)
}

// RecordLogAsync 异步记录日志
func (t *BasePayTemplate) RecordLogAsync(ctx context.Context, req *PayRequest, resp *PayResponse) {
t.asyncPool.Add(1)
go func() {
defer t.asyncPool.Done()
// 模拟日志写入(可替换为ES/数据库)
fmt.Printf("[异步日志] 订单:%s,支付方式:%s,金额:%.2f,结果:%t\n",
req.OrderID, req.PayCode, req.Amount, resp.Success)
}()
}

// RetryNotify 重试结果通知
func (t *BasePayTemplate) RetryNotify(ctx context.Context, req *PayRequest, resp *PayResponse) {
// 简单重试逻辑:最多3次,间隔1秒
for i := 0; i < 3; i++ {
err := t.NotifyResult(ctx, req, resp)
if err == nil {
fmt.Printf("[重试通知成功] 订单:%s,次数:%d\n", req.OrderID, i+1)
return
}
time.Sleep(1 * time.Second)
}
fmt.Printf("[重试通知失败] 订单:%s\n", req.OrderID)
}

// ------------------------------ 钩子方法(子类可选覆盖) ------------------------------
// BeforePay 前置处理(默认空实现)
func (t *BasePayTemplate) BeforePay(ctx context.Context, req *PayRequest) error {
return nil
}

// AfterPay 后置处理(默认空实现)
func (t *BasePayTemplate) AfterPay(ctx context.Context, req *PayRequest, resp *PayResponse) error {
return nil
}

// NeedNotifyResult 是否需要结果通知(默认需要)
func (t *BasePayTemplate) NeedNotifyResult(ctx context.Context, req *PayRequest) bool {
return true
}

// ------------------------------ 抽象方法(子类必须实现) ------------------------------
// ValidateCustomParam 自定义参数校验(子类实现)
func (t *BasePayTemplate) ValidateCustomParam(ctx context.Context, req *PayRequest) error {
panic("必须由子类实现")
}

// CallPayAPI 实际支付接口调用(子类实现)
func (t *BasePayTemplate) CallPayAPI(ctx context.Context, req *PayRequest) (*PayResponse, error) {
panic("必须由子类实现")
}

// NotifyResult 结果通知(子类实现)
func (t *BasePayTemplate) NotifyResult(ctx context.Context, req *PayRequest, resp *PayResponse) error {
panic("必须由子类实现")
}

// GetMeta 获取支付元信息(动态配置)
func (t *BasePayTemplate) GetMeta() *PayMeta {
return t.meta
}

3. 具体支付策略实现

3.1 微信支付实现

package payment

import (
"context"
"errors"
"fmt"
)

// WechatPay 微信支付策略
type WechatPay struct {
*BasePayTemplate // 继承模板方法
}

// NewWechatPay 初始化微信支付(加载动态配置)
func NewWechatPay(redisClient *redis.Client, meta *PayMeta) *WechatPay {
return &WechatPay{
BasePayTemplate: NewBasePayTemplate(meta, redisClient),
}
}

// ValidateCustomParam 微信支付自定义参数校验
func (w *WechatPay) ValidateCustomParam(ctx context.Context, req *PayRequest) error {
// 校验微信支付必填的openid(扩展参数中获取)
openID, ok := req.ExtParams["openid"]
if !ok || openID == "" {
return errors.New("微信支付需要传入openid")
}
// 校验实名(动态配置)
if w.meta.NeedRealName && req.ExtParams["real_name_verified"] != "true" {
return errors.New("微信支付需要实名验证")
}
return nil
}

// CallPayAPI 调用微信支付接口
func (w *WechatPay) CallPayAPI(ctx context.Context, req *PayRequest) (*PayResponse, error) {
// 模拟调用微信支付统一下单接口
fmt.Printf("调用微信支付接口:订单%s,金额%.2f,openid=%s\n",
req.OrderID, req.Amount, req.ExtParams["openid"])
// 模拟返回支付结果
return &PayResponse{
Success: true,
TradeNo: fmt.Sprintf("WX%s%d", req.OrderID, time.Now().Unix()),
Msg: "支付成功",
PayTime: time.Now(),
}, nil
}

// NotifyResult 微信支付结果通知
func (w *WechatPay) NotifyResult(ctx context.Context, req *PayRequest, resp *PayResponse) error {
// 模拟调用商户回调接口
fmt.Printf("微信支付结果通知:订单%s,交易号%s\n", req.OrderID, resp.TradeNo)
return nil
}

3.2 支付宝支付实现

package payment

import (
"context"
"errors"
"fmt"
)

// Alipay 支付宝支付策略
type Alipay struct {
*BasePayTemplate // 继承模板方法
}

// NewAlipay 初始化支付宝支付(加载动态配置)
func NewAlipay(redisClient *redis.Client, meta *PayMeta) *Alipay {
return &Alipay{
BasePayTemplate: NewBasePayTemplate(meta, redisClient),
}
}

// ValidateCustomParam 支付宝自定义参数校验
func (a *Alipay) ValidateCustomParam(ctx context.Context, req *PayRequest) error {
// 校验支付宝账号
aliAccount, ok := req.ExtParams["ali_account"]
if !ok || aliAccount == "" {
return errors.New("支付宝支付需要传入支付宝账号")
}
return nil
}

// CallPayAPI 调用支付宝支付接口
func (a *Alipay) CallPayAPI(ctx context.Context, req *PayRequest) (*PayResponse, error) {
// 模拟调用支付宝预下单接口
fmt.Printf("调用支付宝接口:订单%s,金额%.2f,账号=%s\n",
req.OrderID, req.Amount, req.ExtParams["ali_account"])
// 模拟返回支付结果
return &PayResponse{
Success: true,
TradeNo: fmt.Sprintf("ALI%s%d", req.OrderID, time.Now().Unix()),
Msg: "支付成功",
PayTime: time.Now(),
}, nil
}

// NotifyResult 支付宝结果通知
func (a *Alipay) NotifyResult(ctx context.Context, req *PayRequest, resp *PayResponse) error {
// 模拟调用商户回调接口
fmt.Printf("支付宝结果通知:订单%s,交易号%s\n", req.OrderID, resp.TradeNo)
return nil
}

4. 动态配置工厂实现

package payment

import (
"context"
"errors"
"fmt"
"sync"

"github.com/go-redis/redis/v8"
"github.com/spf13/viper"
)

// PayFactory 支付工厂(动态配置+懒加载)
type PayFactory struct {
redisClient *redis.Client
config *viper.Viper // 动态配置客户端
payConfig map[string]*PayMeta // 支付配置缓存
instanceMap map[string]PayStrategy // 支付实例缓存
lock sync.RWMutex // 读写锁(保证并发安全)
}

// NewPayFactory 初始化工厂
func NewPayFactory(redisClient *redis.Client, configFile string) (*PayFactory, error) {
v := viper.New()
v.SetConfigFile(configFile)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("加载配置文件失败:%w", err)
}

// 初始化配置缓存
payConfig := make(map[string]*PayMeta)
if err := v.UnmarshalKey("payment", &payConfig); err != nil {
return nil, fmt.Errorf("解析支付配置失败:%w", err)
}

// 监听配置变更(动态更新)
v.WatchConfig()
factory := &PayFactory{
redisClient: redisClient,
config: v,
payConfig: payConfig,
instanceMap: make(map[string]PayStrategy),
}

// 配置变更回调
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("支付配置发生变更,开始重新加载")
factory.lock.Lock()
defer factory.lock.Unlock()
// 重新解析配置
newConfig := make(map[string]*PayMeta)
if err := v.UnmarshalKey("payment", &newConfig); err != nil {
fmt.Printf("重新加载配置失败:%v\n", err)
return
}
factory.payConfig = newConfig
// 清空实例缓存,下次获取时重新创建
factory.instanceMap = make(map[string]PayStrategy)
fmt.Println("支付配置重新加载完成")
})

return factory, nil
}

// GetPayInstance 获取支付实例(懒加载+动态配置)
func (f *PayFactory) GetPayInstance(ctx context.Context, payCode string) (PayStrategy, error) {
// 先读缓存(读锁)
f.lock.RLock()
if instance, ok := f.instanceMap[payCode]; ok {
f.lock.RUnlock()
return instance, nil
}
f.lock.RUnlock()

// 缓存未命中,写锁创建
f.lock.Lock()
defer f.lock.Unlock()

// 二次检查(防止并发创建)
if instance, ok := f.instanceMap[payCode]; ok {
return instance, nil
}

// 从动态配置获取支付元信息
meta, ok := f.payConfig[payCode]
if !ok {
return nil, fmt.Errorf("不支持的支付方式:%s", payCode)
}

// 创建支付实例
var instance PayStrategy
switch payCode {
case "WECHAT":
instance = NewWechatPay(f.redisClient, meta)
case "ALIPAY":
instance = NewAlipay(f.redisClient, meta)
default:
return nil, errors.New("未实现的支付方式")
}

// 存入缓存
f.instanceMap[payCode] = instance
return instance, nil
}

// UpdatePayMeta 动态更新支付配置(手动触发)
func (f *PayFactory) UpdatePayMeta(payCode string, meta *PayMeta) {
f.lock.Lock()
defer f.lock.Unlock()
f.payConfig[payCode] = meta
// 清空该支付方式的实例缓存
delete(f.instanceMap, payCode)
}

5. 配置文件示例(config.yaml)

payment:
WECHAT:
PayCode: "WECHAT"
Name: "微信支付"
MaxAmount: 50000.00
FeeRate: 0.006
NeedRealName: true
SupportScenes: ["APP", "H5", "MINI_PROGRAM"]
ALIPAY:
PayCode: "ALIPAY"
Name: "支付宝支付"
MaxAmount: 100000.00
FeeRate: 0.0055
NeedRealName: true
SupportScenes: ["APP", "H5", "PC"]

6. 测试代码

package main

import (
"context"
"fmt"
"payment"

"github.com/go-redis/redis/v8"
)

func main() {
// 初始化Redis(幂等校验)
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})

// 初始化支付工厂(加载动态配置)
factory, err := payment.NewPayFactory(redisClient, "config.yaml")
if err != nil {
fmt.Printf("初始化工厂失败:%v\n", err)
return
}

// 构造支付请求
req := &payment.PayRequest{
OrderID: "ORDER_123456",
PayCode: "WECHAT",
Amount: 100.00,
UserID: "USER_789",
Scene: "H5",
ExtParams: map[string]string{
"openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
"real_name_verified": "true",
},
}

// 获取微信支付实例
wechatPay, err := factory.GetPayInstance(context.Background(), req.PayCode)
if err != nil {
fmt.Printf("获取支付实例失败:%v\n", err)
return
}

// 执行支付
resp, err := wechatPay.Pay(context.Background(), req)
if err != nil {
fmt.Printf("支付失败:%v\n", err)
return
}
fmt.Printf("支付成功:%+v\n", resp)

// 测试幂等性(重复提交)
_, err = wechatPay.Pay(context.Background(), req)
if err != nil {
fmt.Printf("重复支付校验:%v\n", err) // 输出:幂等校验失败:请勿重复提交支付请求
}
}

四、核心优化点总结

  1. 幂等性校验:基于Redis的SET NX EX指令实现,防止重复支付,是支付系统的核心容灾能力,可避免资损和数据不一致问题;
  2. 动态配置:通过Viper实现配置热更新,支付方式的限额、支持场景等参数可实时调整,无需重启服务,提升系统可用性和维护效率;
  3. 模板方法+策略组合:模板固定支付流程保证标准化,策略模式封装不同支付方式实现灵活扩展,兼顾规范性和扩展性;
  4. 熔断+异步:熔断保护避免单个支付方式故障影响整体,异步处理日志/通知提升主流程响应速度,保证生产级稳定性。