最近在项目中对接微信支付,服务端逻辑实现使用的是我最熟悉的Go语言。
按照惯例,优先寻找微信支付的官方SDK for Go,结果发现官方SDK最后版本v0.2.20,发布时间Aug 20,2024。并且在github上SDK的Issues里,发现有不少问题都还是Open状态。
https://github.com/wechatpay-apiv3/wechatpay-go
因为是新项目,又有AI的加持,所以我就斗胆决定跳过官方SDK,直接从零开始实现微信支付的对接。期间有些踩坑,逐一解决后记录一下。
目前主要对接微信支付的Native支付(后续H5支付审核通过后,再做H5支付的对接),Native支付中要求必须带有“支付成功回调URL”,而遇到的问题也都出在接收支付成功回调的接口上。
问题1 Nonce大小写
微信支付的POST回调请求头中包含以下信息:
参数 | 说明 |
---|---|
Wechatpay-Serial | 验签的微信支付平台证书序列号/微信支付公钥ID |
Wechatpay-Signature | 验签的签名值 |
Wechatpay-Timestamp | 验签的时间戳 |
Wechatpay-Nonce | 验签的随机字符串 |
其中 WeChatpay-Nonce是一条字符串,我收到的Nonce字母都是大写。实际在微信支付验签时,希望Nonce是小写。
在微信支付的文档中并没有相关说明,我也是对照示例,发现示例中打印出的Nonce全是小写,才发现了这个问题。
问题2 Body体需要原始数据
现在实现Restful API,一般都会选用一些框架,比如我使用的是gin。这些框架默认会将Body体转换为JSON等格式,供后续程序处理使用。
但是微信支付回调在验签处理时,需要保持Body数据原始不变,不但是数据结构,甚至连顺序都不能改变。
所以在接收微信支付回调的代码中,第一步需要先把原始body数据保存下来。相关代码如下:
func WechatPaymentNotify(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Printf("读取请求体失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "读取请求失败"})
return
}
// 恢复请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
//其他代码
}
问题3 验签名串
微信支付回调的验签,需要将请求头中的信息,以及Body体中的信息,拼接成一个字符串,然后进行验签。格式如下:
<Wechatpay-Timestamp>\n<Wechatpay-Nonce>\n<body>\n
我最初分别使用Claude 3.5和 Google Gemini 2.5 pro,都生成了类似的代码:
message := fmt.Sprintf("%s\n%s\n%s\n%s\n", timestamp, nonce, string(body),"")
在解决掉前面Nonce大小写的问题后,回调的验签始终还是无法通过。反复调试无果后,我回到微信支付文档,才发现在“2.2开始构造验签串”这一节的第2点,明确说明: “签名串共有三行,行尾以\n结束,包括最后一行。若应答报文主体为空,最后一行仅为一个\n。”
所以,正确的代码应该是
message := fmt.Sprintf("%s\n%s\n%s\n", timestamp, nonce, string(body))
在AI辅助编程中,这样的情况已经出现过好几次。某些隐藏在角落里,不起眼的一点小问题,会让我花费大量的时间去排查调试。