微信支付v3退款必须用v3私钥签名、金额单位为分、退款后需轮询查单确认终态,回调验签前须原样读取body,严禁跳过状态校验。

退款请求签名必须用V3密钥,不是商户APIv2密钥
微信支付V3退款接口要求所有请求头 Authorization 中的签名,必须使用你在微信商户平台「API安全」里下载的 apiclient_key.pem(即V3私钥),而不是老版本的APIv2密钥。用错密钥会导致 401 Unauthorized 或 {"code":"INVALID_SIGNATURE","message":"签名验证失败"}。
实操建议:
- 确认你调用的是微信支付 V3 接口(路径含
/v3/pay/transactions/id/refund或/v3/refund/domestic/refunds),不是V2的/secapi/pay/refund - V3签名需构造
message字符串:HTTP方法 + \n + 路径 + \n + 请求时间戳 + \n + 请求体(空体则为"")+ \n + 签名摘要(SHA256) - Gin中不要手动拼接签名,推荐直接用官方 SDK
wechatpay-go的client.Certificates()和client.Refund(),它自动处理证书加载、序列化、签名和验签
Gin接收微信回调时,body必须原样读取且不被中间件篡改
微信支付退款结果通知是 POST 到你的回调地址,且 body 是原始 JSON(无 BOM,UTF-8 编码),但 Gin 默认的 c.ShouldBindJSON() 会提前读取并解析 body,导致后续无法校验签名;而 c.Request.Body 在被读取一次后就变为空,造成验签失败或 EOF 错误。
实操建议:
- 在路由 handler 开头立刻调用
body, err := io.ReadAll(c.Request.Body),保存原始字节 - 用
wechatpay-go的verifier.Verify(c.Request.Header, body)校验回调合法性,不要跳过这步 - 禁止在验签前使用任何 JSON binding、
c.PostForm、c.GetRawData()等会消耗 body 的操作 - 如果用了
gin-contrib/sessions或自定义日志中间件,确保它们不调用c.Request.Body
退款金额单位是分,且必须与原订单一致
微信支付所有金额字段(amount.total、amount.refund、amount.payer_total 等)单位都是「分」,不是元。传 100.5 或 "100.50" 会直接返回 {"code":"PARAM_ERROR","message":"金额格式错误"}。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
实操建议:
- 前端传来的退款金额,务必先转成整数分:例如用户输入「100.5 元」→
int64(100.5 * 100)→10050 - 退款不能超过订单总金额,也不能超过已成功支付的金额(注意部分退款场景)
- 若原订单含优惠券,
amount.payer_total必须等于用户实际支付金额(也是分),否则微信会拒单 - 调试时用
fmt.Printf("%+v", req)打印请求结构体,确认字段类型是int64而非float64或string
退款成功后,务必查单再更新本地状态
微信支付退款是异步的:接口返回 200 只表示「受理成功」,不代表资金已退。常见错误是收到 200 就立刻把订单状态改成「已退款」,结果实际退款失败,造成资金和状态不一致。
实操建议:
- 发起退款后,应立即启动轮询:调用
client.TransactionRefund().Get()查询退款单状态,间隔建议 3–5 秒,最多 10 次 - 只在收到
status: "SUCCESS"或"ABNORMAL"时才更新数据库;"PROCESSING"和"ACCEPTED"都不算终态 - 回调通知可能延迟或丢失,不能作为唯一依据;必须以查单结果为准
- 本地事务要包裹退款记录创建 + 订单状态更新,避免部分写入
微信支付退款真正麻烦的不是签名或参数,而是状态机的严谨性——受理、处理中、成功、失败、异常,每种状态对应不同业务动作,漏掉一种就容易对不上账。别图快跳过轮询和查单。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










