PHP微信支付v2封装:签名、回调验签与退款避坑指南 简介面向PHP开发者的微信支付与退款功能示例包适用于电商及在线服务平台需要接入JSAPI支付、处理订单退款等场景。资源采用原生PHP编写未依赖微信官方SDK整体仅7KB、共3个PHP文件涵盖支付调用主入口、核心类封装以及回调通知处理脚本结构精简便于快速定位关键逻辑。示例代码完整演示了从统一下单获取prepay_id、生成JSAPI支付签名到前端wx.chooseWXPay拉起支付以及退款申请、退款状态查询和异步回调解析的闭环流程并涉及商户号、密钥等敏感信息的处理提醒。目前已有1008人学习下载对于希望在PHP项目中快速集成微信支付的开发者来说是一份可直接参考运行的实用代码能帮助理解接口参数组织、签名规则与回调机制降低从零对接的试错成本。1. PHP 微信支付和退款类把最容易翻车的支付环节接到能跑把一套微信支付真正接进来比文档里写的要绕。统一下单只是开始后面跟着异步回调验签、退款双向证书、金额单位、对账幂等任何一个环节没处理好线上就会翻车。这个 PHP 微信支付和退款类核心是把 v2 接口里最常用的几件事——小程序/公众号下单、回调验签并解析、发起退款、订单查询——收敛成一批可直接调用的方法依赖基本是 PHP 内置的 openssl 和 curl商户号配好就能跑。适合自己维护支付模块、不想为两个接口引入整套大包、又想完全掌控参数的 PHP 开发者。下面按我实测过的顺序拆先讲参数和签名原理再讲类怎么落地最后是接业务时那些不写在文档里的坑。2. 微信支付 v2 的参数与签名为什么封装比裸调接口稳2.1 支付链路里 6 个必配参数与一个回调地址微信支付 v2 有四个高频接口统一下单、支付结果通知、申请退款、退款查询。它们共用一套参数体系核心是「商户身份 业务单号 金额 签名」。所以第一步是把商户侧配置收拢。我一般这样记录参数说明从哪拿appid小程序/公众号/开放平台 AppID微信公众平台/开放平台mch_id微信支付商户号商户平台首页keyv2 API 密钥32 位商户平台 - 账户中心 - API安全 - APIv2密钥notify_url支付成功通知地址必须公网 HTTPS自己后台配置cert_path退款用的 apiclient_cert.pem 与 apiclient_key.pem 所在目录商户平台下载证书api_url统一下单固定地址官方文档常量这里最容易混的是 key。v2 接口算 sign 用的是 APIv2 密钥不是 v3 的 APIv3 密钥也不是商户 API 证书。配错了最典型的反应就是所有请求都报「签名错误」而且换 MD5、换 HMAC-SHA256 都无效因为身份本身就错了。除了配置还要分清两个单号。out_trade_no 是商户侧唯一订单号由你生成transaction_id 是微信支付侧的交易号支付成功后返回。下单时你传 out_trade_no回调里两者都有申请退款时可以用 out_trade_no 定位原订单也可以直接用 transaction_id。很多人在退款查询时把 out_refund_no退款单号和 out_trade_no 混用后面避坑部分会细说。2.2 签名算法拆解字典序、拼接、MD5/HMAC-SHA256微信 v2 的签名算法看文档只有三句话落地时却有三个细节只对非空参数签名、排除 sign 本身、密钥只放在拼接串末尾。这是我在项目里实际跑通的实现private function buildSign(array $params): string { // 排除空值和 sign 字段这是微信签名规则的硬性要求 $filtered []; foreach ($params as $k $v) { if ($v ! !is_null($v) $k ! sign) { $filtered[$k] $v; } } // 按参数名 ASCII 字典序升序排列 ksort($filtered); // 拼接成 a1b2 的形式 $str ; foreach ($filtered as $k $v) { $str . $k . . $v . ; } // 密钥只放在字符串末尾 $str . key . $this-config[key]; // sign_type 用于 v2 接口signType 用于 JSAPI 调起支付 // 都没有时使用类配置的默认签名类型最安全的是跟随统一下单 $signType $params[sign_type] ?? $params[signType] ?? ($this-config[sign_type] ?? MD5); if ($signType HMAC-SHA256) { return strtoupper(hash_hmac(sha256, $str, $this-config[key])); } return strtoupper(md5($str)); }这段代码有两个关键点。一是拼接时没有做 urlencode微信官方明确参数值不需要 URL 编码直接拼原始值二是在 HMAC-SHA256 分支里hash_hmac 的第二参数是 API 密钥用于生成消息认证码而 $str 里那一份 key 是摘要的对象两者各司其职不能省也不能重复。实际调用时签名是放在 XML 里的 sign 字段。微信服务器收到请求后会取出除 sign 外的所有字段重新算一遍再和 sign 比较。回调验签也是同样的逻辑微信把支付结果参数 POST 到你 notify_url你按同一套规则算一次 sign相同才算验签通过。2.3 为什么用类封装而不是每个接口手写一遍如果项目里只有统一下单一个接口手写没问题。但支付业务一旦跑起来至少会用到下单、回调、查单、退款、退款查询、下载账单六个接口。每个接口都重写一遍签名和 XML 解析就会出现两类典型问题。第一是签名逻辑多副本。改一处算法比如从 MD5 换成 HMAC-SHA256其他文件忘了同步线上就会出现只有部分接口签名正常的诡异现象。第二是 XML 解析不一致。有人用 simplexml有人用正则CDATA 处理方式不同解析结果就差一个空格回调验签时对不上。用一个类把这些收口核心价值是把变化控制在单个文件里。配置通过构造函数注入签名、POST 请求、XML 转换都做成私有方法业务代码只关心下单参数和回调结果。后面就算要迁移到 v3也只需新增一个适配层。我自己维护支付模块时坚持「支付逻辑不进业务模型」所有微信请求都在这一个类里出问题排查起来不用来回翻项目。另外要泼一盆冷水如果项目是多商户平台每个商户有自己的 key 和证书单个单例类不够需要按商户维度的配置工厂。这个类的设计假设是单商户。多商户要么改成传入配置的实例工厂要么在类里维护「商户号 配置」映射。2.4 一次统一下单的参数与返回字段对应关系下单前至少要准备这么一组参数$orderParams [ appid $config[appid], mch_id $config[mch_id], device_info WEB, nonce_str $pay-nonce(), body PHP 微信支付测试单, out_trade_no 20260101120000123, total_fee 1, // 单位分1 表示 0.01 元 spbill_create_ip 8.8.8.8, notify_url $config[notify_url], trade_type JSAPI, openid oUpF8uMuAJOQI2S58DF6uQ, ];这里 total_fee 单位分是 v2 里最容易搞错的字段。1 元就是 100不是 1也不是 0.01。下单成功后返回的 prepay_id 有效期为两小时前端 JSAPI 调起支付时还需要做一次二次签名。这个对应关系搞清楚下一章写类方法时就能直接对上号。3. 把三个核心方法写成 PHP 类下单、回调验签、退款3.1 类骨架与通用工具方法支付类只做一件事把微信 v2 的 HTTP XML 签名协议封装成 PHP 方法。构造参数只收配置不直接依赖框架。先看骨架class WxPayV2 { private $config []; public function __construct(array $config) { $this-config array_merge([ appid , mch_id , key , notify_url , cert_path , sign_type MD5, // 统一下单、退款、回调验签统一使用 ], $config); } private function nonce(): string { return md5(uniqid(mt_rand(), true)); } }nonce_str 建议用 32 位以内的随机串微信要求最长 32 位。下面三个工具方法决定这个类在其他框架里能不能直接跑XML 转数组、数组转 XML、curl 发送请求。private function toXml(array $data): string { $xml xml; foreach ($data as $k $v) { $xml . . $k . ![CDATA[ . $v . ]]/ . $k . ; } return $xml . /xml; } private function fromXml(string $xml): array { $obj simplexml_load_string($xml, SimpleXMLElement, LIBXML_NOCDATA); return json_decode(json_encode($obj), true) ?: []; } private function postXml(string $url, string $xml, string $sslCert , string $sslKey ): string { $ch curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: text/xml]); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($sslCert $sslKey) { // 申请退款必须使用双向证书下单和查询不需要 curl_setopt($ch, CURLOPT_SSLCERT, $sslCert); curl_setopt($ch, CURLOPT_SSLKEY, $sslKey); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); } $resp curl_exec($ch); if (curl_errno($ch)) { throw new RuntimeException(curl 请求失败 . curl_error($ch)); } curl_close($ch); return $resp; }fromXml 的 LIBXML_NOCDATA 必须写否则 CDATA 里的中文和特殊符号会解析出问题。postXml 支持可选证书参数这样退款接口和普通接口可以用同一套发送逻辑。3.2 统一下单与 JSAPI 调起支付统一下单方法接收业务订单数据补上 appid、mch_id、nonce_str、通知地址生成签名后 POST 到微信。成功后返回包含 prepay_id 的数组。public function unifiedOrder(array $order): array { $params [ appid $this-config[appid], mch_id $this-config[mch_id], nonce_str $this-nonce(), body $order[body], out_trade_no $order[out_trade_no], total_fee $order[total_fee], spbill_create_ip $order[ip], notify_url $this-config[notify_url], trade_type $order[trade_type] ?? JSAPI, ]; if ($params[trade_type] JSAPI) { // 小程序或公众号支付必须传 openid $params[openid] $order[openid]; } // 签名类型保持一致默认 MD5 $params[sign_type] $this-config[sign_type] ?? MD5; $params[sign] $this-buildSign($params); $resp $this-postXml( https://api.mch.weixin.qq.com/pay/unifiedorder, $this-toXml($params) ); $data $this-fromXml($resp); if (($data[return_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(通信失败 . ($data[return_msg] ?? )); } if (($data[result_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(业务失败 . ($data[err_code_des] ?? $data[err_code])); } return $data; }注意 total_fee 必须传整数分很多接口报「金额格式错误」就是因为传了小数。trade_type 为 JSAPI 时必须传 openidNATIVE 则不需要 openid而是返回 code_url 给用户扫码。成功后 data 里有 prepay_id但前端调起支付不能直接拿它需要按 JSSDK 要求重组参数并再一次签名public function jsapiParams(array $unifiedResp): array { $signType $this-config[sign_type] ?? MD5; $params [ appId $this-config[appid], timeStamp (string) time(), nonceStr $this-nonce(), package prepay_id . $unifiedResp[prepay_id], signType $signType, ]; $params[paySign] $this-buildSign($params); return $params; }jsapiParams 里的签名类型必须与统一下单一致。timeStamp 是字符串不要转成 int 给前端否则某些版本的 wx.requestPayment 会因类型不对报错。buildSign 会处理 signType 这个字段所以直接传整个 params 就能算出对接微信要求的 paySign。3.3 回调验签与结果解析支付成功后微信服务器会异步 POST 一个 XML 到 notify_url。很多项目在这里翻车只判断 return_code 就直接改订单状态完全没验签。正确顺序是先验签再判断业务结果然后更新订单最后返回成功应答。public function handleNotify(string $xml): array { $data $this-fromXml($xml); if (($data[return_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(回调通信失败); } if (($data[result_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(回调业务失败); } if (($data[sign] ?? ) ! $this-buildSign($data)) { throw new RuntimeException(回调验签失败); } return $data; }这里验签用到的 buildSign 与下单时完全相同。因为 fromXml 已经把 CDATA 去除sign 字段本身也在返回数组里而 buildSign 内部会排除 sign 字段所以可以直接把整个 $data 传进去。签名类型由构造时设置的 sign_type 决定和统一下单保持一致才不会验签失败。微信要求收到通知后返回固定格式的 XML 表示成功。如果不返回微信会在 24 小时内按策略重试。我一般把应答方法也放在类里public function replyOk(): string { return xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; } public function replyFail(): string { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[处理失败]]/return_msg/xml; }return_code 为 SUCCESS 只是告诉微信「我收到了」并不代表订单业务处理成功。不要在业务抛异常时还是返回 SUCCESS微信会认为处理成功就不重试了。3.4 发起退款与退款查询退款是这里唯一必须使用双向证书的接口。证书不是签名的替代品而是传输层的身份认证。发起退款时请求要求带上商户证书微信服务器的证书验证失败会直接返回 SSL 错误。public function refund(array $refund): array { $params [ appid $this-config[appid], mch_id $this-config[mch_id], nonce_str $this-nonce(), out_trade_no $refund[out_trade_no], out_refund_no $refund[out_refund_no], total_fee $refund[total_fee], refund_fee $refund[refund_fee], ]; $params[sign_type] $this-config[sign_type] ?? MD5; $params[sign] $this-buildSign($params); $certDir $this-config[cert_path]; $resp $this-postXml( https://api.mch.weixin.qq.com/secapi/pay/refund, $this-toXml($params), $certDir . /apiclient_cert.pem, $certDir . /apiclient_key.pem ); $data $this-fromXml($resp); if (($data[return_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(退款通信失败 . ($data[return_msg] ?? )); } if (($data[result_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(退款失败 . ($data[err_code_des] ?? $data[err_code])); } return $data; }申请退款成功后返回的是「受理成功」不代表最终退款到账。要看最终结果得靠退款结果通知或主动调用退款查询接口。退款查询参数与退款几乎一样区别是不需要证书按 out_refund_no 或 transaction_id 查询public function refundQuery(string $outRefundNo): array { $params [ appid $this-config[appid], mch_id $this-config[mch_id], nonce_str $this-nonce(), out_refund_no $outRefundNo, ]; $params[sign_type] $this-config[sign_type] ?? MD5; $params[sign] $this-buildSign($params); $resp $this-postXml( https://api.mch.weixin.qq.com/pay/refundquery, $this-toXml($params) ); $data $this-fromXml($resp); if (($data[return_code] ?? FAIL) ! SUCCESS) { throw new RuntimeException(退款查询失败); } return $data; }refund_fee 是本次退款金额total_fee 是订单原始总金额不是「剩余可退金额」。部分退款时total_fee 仍然传原订单总金额累计退款不能超过它。这个点极其容易翻车下一章详细说。4. 避坑记录签名错误、回调丢失、退款超退5 个我踩过的坑4.1 签名与参数signature 错误、密钥混用、金额单位坑 1接口一直报「签名错误」小程序端提示「用户态签名 signature 错误」。现象统一下单或者退款请求返回 return_codeFAILreturn_msg 为签名错误小程序拉起支付时也弹「用户态签名 signature 错误」。原因这个坑我排查过很多次九成是以下三种之一。第一参数名大小写错比如把 mch_id 写成 mchId签名算法里字典序就变了第二AppID 和商户号不是同一主体微信会按这两个参数去找商户配置对不上就认为签名无效第三用了 APIv3 密钥去算 v2 的 sign两者在商户平台是不同的密钥。解决先打印发送前的完整 XML确认 sign 之外的所有参数名与文档一致再核对 appid 和 mch_id 是否匹配最后进商户平台 API 安全里重新设置 APIv2 密钥。改完密钥要等 5 分钟后重试微信端配置有缓存。如果是 JSAPI 调起支付环节报 signature 错误还要检查二次签名的参数里 package 是不是少了 prepay_id 前缀。坑 2total_fee 传了 1用户付了 1 分钱而不是 1 元。现象订单金额全错用户付 0.01 元但后台记录 1 元或者反过来。原因微信 v2 金额单位是分total_fee1 代表 0.01 元很多从其他支付渠道迁移上来的代码习惯传「元」忘了转换。解决入库和传参统一用分所有金额计算用 int 类型。前端展示时再除以 100。项目中所有支付金额相关字段都不要用 float累计退款计算用字符串或 BigDecimal。坑 3回调验签一直失败但同一套签名代码下单却没问题。现象下单接口签名正常微信回调到 notify_urlhandleNotify 里验签抛出异常。原因常见的有两个。一是从 $_POST 或者框架解析后的数据拿回调体丢了原始报文二是 simplexml 没加 LIBXML_NOCDATACDATA 里的内容解析后把前后空格也算进去了。解决不要用 $_POST 或框架解析后的数据直接用 php://input 读取原始请求体fromXml 必须加 LIBXML_NOCDATA。如果还是失败把接收到的 XML 原样存到日志分析里面有没有多余换行和空格。另外要确认回调验签的 sign_type 和统一下单时一致否则必然验签失败。4.2 回调与退款XML 解析、证书路径、单号混用坑 4申请退款报「金额超限」或「订单已退款」。现象同一个订单第二次申请部分退款时微信返回 SYSTEMERROR 或金额超限或者退款金额明明小于订单金额却被判定超退。原因total_fee 传的是剩余可退金额而不是订单原始总额。微信校验规则是「该订单已成功退款金额 本次退款金额 订单原始 total_fee」。部分退款时原始 total_fee 不能变改了就会超过限制。解决申请退款时 total_fee 永远取订单表的原始总金额refund_fee 取本次退款金额。业务侧在退款前查询订单累计已退金额加本次金额大于原始金额就拦截不等微信报错。坑 5退款接口直接 SSL 报错curl error 35 或证书读取失败。现象发起退款时 curl 返回错误 35SSL connect error或者提示 apiclient_cert.pem 无法读取。原因证书路径不对、证书权限不够或者把 apiclient_key.pem 误当 apiclient_cert.pem 传了。还有一种情况是证书目录存的是商户平台下载的 .p12 文件没有用 openssl 转成 PEM 格式。解决确认下载包里解压出来的 apiclient_cert.pem 和 apiclient_key.pem 存在且可读路径用绝对路径或者以框架 root 目录为基准的路径如果只有 apiclient.p12用openssl pkcs12 -in apiclient.p12 -clcerts -nokeys -out apiclient_cert.pem和openssl pkcs12 -in apiclient.p12 -nocerts -nodes -out apiclient_key.pem转换。密钥文件权限设为 600。5. 把支付退款类接进业务订单表、回调幂等与每日对账5.1 订单表和退款单表怎么设计支付类的职责只到「接口返回成功」为止业务侧真正要扛住的是订单状态。我习惯用两张表支付订单表和退款单表。CREATE TABLE pay_order ( id int unsigned NOT NULL AUTO_INCREMENT, out_trade_no varchar(32) NOT NULL COMMENT 商户订单号, transaction_id varchar(32) NOT NULL DEFAULT COMMENT 微信支付交易号, total_fee int NOT NULL COMMENT 订单总金额单位分, refund_fee int NOT NULL DEFAULT 0 COMMENT 累计已退款金额单位分, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭 3已全额退款, notify_raw text COMMENT 最后一次回调原始报文, pay_time datetime DEFAULT NULL COMMENT 支付成功时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_out_trade_no (out_trade_no), KEY idx_transaction_id (transaction_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT支付订单表;out_trade_no 唯一约束是防单号重复的最后一道防线。transaction_id 建立索引对账时按微信订单号查本地记录用得上。notify_raw 存最后一次回调原文排查问题时能看到微信到底传了什么。退款单表记录每次退款请求一张表会多次插入记录一个订单可以存在多条部分退款单CREATE TABLE pay_refund ( id int unsigned NOT NULL AUTO_INCREMENT, out_refund_no varchar(32) NOT NULL COMMENT 商户退款单号, out_trade_no varchar(32) NOT NULL COMMENT 关联的商户订单号, refund_fee int NOT NULL COMMENT 本次退款金额单位分, status tinyint NOT NULL DEFAULT 0 COMMENT 0处理中 1成功 2失败, refund_id varchar(32) NOT NULL DEFAULT COMMENT 微信退款单号, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_out_refund_no (out_refund_no), KEY idx_out_trade_no (out_trade_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT退款单表;设计上有意识地区分订单状态和退款状态。订单状态里 3 表示「全额退款」部分退款时订单仍是 1已支付靠 refund_fee 累计值判断还能不能退。5.2 回调更新订单幂等与状态机回调处理方法里最容易犯的错是收到一次通知就更新一次订单不判断当前状态导致重复入账、发货多次。我的做法是先读订单再校验金额再判状态最后落库并以事务提交。public function onNotify(string $xml): string { $data $this-pay-handleNotify($xml); $order OrderModel::where(out_trade_no, $data[out_trade_no])-first(); if (!$order || $order-total_fee ! $data[total_fee]) { // 单号不存在或金额不一致记日志后告知微信失败 return $this-pay-replyFail(); } if ($order-status 1 $order-transaction_id $data[transaction_id]) { // 已处理过直接返回成功避免微信重试时重复处理 return $this-pay-replyOk(); } $order-transaction_id $data[transaction_id]; $order-status 1; $order-pay_time date(Y-m-d H:i:s, strtotime($data[time_end])); $order-notify_raw $xml; $order-save(); // 到这里才扣库存、加余额、发通知 dispatch(new OrderPaidEvent($order-out_trade_no)); return $this-pay-replyOk(); }这段逻辑有三个关键点。第一先校验金额和单号防止别人伪造回调第二用「订单状态 transaction_id 相同」判断幂等而不是只查一次状态第三业务事件放在 save 之后且与订单更新在同一事务里处理避免订单已改但库存没扣的情况。订单状态流转我固定为 0 → 1 → 2/3。0 是待支付支付成功后是 1如果用户主动取消或超时未付关闭为 2退款累计达到 total_fee 时置为 3。状态只允许向后流转不允许从 1 回到 0。在更新语句上可以加where(status, 0)条件防止并发覆盖。5.3 每日对账从微信账单到本地订单支付跑起来以后最怕「用户说付了系统说没付」。靠回调不保险因为回调可能丢失所以要拉微信账单做每日对账。微信 v2 的下载对账单接口返回的是文本不是 XMLcurl https://api.mch.weixin.qq.com/pay/downloadbill?appidAPPIDmch_idMCH_IDnonce_strNONCEbill_date2026-01-01bill_typeALLsignSIGN -o /tmp/wx_bill.txt签名字段仍然用第二章的 buildSign 生成。下载后账单是一个类似 CSV 的文件前几行是标题最后一行是汇总。我一般在 PHP 脚本里逐行解析按 out_trade_no 和 transaction_id 与本地 pay_order 比对$localCount OrderModel::whereBetween(pay_time, [ $start, $end ]) -where(status, 1)-count(); $wxCount parseBill($billFile); // 统计账单中的成功交易笔数 if ($localCount ! $wxCount) { // 差异订单写入对账异常表人工介入 }严格做法是按交易号维度比对统计笔数只能查出总数不一致。把账单里的 transaction_id 集合取出来与库里当天 transaction_id 集合做差集两边多出来的都记录账单多而库少说明回调丢失需要调用订单查询接口补单库多而账单少说明数据有问题需要人工核。6. 从 v2 到 v3 的迁移思路用平台证书验证回调真实性微信支付现在新商户默认用 v3 接口。v3 与 v2 最大的区别不只是 REST API JSON而是签名方向反过来了v2 里你用 APIv2 密钥给请求算签名微信返回的验签逻辑在同一套规则里v3 里你的请求用商户 API 证书私钥签名而回调的验签要反过来用微信支付平台证书的公钥验。这意味着不能沿用buildSign($data)那套。先看 v3 回调验签的骨架function verifyV3Notify(string $body, string $timestamp, string $nonce, string $serial, string $signature): bool { // 1. 用 serial 找到对应的微信支付平台证书 // 2. 拼接验签串timestamp \n nonce \n body \n $message $timestamp . \n . $nonce . \n . $body . \n; // 3. 用平台证书公钥验证 RSA-SHA256 签名 $publicKey openssl_pkey_get_public($platformCert); return openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256) 1; }这里关键点是验签串里的 body 必须是原始请求体不能是框架解析后的数组。验签成功后回调里的 resource 字段还是加密的 JSON需要用 APIv3 密钥做 AES-256-GCM 解密拿到里面的 out_trade_no 和 transaction_id。解密完成后再走一遍第 5 章的幂等更新逻辑。从 v2 类迁移到 v3我并不建议直接把类内部推翻重写而是保留统一的下单/退款封装接口新增一个 WxPayV3 适配类。业务侧只改实例化那一行订单表和回调逻辑全部复用。判断新项目用什么版本最简单的标准是看商户平台是否能看到 APIv3 密钥设置入口能看到就以 v3 为准看不到就先用 v2。这套 WxPayV2 类代码、证书配置示例和两个示例脚本都在资源包里下载后按第二章的配置表填好参数直接跑一遍 examples 下的下单脚本就能看到完整链路。我自己吃过亏早期做回调时直接信任推送数据不禁验签结果被第三方拿伪造通知把订单状态刷成已支付虽然金额对不上没造成实际资损但排查了整整一天。从那以后我每次上线支付相关变更都会强制走一遍「下单 → 模拟回调 → 验签 → 查单确认 → 退款 → 退款查单」六个步骤并且把每一段的日志单独留一份。多商户项目还会配一份「证书与密钥更换登记表」谁在什么时间换过 key记录得清清楚楚。希望帮到你。本文还有配套的精品资源点击获取