
简介这份资源是面向PHP开发者的微信支付V3完整实例适合需要为线上商城或线下场景接入微信支付、希望掌握V3新接口安全机制的初中级开发者。压缩包共16个文件约61KB以asp与php脚本为主辅以txt说明、pem证书、mdb数据文件、js脚本及gif图片覆盖统一下单、前端调起支付、异步回调通知、订单查询确认等完整链路并涉及API签名、证书管理、沙箱测试、异常处理与退款等关键环节。其中pem证书与配置脚本可用于理解商户私钥、公钥的加载与签名验证流程说明文档则帮助快速理清目录结构与调用顺序。目前已有4630人学习下载读者可借助其中的代码示例与配置文件对照梳理V3支付从下单到回调的落地思路并参考安全与合规注意事项减少接入过程中的试错成本。1. PHP 微信支付v3 完整实例从下单到回调一次跑通全链路很多 PHP 项目接微信支付卡在 v3 版本上文档看了一遍签名验签还是报 401本地用 cURL 调通了一上宝塔 PHP 环境就 500回调地址配好了异步通知却一直没进来。微信支付 v3 相比 v2最大的变化是全面改用 JSON 请求体、SHA256-RSA 签名、AES-256-GCM 解密回调证书也从单一的 API 密钥换成了商户私钥加平台证书。这套机制本身不复杂但 PHP 生态里现成的完整实例偏少很多人只能对着官方文档一段段拼。这篇笔记就围绕「PHP 微信支付v3 完整实例」这个目标把 JSAPI 下单、签名生成、回调验签解密、订单查询这几步串成一条能直接复现的链路。适合正在用 PHP 8 做商城、知识付费、会员充值这类需要微信支付的开发者也适合从 v2 迁移过来、被签名和证书绕晕的熟手。下面所有代码都基于 PHP 8 OpenSSL 扩展不依赖官方 SDK方便你理解每一步到底在干什么。2. 微信支付v3 的签名与证书先把黑匣子拆开2.1 为什么 v3 的签名总报 401v2 时代签名用的是 MD5 或 HMAC-SHA256密钥就是那串 32 位的 API 密钥拼参数排序后算个哈希就行。v3 换成了非对称加密你用商户私钥对请求做 SHA256-RSA 签名微信平台用你上传的公钥验签反过来微信返回的数据用平台私钥签名你要用平台证书里的公钥验签。401 报错绝大多数不是代码写错而是签名串拼错了。v3 的签名串有固定格式五行每行以\n结尾最后一行也要有HTTP请求方法\n URL路径\n 请求时间戳\n 随机字符串\n 请求报文主体\n注意几个细节URL 路径要带 query string比如/v3/pay/transactions/jsapi不带域名请求时间戳是秒级请求报文主体对 GET 请求是空字符串但那个\n不能省。我见过太多人栽在最后一行没换行或者 GET 请求把 body 写成了null字符串。2.2 商户私钥、证书序列号、APIv3 密钥三件套怎么准备在微信商户平台「账户中心 - API 安全」里你需要拿到三样东西材料用途存放建议商户 API 私钥 apiclient_key.pem请求签名放在项目外目录权限 600商户证书序列号请求头 Authorization 里标识用哪把钥匙从 apiclient_cert.pem 里读或平台直接看APIv3 密钥解密回调里的敏感字段32 位自己设置的别和 API 密钥搞混商户私钥和证书是一对用官方工具生成后会给你apiclient_key.pem和apiclient_cert.pem。序列号可以用这条命令读出来openssl x509 -in apiclient_cert.pem -noout -serial | awk -F {print $2}读出来是一串十六进制注意大小写要和平台显示一致通常是大写。APIv3 密钥是你在商户平台单独设置的 32 位字符串只用于 AES-256-GCM 解密不参与签名。这三样东西搞混是新手最常见的翻车点。2.3 用 PHP 8 生成一次合法签名下面这段代码是签名的最小实现不依赖任何 SDK?php // 商户私钥路径、证书序列号、APIv3密钥 $mchPrivateKeyPath /secure/apiclient_key.pem; $mchSerialNo 4A3B...; // 你的证书序列号 $apiV3Key your32charapiv3key000000000000; /** * 生成 v3 请求签名 * param string $method HTTP 方法大写 * param string $urlPath 带 query 的路径如 /v3/pay/transactions/jsapi * param string $body 请求体 JSON 字符串GET 传空串 */ function buildAuthorization(string $method, string $urlPath, string $body): string { $mchPrivateKeyPath /secure/apiclient_key.pem; $mchSerialNo 4A3B...; $timestamp time(); $nonce bin2hex(random_bytes(16)); // 32 位随机串 // 拼签名串五行每行都以 \n 结尾 $message $method . \n . $urlPath . \n . $timestamp . \n . $nonce . \n . $body . \n; // 读取私钥并签名 $privateKey openssl_pkey_get_private(file_get_contents($mchPrivateKeyPath)); openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256); $sign base64_encode($signature); // 拼 Authorization 头 return sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%s,serial_no%s, 你的商户号, $nonce, $sign, $timestamp, $mchSerialNo ); }逻辑说明$message的拼接顺序和换行是微信规定的错一个字符验签就失败。random_bytes(16)生成 16 字节再转十六进制正好 32 位符合 nonce_str 要求。openssl_sign用 SHA256 算法对应头里的SHA256-RSA2048。参数上$body必须是最终发送的原始 JSON 字符串不能是数组再json_encode一次否则空格和转义会不一致。提示私钥文件用file_get_contents读进来即可不要用openssl_pkey_get_private直接传路径PHP 8 下传路径在某些环境会失败。3. JSAPI 下单完整实例从组装请求到拿到 prepay_id3.1 下单接口的请求体字段怎么填JSAPI 下单接口是POST /v3/pay/transactions/jsapi请求体是 JSON。核心字段如下字段必填说明appid是公众号或小程序的 appidmchid是商户号description是商品描述会显示在用户账单out_trade_no是商户订单号自己生成唯一notify_url是回调地址必须 HTTPS不能带参数amount.total是金额单位分整数payer.openid是用户 openidJSAPI 必填scene_info否场景信息PC 网站支付需要金额单位是分这点和 v2 一样别写成元。out_trade_no建议用「业务前缀 时间戳 随机数」长度 6 到 32 位只允许数字、字母、下划线、横线。3.2 组装请求并拿到 prepay_id?php function jsapiOrder(string $openid, int $totalFee, string $outTradeNo): array { $urlPath /v3/pay/transactions/jsapi; $bodyArr [ appid wx1234567890abcdef, mchid 1900000001, description 会员充值-月度, out_trade_no $outTradeNo, notify_url https://yourdomain.com/notify.php, amount [total $totalFee, currency CNY], payer [openid $openid], ]; $body json_encode($bodyArr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $authorization buildAuthorization(POST, $urlPath, $body); $ch curl_init(https://api.mch.weixin.qq.com . $urlPath); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS $body, CURLOPT_RETURNTRANSFER true, CURLOPT_HTTPHEADER [ Authorization: . $authorization, Content-Type: application/json, Accept: application/json, User-Agent: your-app/1.0, ], CURLOPT_TIMEOUT 10, ]); $resp curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode ! 200) { throw new RuntimeException(下单失败: . $resp); } return json_decode($resp, true); // 含 prepay_id }逻辑说明json_encode时加了JSON_UNESCAPED_UNICODE和JSON_UNESCAPED_SLASHES保证中文和斜杠不被转义这样签名用的 body 和实际发送的 body 完全一致。notify_url必须是 HTTPS 且不带 query微信会校验。返回的prepay_id是下一步生成支付参数的关键。参数上totalFee是分outTradeNo要保证全局唯一重复下单会返回OUT_TRADE_NO_USED。CURLOPT_TIMEOUT设 10 秒微信接口偶尔慢但别设太长拖垮页面。3.3 把 prepay_id 转成前端能调起的支付参数拿到prepay_id后还要再签一次名生成paySign给前端WeixinJSBridge或wx.chooseWXPay用?php function buildPayParams(string $prepayId): array { $appId wx1234567890abcdef; $timeStamp (string)time(); $nonceStr bin2hex(random_bytes(16)); $package prepay_id . $prepayId; // 注意这里签名串是四行不是五行 $message $appId . \n . $timeStamp . \n . $nonceStr . \n . $package . \n; $privateKey openssl_pkey_get_private(file_get_contents(/secure/apiclient_key.pem)); openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256); return [ appId $appId, timeStamp $timeStamp, nonceStr $nonceStr, package $package, signType RSA, paySign base64_encode($signature), ]; }逻辑说明前端调起支付的签名串和请求接口的签名串不一样这里是四行appId、时间戳、随机串、prepay_idxxx。很多人直接复用请求签名结果前端报「支付签名验证失败」。signType固定RSA不是 v2 的MD5。注意timeStamp必须是字符串前端拿到后原样传给微信别转成数字否则签名对不上。4. 回调验签与解密异步通知最容易翻车的地方4.1 回调的验签流程和请求头微信支付成功后会向你的notify_url发一个 POST 请求请求头里带Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Serial。验签要用平台证书的公钥而不是商户自己的公钥。验签串的拼法和请求签名类似但顺序不同应答时间戳\n 应答随机串\n 应答报文主体\n注意这里没有 HTTP 方法和 URL 路径只有三行。Wechatpay-Timestamp和Wechatpay-Nonce从请求头取报文主体是原始 POST body。4.2 用平台证书验签并解密 resource平台证书需要先下载或者用「获取平台证书」接口拉取。验签通过后回调 body 里的resource是 AES-256-GCM 加密的需要用 APIv3 密钥解密?php function verifyAndDecrypt(string $body, array $headers, string $platformCertPath, string $apiV3Key): array { $timestamp $headers[Wechatpay-Timestamp]; $nonce $headers[Wechatpay-Nonce]; $signature base64_decode($headers[Wechatpay-Signature]); // 拼验签串三行 $message $timestamp . \n . $nonce . \n . $body . \n; // 用平台证书公钥验签 $pubKey openssl_pkey_get_public(file_get_contents($platformCertPath)); $ok openssl_verify($message, $signature, $pubKey, OPENSSL_ALGO_SHA256); if ($ok ! 1) { throw new RuntimeException(回调验签失败); } // 解密 resource $data json_decode($body, true); $ciphertext base64_decode($data[resource][ciphertext]); $nonceStr $data[resource][nonce]; $associatedData $data[resource][associated_data]; $plaintext openssl_decrypt( $ciphertext, aes-256-gcm, $apiV3Key, OPENSSL_RAW_DATA, $nonceStr, $associatedData ); if ($plaintext false) { throw new RuntimeException(解密失败); } return json_decode($plaintext, true); }逻辑说明openssl_verify返回 1 才算验签通过返回 0 是签名不匹配返回 -1 是出错。AES-256-GCM 解密时$nonceStr是 12 字节的 IV$associatedData是附加数据两者都从resource里取不能自己编。$apiV3Key必须正好 32 字节短了或长了都会解密失败。参数上$body必须是原始 POST 数据用file_get_contents(php://input)读不能用$_POST因为$_POST会做 URL 解码破坏原始报文。4.3 回调里必须做的幂等和应答解密后拿到的是支付结果里面有out_trade_no、transaction_id、trade_state。处理逻辑要注意两点第一幂等。微信会重复推送回调直到你返回成功。所以要先查订单状态已处理过的直接返回成功别重复发货。第二应答格式。成功返回 HTTP 200body 是{code:SUCCESS,message:成功}失败返回非 200body 里带code和message微信会按策略重试。?php // 回调入口 notify.php $body file_get_contents(php://input); $headers array_change_key_case(getallheaders(), CASE_LOWER); try { $result verifyAndDecrypt($body, $headers, /secure/wechat_platform_cert.pem, $apiV3Key); // 幂等检查 if (orderAlreadyPaid($result[out_trade_no])) { echo json_encode([code SUCCESS, message 成功]); exit; } // 处理业务更新订单、发货 handlePaidOrder($result); echo json_encode([code SUCCESS, message 成功]); } catch (Throwable $e) { http_response_code(500); echo json_encode([code FAIL, message $e-getMessage()]); }逻辑说明getallheaders()在部分 PHP-FPM 环境可能不存在可以用$_SERVER里HTTP_WECHATPAY_*手动拼。幂等检查建议用数据库唯一索引兜底别只靠代码判断并发下会漏。5. 订单查询与退款把状态对账做扎实5.1 主动查询订单状态回调可能因为网络问题丢失所以要有主动查询兜底。查询接口是GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx?php function queryOrder(string $outTradeNo, string $mchid): array { $urlPath /v3/pay/transactions/out-trade-no/ . $outTradeNo . ?mchid . $mchid; $authorization buildAuthorization(GET, $urlPath, ); // GET body 传空串 $ch curl_init(https://api.mch.weixin.qq.com . $urlPath); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER true, CURLOPT_HTTPHEADER [ Authorization: . $authorization, Accept: application/json, ], CURLOPT_TIMEOUT 10, ]); $resp curl_exec($ch); curl_close($ch); return json_decode($resp, true); }逻辑说明GET 请求签名时 body 传空字符串但签名串里那个\n不能省。urlPath要带 query string因为签名串里的 URL 路径包含 query。返回的trade_state有SUCCESS、REFUND、NOTPAY、CLOSED等只有SUCCESS才是支付成功。参数上out_trade_no和下单时一致mchid是商户号。查询频率别太高建议订单创建后 5 秒查一次最多查 3 次之后靠回调。5.2 退款接口的签名和回调退款是POST /v3/refund/domestic/refunds请求体里out_trade_no和out_refund_no二选一amount里refund是退款金额total是原订单金额单位都是分?php function refund(string $outTradeNo, int $refundFee, int $totalFee, string $outRefundNo): array { $urlPath /v3/refund/domestic/refunds; $body json_encode([ out_trade_no $outTradeNo, out_refund_no $outRefundNo, amount [refund $refundFee, total $totalFee, currency CNY], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $authorization buildAuthorization(POST, $urlPath, $body); // 后续 cURL 同下单略 return []; }逻辑说明退款金额不能大于原订单金额out_refund_no也要唯一。退款结果也是异步回调回调地址在商户平台配置验签解密流程和支付回调一样只是resource里的字段不同。参数上refundFee和totalFee都是分totalFee要和原订单一致否则会报PARAM_ERROR。6. 避坑与排查那些让我熬夜的报错6.1 签名报 401 或 SIGN_ERROR现象调接口返回{code:SIGN_ERROR,message:签名错误}或 HTTP 401。原因签名串拼接错误最常见的是 GET 请求 body 没传空串、最后一行没换行、URL 路径没带 query、或者json_encode后又被转义了一次。解决把签名串打印出来逐行对比。特别注意\n的位置用var_dump看字符串里是不是真的有换行。GET 请求的 body 传不是null。6.2 回调验签失败现象回调进来后openssl_verify返回 0。原因平台证书不对或者验签串拼错。平台证书要用「获取平台证书」接口拉取不能拿商户证书当平台证书。验签串是三行不是五行。解决先确认Wechatpay-Serial对应的平台证书是否正确再检查验签串。注意$body必须是原始报文用php://input读别用$_POST。6.3 解密报错或返回 false现象openssl_decrypt返回 false。原因APIv3 密钥长度不对或者nonce、associated_data取错。APIv3 密钥必须正好 32 字节nonce是 12 字节。解决检查 APIv3 密钥是不是 32 位别把 API 密钥当 APIv3 密钥用。nonce和associated_data从resource里取别自己生成。6.4 回调一直没进来现象支付成功但notify_url没收到请求。原因notify_url不是 HTTPS或者带了 query 参数或者服务器防火墙拦了微信的 IP。解决notify_url必须 HTTPS 且不带参数域名要能公网访问。检查服务器日志看有没有微信的请求进来。如果用了宝塔注意 PHP 版本和 OpenSSL 扩展是否开启。6.5 金额单位写错现象下单成功但金额不对或者报PARAM_ERROR。原因金额单位是分写成了元。解决所有金额字段统一用分前端传元的话在 PHP 里乘 100 再取整。别用浮点数用整数。7. 平台证书自动更新与本地调试技巧平台证书有有效期微信会定期更换。手动下载证书迟早会过期所以生产环境要做自动更新。思路是启动时或定时调用GET /v3/certificates拉取证书列表用 APIv3 密钥解密每个证书的encrypt_certificate拿到 PEM 格式存到本地并记录序列号。回调验签时根据Wechatpay-Serial找到对应证书。?php function refreshPlatformCerts(string $apiV3Key): void { $urlPath /v3/certificates; $authorization buildAuthorization(GET, $urlPath, ); // cURL 请求略拿到 $resp $data json_decode($resp, true); foreach ($data[data] as $item) { $ciphertext base64_decode($item[encrypt_certificate][ciphertext]); $nonce $item[encrypt_certificate][nonce]; $ad $item[encrypt_certificate][associated_data]; $pem openssl_decrypt($ciphertext, aes-256-gcm, $apiV3Key, OPENSSL_RAW_DATA, $nonce, $ad); // 存到 /secure/certs/{serial_no}.pem file_put_contents(/secure/certs/ . $item[serial_no] . .pem, $pem); } }逻辑说明encrypt_certificate里的ciphertext解密后就是 PEM 格式的证书直接存文件。序列号作为文件名回调时按Wechatpay-Serial取。建议每天凌晨跑一次或者每次验签失败时触发更新。本地调试时微信回调进不来可以用「查单」接口模拟。把out_trade_no填进去看返回的trade_state是不是SUCCESS。另外微信支付有沙箱环境但 v3 的沙箱和正式环境差异较大建议直接用 1 分钱真实下单测试回调地址用内网穿透工具映射到本地。调试时把curl的CURLOPT_VERBOSE打开能看到完整的请求和响应头排查签名问题很管用。我自己踩过最深的坑是回调验签一开始拿商户证书当平台证书用验签一直失败查了两天才发现证书用错了。后来养成习惯所有证书文件按用途命名mch_开头是商户的plat_开头是平台的再也没混过。希望帮到你。本文还有配套的精品资源点击获取