README
¶
go-pay
go-pay 是一个 Go 支付业务统一抽象层——在官方/主流 SDK 之上提供渠道无关的统一下单、查询、退款、通知 API,附带 Channel 注册路由与聚合二维码编排能力。它不是 SDK 的薄包装,而是把多家支付渠道的差异收敛在 provider 层,让业务代码只面向统一接口。
底层 SDK 选型:
- 微信支付:
github.com/wechatpay-apiv3/wechatpay-go(腾讯官方 V3 SDK) - 支付宝:
github.com/smartwalle/alipay/v3(活跃维护、覆盖支付宝 OpenAPI 1.0 网关,公钥 / 公钥证书两种加签模式都支持)
当前提供的核心能力:
- 统一下单 / 订单查询 / 关闭订单
- 退款 / 退款查询(v1.1.0+)
- 异步通知解析与应答(含退款通知,v1.1.0+)
- 聚合二维码编排(v1.2.0+)
项目内部通过 paymgr.Manager 管理不同支付渠道,业务方只需要:
- 初始化各渠道
Provider - 注册到
Manager - 按渠道调用统一方法
升级到 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 引入的 JSAPIproduct_code/op_app_id修复 +QueryRefund退款单不存在边界修复。升级到 v1.1.0 注意:
paymgr.Provider接口新增QueryRefund和ParseRefundNotify两个方法。 官方alipay/Provider,升级后会编译失败,需要补齐这两个方法。完整变更见 CHANGELOG.md。
1. 安装
go get github.com/gtkit/go-pay
要求:
- Go
1.26+ - 已开通微信支付 / 支付宝商户能力
- 已准备好商户私钥、平台证书、公钥等支付材料
2. 项目结构
.
├── aggregate/ # 聚合二维码支付编排
├── alipay/ # 支付宝实现
├── wechat/ # 微信支付实现
├── paymgr/ # 统一抽象层
└── example/ # 简单 HTTP 示例
包职责:
paymgr:统一的请求、响应、错误和管理器接口wechat:微信支付 Provideralipay:支付宝 Provideraggregate:聚合二维码入口分流与真实支付单编排
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 | app、native、jsapi、h5 |
| 微信支付 V2 | app、native、jsapi、h5 |
| 支付宝 | native、jsapi、app、h5、page |
如果传入未实现的类型,会返回 paymgr.ErrUnsupportedType。
3.3 下单字段与返回矩阵
| 渠道 | 交易类型 | 适用场景 | 额外必填字段 | 重点返回字段 |
|---|---|---|---|---|
| 微信支付 | app |
微信开放平台 APP 支付 | 无 | AppParams |
| 微信支付 | jsapi |
公众号 / 小程序支付 | OpenID |
PrepayID、JSAPIParams |
| 微信支付 | 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.Config、wechat/v2.Config、alipay.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 当前实现的方法:
UnifiedOrderQueryOrderCloseOrderRefundQueryRefundParseNotifyParseRefundNotify(微信独立的退款异步通知)ACKNotify
其中下单只支持:
paymgr.TradeTypeApppaymgr.TradeTypeJSAPIpaymgr.TradeTypeNativepaymgr.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 |
PrivateKey 或 PrivateKeyPath |
是 | 应用私钥内容或文件路径 |
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 当前实现的方法:
UnifiedOrderQueryOrderCloseOrderRefundQueryRefundParseNotify(当GmtRefund或RefundFee非空时,TradeStatus会映射为TradeStatusRefunded)ParseRefundNotify(支付宝无独立退款通知端点,本方法直接返回paymgr.ErrNotSupported;请使用ParseNotify识别退款事件)ACKNotify
下单支持的交易类型:
paymgr.TradeTypeNativepaymgr.TradeTypeJSAPIpaymgr.TradeTypeApppaymgr.TradeTypeH5paymgr.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):获取底层 Providermgr.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.PrepayIDresp.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.PrepayIDresp.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.H5URLqr_code:返回二维码内容,取result.Response.CodeURLjsapi:返回微信前端调起参数,取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
}
说明:
OutTradeNo和TransactionID二选一- 至少传一个
示例:
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)
常见返回字段:
TradeStatusTransactionIDPaidAtBuyerID
统一状态值:
pendingpaidclosedrefundederror
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
}
字段要求:
OutTradeNo和TransactionID二选一OutRefundNo必填,且需唯一RefundAmount必须大于 0TotalAmount必须大于 0RefundAmount <= 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必填- 支付宝渠道需要额外提供
OutTradeNo或TransactionID(微信可留空)
示例:
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
推荐处理顺序:
- 根据回调路由确定渠道
- 调用
ParseNotify - 校验订单和金额
- 做幂等更新
- 调用
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.NotifyURL,event_type 为 REFUND.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_refund 或 refund_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 requiredpayment: total_amount must be positivepayment: 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 当前覆盖
app、jsapi、native、h5四种直连下单场景 - 支付宝 Provider 覆盖
app、jsapi、native、h5、page五种场景 - 推荐用函数选项模式初始化,结构体配置作为兼容方式保留
- 微信 V2(XML 协议)商户号兼容由内置的
wechat/v2包提供(详见第 14 章);新接入优先使用 V3 - 渠道 HTTP 客户端均带 30 秒超时(微信为 SDK 默认值,支付宝由本库显式注入),可通过
context进一步收紧单次调用时限 - 示例代码展示的是接入方式,不是可直接上线的完整业务代码
如果你要在自己的业务项目中使用,通常下一步会做两件事:
- 把证书、密钥、回调地址抽到你自己的配置系统里
- 在订单服务里封装一层业务适配,把
go-pay的统一请求/响应转换成你自己的领域对象
14. 微信 V2 接入
针对仅支持 V2(XML 协议)的老商户号,本库内置 wechat/v2 包,以 paymgr.ChannelWechatV2(wxpayv2)实现 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:V2spbill_create_ip对所有交易类型必填,缺失返回paymgr.ErrInvalidParam。 - 退款必须配置商户证书:未配置
apiclient_cert/apiclient_key时Refund返回明确错误且不发请求;查询、关单、下单等普通接口无需证书。 - 签名算法可配:默认 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 时被软降级为
ErrNotSupported的WithAlipayPublicKey(...)配置现在正常工作 - ✅ JSAPI / QueryRefund 边界 bug 修复保留(v1.3.1 / v1.3.2 修复)
- ⚠️ 渠道错误的原始错误来源从 gopay v3
ErrResponse变回 smartwallesub_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_PAY与op_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。事后核实发现:
smartwalle/alipay/v3仍在活跃维护——「停滞」判定不成立- 切换后包尺寸增大约 5 倍(双 SDK 传递依赖)
- 公钥模式被强制软降级,对仅有公钥模式商户号的下游造成阻塞
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 ¶
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 协议)的统一支付接口。 |