微信支付回调必须用raw xml解析,正确做法是io.readall(r.body)后xml.unmarshal;验签需按微信md5规则排序拼接参数并剔除sign;幂等性需缓存+db双重校验,异步任务推荐nightpay。

微信支付回调必须用 raw XML 解析,不能走 r.ParseForm()
微信回调是 POST 请求,body 是原始 XML,不是表单也不是 JSON。一旦调用 r.ParseForm() 或 r.FormValue(),Go 会尝试解析为 URL-encoded 数据,结果是空值或 panic;用 json.Unmarshal() 更不行——字段名、嵌套结构、类型(比如 total_fee 是 int,但 XML 里是字符串)全对不上。
- 正确做法:用
io.ReadAll(r.Body)拿到完整字节流,再用xml.Unmarshal()解到结构体 - 结构体字段必须加
xmltag,且注意大小写(微信字段全小写,如return_code) - 响应必须是纯文本
"success",且只能输出这一行——中间件打日志、fmt.Println()、甚至 HTTP header 后多一个换行都会让微信重试
验签失败的三个高频原因:参数排序、密钥拼接、签名算法错配
不验签等于把订单状态完全交给攻击者控制。微信用 MD5,支付宝用 RSA2+SHA256,两者规则差异大,容易混用。
- 微信:所有非空参数按 key 字典序升序拼成
key1=val1&key2=val2&key=YOUR_KEY,再算 MD5;sign字段必须从排序中剔除 - 支付宝:同样字典序,但要剔除
sign和sign_type;签名前需确认sign_type=RSA2,否则公钥验签失败 - 密钥来源要核对清楚:微信是商户 API 密钥(v2)或平台证书私钥(v3),支付宝是应用私钥——拿错就永远验不过
回调重试导致重复处理,幂等性不能只靠数据库唯一索引
微信/支付宝在没收到 "success" 响应时会每 15 分钟重试一次,最长持续 24 小时。仅靠 out_trade_no 唯一索引,可能因事务未提交、网络超时、panic 中断等原因漏判。
- 建议加一层内存缓存(如
sync.Map或本地 LRU),记录已成功处理的out_trade_no,有效期设为 2 小时以上 - 业务逻辑入口先查缓存 → 再查 DB → 最后更新状态,三步缺一不可
- 退款回调、关单通知也适用同一套幂等逻辑,别只盯着支付成功场景
异步任务解耦别硬上消息队列,nightpay 这类轻量框架更合适
微服务里处理回调后的后续动作(比如发短信、改库存、推消息)不该阻塞 HTTP 响应。但为这点需求单独部署 RabbitMQ/Kafka,运维成本远超收益。
-
nightpay是 Go 原生库,无外部依赖,直接go get引入,定义任务结构体 + 处理器即可启动消费者 - 它内置重试、死信、延迟调度,比手写 goroutine + channel 更可靠——后者容易漏 recover、无持久化、崩溃即丢任务
- 注意:任务执行函数必须是幂等的,因为 nightpay 的重试机制不保证“只执行一次”
Wechatpay-Timestamp 和 Wechatpay-Nonce 要校验,支付宝回调里 notify_time 要防重放,这些细节一旦漏掉,安全防线就形同虚设。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











