简介这是一份面向PHP开发者的微信支付V3接口接入案例适合具备PHP面向对象基础、需要为项目快速集成微信支付能力的开发者。资源围绕V3版本API展开同时兼容V2部分功能覆盖订单创建、支付验证、退款申请、支付结果异步通知、支付状态查询等核心环节并涉及OAuth2.0授权获取OpenID、RSA2048证书加解密与安全规范等关键知识点。压缩包共15个文件以14个PHP源码文件与1个Markdown说明文档为主PHP文件分别承担配置、核心支付类、JSAPI与Native扫码支付、订单与退款操作、回调通知及日志记录等职责整体约42KB结构紧凑便于按模块查阅。目前已有2012人学习下载。通过阅读与调试这些封装好的类读者可掌握从预支付下单、前端唤起支付到回调验证、退款查询的完整链路并借鉴其中的错误处理与日志调试思路降低接入微信支付的复杂度。1. 从一次支付回调翻车说起PHP 微信支付V3 案例能帮你省掉哪些事去年帮一个做知识付费的朋友排查线上问题用户付了钱后台订单还是待支付客服手动补单补到崩溃。翻日志发现是回调验签失败用的还是 V2 那套 MD5 拼串逻辑而商户号早就升级到 V3 了。这个 PHP 微信支付V3 案例就是冲着这类场景来的——它把 V3 接口的签名、验签、回调、退款、查单整条链路用 PHP 完整跑通代码直接能跑不是伪代码。适合两类人一类是接支付接得少、每次都被证书和签名绕晕的后端另一类是想把支付模块从老项目里剥出来、换成 V3 的维护者。下面按「能干什么 → 怎么跑 → 怎么改」拆开讲。2. 项目定位与技术选型为什么是 PHP V3 而不是继续用 V22.1 这个案例到底封装了哪些支付能力从标题和常见实现来看这个案例覆盖的是微信支付 V3 的核心交易链路不是只给一个「统一下单」的 demo。它一般会包含这几块JSAPI 下单公众号/小程序内支付、Native 下单扫码支付、订单查询、申请退款、退款查询以及最容易被忽略的回调通知处理。V3 和 V2 最大的区别在于V2 用 MD5/HMAC-SHA256 拼参数签名V3 改用 SHA256-RSA 对「方法 URL 时间戳 随机串 请求体」拼成的签名串做私钥签名平台返回和回调则要用平台证书公钥验签。这个案例的价值就在于把这套非对称加密流程用 PHP 的 openssl 扩展落地了而不是让你自己去啃文档里那段签名规则。2.2 技术栈与运行环境项目说明语言PHP 7.4V3 用到部分类型特性7.4 起步比较稳依赖官方 wechatpay-php SDK 或自行封装的 openssl 签名类扩展openssl必须、curl、json证书商户 API 私钥 apiclient_key.pem、商户证书序列号、APIv3 密钥运行方式原生 PHP 起服务或挂到已有框架里常见做法是直接引入官方wechatpay/wechatpay这个 composer 包它把签名器、验签器、平台证书下载都封装好了。但很多老项目不方便上 composer所以这个案例里通常也会给一份纯 openssl 手写的签名实现方便你直接抄进老代码。选 PHP 而不是换语言是因为存量项目里 PHP 占比高支付这种和订单强耦合的模块重写成本远大于原地升级。2.3 目录结构长什么样一个典型的案例目录大致是这样组织的wechat-pay-v3/ ├── config/ │ └── wxpay.php # 商户号、序列号、密钥路径等配置 ├── cert/ │ ├── apiclient_key.pem # 商户私钥 │ └── wechatpay_cert.pem # 平台证书用于验签 ├── src/ │ ├── Signer.php # 签名与验签核心类 │ ├── Http.php # 带签名的请求封装 │ └── Pay.php # 下单/退款/查单业务方法 ├── notify.php # 回调通知入口 └── demo.php # 各接口调用示例这个结构的好处是签名逻辑和业务逻辑分开Signer.php只干签名验签Pay.php只拼业务参数。你二次开发时改业务只动Pay.php换证书只动config和cert互不干扰。很多新手把签名和下单写在一个文件里后面加个退款就得复制一遍签名代码维护起来是灾难。3. 核心模块拆解签名、下单、回调三段代码逐段看3.1 签名与验签V3 最容易翻车的地方V3 的请求签名串由五行拼成每行以\n结尾最后一行也要带换行。这是最容易被忽略的细节少一个换行签名就过不了。?php // Signer.php 核心签名方法 class Signer { // 构造签名串方法\nURL\n时间戳\n随机串\n请求体\n public function buildMessage(string $method, string $url, int $timestamp, string $nonce, string $body): string { return $method . \n . $url . \n . $timestamp . \n . $nonce . \n . $body . \n; // 末尾这个换行不能省 } // 用商户私钥做 SHA256-RSA 签名 public function sign(string $message, string $privateKeyPath): string { $key openssl_pkey_get_private(file_get_contents($privateKeyPath)); openssl_sign($message, $signature, $key, OPENSSL_ALGO_SHA256); return base64_encode($signature); // 请求头里要 base64 } // 用平台证书公钥验签回调/应答都用它 public function verify(string $message, string $signature, string $platformCertPath): bool { $cert openssl_x509_read(file_get_contents($platformCertPath)); $pubKey openssl_pkey_get_public($cert); return openssl_verify($message, base64_decode($signature), $pubKey, OPENSSL_ALGO_SHA256) 1; } }逻辑说明buildMessage严格按官方规则拼五行$body对 GET 请求传空字符串但换行符依然要保留。sign用商户私钥签名结果 base64 后放进Authorization头。verify用于回调注意验签用的签名串是「时间戳\n随机串\n回调body\n」和请求签名串的构造规则不同这是第二个高频翻车点。参数上$privateKeyPath指向apiclient_key.pem$platformCertPath指向从微信下载的平台证书两者不能混用。3.2 统一下单拼参数与发请求?php // Pay.php 里 JSAPI 下单示例 public function jsapiOrder(string $openid, int $totalFee, string $outTradeNo, string $desc) { $url https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi; $body json_encode([ appid $this-config[appid], mchid $this-config[mchid], description $desc, out_trade_no $outTradeNo, notify_url $this-config[notify_url], amount [total $totalFee, currency CNY], payer [openid $openid], ], JSON_UNESCAPED_UNICODE); $timestamp time(); $nonce bin2hex(random_bytes(16)); $message $this-signer-buildMessage(POST, /v3/pay/transactions/jsapi, $timestamp, $nonce, $body); $signature $this-signer-sign($message, $this-config[private_key]); // 组装 Authorization 头 $auth sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-config[mchid], $nonce, $signature, $timestamp, $this-config[serial_no] ); // 后续用 curl 带 Authorization 头请求 $url返回 prepay_id }逻辑说明total单位是分传 1 元要写 100这是新手最常见的金额错误。out_trade_no是商户侧订单号必须唯一重复下单会报错。签名串里的 URL 只取路径部分/v3/pay/transactions/jsapi不带域名带域名签名必挂。拿到prepay_id后还要用商户私钥再签一次生成前端wx.requestPayment需要的paySign这一步和请求签名是两码事别搞混。3.3 回调通知验签、解密、幂等三件事?php // notify.php 回调处理骨架 $raw file_get_contents(php://input); $headers getallheaders(); $timestamp $headers[Wechatpay-Timestamp]; $nonce $headers[Wechatpay-Nonce]; $signature $headers[Wechatpay-Signature]; // 1. 验签签名串是 时间戳\n随机串\nbody\n $message $timestamp . \n . $nonce . \n . $raw . \n; if (!$signer-verify($message, $signature, $platformCertPath)) { http_response_code(401); exit(sign fail); } // 2. 解密 resource 里的密文AES-256-GCM $data json_decode($raw, true); $plain $aesGcm-decrypt($data[resource][ciphertext], $data[resource][nonce], $data[resource][associated_data]); // 3. 幂等同一笔订单只处理一次 $order json_decode($plain, true); if ($this-isProcessed($order[out_trade_no])) { exit(success); // 已处理也要回 success否则微信会重推 } $this-markPaid($order[out_trade_no]); echo json_encode([code SUCCESS]);逻辑说明回调验签的签名串和请求签名串构造规则不同这里是「时间戳\n随机串\nbody\n」没有方法、URL 和请求体那几行。解密用 APIv3 密钥做 AES-256-GCMassociated_data和nonce都从回调体里取。幂等这步很多人不做结果微信重推时重复发货血泪经验。处理成功必须返回{code:SUCCESS}返回别的微信会按策略重推别想着返回个 200 空 body 糊弄过去。4. 跑起来从配置证书到本地验证的完整步骤4.1 准备三样东西私钥、序列号、APIv3 密钥在商户平台申请 API 证书后你会拿到apiclient_key.pem商户私钥和apiclient_cert.pem商户证书证书序列号在证书详情里能看到。APIv3 密钥是单独在「账户中心-API安全」里设置的 32 位字符串用于解密回调和签名私钥不是一回事。这三样缺一不可很多人只配了私钥忘了 APIv3 密钥结果回调解密直接报错。平台证书不用手动下载SDK 或案例里的下载逻辑会自动拉取并缓存但纯手写实现的话需要自己调/v3/certificates接口下载。4.2 配置文件怎么填?php // config/wxpay.php return [ appid 你的公众号或小程序appid, mchid 你的商户号, serial_no 商户证书序列号, private_key __DIR__ . /../cert/apiclient_key.pem, apiv3_key 你的32位APIv3密钥, notify_url https://你的域名/notify.php, ];notify_url必须是公网可访问的 https 地址本地开发可以用内网穿透工具映射一个临时域名但注意回调地址要和下单时传的一致。serial_no是商户证书序列号不是平台证书序列号这两个在验签和签名时分别用到填反了签名能过但验签会挂。4.3 本地跑通下单与回调先起一个本地服务php -S 0.0.0.0:8000然后访问demo.php触发下单。JSAPI 下单需要真实 openid本地测试建议先用 Native 下单它会返回一个二维码链接用微信扫码就能付。支付完成后微信会回调你的notify.php本地没公网地址的话可以先用案例里带的「手动触发回调」脚本把一段模拟回调报文喂给notify.php验证验签和解密逻辑是否跑通。这一步能过说明证书和密钥配置没问题再上真实环境就稳了。提示Native 下单返回的code_url用在线二维码工具转成图片就能扫测试阶段比 JSAPI 省事不用折腾 openid 获取。5. 避坑与排查支付对接里那些反复出现的坑5.1 签名失败99% 是签名串拼错了现象是接口返回 401 或「签名错误」。原因通常是签名串末尾少了换行、URL 带了域名、或者 GET 请求的 body 传了 null 而不是空字符串。解决方法是把拼好的签名串打印出来逐行对照官方文档重点看每行结尾是不是都有\n最后一行有没有。我一般会在签名方法里加一行日志把$message原样输出对比一次就能定位。5.2 回调验签失败平台证书过期或没更新现象是回调一直验签不过但下单正常。原因是平台证书有有效期微信会定期轮换你本地缓存的是旧证书。解决方法是实现证书自动更新逻辑每次验签前检查证书序列号是否在最新列表里不在就重新下载。案例里如果没带这个逻辑建议自己补一个定时任务每天拉一次/v3/certificates。5.3 金额对不上单位是分不是元现象是用户付了 1 元订单记成 100 元或者反过来。原因是 V3 的amount.total单位是分整数。解决方法是下单前统一做一次intval($yuan * 100)回调里拿到的也是分入库前想清楚存分还是存元别两处不一致。这个坑我见过不止一个项目踩退款时金额算错更麻烦。5.4 重复回调导致重复发货现象是用户收到两笔发货通知。原因是微信在没收到 success 应答时会重推而你的处理逻辑没有幂等。解决方法是用out_trade_no做唯一索引处理前先查状态已处理直接返回 success。别用「先查再写」这种有并发窗口的方式直接靠数据库唯一约束兜底最稳。5.5 证书路径写错导致 openssl 报错现象是openssl_sign(): supplied key param cannot be coerced。原因是私钥路径不对或者文件内容被编辑器加了 BOM 头。解决方法是确认file_get_contents能读到内容且文件开头是-----BEGIN PRIVATE KEY-----没有多余字符。用绝对路径比相对路径靠谱相对路径在不同入口文件下解析结果不一样。6. 二次开发把案例改造成能进生产环境的支付模块案例能跑通不代表能直接上生产中间还差几块。第一块是配置外置别把商户号和密钥硬编码在代码里挪到环境变量或配置中心config/wxpay.php里读getenv(WX_MCHID)这种。第二块是日志支付出问题没有日志就是黑匣子建议在签名、请求、回调三个节点各打一条日志记录out_trade_no、请求参数、返回码但注意别把私钥和 APIv3 密钥打进日志。第三块是异常处理网络超时、微信返回错误码都要捕获下单失败要能回滚本地订单状态不能用户付了钱订单还是待支付。改造时我一般会抽一个PayService类把下单、查单、退款、回调处理都收进去对外只暴露业务方法签名和 HTTP 请求藏在内部。这样换 SDK 或者升级接口版本时只动PayService内部调用方无感。验证改造是否成功我会写一个最小回归脚本Native 下单 → 扫码支付 → 回调入库 → 查单确认 → 发起退款 → 退款回调整条链路跑一遍每步断言状态。这个脚本跑通基本就能上生产了。还有一个具体技巧把平台证书的下载和缓存单独封装缓存到本地文件并记录序列号和过期时间验签时先读缓存过期或序列号不匹配再下载。这样既避免每次回调都请求微信接口又不会因为证书轮换导致验签失败。代码上就是给Signer加一个getPlatformCert()方法内部判断缓存有效性。从那以后我每次接支付都强制先把「签名串打印 回调幂等 金额单位」这三件事过一遍再动手写业务。这三样不出问题支付模块基本就稳了。希望帮到你。本文还有配套的精品资源点击获取