微信支付V3接口必须用Guzzle而非Laravel Http门面,因需mTLS双向认证;证书须PEM格式且私钥无密码;APIv3密钥为商户平台设置的32位字符串;请求体为JSON并设Content-Type;需手动下载平台证书;回调URL须公网HTTPS且无参数;验签失败须返回500;沙箱与正式环境配置完全隔离。

微信支付 V3 接口必须用 curl 或 Guzzle,别碰 Laravel 的 Http 门面
因为微信支付 V3 要求带证书的双向 TLS(mTLS),而 Laravel Http 门面底层用的是 PHP 的 curl 扩展,但不暴露 CURLOPT_SSLCERT、CURLOPT_SSLKEY 等关键选项,强行封装反而容易漏掉签名头或证书加载失败。
实操建议:
- 用
GuzzleHttp\Client显式配置证书路径和签名中间件,证书文件必须是 PEM 格式且私钥无密码(微信要求) - 商户 APIv3 密钥不是「API 密钥」,而是你在微信商户平台「API 安全 → APIv3 密钥」里设置的 32 位字符串,用于生成
AUTHORIZATION请求头 - 请求体必须是 JSON,且
Content-Type必须为application/json,微信会校验;Laravel 默认发 form-data 会直接 401
wechatpay-php SDK 能省事,但别跳过「平台证书下载」这步
官方 SDK wechatpay-php 封装了签名、验签、证书自动刷新,但它不会帮你下载平台证书——这个动作必须手动触发一次,否则初始化客户端就报 Platform certificate not found。
常见错误现象:
- 调
createOrder时抛出InvalidCertificateException,实际是没运行过证书拉取命令 - 本地测试能过,上线后验签失败:因为线上环境没执行过
php artisan wechatpay:download-certs(假设你用了封装命令)
使用场景:证书需每 24 小时自动刷新,SDK 内部用 filemtime 判断是否过期,所以务必确保 web 进程有写入 storage_path('app/wechat/certs') 的权限
回调通知地址必须是公网可访问的 HTTPS,且不能带查询参数
微信服务器只往你填在商户平台里的那个完整 URL 发 POST 请求,路径末尾多一个 ? 或 # 都会导致 400;更常见的是 Laravel 开发时用 php artisan serve 或本地 ngrok,但忘记把域名同步到微信后台。
实操要点:
- 回调路由必须关闭 CSRF 验证:
Route::post('/wechat/notify', [WechatPayController::class, 'notify'])->withoutMiddleware(VerifyCsrfToken::class) - 收到通知后第一件事是调用 SDK 的
verifyNotify方法验签,验证失败必须返回500,微信会重试;返回200但内容不是{"result":"success"}也会被当失败 - 不要在回调里做耗时操作(如发邮件、调外部 API),先落库再异步处理,否则超时(微信要求 5 秒内响应)
沙箱环境和正式环境的密钥、证书、URL 全都不通用
很多人在沙箱调通后直接改配置切正式环境,结果一直 401 Unauthorized。原因很实在:沙箱的 APIv3 密钥、平台证书、商户号、APPID 全部独立,连接口域名都不同(沙箱是 https://api.sandbox.wechatpay.dev)。
容易踩的坑:
- 在
.env里混用同一组变量名,比如WECHAT_PAY_MCH_ID同时用于沙箱和正式,切换环境时忘了改值 - 用
config/wechatpay.php做环境判断,但没清 config 缓存,导致php artisan config:clear没跑,缓存还是旧的 - 沙箱支付成功后,回调地址填的是正式域名,微信沙箱服务器根本打不通
最稳妥的做法是:在 config/wechatpay.php 里按 APP_ENV 分开配置,且所有密钥类字段都走 env(),别硬编码



















