gopay

package module
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 0 Imported by: 0

README

go-pay

go-pay 是一个 Go 支付业务统一抽象层——在官方/主流 SDK 之上提供渠道无关的统一下单、查询、退款、通知 API,附带 Channel 注册路由与聚合二维码编排能力。它不是 SDK 的薄包装,而是把多家支付渠道的差异收敛在 provider 层,让业务代码只面向统一接口。

底层 SDK 选型:

当前提供的核心能力:

  • 统一下单 / 订单查询 / 关闭订单
  • 退款 / 退款查询(v1.1.0+)
  • 异步通知解析与应答(含退款通知,v1.1.0+)
  • 聚合二维码编排(v1.2.0+)

项目内部通过 paymgr.Manager 管理不同支付渠道,业务方只需要:

  1. 初始化各渠道 Provider
  2. 注册到 Manager
  3. 按渠道调用统一方法

升级到 v1.4.0 注意:v1.3.x 曾将支付宝底层 SDK 短暂切换为 go-pay/gopay/alipay/v3,导致普通公钥模式被软降级为 paymgr.ErrNotSupported。v1.4.0 路线调整:经核实 smartwalle/alipay/v3 仍在活跃维护、且对本项目核心场景覆盖完整,遂切回 smartwalle,普通公钥模式恢复支持——下游公钥模式商户号无需切换证书即可继续工作。alipay.NewProvider / alipay.Config / alipay.WithXxx 公开 API 100% 不变;保留 v1.3.x 引入的 JSAPI product_code / op_app_id 修复 + QueryRefund 退款单不存在边界修复。

升级到 v1.1.0 注意paymgr.Provider 接口新增 QueryRefundParseRefundNotify 两个方法。 官方 alipay / wechat 两个实现已就位,但若你在项目里自定义实现过 Provider,升级后会编译失败,需要补齐这两个方法。

完整变更见 CHANGELOG.md

1. 安装

go get github.com/gtkit/go-pay

要求:

  • Go 1.26+
  • 已开通微信支付 / 支付宝商户能力
  • 已准备好商户私钥、平台证书、公钥等支付材料

2. 项目结构

.
├── aggregate/       # 聚合二维码支付编排
├── alipay/         # 支付宝实现
├── wechat/         # 微信支付实现
├── paymgr/         # 统一抽象层
└── example/        # 简单 HTTP 示例

包职责:

  • paymgr:统一的请求、响应、错误和管理器接口
  • wechat:微信支付 Provider
  • alipay:支付宝 Provider
  • aggregate:聚合二维码入口分流与真实支付单编排

3. 支持的渠道与交易类型

3.1 支付渠道

在代码里使用以下常量:

paymgr.ChannelWechat   // 值为 "wxpay"      微信 V3
paymgr.ChannelAlipay   // 值为 "alipay"     支付宝
paymgr.ChannelWechatV2 // 值为 "wxpayv2"    微信 V2(XML 协议),由内置 wechat/v2 包实现,见第 14 章
3.2 交易类型
paymgr.TradeTypeNative // 扫码支付
paymgr.TradeTypeJSAPI  // JSAPI / 小程序 / 公众号场景
paymgr.TradeTypeApp    // APP 支付
paymgr.TradeTypeH5     // H5 支付
paymgr.TradeTypePage   // PC 网页支付 / 支付宝收银台

当前实现支持情况:

渠道 支持的交易类型
微信支付 V3 appnativejsapih5
微信支付 V2 appnativejsapih5
支付宝 nativejsapiapph5page

如果传入未实现的类型,会返回 paymgr.ErrUnsupportedType

3.3 下单字段与返回矩阵
渠道 交易类型 适用场景 额外必填字段 重点返回字段
微信支付 app 微信开放平台 APP 支付 AppParams
微信支付 jsapi 公众号 / 小程序支付 OpenID PrepayIDJSAPIParams
微信支付 native PC 或收银台扫码支付 CodeURL
微信支付 h5 移动浏览器 H5 支付 ClientIP H5URL
支付宝 app 支付宝 APP 支付 AppParams
支付宝 jsapi 支付宝小程序支付 视业务传 OpenID 作为 buyer_id PrepayID
支付宝 native 当面付扫码支付 CodeURL
支付宝 h5 手机网站支付 建议传 ReturnURL PayURL
支付宝 page PC 收银台页面支付 建议传 ReturnURL PayURL

聚合二维码入口由 aggregate.Service 编排:微信环境走微信 jsapi,支付宝移动端走支付宝 h5,支付宝 PC 端走支付宝 page,普通浏览器需要业务页面先选择渠道。

4. 统一接入流程

最典型的接入顺序如下:

ctx := context.Background()

mgr := paymgr.NewManager()

wechatProvider, err := wechat.NewProvider(
	ctx,
	wechat.WithAppID("wx1234567890abcdef"),
	wechat.WithMerchant(
		"1900000001",
		"3775B6A45ACD588826D15E583A95F5DD********",
		"your-apiv3-key-32-characters-long",
	),
	wechat.WithMerchantPrivateKeyPath("/path/to/apiclient_key.pem"),
	wechat.WithPlatformCertificatePath("/path/to/wechatpay_cert.pem"),
)
if err != nil {
	return err
}
mgr.Register(wechatProvider)

alipayProvider, err := alipay.NewProvider(
	alipay.WithAppID("2021000000000001"),
	alipay.WithPrivateKeyPath("/path/to/alipay_app_private_key.pem"),
	alipay.WithProduction(true),
	alipay.WithCertModePaths(
		"/path/to/appCertPublicKey.crt",
		"/path/to/alipayRootCert.crt",
		"/path/to/alipayCertPublicKey_RSA2.crt",
	),
)
if err != nil {
	return err
}
mgr.Register(alipayProvider)

后续所有能力都通过 mgr 调用:

resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelWechat, req)
queryResp, err := mgr.QueryOrder(ctx, paymgr.ChannelAlipay, queryReq)
err = mgr.CloseOrder(ctx, paymgr.ChannelWechat, closeReq)
refundResp, err := mgr.Refund(ctx, paymgr.ChannelAlipay, refundReq)
notifyResult, err := mgr.ParseNotify(ctx, paymgr.ChannelWechat, r)
err = mgr.ACKNotify(paymgr.ChannelWechat, w)

5. 怎么添加配置

本项目没有内置读取 YAML / TOML / JSON / ENV 的逻辑。接入方仍然需要先把自己的配置文件读到业务配置结构体里,再传给支付库。

也就是说:

  • 你自己的项目负责“从配置中心 / 环境变量 / 配置文件读取”
  • go-pay 负责“校验并使用这些配置初始化支付客户端”

当前推荐的接入方式是函数选项模式:

  • 微信:wechat.NewProvider(ctx, wechat.WithXXX(...), ...)
  • 支付宝:alipay.NewProvider(alipay.WithXXX(...), ...)

这样有几个好处:

  • 初始化代码可读性更好
  • 配置来源更灵活,路径、原始 PEM 文本、已解析对象都可以分别设置
  • 不会把所有字段都堆在一个大结构体里

同时为了兼容旧用法,*wechat.Config*alipay.Config 仍然可以直接传给 NewProvider(...),或者使用 NewProviderWithConfig(...)

5.1 配置打印自动脱敏

wechat.Configwechat/v2.Configalipay.Config 均实现了 fmt.Stringer / fmt.GoStringer:用 %v%+v%s%#v 打印(含写日志)时输出脱敏摘要——私钥、API 密钥显示为 "****",证书等大块内容仅标注 <set>,AppID、商户号、文件路径等排障字段原样保留。

注意边界:脱敏只对 fmt 系列打印生效,json.Marshal(cfg) 或反射遍历仍会暴露字段原文,请勿将 Config 序列化输出。

下面分别说明微信和支付宝的配置方式。

6. 微信支付配置

6.1 推荐方式:函数选项模式

推荐写法:

provider, err := wechat.NewProvider(
	ctx,
	wechat.WithAppID(cfg.Pay.Wechat.AppID),
	wechat.WithMerchant(
		cfg.Pay.Wechat.MchID,
		cfg.Pay.Wechat.MchCertSerialNumber,
		cfg.Pay.Wechat.MchAPIv3Key,
	),
	wechat.WithMerchantPrivateKeyPath(cfg.Pay.Wechat.MchPrivateKeyPath),
	wechat.WithPlatformCertificatePath(cfg.Pay.Wechat.WechatPayCertificatePath),
)

当前可用的主要选项:

选项 说明
wechat.WithAppID(appID) 设置微信应用 appid
wechat.WithMerchant(mchID, serial, apiV3Key) 设置商户号、商户证书序列号、APIv3 Key
wechat.WithMerchantPrivateKeyPath(path) 通过文件路径设置商户私钥
wechat.WithMerchantPrivateKeyPEM(pem) 通过 PEM 文本设置商户私钥
wechat.WithMerchantPrivateKey(key) 直接设置已解析的私钥对象
wechat.WithPlatformCertificatePath(path) 通过文件路径设置微信支付平台证书
wechat.WithPlatformCertificatePEM(pem) 通过 PEM 文本设置微信支付平台证书
wechat.WithPlatformCertificate(cert) 直接设置已解析的平台证书对象
wechat.WithPublicKeyID(keyID) 设置微信支付公钥 ID(形如 PUB_KEY_ID_xxx
wechat.WithPublicKeyPath(path) 通过文件路径设置微信支付公钥
wechat.WithPublicKeyPEM(pem) 通过 PEM 文本设置微信支付公钥
wechat.WithPublicKey(key) 直接设置已解析的公钥对象

验签模式自动切换:配置了公钥 ID 与公钥来源之一时,自动启用「微信支付公钥」验签(适用于 2024 年起只下发公钥的新进件商户);否则沿用平台证书模式。API 请求侧两种模式互斥,公钥优先。注意:公钥模式下商户私钥与商户证书序列号仍必填(用于请求签名)。

平台证书 → 公钥灰度迁移:存量商户从平台证书切换到公钥期间,同时配置公钥与平台证书即可让回调通知自动组合验签——平台证书签名的存量回调与公钥签名的新回调均可通过(对应官方要求的灰度期双验签)。完全切换后移除平台证书配置,即回到纯公钥验签,不再依赖证书下载接口。

凭据轮换限制:平台证书模式(含迁移期配置)的证书下载器在进程内按商户号全局复用;更换 APIv3 密钥、商户私钥或商户证书序列号后必须重启进程,否则证书下载与回调解密仍使用旧凭据。

6.2 必填信息

微信初始化必须提供这些信息:

是否必填 说明
AppID 微信应用 appid
MchID 微信商户号
MchCertSerialNumber 商户证书序列号
MchAPIv3Key APIv3 密钥,主要用于敏感信息解密和回调解密,必须正好 32 字节
商户私钥 三选一 路径 / PEM 文本 / *rsa.PrivateKey
平台证书 公钥 至少其一 平台证书:路径 / PEM 文本 / *x509.Certificate;公钥:公钥 ID + 路径 / PEM 文本 / *rsa.PublicKey同时配置两者即启用灰度迁移期回调组合验签(见 6.1 说明)
6.3 配置文件映射示例

如果你自己的项目配置文件是这样的:

pay:
  wechat:
    app_id: wx1234567890abcdef
    mch_id: "1900000001"
    mch_cert_serial_number: "3775B6A45ACD588826D15E583A95F5DD********"
    mch_apiv3_key: "your-apiv3-key-32-characters-long"
    mch_private_key_path: "/data/keys/wechat/apiclient_key.pem"
    wechatpay_certificate_path: "/data/keys/wechat/wechatpay_cert.pem"

那你在业务代码里可以这样组装:

wechatProvider, err := wechat.NewProvider(
	ctx,
	wechat.WithAppID(cfg.Pay.Wechat.AppID),
	wechat.WithMerchant(
		cfg.Pay.Wechat.MchID,
		cfg.Pay.Wechat.MchCertSerialNumber,
		cfg.Pay.Wechat.MchAPIv3Key,
	),
	wechat.WithMerchantPrivateKeyPath(cfg.Pay.Wechat.MchPrivateKeyPath),
	wechat.WithPlatformCertificatePath(cfg.Pay.Wechat.WechatPayCertificatePath),
)
if err != nil {
	return fmt.Errorf("init wechat provider: %w", err)
}
6.4 兼容方式:结构体配置

如果你更喜欢先组装结构体,也可以:

wechatProvider, err := wechat.NewProvider(ctx, &wechat.Config{
	AppID:                    cfg.Pay.Wechat.AppID,
	MchID:                    cfg.Pay.Wechat.MchID,
	MchCertSerialNumber:      cfg.Pay.Wechat.MchCertSerialNumber,
	MchAPIv3Key:              cfg.Pay.Wechat.MchAPIv3Key,
	MchPrivateKeyPath:        cfg.Pay.Wechat.MchPrivateKeyPath,
	WechatPayCertificatePath: cfg.Pay.Wechat.WechatPayCertificatePath,
})

或者显式写成:

wechatProvider, err := wechat.NewProviderWithConfig(ctx, &wechat.Config{...})
6.5 回调证书说明

wechat.NewProvider 初始化时不仅会初始化商户侧 client,还会初始化通知验签处理器 notify.Handler

和之前不同的是,微信平台证书现在已经正式进入配置项,不需要再去改源码里的固定路径。

例如:

wechat.WithPlatformCertificatePath("/path/to/wechatpay_cert.pem")
6.6 微信支持的方法

微信 Provider 当前实现的方法:

  • UnifiedOrder
  • QueryOrder
  • CloseOrder
  • Refund
  • QueryRefund
  • ParseNotify
  • ParseRefundNotify(微信独立的退款异步通知)
  • ACKNotify

其中下单只支持:

  • paymgr.TradeTypeApp
  • paymgr.TradeTypeJSAPI
  • paymgr.TradeTypeNative
  • paymgr.TradeTypeH5

7. 支付宝配置

7.1 推荐方式:函数选项模式

推荐写法:

provider, err := alipay.NewProvider(
	alipay.WithAppID(cfg.Pay.Alipay.AppID),
	alipay.WithPrivateKeyPath(cfg.Pay.Alipay.PrivateKeyPath),
	alipay.WithProduction(cfg.Pay.Alipay.IsProduction),
	alipay.WithCertModePaths(
		cfg.Pay.Alipay.AppCertPublicKeyPath,
		cfg.Pay.Alipay.AlipayRootCertPath,
		cfg.Pay.Alipay.AlipayCertPublicKeyPath,
	),
)

当前可用的主要选项:

选项 说明
alipay.WithAppID(appID) 设置支付宝应用 ID
alipay.WithProduction(bool) 设置生产或沙箱环境
alipay.WithPrivateKey(key) 直接设置应用私钥内容
alipay.WithPrivateKeyPath(path) 通过文件路径设置应用私钥
alipay.WithCertMode(appCert, rootCert, alipayCert) 通过证书内容启用证书模式
alipay.WithCertModePaths(appCertPath, rootCertPath, alipayCertPath) 通过证书路径启用证书模式
alipay.WithAlipayPublicKey(publicKey) 使用普通公钥模式
7.2 必填信息
是否必填 说明
AppID 支付宝应用 ID
PrivateKeyPrivateKeyPath 应用私钥内容或文件路径
IsProduction true 生产,false 沙箱
证书模式三件套 证书模式必填 应用公钥证书、支付宝根证书、支付宝公钥证书
AlipayPublicKey 普通公钥模式必填 当不使用证书模式时必填
7.3 两种配置模式
方式一:证书模式(推荐)

支付宝官方 2018 年起推荐使用证书模式(更安全、支持证书自动续期)。只要应用公钥证书、支付宝根证书、支付宝公钥证书三项都提供,就会走证书模式。

证书在支付宝商户后台「开发设置 → 接口加签方式(公钥证书)」处生成下载。

示例:

alipayProvider, err := alipay.NewProvider(
	alipay.WithAppID(cfg.Pay.Alipay.AppID),
	alipay.WithPrivateKeyPath(cfg.Pay.Alipay.PrivateKeyPath),
	alipay.WithProduction(true),
	alipay.WithCertModePaths(
		cfg.Pay.Alipay.AppCertPublicKeyPath,    // 应用公钥证书
		cfg.Pay.Alipay.AlipayRootCertPath,      // 支付宝根证书
		cfg.Pay.Alipay.AlipayCertPublicKeyPath, // 支付宝公钥证书
	),
)
if err != nil {
	return fmt.Errorf("init alipay provider: %w", err)
}
方式二:普通公钥模式

如果不使用证书模式,则必须提供 AlipayPublicKey

alipayProvider, err := alipay.NewProvider(
	alipay.WithAppID(cfg.Pay.Alipay.AppID),
	alipay.WithPrivateKeyPath(cfg.Pay.Alipay.PrivateKeyPath),
	alipay.WithProduction(false),
	alipay.WithAlipayPublicKey(cfg.Pay.Alipay.AlipayPublicKey),
)
if err != nil {
	return fmt.Errorf("init alipay provider: %w", err)
}
7.4 兼容方式:结构体配置

如果你希望继续使用结构体,也可以:

alipayProvider, err := alipay.NewProvider(&alipay.Config{
	AppID:                   cfg.Pay.Alipay.AppID,
	PrivateKeyPath:          cfg.Pay.Alipay.PrivateKeyPath,
	IsProduction:            cfg.Pay.Alipay.IsProduction,
	AppCertPublicKeyPath:    cfg.Pay.Alipay.AppCertPublicKeyPath,
	AlipayRootCertPath:      cfg.Pay.Alipay.AlipayRootCertPath,
	AlipayCertPublicKeyPath: cfg.Pay.Alipay.AlipayCertPublicKeyPath,
	AlipayPublicKey:         cfg.Pay.Alipay.AlipayPublicKey,
})

或者显式调用:

alipayProvider, err := alipay.NewProviderWithConfig(&alipay.Config{...})
7.5 支付宝支持的方法

支付宝 Provider 当前实现的方法:

  • UnifiedOrder
  • QueryOrder
  • CloseOrder
  • Refund
  • QueryRefund
  • ParseNotify(当 GmtRefundRefundFee 非空时,TradeStatus 会映射为 TradeStatusRefunded
  • ParseRefundNotify(支付宝无独立退款通知端点,本方法直接返回 paymgr.ErrNotSupported;请使用 ParseNotify 识别退款事件)
  • ACKNotify

下单支持的交易类型:

  • paymgr.TradeTypeNative
  • paymgr.TradeTypeJSAPI
  • paymgr.TradeTypeApp
  • paymgr.TradeTypeH5
  • paymgr.TradeTypePage

8. 初始化并注册 Provider

完整示例:

package main

import (
	"context"
	"fmt"

	"github.com/gtkit/go-pay/alipay"
	"github.com/gtkit/go-pay/paymgr"
	"github.com/gtkit/go-pay/wechat"
)

func InitPay(ctx context.Context, cfg *Config) (*paymgr.Manager, error) {
	mgr := paymgr.NewManager()

	wechatProvider, err := wechat.NewProvider(
		ctx,
		wechat.WithAppID(cfg.Pay.Wechat.AppID),
		wechat.WithMerchant(
			cfg.Pay.Wechat.MchID,
			cfg.Pay.Wechat.MchCertSerialNumber,
			cfg.Pay.Wechat.MchAPIv3Key,
		),
		wechat.WithMerchantPrivateKeyPath(cfg.Pay.Wechat.MchPrivateKeyPath),
		wechat.WithPlatformCertificatePath(cfg.Pay.Wechat.WechatPayCertificatePath),
	)
	if err != nil {
		return nil, fmt.Errorf("init wechat provider: %w", err)
	}
	mgr.Register(wechatProvider)

	alipayProvider, err := alipay.NewProvider(
		alipay.WithAppID(cfg.Pay.Alipay.AppID),
		alipay.WithPrivateKeyPath(cfg.Pay.Alipay.PrivateKeyPath),
		alipay.WithProduction(cfg.Pay.Alipay.IsProduction),
		alipay.WithCertModePaths(
			cfg.Pay.Alipay.AppCertPublicKeyPath,
			cfg.Pay.Alipay.AlipayRootCertPath,
			cfg.Pay.Alipay.AlipayCertPublicKeyPath,
		),
	)
	if err != nil {
		return nil, fmt.Errorf("init alipay provider: %w", err)
	}
	mgr.Register(alipayProvider)

	return mgr, nil
}

可用辅助方法:

  • mgr.Register(p):注册渠道
  • mgr.Deregister(ch):取消注册
  • mgr.Provider(ch):获取底层 Provider
  • mgr.Channels():列出已注册渠道

9. 怎么调用需要的方法

这里按业务里最常见的几个方法分别说明。

9.1 统一下单 UnifiedOrder

方法签名:

func (m *Manager) UnifiedOrder(ctx context.Context, ch Channel, req *UnifiedOrderRequest) (*UnifiedOrderResponse, error)

请求结构:

type UnifiedOrderRequest struct {
	OutTradeNo  string
	TotalAmount int64
	Subject     string
	TradeType   TradeType
	NotifyURL   string
	ReturnURL   string
	ClientIP    string
	OpenID      string
	ExpireAt    time.Time
	Metadata    map[string]string
}

字段说明:

字段 必填 说明
OutTradeNo 商户订单号,必须唯一
TotalAmount 金额,单位分
Subject 商品描述
TradeType 视场景 交易类型
NotifyURL 异步通知地址
ReturnURL 支付宝 H5 / PC 页面支付同步跳转地址
ClientIP 某些渠道场景需要 用户 IP;微信 H5 必填
OpenID 某些渠道场景需要 微信 JSAPI 或支付宝买家标识场景
ExpireAt 订单过期时间;支付宝渠道要求距今至少 1 分钟,否则返回 ErrInvalidParam
Metadata 附加数据,回调时会带回
微信 APP 下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelWechat, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250001",
	TotalAmount: 100,
	Subject:     "VIP月卡",
	TradeType:   paymgr.TradeTypeApp,
	NotifyURL:   "https://api.example.com/pay/notify/wechat",
	ExpireAt:    time.Now().Add(30 * time.Minute),
	Metadata: map[string]string{
		"uid": "10001",
	},
})
if err != nil {
	return err
}

// 微信 APP 支付时重点取这个字段
appParams := resp.AppParams

返回值重点字段:

  • resp.PrepayID
  • resp.AppParams

其中 AppParams 是 JSON 字符串,APP 端解析后传给微信 SDK。

微信 JSAPI 下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelWechat, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250005",
	TotalAmount: 100,
	Subject:     "小程序订单",
	TradeType:   paymgr.TradeTypeJSAPI,
	NotifyURL:   "https://api.example.com/pay/notify/wechat",
	OpenID:      "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
})
if err != nil {
	return err
}

jsapiParams := resp.JSAPIParams

重点返回:

  • resp.PrepayID
  • resp.JSAPIParams:JSON 字符串,前端解析后用于调起微信 JSAPI / 小程序支付
微信 H5 下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelWechat, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250006",
	TotalAmount: 100,
	Subject:     "微信H5订单",
	TradeType:   paymgr.TradeTypeH5,
	NotifyURL:   "https://api.example.com/pay/notify/wechat",
	ClientIP:    "203.0.113.10",
})
if err != nil {
	return err
}

h5URL := resp.H5URL

重点返回:

  • resp.H5URL:微信 H5 拉起支付链接

当前实现会按最常见的移动浏览器场景构造 scene_info.h5_info.type = "Wap"

微信 Native 扫码下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelWechat, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250002",
	TotalAmount: 100,
	Subject:     "扫码支付订单",
	TradeType:   paymgr.TradeTypeNative,
	NotifyURL:   "https://api.example.com/pay/notify/wechat",
})
if err != nil {
	return err
}

codeURL := resp.CodeURL

重点返回:

  • resp.CodeURL:二维码内容
支付宝 APP 下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelAlipay, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250003",
	TotalAmount: 100,
	Subject:     "支付宝APP订单",
	TradeType:   paymgr.TradeTypeApp,
	NotifyURL:   "https://api.example.com/pay/notify/alipay",
})
if err != nil {
	return err
}

orderString := resp.AppParams

重点返回:

  • resp.AppParams:支付宝签名后的订单字符串,APP 端直接调用 SDK
支付宝 H5 下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelAlipay, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250004",
	TotalAmount: 100,
	Subject:     "支付宝H5订单",
	TradeType:   paymgr.TradeTypeH5,
	NotifyURL:   "https://api.example.com/pay/notify/alipay",
	ReturnURL:   "https://www.example.com/pay/return",
})
if err != nil {
	return err
}

payURL := resp.PayURL

重点返回:

  • resp.PayURL:跳转支付链接
支付宝 PC 页面下单
resp, err := mgr.UnifiedOrder(ctx, paymgr.ChannelAlipay, &paymgr.UnifiedOrderRequest{
	OutTradeNo:  "ORD202603250007",
	TotalAmount: 100,
	Subject:     "支付宝PC订单",
	TradeType:   paymgr.TradeTypePage,
	NotifyURL:   "https://api.example.com/pay/notify/alipay",
	ReturnURL:   "https://www.example.com/pay/return",
})
if err != nil {
	return err
}

pageURL := resp.PayURL

重点返回:

  • resp.PayURL:支付宝收银台跳转链接
聚合二维码编排

聚合二维码不直接承载微信或支付宝原始支付码,而是承载你自己的业务入口 URL。扫码进入入口页后,可以通过 aggregate.Service 统一决定真实下单渠道与交易类型。

resolver := aggregate.NewService(mgr)

result, err := resolver.Resolve(ctx, &aggregate.ResolveRequest{
	UserAgent:       r.UserAgent(),
	SelectedChannel: paymgr.Channel(r.URL.Query().Get("channel")),
	OpenID:          openID,
	BuildUnifiedOrder: func(ch paymgr.Channel, tt paymgr.TradeType) (*paymgr.UnifiedOrderRequest, error) {
		return &paymgr.UnifiedOrderRequest{
			OutTradeNo:  orderNum,
			TotalAmount: amount,
			Subject:     subject,
			TradeType:   tt,
			NotifyURL:   notifyURL,
			ReturnURL:   returnURL,
			ClientIP:    clientIP,
			OpenID:      openID,
		}, nil
	},
})
if err != nil {
	return err
}

result.Action 的语义:

  • choose_channel:普通浏览器尚未选择支付渠道,此时由你的页面展示“微信 / 支付宝”入口
  • redirect:跳转支付链接,支付宝取 result.Response.PayURL,微信 H5 取 result.Response.H5URL
  • qr_code:返回二维码内容,取 result.Response.CodeURL
  • jsapi:返回微信前端调起参数,取 result.Response.JSAPIParams

当前决策表固定为:

  • 微信环境:微信 JSAPI
  • 支付宝环境:移动端走支付宝 H5,PC 走支付宝 page
  • 普通浏览器移动端:用户选微信走微信 H5,选支付宝走支付宝 H5
  • 普通浏览器 PC:用户选微信走微信 native,选支付宝走支付宝 page
9.2 查询订单 QueryOrder

方法签名:

func (m *Manager) QueryOrder(ctx context.Context, ch Channel, req *QueryOrderRequest) (*QueryOrderResponse, error)

请求结构:

type QueryOrderRequest struct {
	OutTradeNo    string
	TransactionID string
}

说明:

  • OutTradeNoTransactionID 二选一
  • 至少传一个

示例:

resp, err := mgr.QueryOrder(ctx, paymgr.ChannelWechat, &paymgr.QueryOrderRequest{
	OutTradeNo: "ORD202603250001",
})
if err != nil {
	return err
}

fmt.Println(resp.TradeStatus)
fmt.Println(resp.TransactionID)
fmt.Println(resp.TotalAmount)

常见返回字段:

  • TradeStatus
  • TransactionID
  • PaidAt
  • BuyerID

统一状态值:

  • pending
  • paid
  • closed
  • refunded
  • error
9.3 关闭订单 CloseOrder

方法签名:

func (m *Manager) CloseOrder(ctx context.Context, ch Channel, req *CloseOrderRequest) error

请求结构:

type CloseOrderRequest struct {
	OutTradeNo string
}

示例:

err := mgr.CloseOrder(ctx, paymgr.ChannelWechat, &paymgr.CloseOrderRequest{
	OutTradeNo: "ORD202603250001",
})
if err != nil {
	return err
}

适用场景:

  • 订单超时未支付,主动关闭
  • 用户取消订单后关闭支付单
9.4 退款 Refund

方法签名:

func (m *Manager) Refund(ctx context.Context, ch Channel, req *RefundRequest) (*RefundResponse, error)

请求结构:

type RefundRequest struct {
	OutTradeNo    string
	TransactionID string
	OutRefundNo   string
	RefundAmount  int64
	TotalAmount   int64
	Reason        string
	NotifyURL     string
}

字段要求:

  • OutTradeNoTransactionID 二选一
  • OutRefundNo 必填,且需唯一
  • RefundAmount 必须大于 0
  • TotalAmount 必须大于 0
  • RefundAmount <= TotalAmount

示例:

resp, err := mgr.Refund(ctx, paymgr.ChannelAlipay, &paymgr.RefundRequest{
	OutTradeNo:   "ORD202603250003",
	OutRefundNo:  "REF202603250001",
	RefundAmount: 100,
	TotalAmount:  100,
	Reason:       "用户申请退款",
	NotifyURL:    "https://api.example.com/pay/refund/notify/alipay",
})
if err != nil {
	return err
}

fmt.Println(resp.OutRefundNo)
fmt.Println(resp.RefundID)
fmt.Println(resp.RefundAmount)

注意(支付宝幂等语义):同一 OutRefundNo 重复调用 Refund,支付宝会幂等返回成功 (本次不再发生资金变化),本库不视为错误;RefundResponse.RefundAmount 始终为本次 请求的退款金额,需要精确对账时请使用 QueryRefund

9.5 查询退款 QueryRefund

方法签名:

func (m *Manager) QueryRefund(ctx context.Context, ch Channel, req *QueryRefundRequest) (*QueryRefundResponse, error)

请求结构:

type QueryRefundRequest struct {
	OutTradeNo    string // 支付宝用;与 TransactionID 二选一
	TransactionID string // 支付宝用;与 OutTradeNo 二选一
	OutRefundNo   string // 必填
}

响应结构:

type QueryRefundResponse struct {
	Channel       Channel
	OutTradeNo    string
	TransactionID string
	OutRefundNo   string
	RefundID      string
	RefundStatus  RefundStatus // processing / success / closed / abnormal / error
	RefundAmount  int64        // 分
	TotalAmount   int64        // 分
	RefundedAt    time.Time    // 退款成功时才有值
}

字段要求:

  • OutRefundNo 必填
  • 支付宝渠道需要额外提供 OutTradeNoTransactionID(微信可留空)

示例:

resp, err := mgr.QueryRefund(ctx, paymgr.ChannelWechat, &paymgr.QueryRefundRequest{
	OutRefundNo: "REF20250305001",
})
if err != nil {
	return err
}

if resp.RefundStatus == paymgr.RefundStatusSuccess {
	// 退款成功
}
9.6 处理异步通知 ParseNotify + ACKNotify

方法签名:

func (m *Manager) ParseNotify(ctx context.Context, ch Channel, r *http.Request) (*NotifyResult, error)
func (m *Manager) ACKNotify(ch Channel, w http.ResponseWriter) error

推荐处理顺序:

  1. 根据回调路由确定渠道
  2. 调用 ParseNotify
  3. 校验订单和金额
  4. 做幂等更新
  5. 调用 ACKNotify

示例:

func handleWechatNotify(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	result, err := mgr.ParseNotify(ctx, paymgr.ChannelWechat, r)
	if err != nil {
		http.Error(w, "invalid notify", http.StatusBadRequest)
		return
	}

	// 1. 查订单
	// 2. 校验金额
	// 3. 幂等更新支付状态
	// 4. 触发后续业务
	_ = result

	if err := mgr.ACKNotify(paymgr.ChannelWechat, w); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

通知解析结果:

type NotifyResult struct {
	Channel       Channel
	OutTradeNo    string
	TransactionID string
	TradeStatus   TradeStatus
	TotalAmount   int64
	PaidAt        time.Time
	BuyerID       string
	Metadata      map[string]string
}

注意:

  • ParseNotify 成功不代表业务已经处理完成,只代表验签和解析通过
  • 业务层必须自己做订单存在性校验、金额校验、状态幂等控制
  • ACKNotify 必须在业务确认处理完成后再回写
  • 验签之外,库会校验通知的事件类型与商户/应用身份(微信核对 event_type 前缀与解密后的 mchid/appid,微信 V2 核对报文 appid/mch_id,支付宝核对 app_id),不符返回 paymgr.ErrInvalidNotify——退款通知错投支付端点、同商户号下 其它应用的通知都会被拒绝

不同平台的成功应答:

  • 微信:返回 JSON {"code":"SUCCESS","message":"OK"}
  • 支付宝:返回纯文本 success

Manager 已经帮你按渠道封装好了,不需要业务自己区分响应格式。

9.7 处理退款异步通知 ParseRefundNotify

方法签名:

func (m *Manager) ParseRefundNotify(ctx context.Context, ch Channel, r *http.Request) (*RefundNotifyResult, error)

响应结构:

type RefundNotifyResult struct {
	Channel             Channel
	OutTradeNo          string
	TransactionID       string
	OutRefundNo         string
	RefundID            string
	RefundStatus        RefundStatus
	RefundAmount        int64     // 分
	TotalAmount         int64     // 分
	RefundedAt          time.Time
	UserReceivedAccount string    // 仅微信返回,如 "招商银行信用卡0403"
}
微信

微信退款会独立推送异步通知到 RefundRequest.NotifyURLevent_typeREFUND.SUCCESS / REFUND.ABNORMAL / REFUND.CLOSED。使用本方法解析:

func handleWechatRefundNotify(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	result, err := mgr.ParseRefundNotify(ctx, paymgr.ChannelWechat, r)
	if err != nil {
		http.Error(w, "invalid notify", http.StatusBadRequest)
		return
	}

	// 幂等更新退款单状态;RefundStatusSuccess 才代表退款入账成功
	_ = result

	_ = mgr.ACKNotify(paymgr.ChannelWechat, w)
}
支付宝

支付宝没有独立的退款异步通知端点,退款结果会复用支付通知端点(即 UnifiedOrderRequest.NotifyURL)推送回来。直接调用 ParseRefundNotify 会返回 paymgr.ErrNotSupported

正确做法:使用 ParseNotify 并通过 TradeStatus == paymgr.TradeStatusRefunded 识别退款事件。当回调的 gmt_refundrefund_fee 字段非空时,go-pay 会自动把 TradeStatus 映射为 TradeStatusRefunded

退款事件的 NotifyResult.Metadata 中会透传三个定位字段(报文缺失的键不写入):

Metadata 键 含义
out_biz_no 商户退款请求号(发起退款时的 out_request_no),用于定位是哪笔退款单
refund_fee 累计总退款金额(单位分,字符串)——注意支付宝通知中该字段是累计值,不是本次退款金额
gmt_refund 退款时间(原始字符串)

多次部分退款时,通知本身无法给出"本次退款"的精确金额,本次退款金额与最终退款状态必须以 QueryRefund 主动查询为准,Metadata 仅用于定位退款单。

result, err := mgr.ParseNotify(ctx, paymgr.ChannelAlipay, r)
if err != nil { /* ... */ }

switch result.TradeStatus {
case paymgr.TradeStatusPaid:
	// 支付成功
case paymgr.TradeStatusRefunded:
	// 退款成功(部分或全额)
}

10. 常见错误与排查

10.1 渠道未注册

如果调用时报错:

payment: channel not registered: "xxx"

说明你还没有执行:

mgr.Register(provider)

或者传错了渠道值。可用哨兵错误精确判断:

if errors.Is(err, paymgr.ErrChannelNotRegistered) {
	// 渠道未注册
}
10.2 请求参数校验失败

例如:

  • payment: out_trade_no is required
  • payment: total_amount must be positive
  • payment: notify_url is required

这类错误来自 paymgr 的统一校验逻辑,先检查你传入的请求字段。

10.3 微信回调处理器初始化失败

重点排查:

  • 商户私钥是否正确
  • APIv3 Key 是否正确
  • 微信平台证书是否已通过 WithPlatformCertificatePath / WithPlatformCertificatePEM / WithPlatformCertificate 正确传入
10.4 渠道错误

项目会把底层 SDK 错误包装成 paymgr.ChannelError

type ChannelError struct {
	Channel Channel
	Code    string
	Message string
	Err     error
}

你可以这样判断:

var chErr *paymgr.ChannelError
if errors.As(err, &chErr) {
	fmt.Println(chErr.Channel)
	fmt.Println(chErr.Code)
	fmt.Println(chErr.Message)
}

11. 推荐接入方式

生产接入建议:

  • paymgr.Manager 做成应用级单例,在服务启动时初始化
  • 配置从环境变量、配置文件或配置中心读取,不要写死在代码里
  • 商户私钥和证书放在安全目录,不要提交到仓库
  • 回调处理必须做金额校验和幂等
  • 订单号、退款单号必须全局唯一
  • HTTP 回调地址必须使用可公网访问的 HTTPS 地址

12. 参考示例

可以直接参考项目里的示例:

这个示例展示了:

  • 初始化并注册微信 / 支付宝 Provider
  • 创建订单
  • 查询订单
  • 处理退款
  • 处理支付回调

13. 当前实现备注

在接入前建议先了解当前代码边界:

  • 底层 SDK:微信使用腾讯官方 wechatpay-apiv3/wechatpay-go(V3 协议),支付宝使用 smartwalle/alipay/v3(OpenAPI 1.0 网关,公钥 / 公钥证书两种加签模式都支持)
  • 微信 Provider 当前覆盖 appjsapinativeh5 四种直连下单场景
  • 支付宝 Provider 覆盖 appjsapinativeh5page 五种场景
  • 推荐用函数选项模式初始化,结构体配置作为兼容方式保留
  • 微信 V2(XML 协议)商户号兼容由内置的 wechat/v2 包提供(详见第 14 章);新接入优先使用 V3
  • 渠道 HTTP 客户端均带 30 秒超时(微信为 SDK 默认值,支付宝由本库显式注入),可通过 context 进一步收紧单次调用时限
  • 示例代码展示的是接入方式,不是可直接上线的完整业务代码

如果你要在自己的业务项目中使用,通常下一步会做两件事:

  1. 把证书、密钥、回调地址抽到你自己的配置系统里
  2. 在订单服务里封装一层业务适配,把 go-pay 的统一请求/响应转换成你自己的领域对象

14. 微信 V2 接入

针对仅支持 V2(XML 协议)的老商户号,本库内置 wechat/v2 包,以 paymgr.ChannelWechatV2wxpayv2)实现 paymgr.Provider,与 V3、支付宝共用同一抽象层。新接入仍应优先使用 V3——V2 是微信维护期协议。

14.1 创建与注册
import (
	"context"

	"github.com/gtkit/go-pay/paymgr"
	v2 "github.com/gtkit/go-pay/wechat/v2"
)

provider, err := v2.NewProvider(ctx,
	v2.WithAppID("wxYOUR_APPID"),
	v2.WithMerchant("YOUR_MCH_ID", "<32位 V2 API 密钥>"),
	v2.WithNotifyURL("https://example.com/wechat/notify"),
	// 退款必须配置商户 API 证书(二选一):
	v2.WithCertPath("apiclient_cert.pem", "apiclient_key.pem"),
	// v2.WithCertPEM(certPEM, keyPEM),
)
if err != nil {
	// 处理初始化错误
}

mgr.Register(provider)
// 之后 mgr.UnifiedOrder(ctx, paymgr.ChannelWechatV2, req) 路由到 V2

密钥区别WithMerchant 的第二个参数是商户平台的 V2 API 密钥(32 位),与 V3 的 APIv3 密钥(wechat.Config.MchAPIv3Key)是两个不同的密钥,不可混用。

14.2 覆盖的方法
方法 V2 接口 说明
UnifiedOrder /pay/unifiedorder 支持 APP / JSAPI / Native / H5;APP、JSAPI 自动生成调起支付二次签名
QueryOrder /pay/orderquery trade_state 映射为统一交易状态
CloseOrder /pay/closeorder
Refund /secapi/pay/refund 走商户证书双向 TLS
QueryRefund /pay/refundquery out_refund_no 匹配退款明细下标
ParseNotify 支付回调 XML 验签后映射结果
ParseRefundNotify 退款回调 AES-256-ECB 解密 req_info
ACKNotify 应答 回写 <xml>...SUCCESS...</xml>

下单二次签名结果:APP 写入 UnifiedOrderResponse.AppParams,JSAPI/小程序写入 JSAPIParams,均为可直接下发给客户端的 JSON 字符串。

14.3 注意事项
  • 统一下单必填 ClientIP:V2 spbill_create_ip 对所有交易类型必填,缺失返回 paymgr.ErrInvalidParam
  • 退款必须配置商户证书:未配置 apiclient_cert / apiclient_keyRefund 返回明确错误且不发请求;查询、关单、下单等普通接口无需证书。
  • 签名算法可配:默认 MD5,可用 v2.WithSignType(v2.SignTypeHMACSHA256) 切换;下单签名与 JSAPI 二次签名的 signType 自动保持一致。
  • 支付通知验签回退 MD5:微信 V2 支付结果通知历史上固定用 MD5 且常不回传 sign_type,故 ParseNotify 在报文未声明 sign_type 时回退 MD5 验签(即便 Provider 配置为 HMAC-SHA256),贴合微信网关实际行为。
  • 零新增依赖:HTTP 用标准库 net/http,随机串复用已依赖的 wechatpay-go/utils,JSON 复用已依赖的 gtkit/json

15. v1.4.0 升级指南

15.1 v1.3.x 升级到 v1.4.0

代码 0 改动alipay.NewProvider / Config / WithXxx / paymgr 全部 8 个统一 API 形状不变,go get -u 一行升级即可。

行为变化

  • ✅ 普通公钥模式恢复支持——v1.3.x 时被软降级为 ErrNotSupportedWithAlipayPublicKey(...) 配置现在正常工作
  • ✅ JSAPI / QueryRefund 边界 bug 修复保留(v1.3.1 / v1.3.2 修复)
  • ⚠️ 渠道错误的原始错误来源从 gopay v3 ErrResponse 变回 smartwalle sub_code/sub_msg 风格——下游若用 errors.As(err, &chErr); chErr.Code == "ACQ.*" 判断仍命中(错误码 prefix 不变),用 errors.Is(err, paymgr.ErrXxx) 判断仍稳定
15.2 v1.2.x 直接升级到 v1.4.0(跳过 v1.3.x)

直接升级,0 代码改动。v1.4.0 保留 v1.3.x 引入的两个 bug 修复:

  • JSAPI 下单补齐 product_code=JSAPI_PAYop_app_id(修复 v1.2.x 时代沙箱"missing required parameter")
  • QueryRefund 在退款单不存在时返回 paymgr.ErrOrderNotFound(v1.2.x 时代会返回空响应误导下游)
15.3 微信 V2 兼容(可选)

微信 V2 渠道现已内置 wechat/v2 包(见第 14 章)。仅当你有 V2 商户号需求才需要创建并注册 wechat/v2 Provider;否则忽略即可,对现有 V3 与支付宝无任何影响。

15.4 v1.3.x 路线调整说明

v1.3.x(v1.3.0 / v1.3.1 / v1.3.2)曾尝试将支付宝底层 SDK 切换为 go-pay/gopay/alipay/v3。事后核实发现:

  1. smartwalle/alipay/v3 仍在活跃维护——「停滞」判定不成立
  2. 切换后包尺寸增大约 5 倍(双 SDK 传递依赖)
  3. 公钥模式被强制软降级,对仅有公钥模式商户号的下游造成阻塞

v1.4.0 切回 smartwalle,单 SDK 路线最简、最轻、对公钥商户号兼容。v1.3.x 三个 tag 留在仓库历史,作为实验性中间态参考。

16. 自定义渠道接入

paymgr.Channel 是开放的字符串类型,Manager 对渠道值没有白名单限制——接入新支付渠道(银联、PayPal、Stripe 等)不需要改本库代码,实现 paymgr.Provider 接口并注册即可。

16.1 最小接入步骤

推荐嵌入 paymgr.UnimplementedProvider 基座,只覆写渠道支持的能力;未来 Provider 接口新增方法时,嵌入者自动获得默认实现(返回 ErrNotSupported),编译不会被破坏:

const ChannelUnionPay = paymgr.Channel("unionpay")

type UnionPayProvider struct {
    paymgr.UnimplementedProvider // 未覆写的能力默认返回 ErrNotSupported

    client *unionpay.Client // 你的渠道 SDK
}

func (p *UnionPayProvider) Channel() paymgr.Channel { return ChannelUnionPay }

func (p *UnionPayProvider) UnifiedOrder(ctx context.Context, req *paymgr.UnifiedOrderRequest) (*paymgr.UnifiedOrderResponse, error) {
    if err := req.Validate(); err != nil {
        return nil, err
    }
    // 调用渠道 SDK ...
}

// 注册后与内置渠道完全同等使用
mgr.Register(&UnionPayProvider{client: c})
resp, err := mgr.UnifiedOrder(ctx, ChannelUnionPay, req)

注意:Channel() 没有默认实现,必须自行提供——渠道标识是注册身份,不能有默认值。

16.2 实现契约 checklist

实现方法时必须遵守以下约定,保证与内置渠道行为一致:

契约 说明
金额单位为分 所有请求/响应金额是 int64 分;渠道按元计价的,在 Provider 内换算(参考 alipay 包的 centToYuan
入口先校验 每个请求方法第一行调用 req.Validate(),非法请求不得触达渠道
ErrInvalidParam 参数错误用它包装(Validate() 已自动处理)
ErrOrderNotFound 订单/退款单不存在时返回,不要把渠道的"查无此单"当成功响应透传
ErrNotSupported 渠道不支持的能力返回它(嵌入基座后未覆写的方法自动如此)
NewChannelError 渠道业务失败用 paymgr.NewChannelError(渠道, 错误码, 错误信息, 原始错误) 包装,调用方可统一提取错误码
验签 + 身份核对 ParseNotify / ParseRefundNotify 必须完成签名校验,并核对通知中的商户/应用标识,防止同主体其它应用的通知被误收
并发安全 所有方法必须可被多个 goroutine 并发调用(Manager 在锁外调用 Provider)

只依赖部分能力的业务代码,参数可声明为小接口 paymgr.OrderProvider / paymgr.RefundProvider / paymgr.NotifyParser,任意完整 Provider 都能传入。

16.3 边界说明

aggregate 聚合扫码包只编排内置的微信/支付宝渠道(聚合码是两者特有的场景),自定义渠道经 paymgr.Manager 的全部能力可用,但不参与 aggregate 的渠道决策。

许可证

本项目基于 MIT License 开源。

Documentation

Overview

Package gopay 是多渠道支付 SDK 的根包,仅承载模块版本信息; 支付能力见 paymgr(统一抽象)、alipay、wechat、wechat/v2 与 aggregate 子包。

Index

Constants

View Source
const Version = "v1.7.0"

Version 当前模块版本号,与仓库发布标签保持一致(SemVer,vX.Y.Z)。

Variables

This section is empty.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
Package aggregate 在统一支付能力之上提供聚合二维码分流编排。
Package aggregate 在统一支付能力之上提供聚合二维码分流编排。
Package alipay 实现支付宝渠道的统一支付接口。
Package alipay 实现支付宝渠道的统一支付接口。
Package paymgr 提供统一支付接口抽象层。
Package paymgr 提供统一支付接口抽象层。
Package wechat 实现微信支付 APIv3 渠道的统一支付接口。
Package wechat 实现微信支付 APIv3 渠道的统一支付接口。
v2
Package v2 实现微信支付 v2(XML 协议)的统一支付接口。
Package v2 实现微信支付 v2(XML 协议)的统一支付接口。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL