news 2026/10/17 2:52:42

PHP微信支付V3实战:从签名验签到回调解密完整案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP微信支付V3实战:从签名验签到回调解密完整案例

简介:这是一份面向PHP开发者的微信支付V3接口接入案例,适合具备PHP面向对象基础、需要为项目快速集成微信支付能力的开发者。资源围绕V3版本核心支付功能展开,同时兼容V2部分能力,覆盖订单创建、支付验证、退款申请、支付结果异步通知、支付状态查询等关键环节,并对OAuth2.0授权获取OpenID、RSA2048证书加解密、安全规范与错误日志处理等要点做了封装,降低接入复杂度。压缩包共15个文件,以14个PHP源码为主,辅以1个Markdown说明文档,整体约42KB,涵盖配置、核心支付类、JSAPI与Native扫码支付、订单与退款操作、回调通知及示例代码等模块,目录结构清晰便于按需查阅。目前已有2012人学习下载,读者可借此理解完整支付流程与各接口交互逻辑,掌握证书管理、回调验证与退款查询的落地写法,并参考示例快速调试自己的支付模块。

1. 从一次支付回调超时说起:这个 PHP 微信支付 V3 案例到底能干什么

去年帮一个做知识付费的朋友排查线上问题,用户付完款课程没开通,查日志发现回调接口返回了 500,微信那边重试了三次全挂。翻代码一看,用的是 V2 版本的 XML 回调解析,签名校验逻辑还写错了。后来换成 V3 版本,用平台证书验签、AES-GCM 解密回调数据,问题才彻底解决。这个 PHP 微信支付 V3 案例就是干这个的——它把 V3 版本的下单、回调、退款、查单这些核心链路都跑通了,用的是 PHP 语言,适合正在做微信支付接入或者从 V2 迁移到 V3 的开发者。不管你是接 Native 扫码支付还是 JSAPI 公众号支付,这个案例里的代码结构和签名逻辑都能直接抄作业。

2. 技术选型与目录结构:为什么用 PHP 原生 + cURL 而不是框架

2.1 技术栈拆解

这个案例没有依赖 Laravel、ThinkPHP 这类重型框架,用的是 PHP 原生语法加 cURL 扩展。为什么这么选?因为微信支付 V3 的核心难点不在框架,而在签名生成、证书管理和回调验签。用原生写反而能把每个环节看清楚,你往任何框架里移植都方便。

具体依赖就几个:

  • PHP 7.4 以上,需要开启openssl、curl、json扩展
  • 商户 API 证书(apiclient_cert.pem、apiclient_key.pem)
  • 微信支付平台证书(用于回调验签,V3 必须)
  • 一个能接收 HTTPS 回调的公网域名

常见做法是把商户私钥和平台证书放在项目外的安全目录,通过环境变量或者配置文件读取,不要直接提交到代码仓库。我一般会在项目根目录建一个cert/文件夹,加进.gitignore,部署时手动上传。

2.2 目录结构说明

案例的目录结构很扁平,没有过度设计:

wechat-pay-v3/ ├── config/ │ └── config.php # 商户号、appid、证书路径等配置 ├── cert/ │ ├── apiclient_cert.pem # 商户证书 │ ├── apiclient_key.pem # 商户私钥 │ └── platform_cert.pem # 平台证书(用于验签) ├── src/ │ ├── WxPayV3.php # 核心类:签名、请求、验签 │ └── WxPayException.php # 异常处理 ├── demo/ │ ├── native_pay.php # Native 扫码支付示例 │ ├── jsapi_pay.php # JSAPI 支付示例 │ ├── notify.php # 回调处理示例 │ └── refund.php # 退款示例 └── README.md

src/WxPayV3.php是整个项目的核心,所有请求都经过它。demo/目录下的文件是给你直接复制到项目里改的,每个文件对应一个支付场景。这种结构的好处是,你不需要理解整个框架,只要看懂一个核心类加几个示例文件就能上手。

2.3 核心类初始化逻辑

先看WxPayV3.php的构造函数和基础配置:

<?php class WxPayV3 { private $mchId; // 商户号 private $appId; // 公众号或小程序 appid private $serialNo; // 商户证书序列号 private $privateKey; // 商户私钥内容 private $platformCert; // 平台证书内容 private $apiV3Key; // APIv3 密钥 public function __construct($config) { $this->mchId = $config['mch_id']; $this->appId = $config['app_id']; $this->apiV3Key = $config['api_v3_key']; // 读取商户私钥 $this->privateKey = file_get_contents($config['private_key_path']); // 读取平台证书 $this->platformCert = file_get_contents($config['platform_cert_path']); // 获取证书序列号 $this->serialNo = $this->getCertSerialNo($config['cert_path']); } // 从商户证书中提取序列号 private function getCertSerialNo($certPath) { $cert = file_get_contents($certPath); $parsed = openssl_x509_parse($cert); return $parsed['serialNumberHex']; } }

这里有几个关键点。serialNo不是随便填的,必须从商户证书里解析出来,微信服务端会用这个序列号找到对应的公钥来验证你的签名。apiV3Key是你在商户平台设置的 32 位密钥,用于解密回调数据里的resource字段。平台证书在 V3 里是必须的,因为微信返回的数据和回调通知都需要用它来验签,不像 V2 那样用 MD5 密钥就能糊弄过去。

3. 核心模块拆解:签名、请求、回调验签的完整链路

3.1 签名生成:V3 和 V2 最大的区别

V3 的签名机制比 V2 复杂得多,但安全性也高得多。V2 是用 MD5 拼参数,V3 是用 SHA256-RSA 对特定格式的字符串签名。看代码:

// 生成请求签名 public function buildSignature($method, $url, $body, $timestamp, $nonce) { // 1. 构造待签名字符串 $message = $method . "\n" . $url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; // 2. 用商户私钥进行 SHA256-RSA 签名 openssl_sign($message, $signature, $this->privateKey, OPENSSL_ALGO_SHA256); // 3. Base64 编码 return base64_encode($signature); } // 生成 Authorization 头 public function buildAuthorization($method, $url, $body) { $timestamp = time(); $nonce = bin2hex(random_bytes(16)); $signature = $this->buildSignature($method, $url, $body, $timestamp, $nonce); return sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"', $this->mchId, $nonce, $signature, $timestamp, $this->serialNo ); }

待签名字符串的格式是固定的:请求方法、URL、时间戳、随机串、请求体,每行一个,最后加一个换行符。这里最容易翻车的地方是 URL 必须带 query string,比如/v3/pay/transactions/native后面如果跟了参数,签名时也要带上。另外nonce用random_bytes生成,不要用rand()或者mt_rand(),微信对随机性有要求。

3.2 发起请求:cURL 封装与超时处理

签名生成后,用 cURL 把请求发出去:

public function request($method, $url, $body = '') { $authorization = $this->buildAuthorization($method, $url, $body); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'https://api.mch.weixin.qq.com' . $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, // 超时 10 秒 CURLOPT_CONNECTTIMEOUT => 5, // 连接超时 5 秒 CURLOPT_HTTPHEADER => [ 'Authorization: ' . $authorization, 'Content-Type: application/json', 'Accept: application/json', 'User-Agent: wechat-pay-v3-php/1.0' ], CURLOPT_CUSTOMREQUEST => $method, ]); if ($method === 'POST' || $method === 'PUT') { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { throw new WxPayException('cURL 错误: ' . $error); } return ['code' => $httpCode, 'body' => json_decode($response, true)]; }

超时设置很关键。微信支付接口偶尔会抽风,如果不设超时,PHP 进程会一直卡住,在高并发场景下直接把服务器拖死。我一般设连接超时 5 秒、总超时 10 秒,超过就抛异常让上层重试。另外注意CURLOPT_CUSTOMREQUEST要跟签名时的$method一致,有人签名用 POST 请求用 GET,微信直接返回 401。

3.3 回调验签与解密:V3 最绕的一环

回调处理是 V3 里最容易出问题的地方。微信回调过来的是加密数据,需要先用平台证书验签,再用 APIv3 密钥解密:

// 验证回调签名 public function verifyNotify($headers, $body) { // 1. 从请求头获取签名信息 $timestamp = $headers['Wechatpay-Timestamp']; $nonce = $headers['Wechatpay-Nonce']; $signature = $headers['Wechatpay-Signature']; $serial = $headers['Wechatpay-Serial']; // 2. 构造待验签字符串 $message = $timestamp . "\n" . $nonce . "\n" . $body . "\n"; // 3. 用平台证书公钥验签 $verify = openssl_verify( $message, base64_decode($signature), $this->platformCert, OPENSSL_ALGO_SHA256 ); if ($verify !== 1) { throw new WxPayException('回调验签失败'); } // 4. 解密 resource 数据 $data = json_decode($body, true); $resource = $data['resource']; $ciphertext = base64_decode($resource['ciphertext']); $associatedData = $resource['associated_data']; $nonceStr = $resource['nonce']; $plaintext = openssl_decrypt( $ciphertext, 'aes-256-gcm', $this->apiV3Key, OPENSSL_RAW_DATA, $nonceStr, $tag, $associatedData ); return json_decode($plaintext, true); }

验签失败最常见的原因是平台证书不对。V3 的平台证书会定期更换,如果你写死了证书文件,过一段时间就会验签失败。常见做法是定期调用/v3/certificates接口下载最新平台证书,或者用微信提供的证书管理器自动更新。解密时aes-256-gcm模式需要传$tag参数,这个在openssl_decrypt里是引用传递,别漏了。

4. 跑起来:从配置到第一笔支付成功的完整步骤

4.1 环境准备与配置填写

先把项目克隆到本地,然后改config/config.php:

<?php return [ 'mch_id' => '1900000001', // 商户号 'app_id' => 'wx1234567890abcdef', // 公众号 appid 'api_v3_key' => 'your32charapiv3key', // APIv3 密钥 'cert_path' => __DIR__ . '/../cert/apiclient_cert.pem', 'private_key_path' => __DIR__ . '/../cert/apiclient_key.pem', 'platform_cert_path' => __DIR__ . '/../cert/platform_cert.pem', 'notify_url' => 'https://yourdomain.com/demo/notify.php' ];

api_v3_key是 32 位字符串,在商户平台「账户中心-API安全」里设置。notify_url必须是 HTTPS,而且不能带参数,微信回调时会往这个地址 POST 数据。证书文件从商户平台下载后放到cert/目录,注意文件权限设为 600,别让其他用户读到。

4.2 Native 扫码支付示例

Native 支付是最容易测试的场景,不需要公众号授权。看demo/native_pay.php:

<?php require_once __DIR__ . '/../src/WxPayV3.php'; $config = require __DIR__ . '/../config/config.php'; $wxPay = new WxPayV3($config); $outTradeNo = 'TEST' . time(); // 商户订单号 $body = [ 'appid' => $config['app_id'], 'mchid' => $config['mch_id'], 'description' => '测试商品', 'out_trade_no' => $outTradeNo, 'notify_url' => $config['notify_url'], 'amount' => [ 'total' => 1, // 金额,单位分 'currency' => 'CNY' ] ]; $result = $wxPay->request('POST', '/v3/pay/transactions/native', json_encode($body)); if ($result['code'] === 200) { // 返回 code_url,生成二维码 echo "二维码链接: " . $result['body']['code_url']; } else { echo "下单失败: " . $result['body']['message']; }

out_trade_no是商户系统内部的订单号,必须唯一。amount.total单位是分,1 就是 1 分钱,测试时用这个金额最划算。返回的code_url是一个weixin://开头的链接,用二维码生成库转成图片给用户扫。扫码后微信会回调notify_url,你在回调里处理业务逻辑。

4.3 回调处理与订单状态更新

demo/notify.php是回调入口,部署到公网后微信会往这里发通知:

<?php require_once __DIR__ . '/../src/WxPayV3.php'; $config = require __DIR__ . '/../config/config.php'; $wxPay = new WxPayV3($config); // 获取请求头和请求体 $headers = getallheaders(); $body = file_get_contents('php://input'); try { // 验签并解密 $data = $wxPay->verifyNotify($headers, $body); // 处理业务逻辑 if ($data['trade_state'] === 'SUCCESS') { $outTradeNo = $data['out_trade_no']; $transactionId = $data['transaction_id']; // 更新订单状态(示例用文件记录,实际用数据库) file_put_contents( __DIR__ . '/../orders.log', date('Y-m-d H:i:s') . " 订单 {$outTradeNo} 支付成功,微信单号 {$transactionId}\n", FILE_APPEND ); } // 必须返回 200,否则微信会重试 http_response_code(200); echo json_encode(['code' => 'SUCCESS', 'message' => '成功']); } catch (Exception $e) { // 验签失败返回 500,微信会重试 http_response_code(500); echo json_encode(['code' => 'FAIL', 'message' => $e->getMessage()]); }

回调处理有两个铁律:第一,必须返回 200 告诉微信收到了,否则微信会按 15s、15s、30s、3m、10m 的频率重试;第二,业务逻辑要幂等,同一笔订单可能收到多次回调,别重复加钱。我一般会在数据库里加一个transaction_id唯一索引,插入失败就说明已经处理过了。

5. 避坑与排查:V3 支付接入中最容易翻车的 5 个点

5.1 签名报错 401 Unauthorized

现象:请求返回 401,错误信息是Signature verification failed。

原因:待签名字符串拼错了。最常见的是 URL 没带 query string,或者body为空时多拼了一个换行符。另外serial_no必须是小写十六进制,有人从证书里解析出来是大写,微信不认。

解决:把待签名字符串打印出来逐字符对比,特别注意\n的位置和数量。serial_no用strtolower()转一下。

5.2 回调验签失败

现象:回调接口返回 500,日志显示openssl_verify返回 0。

原因:平台证书过期或者用错了。V3 平台证书有效期只有一年,而且微信会不定期更换。如果你从商户平台下载的是旧的平台证书,验签必然失败。

解决:调用/v3/certificates接口获取最新平台证书,或者用微信提供的Wechatpay-Serial头去匹配对应的证书。别写死一个证书文件。

5.3 解密报错error:1C80006B:Provider routines::wrong final block length

现象:openssl_decrypt返回 false,错误信息是wrong final block length。

原因:api_v3_key不对,或者nonce和associated_data传错了。V3 回调的resource里有nonce和associated_data,解密时必须原样传入。

解决:检查api_v3_key是不是 32 位,nonce是不是 12 位。openssl_decrypt的$tag参数是引用传递,确保传入了正确的变量。

5.4 回调重复触发导致重复发货

现象:用户付了一笔钱,但订单被处理了两次,发了两次货。

原因:微信回调是至少一次语义,如果第一次回调你返回了非 200,或者处理超时,微信会重试。如果你的业务逻辑没有幂等控制,就会重复处理。

解决:在数据库里对transaction_id加唯一索引,或者用 Redis 做分布式锁。处理前先查一下这笔微信单号是否已经处理过。

5.5 cURL 请求超时导致订单状态不一致

现象:用户扫码付了钱,但你的系统显示未支付。

原因:下单请求超时了,但微信那边其实已经创建了订单。用户扫码支付后,回调可能因为网络问题没收到。

解决:下单超时后不要直接返回失败,而是调用查单接口/v3/pay/transactions/out-trade-no/{out_trade_no}确认订单状态。回调没收到时,也要有定时任务主动查单补偿。

6. 二次开发:把这套代码改造成生产级支付模块

6.1 从示例代码到生产模块的改造思路

示例代码能跑通,但直接上生产还差几个关键改造。第一是配置管理,别把商户号和密钥写在 PHP 文件里,用环境变量或者配置中心。第二是日志,每次请求和回调都要记录完整的请求头、请求体、响应体,出问题时能快速定位。第三是异常处理,把WxPayException细分,网络异常、签名异常、业务异常分开捕获,上层好做不同的重试策略。

我一般会加一个WxPayLogger类,把请求前后的数据写到按天切割的日志文件里:

class WxPayLogger { public static function log($type, $data) { $file = __DIR__ . '/../logs/wxpay_' . date('Ymd') . '.log'; $line = date('Y-m-d H:i:s') . " [{$type}] " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n"; file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } }

然后在request()方法里,发请求前记一次,收到响应后记一次。回调入口也记一次。这样出问题时,把日志拉出来一看就知道是签名错了还是微信那边返回了错误码。

6.2 平台证书自动更新

平台证书过期是生产环境最常见的故障。我一般会写一个定时任务,每天凌晨调用/v3/certificates接口拉取最新证书,对比序列号,有变化就更新本地文件。代码大概长这样:

public function updatePlatformCert() { $result = $this->request('GET', '/v3/certificates'); if ($result['code'] !== 200) { throw new WxPayException('获取平台证书失败'); } foreach ($result['body']['data'] as $item) { $cert = $this->decryptResource($item); // 解密后得到证书内容 $serialNo = $item['serial_no']; // 保存证书,文件名带序列号 file_put_contents( __DIR__ . '/../cert/platform_' . $serialNo . '.pem', $cert['certificate'] ); } }

验签时根据回调头里的Wechatpay-Serial找到对应的证书文件,而不是固定用一个platform_cert.pem。这样即使微信换了证书,你的系统也能自动适配。

6.3 对账与补偿机制

支付系统不能只依赖回调,必须有主动对账。我一般会做两层保障:第一层是定时任务,每 5 分钟查一次最近 10 分钟未支付的订单,调用查单接口确认状态;第二层是每天凌晨下载微信的对账单,跟本地订单逐笔核对,发现差异就告警。

查单接口的调用很简单:

$result = $wxPay->request('GET', '/v3/pay/transactions/out-trade-no/' . $outTradeNo . '?mchid=' . $config['mch_id']); if ($result['code'] === 200 && $result['body']['trade_state'] === 'SUCCESS') { // 补偿处理 }

注意查单接口的 URL 带了 query string,签名时要把?mchid=xxx也拼进去,否则会返回 401。这个坑我踩过,当时排查了半天才发现是签名 URL 没带参数。

6.4 一个容易忽略的细节:金额单位

微信支付所有金额单位都是分,不是元。下单时传 1 就是 1 分钱,查单返回的amount.total也是分。我见过有人在数据库里存的是元,回调回来直接存进去,结果对账时发现金额差了 100 倍。从那以后我每次对接支付,都强制在代码里加一层金额转换函数,所有跟微信交互的地方统一用分,展示给用户时才转成元。这个习惯帮我省了不少对账的麻烦。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:52:37

盗亦有道:Python爬虫robots协议与合规抓取实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 2:51:58

SSM整合实战:机票/火车票预订系统设计与部署详解

简介&#xff1a;基于SSM框架的机票/火车票预定系统课程设计完整项目包&#xff0c;面向软件工程专业学生以及需要完成类似课设的开发者&#xff0c;旨在解决预订系统从需求分析到编码实现的全流程参考问题&#xff0c;帮助读者掌握Spring、SpringMVC、MyBatis三大框架的整合与…

作者头像 李华
网站建设 2026/10/17 2:51:25

兰亭妙微原创作品| 竞业达智慧教室大屏端 UI/UX 设计解析

今天介绍一例兰亭妙微的原创案例——竞业达智慧课程平台的另一个核心终端——教室端&#xff08;大屏触控一体机&#xff09;。在智慧课堂中&#xff0c;大屏不仅是展示工具&#xff0c;更是师生互动的中枢。我们针对“远距离观看”和“触控操作”的双重特性&#xff0c;打造了…

作者头像 李华
网站建设 2026/10/17 2:50:50

微信PC端二维码自定义样式实现方案

1. 项目概述&#xff1a;为什么改微信PC端内嵌二维码不是“换个图片”那么简单“微信PC内嵌二维码的自定义样式更改”——这十个字背后&#xff0c;藏着大量开发者在实际协作、产品落地和用户运营中反复踩坑的真实场景。我接触过不下二十个团队&#xff0c;从某高校教务系统对接…

作者头像 李华
网站建设 2026/10/17 2:49:23

freeRTOS信号量(Semaphore)、互斥量(Mutex)

欢迎友友指正&#x1f917;&#x1f917;&#x1f917;&#xff0c;有输出一起进步~复习一下rtos理论&#xff08;自己的感悟结合韦东山老师部分讲解&#xff09;先说重难点&#xff0c;方便已有基础的同学快速联想&#xff08;前置知识可以划到下面“前言”部分开始&#xff0…

作者头像 李华