微信支付v3回调需公网HTTPS地址、原始JSON解析、严格验签解密、200空响应及幂等处理。本地用ngrok测试,线上确保服务器透传HTTPS头,证书动态管理,日志全覆盖链路。

微信支付回调地址必须能被公网访问且走HTTPS
微信服务器只会向你配置在商户平台的回调地址发起 POST 请求,如果本地开发环境用 http://localhost:8080 或内网 IP,微信根本连不到,直接超时失败。线上部署后还要确认 Nginx/Apache 是否透传了原始请求头(特别是 X-Forwarded-Proto),否则 ThinkPHP 可能误判为 HTTP,导致签名验签失败。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 测试阶段用
ngrok或localtunnel映射本地端口,获取临时 HTTPS 地址填入微信商户平台「APIv3 商户证书」和「支付回调URL」 - 线上务必检查 Web 服务器是否将
$_SERVER['HTTPS']正确设为on,ThinkPHP 的Request::isSsl()依赖它 - 回调 URL 路径不要带 query 参数(如
/pay/notify?from=wx),微信只认路径部分,多余参数会被忽略甚至触发签名不匹配
ThinkPHP 6.x 中正确读取并验签微信支付 v3 回调通知
v3 回调不是普通 POST 表单,而是 JSON 加密体 + 特殊 HTTP 头(Wechatpay-Serial、Wechatpay-Nonce、Wechatpay-Timestamp、Wechatpay-Signature),且原始请求体是 raw JSON,不能用 $request->post() 直接取——那会触发自动解析并破坏原始字节流,导致验签失败。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
$request->getRawBody()获取原始 JSON 字符串,别碰$request->param()或$request->post() - 从
$request->header()提取四个关键 header 字段,注意 key 名大小写要完全一致(如Wechatpay-Timestamp不是wechatpay-timestamp) - 验签必须使用微信平台证书(
apiclient_cert.pem)中的公钥,不是你自己生成的 RSA 公钥;证书需提前下载并存放在可读路径,如runtime/cert/apiclient_cert.pem - 验签通过后,再用
json_decode($rawBody, true)解析业务数据,此时才能安全读取resource字段里的加密内容
解密 resource.ciphertext 前必须先校验 platform_certificate
微信回调里 resource 是 AES-256-GCM 加密的,但密钥(associated_data 和 nonce)都来自 header 和 resource 结构本身,而解密前必须确认当前使用的平台证书没过期、且与回调中 Wechatpay-Serial 匹配——否则解出来全是乱码,还可能抛出 OpenSSL 异常。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 不要硬编码证书路径,用
Config::get('wechat.platform_cert')统一管理,并支持运行时热更新(微信证书每三个月轮换一次) - 每次回调进来先查缓存里有没有对应
Wechatpay-Serial的证书,没有就去微信接口拉取并缓存(调用GET https://api.mch.weixin.qq.com/v3/certificates) - 解密时严格按文档顺序拼接:AAD = “weixinpay/
resource.algorithm”,nonce =resource.nonce,ciphertext =resource.ciphertext,密钥是resource.encrypted_key用商户私钥解出来的 32 字节 AES key - 解密失败时记录完整原始请求(headers + raw body)到日志,别只打“解密失败”,否则无法定位是证书错、key 错还是字段名写错
回调处理完必须返回 200 OK 且响应体为空 JSON
微信收到非 200 状态码或响应体含非空内容(比如 ThinkPHP 默认输出的 trace 信息、debug 页面、甚至多一个换行),都会认为通知失败并重试(最多 5 次)。而重复回调又没做幂等,会导致订单重复发货、金额重复扣减等严重问题。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 整个回调逻辑包在 try-catch 里,无论成功失败,最后统一
return json([])->code(200),确保响应干净 - 用数据库唯一索引(如
transaction_id或out_trade_no)或 Redis SETNX 做幂等控制,第一次处理成功才执行业务逻辑,后续直接 return 200 - 避免在回调里调用微信其他接口(如查单、退款),网络抖动会导致超时,微信重试后更难收敛;查单应放异步队列或定时任务里做
- 日志级别设为 info 或 debug,但别把敏感字段(如
resource.ciphertext)全打出来,防止密钥泄露



















