简介这份资源是面向PHP开发者的微信支付V3接口完整实例适合需要为线上商城或线下场景接入微信支付、希望掌握V3新版流程与安全机制的中初级开发者参考。压缩包共16个文件、约61KB以asp与php脚本为主体辅以txt说明文档、pem证书文件、js脚本及mdb数据文件覆盖统一下单、前端调起支付、异步回调通知、订单查询确认等核心环节并涉及API签名、证书管理、沙箱测试、异常处理与退款等要点。资源中附有中转文件与demo示例便于对照理解配置与调用逻辑同时提醒开发者关注SQL注入、XSS防护及敏感信息传输存储等安全问题。目前已有4669人学习下载可作为快速上手微信支付V3、梳理支付链路与排错思路的实践参考。1. PHP 接微信支付 v3为什么老教程一跑就废这份实例能省你两天上周帮一个做小程序商城的朋友排查支付回调他拿着两年前的 v2 代码往新项目里塞签名报错、证书报错、回调验签失败一个下午全耗在环境上。微信支付 v3 和 v2 的差别不是换个接口地址那么简单v3 全面改用 AES-256-GCM 加密回调、SHA256-RSA 签名、平台证书自动轮换密钥体系整个换了一套。你手上如果只有一份「能跑」的旧 demo基本等于没有。这份 PHP 微信支付 v3 完整实例价值就在于把下单、签名、回调验签、退款、查单这几条主链路串成一个能直接改参数就跑的闭环而不是散落在各处的代码片段。适合谁手里有 PHP 项目、要接最新微信支付、又不想从官方文档的术语堆里自己拼装的人。下面按「先跑通再抠细节」的顺序拆。2. 先把密钥体系搭对v3 的三把钥匙和目录结构2.1 v3 到底要哪几个文件很多人卡在第一步不是代码写错是文件放错。微信支付 v3 的商户侧凭证一共三类缺一个都签不出请求文件/参数来源用途常见命名商户 API 私钥商户平台下载请求签名SHA256-RSAapiclient_key.pem商户证书序列号商户平台查看放在请求头 Authorization 里一串 40 位十六进制APIv3 密钥商户平台自行设置回调报文解密、敏感字段加密32 位字符串平台证书通过接口下载验证微信回调签名动态获取会轮换注意最后一行平台证书不是你去后台下载的静态文件v3 设计上就是让商户用「下载平台证书」接口定期拉取因为微信会轮换。实例里如果还是让你手动塞一个 cert.pem 进去那它大概率没处理轮换上线几个月后回调验签会突然全挂——这是血泪经验里最常见的一种。2.2 目录怎么摆配置怎么抽我一般把凭证和业务代码彻底分开避免私钥被误提交。实例的目录结构建议改成这样project/ ├── config/ │ └── wxpay.php # 只放配置返回数组 ├── cert/ │ ├── apiclient_key.pem # 商户私钥权限 600 │ └── platform/ # 平台证书缓存目录可写 ├── src/ │ ├── WxPayClient.php # 签名、请求、验签 │ └── WxPayNotify.php # 回调处理 └── public/ └── notify.php # 回调入口配置文件长这样把易变项全抽出来?php // config/wxpay.php return [ mchid 1900000001, // 商户号 appid wxxxxxxxxxxxxxxxxx, // 关联的 appid serial_no 5157F09EFDC096DE15EBE81A47057A72, // 商户证书序列号 private_key __DIR__ . /../cert/apiclient_key.pem, apiv3_key your32charapiv3keyxxxxxxxxxxxxx, // 必须 32 位 platform_dir __DIR__ . /../cert/platform, notify_url https://your.domain/public/notify.php, ];参数说明serial_no不是私钥里的内容要去商户平台「API 安全」里复制填错会直接返回 401 签名错误apiv3_key长度必须 32少一位解密回调时就抛openssl_decrypt失败platform_dir要给 PHP 进程写权限否则拉不到平台证书。私钥文件权限设 600别跟着代码进 git这是最低要求。2.3 平台证书的自动下载逻辑实例里最值得抄的一段是平台证书拉取。核心思路调GET /v3/certificates返回的每个证书用 APIv3 密钥 AES-256-GCM 解密按序列号存成文件请求时按响应头里的Wechatpay-Serial去匹配。?php // 拉取并缓存平台证书简化版 function refreshPlatformCerts(array $cfg): array { $client new WxPayClient($cfg); $resp $client-get(/v3/certificates); // 内部已带签名头 $certs []; foreach ($resp[data] as $item) { // 用 APIv3 密钥解密 encrypt_certificate $plain decryptAesGcm( $cfg[apiv3_key], $item[encrypt_certificate][nonce], $item[encrypt_certificate][associated_data], $item[encrypt_certificate][ciphertext] ); $certs[$item[serial_no]] $plain; file_put_contents( $cfg[platform_dir] . / . $item[serial_no] . .pem, $plain ); } return $certs; }逻辑说明decryptAesGcm里openssl_decrypt的$tag参数要传密文末尾 16 字节很多人直接把整个 ciphertext 丢进去结果解密出空串。参数上associated_data通常是certificate别写死成空。缓存策略我一般设成「本地没有就拉验签失败再拉一次」既避免每次请求都多打一次接口又能在轮换后自愈。3. 下单与签名一个请求头写错就 4013.1 Authorization 头的拼装规则v3 的签名不是把参数排序拼串那么简单它签的是「方法 URL 时间戳 随机串 请求体」五行拼接后的 SHA256-RSA。请求头格式固定Authorization: WECHATPAY2-SHA256-RSA2048 mchid...,nonce_str...,signature...,timestamp...,serial_no...实例里这段封装在buildAuthorization()我把它单独拎出来讲因为这里错一个字符就 401而且报错信息只有一句「签名错误」非常黑匣子。?php function buildAuthorization(string $method, string $url, string $body, array $cfg): string { $timestamp time(); $nonce bin2hex(random_bytes(16)); // 32 位随机串 // 待签名串五行每行以 \n 结尾 $message $method . \n . $url . \n . $timestamp . \n . $nonce . \n . $body . \n; $key openssl_pkey_get_private(file_get_contents($cfg[private_key])); openssl_sign($message, $sign, $key, OPENSSL_ALGO_SHA256); $signature base64_encode($sign); return sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%s,serial_no%s, $cfg[mchid], $nonce, $signature, $timestamp, $cfg[serial_no] ); }参数说明$url必须是带 query 的完整路径比如/v3/pay/transactions/jsapi不带域名$body对 GET 请求传空串但那个\n不能省random_bytes(16)转 hex 正好 32 位用uniqid()会短签名照样失败。openssl_sign第四个参数必须显式写OPENSSL_ALGO_SHA256默认是 SHA1这是老代码迁移时最容易漏的一处。3.2 JSAPI 下单的完整请求体以小程序/公众号支付为例下单接口是POST /v3/pay/transactions/jsapi。请求体字段不多但amount.total单位是分payer.openid必须和 appid 对得上这两点错一个就报「参数错误」或「openid 不匹配」。?php $payload [ appid $cfg[appid], mchid $cfg[mchid], description 测试商品-1件, out_trade_no T . date(YmdHis) . mt_rand(1000, 9999), notify_url $cfg[notify_url], amount [total 1, currency CNY], // 单位分 payer [openid $openid], ]; $body json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $resp $client-post(/v3/pay/transactions/jsapi, $body);逻辑说明json_encode必须带JSON_UNESCAPED_UNICODE否则中文描述被转成\uXXXX虽然微信能收但你本地日志排查时很难读JSON_UNESCAPED_SLASHES是为了 notify_url 里的斜杠不被转义避免签名串和实际发送体不一致。out_trade_no建议带时间戳加随机数重复会返回「订单号已存在」。返回的prepay_id还要再签一次名给前端调起支付那一步的签名串是appId\ntimeStamp\nnonceStr\nprepay_idxxx\n格式和请求签名不同实例里单独有buildPayParams()别混用。3.3 回调验签与解密顺序不能反回调处理是翻车重灾区。正确顺序是先验签用平台证书验Wechatpay-Signature再解密resource用 APIv3 密钥解 AES-256-GCM最后才处理业务。顺序反了等于拿未验证的数据做业务安全上直接失守。?php // public/notify.php 核心片段 $rawBody file_get_contents(php://input); $headers array_change_key_case(getallheaders(), CASE_LOWER); // 1. 验签构造验签串 $message $headers[wechatpay-timestamp] . \n . $headers[wechatpay-nonce] . \n . $rawBody . \n; $serial $headers[wechatpay-serial]; $pubKey file_get_contents($cfg[platform_dir] . / . $serial . .pem); $ok openssl_verify($message, base64_decode($headers[wechatpay-signature]), $pubKey, OPENSSL_ALGO_SHA256); if ($ok ! 1) { http_response_code(401); exit(sign fail); } // 2. 解密 resource $res json_decode($rawBody, true)[resource]; $plain decryptAesGcm($cfg[apiv3_key], $res[nonce], $res[associated_data], $res[ciphertext]); $data json_decode($plain, true); // 3. 幂等处理 返回成功 if ($data[trade_state] SUCCESS) { // 按 out_trade_no 更新订单注意加唯一索引防重复 } http_response_code(200); echo json_encode([code SUCCESS]);参数说明getallheaders()在部分 FPM 环境拿不到实例里做了$_SERVER兜底验签用的平台证书按Wechatpay-Serial匹配匹配不到就触发一次refreshPlatformCerts再试返回体必须是{code:SUCCESS}且 HTTP 200返回其他内容微信会持续重试最多重试多次你的订单表如果没有幂等保护就会被重复更新。这一点我在一个模拟项目里吃过亏回调重试把库存扣成了负数。4. 退款、查单与对账单把边界情况先想好4.1 退款接口的金额与回调退款走POST /v3/refund/domestic/refunds和下单不同它有自己的notify_url退款结果异步通知。请求体关键字段?php $refund [ out_trade_no $orderNo, out_refund_no R . $orderNo, // 退款单号唯一 amount [ refund 1, // 退款金额分 total 1, // 原订单金额分 currency CNY, ], ]; $resp $client-post(/v3/refund/domestic/refunds, json_encode($refund, JSON_UNESCAPED_UNICODE));逻辑说明refund不能大于total否则报「退款金额大于订单金额」out_refund_no重复提交会返回同一笔退款结果这其实是幂等特性可以善用。退款回调的解密流程和支付回调完全一致只是event_type是REFUND.SUCCESS实例里复用了同一个WxPayNotify类按事件类型分发这个设计比复制两份代码干净。4.2 查单与对账单的用途区分查单GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx用于主动补偿回调没来、用户说付了但订单没变就靠它兜底。对账单GET /v3/bill/tradebill是 T1 的用于财务对账别拿它做实时校验。实例里把查单封装成queryOrder()建议在定时任务里对「待支付超过 5 分钟」的订单跑一遍比干等回调稳。4.3 一个容易忽略的时区与时间戳问题v3 请求头里的timestamp是秒级 Unix 时间微信允许的偏差是 5 分钟。服务器时间如果没同步签名会报「时间戳错误」。我一般在上线前跑一次date和ntpdate校验容器环境尤其注意基础镜像的时区。另外回调验签时也要校验Wechatpay-Timestamp与本地时间差超过 5 分钟直接拒绝防重放。5. 避坑排查五个真实踩过的坑5.1 现象所有请求返回 401「签名错误」原因九成是待签名串拼错。常见三种——URL 带了域名、GET 请求 body 没传空串导致少一个\n、openssl_sign没指定 SHA256。解决把待签名串原样打日志注意别把私钥打进去逐行比对用官方提供的签名校验工具对一遍。我习惯在开发环境把$message输出到独立日志文件肉眼数换行。5.2 现象回调解密得到空字符串原因openssl_decrypt的$tag参数没传或传错。AES-256-GCM 的认证标签是密文最后 16 字节必须单独截出来传给第四个参数。解决$ciphertext base64_decode($res[ciphertext]); $tag substr($ciphertext, -16); $data openssl_decrypt(substr($ciphertext, 0, -16), aes-256-gcm, $key, OPENSSL_RAW_DATA, $nonce, $tag, $res[associated_data]);参数说明$key是 32 位 APIv3 密钥原文不要做 md5$nonce是 12 字节associated_data原样传。少任何一个都解不出来。5.3 现象上线一两个月后回调突然全部验签失败原因平台证书轮换了本地缓存的旧证书对不上新的Wechatpay-Serial。解决验签前先按序列号找本地证书找不到就调一次证书下载接口刷新再验同时加个定时任务每天刷一次。别把平台证书写死在代码里。5.4 现象订单被重复更新、库存扣成负数原因微信回调会重试你的处理逻辑没有幂等。解决给out_trade_no加唯一索引更新时用UPDATE ... WHERE status 待支付带状态条件影响行数为 0 就直接返回成功不重复执行业务。这个坑和支付本身无关但十个项目里八个中招。5.5 现象本地能跑服务器报 cURL 证书错误原因服务器 CA 证书过期或没装v3 请求走 HTTPS 双向校验。解决更新系统 CA 包或在 cURL 里显式指定CURLOPT_CAINFO别图省事关掉CURLOPT_SSL_VERIFYPEER那是给自己埋雷。6. 进阶把签名和证书做成可观测的真正让这套实例在生产里站住的不是它能下单而是出问题时你能快速定位。我的习惯是给WxPayClient加一层轻量日志每次请求记录method、url、http_code、Wechatpay-Request-ID响应头里有失败时把响应体前 500 字符也记下来。微信的报错信息很吝啬但Request-ID是它侧排查的唯一线索报给技术支持时带上这个 ID对方能直接查到那次请求。再进一步把平台证书的序列号和过期时间做成一个健康检查接口?php // 健康检查返回当前缓存的平台证书序列号与到期时间 $certs glob($cfg[platform_dir] . /*.pem); $out []; foreach ($certs as $f) { $info openssl_x509_parse(file_get_contents($f)); $out[] [ serial basename($f, .pem), valid_to date(Y-m-d H:i:s, $info[validTo_time_t]), ]; } header(Content-Type: application/json); echo json_encode($out);参数说明openssl_x509_parse返回的validTo_time_t是到期时间戳提前一周告警就不会出现「证书过期导致回调全挂」的深夜事故。这个接口别暴露公网挂在内网或加个简单 token。还有一个验证技巧上线前用沙箱环境把「下单→支付→回调→退款→退款回调」整条链路跑一遍沙箱的商户号和正式环境是分开的配置里加个env开关切换 base URL 即可。我一般会在沙箱里故意把apiv3_key改错一位确认解密失败时日志能定位到具体字段而不是只抛一个空异常。从那以后我每次接新的支付渠道都强制先把「验签失败」「解密失败」「证书过期」三种异常各造一次看日志能不能说清问题再谈业务逻辑。支付这块没有后悔药能提前看见的错就别留给线上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?