简介面向PHP开发者的微信支付V3完整实例包适合线上商城、线下门店等需要接入最新微信支付接口的场景。资源围绕统一下单、支付回调、订单查询、API签名与证书管理等核心环节展开覆盖从沙箱测试到上线部署的主要步骤。压缩包共16个文件大小仅61KB其中包含7个asp脚本、3个php文件以及pem证书、txt说明文档、mdb数据库、js和gif等辅助素材既可作为功能参考也可辅助理解中转与兼容性设计。包内含可直接运行的demo并细分了支付配置、签名算法、证书加载、通知验签、异常处理及退款接口等模块可帮助开发者快速搭建起可用的支付链路。目前已有4644人学习/下载。对希望用PHP快速接入微信支付V3、避免重复踩坑的中高级开发者来说这套实例提供了从商户配置到支付完成页的完整代码级参考具有较强的实用价值。1. PHP微信支付v3不是换接口是换了一套签名体系很多开发者第一次接触 PHP微信支付v3 时第一反应是“把 v2 的 XML 改成 JSON 不就行了”。真正动手才发现v3 不是换了个数据格式而是把整套签名、加密、证书体系都换掉了请求头从 MD5 签名变成了 SHA256-RSA2048回调里多了一层 AES-256-GCM 解密连证书都分成了 API 证书和平台证书两套。这意味着你没法靠“照着 v2 改改参数”来接入必须重新理解一遍流程。这篇文章把 v3 从证书准备、下单请求、回调验收到排错思路完整走一遍代码是 PHP 的适合有 PHP 基础但没碰过 v3 的开发者照着重现。读完你能独立跑通 JSAPI 下单和回调全链路也知道签名失败、解密乱码这类常见问题到底出在哪一步。2. 先把证书和密钥捋清楚v3 的签名体系与 PHP 配置准备2.1 四样东西各管什么API 证书、平台证书、商户私钥、APIv3 密钥微信支付 v3 里你手里需要管四样东西很多人就是在这里开始乱的。第一样是商户 API 证书这是商户自己的身份凭证用来给请求签名第二样是商户 API 私钥和 API 证书成对签名时用私钥验签时用公钥。第三样是微信支付平台证书这是微信那边的公钥证书用来验证回调是不是微信发来的以及加密敏感字段。第四样是APIv3 密钥这是你在商户平台自己设置的一个 32 字节密钥专门用来解密回调报文和 API 证书私钥完全不是一回事。这四样东西的关系可以这样记发请求时用商户私钥签名收回调时用平台证书验签加密 openid 之类的敏感字段也用平台证书公钥解密回调内容用 APIv3 密钥。前两样是“证明我是我”后两样是“证明它是它”混了任何一组后面全是莫名其妙的签名错误或解密失败。2.2 用 OpenSSL 生成 API 证书把私钥转成 PHP 能读的格式商户 API 证书的正常申请流程是登录商户平台在“API 安全”里下载证书工具生成 CSR再提交给微信签发。但如果你只是本地开发或者做自动化部署可以用 OpenSSL 先自己生成一套密钥对再把公钥部分转成 CSR 提交给微信走一遍签发。常见的做法是# 生成 2048 位 RSA 私钥 openssl genrsa -out merchant_private_key.pem 2048 # 从私钥导出公钥 openssl rsa -in merchant_private_key.pem -pubout -out merchant_public_key.pem # 生成 CSRCommon Name 填商户号 openssl req -new -key merchant_private_key.pem -out merchant.csr -subj /CN商户号这里有几个细节要提醒你。私钥文件生成后微信支付 v3 的 PHP SDK 一般要求 PKCS8 格式的私钥OpenSSL 默认输出的BEGIN PRIVATE KEY就是 PKCS8不需要额外转换但如果你在别的工具里拿到的是BEGIN RSA PRIVATE KEY那就是 PKCS1需要先转换一次openssl pkcs8 -topk8 -inform PEM -in merchant_private_key.pem -outform PEM -nocrypt -out merchant_private_key_pkcs8.pem还有一个容易翻车的点私钥文件里不能有额外的空格或换行被 PHP 读到。file_get_contents读进来之后最好做一次 trim否则openssl_sign可能直接返回 false不给你任何有用报错。2.3 写一个配置类把商户号、私钥路径、APIv3 密钥集中管理证书和密钥分散在各个目录里业务代码到处去读文件后面排查问题会非常痛苦。我一般会先写一个配置类把入口收拢?php class WxPayConfig { public static function mchid(): string { return 你的商户号; } /** 商户 API 证书序列号在证书详情里能看到 */ public static function serialNo(): string { return 你的证书序列号; } /** 商户私钥文件路径 */ public static function privateKeyPath(): string { return /path/to/merchant_private_key_pkcs8.pem; } /** 商户私钥内容加个 trim 防止读取到空白字符 */ public static function privateKey(): string { return trim(file_get_contents(self::privateKeyPath())); } /** 微信支付平台证书路径从商户平台下载 */ public static function platformCertPath(): string { return /path/to/wechatpay_platform_cert.pem; } /** APIv3 密钥32 字节在商户平台自己设置的 */ public static function apiV3Key(): string { return 你的APIv3密钥; } /** 回调通知地址公网可访问 */ public static function notifyUrl(): string { return https://你的域名/api/wxpay/notify.php; } }配置类的好处是后续所有地方都通过WxPayConfig::mchid()这种方式取值不会被某个文件路径改了就全局崩。注意证书序列号不是文件名也不是证书内容是在证书详情里看到的那串十六进制字符串很多人在这一步就卡住了。3. 用 PHP 发起 v3 下单请求签名头、敏感字段加密与 cURL 实现3.1 构造 Authorization 头先拼签名串再签名v3 的请求签名和 v2 完全不同它不用把参数排序后拼在一起而是拼一个“签名串”这个串由请求方法、请求路径、时间戳、随机串、请求体五部分组成每行一个POST /v3/pay/transactions/jsapi 1700000000 随机串 请求体注意第二行是完整的请求路径不含域名带 query第五行请求体必须是发送出去的原始字符串不是数组不是格式化后的 JSON。我用 PHP 实现签名时的做法是function buildSignString(string $method, string $urlPath, string $timestamp, string $nonce, string $body): string { return $method . \n . $urlPath . \n . $timestamp . \n . $nonce . \n . $body; }拼好签名串之后再用商户私钥做 SHA256-RSA 签名签名结果是 base64 字符串。关键代码如下function signWithPrivateKey(string $signString, string $privateKey): string { $signature ; $result openssl_sign($signString, $signature, $privateKey, OPENSSL_ALGO_SHA256); if (!$result) { throw new RuntimeException(openssl_sign 失败请检查私钥格式); } return base64_encode($signature); }openssl_sign的第四个参数是摘要算法这里必须是OPENSSL_ALGO_SHA256这是 v3 签名算法SHA256-RSA2048的默认选择。如果你写成了 MD5 或其他算法微信那边验签必定失败。3.2 对 openid 等敏感字段做 RSA 加密v3 接口里openid、退款原因这类敏感字段不能明文放在请求体里需要用微信支付平台证书的公钥加密。加密方式是 RSAES-OAEPSHA1 作为摘要算法结果转 base64 后填入请求体function encryptSensitiveField(string $field, string $platformCertContent): string { $publicKey openssl_pkey_get_public($platformCertContent); $encrypted ; $success openssl_public_encrypt($field, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); if (!$success) { throw new RuntimeException(openid 加密失败检查平台证书路径); } return base64_encode($encrypted); }这里有两个细节值得注意。第一用的是平台证书的公钥不是商户自己的公钥第二padding 必须是OPENSSL_PKCS1_OAEP_PADDING如果用了默认的OPENSSL_PKCS1_PADDING微信那边会解密失败。这个接口在官方文档里叫“敏感信息加解密”你搜索时用这个词能直接找到规范。3.3 JSAPI 下单完整代码与响应处理把上面几块拼起来就是一次完整的 JSAPI 下单。以下是完整的 PHP 函数包含了签名头、请求体构造、发请求和响应解析?php function jsapiOrder(string $openid, string $outTradeNo, int $amountFen, string $description): array { // 1. 构造请求体openid 必须先加密 $payload [ appid WxPayConfig::appId(), mchid WxPayConfig::mchid(), description $description, out_trade_no $outTradeNo, notify_url WxPayConfig::notifyUrl(), amount [total $amountFen, currency CNY], payer [openid encryptSensitiveField($openid, WxPayConfig::platformCertContent())], ]; $body json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); // 2. 拼签名串并签名 $timestamp time(); $nonce bin2hex(random_bytes(16)); $urlPath /v3/pay/transactions/jsapi; $signString POST\n{$urlPath}\n{$timestamp}\n{$nonce}\n{$body}\n; $signature signWithPrivateKey($signString, WxPayConfig::privateKey()); // 3. 构造 Authorization 头 $auth sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, WxPayConfig::mchid(), $nonce, $signature, $timestamp, WxPayConfig::serialNo() ); // 4. cURL 发起 POST $ch curl_init(https://api.mch.weixin.qq.com . $urlPath); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_RETURNTRANSFER true, CURLOPT_HTTPHEADER [ Content-Type: application/json, Accept: application/json, Authorization: . $auth, ], CURLOPT_POSTFIELDS $body, CURLOPT_TIMEOUT 10, ]); $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode 400) { throw new RuntimeException(微信支付下单失败 HTTP . $httpCode . : . $response); } return json_decode($response, true); }几个参数你需要根据自己的实际情况去改amountFen是整数分微信不接受小数元appid是你的公众号或小程序 appid和商户号要有绑定关系。响应里返回的prepay_id是下一步调起支付的核心参数需要再拿它去生成 JSAPI 调起支付的签名参数这一步如果你用的是官方 SDK 会有现成方法手动实现时注意签名串是appid\n时间戳\n随机串\nprepay_id\n这个格式很容易拼错。4. 支付回调处理平台证书验签、AES-256-GCM 解密与幂等落库4.1 回调验签验证消息确实来自微信支付支付成功后微信会向notify_url推送一个 POST 请求body 是 JSON格式和下单接口完全不同。接收回调时第一步不是读内容而是验证签名。验签用微信支付平台证书的公钥验签对象是请求头里的三样东西拼出来的串时间戳 随机串 请求体原始 body具体实现function verifyNotifySignature(array $headers, string $body, string $platformCertContent): bool { $timestamp $headers[Wechatpay-Timestamp] ?? ; $nonce $headers[Wechatpay-Nonce] ?? ; $signature $headers[Wechatpay-Signature] ?? ; $serial $headers[Wechatpay-Serial] ?? ; // 检查证书序列号是否匹配防止拿到过期证书 $certInfo openssl_x509_parse($platformCertContent); $certSerial strtoupper($certInfo[serialNumberHex] ?? ); if (strtoupper($serial) ! $certSerial) { return false; } $message {$timestamp}\n{$nonce}\n{$body}\n; $publicKey openssl_pkey_get_public($platformCertContent); return openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256) 1; }这里最常见的坑是头信息的 key 大小写。PHP 里从getallheaders()拿到的数组 key 保留原始大小写而有些服务器环境会统一转成小写你判断时需要兼容两种写法。另外验签用的始终是微信支付平台证书不是商户 API 证书用错了即使签名是对的也会返回 0。4.2 解密回调数据AES-256-GCM 的参数和坑验签通过之后body 里的resource字段才是你要的数据。它是加密的需要用 APIv3 密钥解密function decryptNotifyData(array $resource, string $apiV3Key): array { $ciphertext base64_decode($resource[ciphertext]); $nonce $resource[nonce]; $associatedData $resource[associated_data] ?? ; $tagLength 16; $tag substr($ciphertext, -$tagLength); $ciphertextWithoutTag substr($ciphertext, 0, -$tagLength); $decrypted openssl_decrypt( $ciphertextWithoutTag, aes-256-gcm, $apiV3Key, OPENSSL_RAW_DATA, $nonce, $tag, $associatedData ); if ($decrypted false) { throw new RuntimeException(回调解密失败检查 APIv3 密钥和证书是否匹配); } return json_decode($decrypted, true); }两个关键参数$apiV3Key是 32 字节字符串不是 API 证书私钥也不是平台证书私钥openssl_decrypt的第四个参数必须用OPENSSL_RAW_DATA否则默认的 base64 处理会对二进制密文做二次解码导致失败。associated_data和nonce都必须原样传入少传一个都解不出来。4.3 把结果落到订单表先查再改别丢回调解密后拿到的数据长这样transaction_id、out_trade_no、trade_state、amount等。拿到之后不要直接改订单状态先查库确认订单存在且当前状态未更新过然后再更新最后给微信返回一个固定格式的成功响应$decrypted decryptNotifyData($resource, WxPayConfig::apiV3Key()); $order findOrderByOutTradeNo($decrypted[out_trade_no]); if (!$order) { // 查不到订单不能报错只能返回成功否则微信会反复重试 echo json_encode([code SUCCESS, message 成功]); exit; } // 幂等如果订单已经是已支付状态直接返回成功 if ($order[status] paid) { echo json_encode([code SUCCESS, message 成功]); exit; } updateOrderStatus($order[id], paid, $decrypted[transaction_id]); echo json_encode([code SUCCESS, message 成功]);这里有一个必须养成的习惯回调处理函数里永远不要先exit把成功响应的输出放在最后。如果查库失败你返回了非 SUCCESS 的 JSON微信会按规则重试多次同一笔订单的回调可能被推送好几遍配合幂等设计才不会重复发券、重复加余额。5. PHP 微信支付 v3 避坑指南签名失败、解密失败、回调不通的排查路径5.1 现象一下单接口返回 401提示签名错误这是第一次接入 v3 的人最常遇到的报错。原因通常是三个方向签名串和实际请求不一致、私钥用错、序列号不对。排查时先自己本地重放整个流程。用json_encode的JSON_UNESCAPED_SLASHES选项以及JSON_UNESCAPED_UNICODE保持中文不转义这两项直接影响请求体字符串内容。然后确认签名串里的路径是否带了 query string——有些接口路径如/v3/refund/domestic/refunds后面带?sub_mchidxxx签名串必须包含这部分。序列号问题则检查证书界面复制的那串字符串是否漏了前缀或多了空格用openssl_x509_parse读证书里的序列号对比最稳妥。5.2 现象二验签一直返回失败换了证书也不行回调验签失败先看serial_no是否和平台证书匹配。微信支付平台证书会定期轮换商户平台上会同时存在多张有效证书。你下载下来的证书如果已经被替换用旧证书去验签新请求当然失败。解决方式是在验签前先读请求头里的Wechatpay-Serial去证书列表里找到对应的那一张再验。另外还要检查openssl_verify函数的第四个参数。很多人传了OPENSSL_ALGO_SHA256却忘了它内部使用的是RSA-SHA256但你无法通过参数看到算法类型只能通过排除证书、签名串格式来逐步确认。最值得怀疑的永远是签名串里的换行符号拼接时少了一个\n或多了尾部空格验签必挂。5.3 现象三回调解密出来是乱码或空字符串解密乱码十有八九是把 APIv3 密钥和证书私钥搞混了。APIv3 密钥是 32 字节的字符串在商户平台的“API 安全”里设置的它和 API 证书没有关系。如果你怎么解都是乱码去商户平台重新设置一次 APIv3 密钥再确认配置类里读的就是那个新值。还有一种情况openssl_decrypt返回false但你没检查返回值直接转 JSON导致业务收到 null。我的习惯是解密之后先判 false再json_last_error()检查 JSON 解析是否成功两步都过了才继续业务逻辑。5.4 现象四回调地址收不到通知本地调试着急收不到回调的常见原因有三个。第一回调地址必须是公网能访问的 HTTPS 地址且证书有效微信支付对回调域名有校验第二你的服务器或框架里存在全局异常捕获回调入口一旦抛出异常返回 HTTP 500微信会重试但你刚好没看到日志第三回调处理里代码执行时间太长超过了微信的超时阈值。本地调试时可以在回调入口第一行就写文件日志记录原始 body 和 headers这样不用打断点也能确认请求是否到达。注意回调请求体不要重新编码直接记录 raw body验签用的必须是它。5.5 现象五退款接口提示密钥不匹配或证书序列号错误退款比下单多一个条件部分退款接口要求用商户私钥签名但验签时用的还是平台证书。很多人在退款时报“证书序列号错误”其实是把请求头serial_no写成了平台证书的序列号而这里应该填商户 API 证书的序列号。6. 收尾进阶把加解密封装成服务类再做一个闭环验证脚本走到这一步你的 PHP微信支付v3 接入基本通了。但业务代码里如果到处直接调用encryptSensitiveField、decryptNotifyData后期维护会相当吃力。我建议把整套加解密、签名、请求逻辑收敛到一个支付服务类里class WechatPayService { public function createJsapiOrder($openid, $outTradeNo, $amount) { // 内部完成加密 openid、构造签名头、发请求 } public function handleNotify($headers, $rawBody): array { $ok verifyNotifySignature($headers, $rawBody, WxPayConfig::platformCertContent()); if (!$ok) { throw new RuntimeException(验签失败); } $data json_decode($rawBody, true); return decryptNotifyData($data[resource], WxPayConfig::apiV3Key()); } }这样业务控制器里只调用两个公开方法涉及证书、密钥的底层细节全部收在类内部。换证书、换密钥时只改配置或者改这个类不会把隐患散落得到处都是。然后写一个验证脚本把一次完整的支付闭环在本地跑通先模拟下单拿到prepay_id再模拟构造回调头和数据来验证handleNotify。手动造回调数据时要注意验签这一步是用私钥签的脚本里可以用商户私钥对测试签名串签名然后喂给handleNotify确认能走到解密那一步。最后想跟你分享一个我自己的习惯每次改动证书、密钥或签名相关代码我都先跑一遍这个闭环脚本再上真实环境。微信支付的报错信息有时候比较模糊但它至少能帮你区分“签名串问题”“证书问题”“密钥问题”这三大类。你能走到解密那一步说明前面都对了。希望这篇笔记能帮你少踩些我当年踩过的坑祝接入顺利。本文还有配套的精品资源点击获取