news 2026/11/4 2:47:23

PHP 微信支付 v3 完整实例:从签名、回调验签到退款查单的闭环实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP 微信支付 v3 完整实例:从签名、回调验签到退款查单的闭环实现

简介:这份资源是面向PHP开发者的微信支付V3接口完整实例,适合需要为线上商城或线下场景接入微信支付、希望掌握V3新版流程与安全机制的中初级开发者参考。压缩包共16个文件、约61KB,以asp与php脚本为主体,辅以txt说明文档、pem证书文件、js脚本及mdb数据文件,覆盖统一下单、前端调起支付、异步回调通知、订单查询确认等核心环节,并涉及API签名、证书管理、沙箱测试、异常处理与退款等要点。资源中附有中转文件与demo示例,便于对照理解配置与调用逻辑,同时提醒开发者关注SQL注入、XSS防护及敏感信息传输存储等安全问题。目前已有4669人学习下载,可作为快速上手微信支付V3、梳理支付链路与排错思路的实践参考。

1. PHP 接微信支付 v3:为什么老教程一跑就废,这份实例能省你两天

上周帮一个做小程序商城的朋友排查支付回调,他拿着两年前的 v2 代码往新项目里塞,签名报错、证书报错、回调验签失败,一个下午全耗在环境上。微信支付 v3 和 v2 的差别不是换个接口地址那么简单:v3 全面改用 AES-256-GCM 加密回调、SHA256-RSA 签名、平台证书自动轮换,密钥体系整个换了一套。你手上如果只有一份「能跑」的旧 demo,基本等于没有。这份 PHP 微信支付 v3 完整实例,价值就在于把下单、签名、回调验签、退款、查单这几条主链路串成一个能直接改参数就跑的闭环,而不是散落在各处的代码片段。适合谁:手里有 PHP 项目、要接最新微信支付、又不想从官方文档的术语堆里自己拼装的人。下面按「先跑通再抠细节」的顺序拆。

2. 先把密钥体系搭对:v3 的三把钥匙和目录结构

2.1 v3 到底要哪几个文件

很多人卡在第一步不是代码写错,是文件放错。微信支付 v3 的商户侧凭证一共三类,缺一个都签不出请求:

文件/参数来源用途常见命名
商户 API 私钥商户平台下载请求签名(SHA256-RSA)apiclient_key.pem
商户证书序列号商户平台查看放在请求头 Authorization 里一串 40 位十六进制
APIv3 密钥商户平台自行设置回调报文解密、敏感字段加密32 位字符串
平台证书通过接口下载验证微信回调签名动态获取,会轮换

注意最后一行:平台证书不是你去后台下载的静态文件,v3 设计上就是让商户用「下载平台证书」接口定期拉取,因为微信会轮换。实例里如果还是让你手动塞一个 cert.pem 进去,那它大概率没处理轮换,上线几个月后回调验签会突然全挂——这是血泪经验里最常见的一种。

2.2 目录怎么摆,配置怎么抽

我一般把凭证和业务代码彻底分开,避免私钥被误提交。实例的目录结构建议改成这样:

project/ ├── config/ │ └── wxpay.php # 只放配置,返回数组 ├── cert/ │ ├── apiclient_key.pem # 商户私钥,权限 600 │ └── platform/ # 平台证书缓存目录,可写 ├── src/ │ ├── WxPayClient.php # 签名、请求、验签 │ └── WxPayNotify.php # 回调处理 └── public/ └── notify.php # 回调入口

配置文件长这样,把易变项全抽出来:

<?php // config/wxpay.php return [ 'mchid' => '1900000001', // 商户号 'appid' => 'wxxxxxxxxxxxxxxxxx', // 关联的 appid 'serial_no' => '5157F09EFDC096DE15EBE81A47057A72', // 商户证书序列号 'private_key' => __DIR__ . '/../cert/apiclient_key.pem', 'apiv3_key' => 'your32charapiv3keyxxxxxxxxxxxxx', // 必须 32 位 'platform_dir' => __DIR__ . '/../cert/platform', 'notify_url' => 'https://your.domain/public/notify.php', ];

参数说明:serial_no不是私钥里的内容,要去商户平台「API 安全」里复制,填错会直接返回 401 签名错误;apiv3_key长度必须 32,少一位解密回调时就抛openssl_decrypt失败;platform_dir要给 PHP 进程写权限,否则拉不到平台证书。私钥文件权限设 600,别跟着代码进 git,这是最低要求。

2.3 平台证书的自动下载逻辑

实例里最值得抄的一段是平台证书拉取。核心思路:调GET /v3/certificates,返回的每个证书用 APIv3 密钥 AES-256-GCM 解密,按序列号存成文件,请求时按响应头里的Wechatpay-Serial去匹配。

<?php // 拉取并缓存平台证书(简化版) function refreshPlatformCerts(array $cfg): array { $client = new WxPayClient($cfg); $resp = $client->get('/v3/certificates'); // 内部已带签名头 $certs = []; foreach ($resp['data'] as $item) { // 用 APIv3 密钥解密 encrypt_certificate $plain = decryptAesGcm( $cfg['apiv3_key'], $item['encrypt_certificate']['nonce'], $item['encrypt_certificate']['associated_data'], $item['encrypt_certificate']['ciphertext'] ); $certs[$item['serial_no']] = $plain; file_put_contents( $cfg['platform_dir'] . '/' . $item['serial_no'] . '.pem', $plain ); } return $certs; }

逻辑说明:decryptAesGcm里openssl_decrypt的$tag参数要传密文末尾 16 字节,很多人直接把整个 ciphertext 丢进去,结果解密出空串。参数上associated_data通常是certificate,别写死成空。缓存策略我一般设成「本地没有就拉,验签失败再拉一次」,既避免每次请求都多打一次接口,又能在轮换后自愈。

3. 下单与签名:一个请求头写错就 401

3.1 Authorization 头的拼装规则

v3 的签名不是把参数排序拼串那么简单,它签的是「方法 + URL + 时间戳 + 随机串 + 请求体」五行拼接后的 SHA256-RSA。请求头格式固定:

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="...",nonce_str="...",signature="...",timestamp="...",serial_no="..."

实例里这段封装在buildAuthorization(),我把它单独拎出来讲,因为这里错一个字符就 401,而且报错信息只有一句「签名错误」,非常黑匣子。

<?php function buildAuthorization(string $method, string $url, string $body, array $cfg): string { $timestamp = time(); $nonce = bin2hex(random_bytes(16)); // 32 位随机串 // 待签名串:五行,每行以 \n 结尾 $message = $method . "\n" . $url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; $key = openssl_pkey_get_private(file_get_contents($cfg['private_key'])); openssl_sign($message, $sign, $key, OPENSSL_ALGO_SHA256); $signature = base64_encode($sign); return sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"', $cfg['mchid'], $nonce, $signature, $timestamp, $cfg['serial_no'] ); }

参数说明:$url必须是带 query 的完整路径,比如/v3/pay/transactions/jsapi,不带域名;$body对 GET 请求传空串,但那个\n不能省;random_bytes(16)转 hex 正好 32 位,用uniqid()会短,签名照样失败。openssl_sign第四个参数必须显式写OPENSSL_ALGO_SHA256,默认是 SHA1,这是老代码迁移时最容易漏的一处。

3.2 JSAPI 下单的完整请求体

以小程序/公众号支付为例,下单接口是POST /v3/pay/transactions/jsapi。请求体字段不多,但amount.total单位是分,payer.openid必须和 appid 对得上,这两点错一个就报「参数错误」或「openid 不匹配」。

<?php $payload = [ 'appid' => $cfg['appid'], 'mchid' => $cfg['mchid'], 'description' => '测试商品-1件', 'out_trade_no' => 'T' . date('YmdHis') . mt_rand(1000, 9999), 'notify_url' => $cfg['notify_url'], 'amount' => ['total' => 1, 'currency' => 'CNY'], // 单位:分 'payer' => ['openid' => $openid], ]; $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $resp = $client->post('/v3/pay/transactions/jsapi', $body);

逻辑说明:json_encode必须带JSON_UNESCAPED_UNICODE,否则中文描述被转成\uXXXX,虽然微信能收,但你本地日志排查时很难读;JSON_UNESCAPED_SLASHES是为了 notify_url 里的斜杠不被转义,避免签名串和实际发送体不一致。out_trade_no建议带时间戳加随机数,重复会返回「订单号已存在」。返回的prepay_id还要再签一次名给前端调起支付,那一步的签名串是appId\ntimeStamp\nnonceStr\nprepay_id=xxx\n,格式和请求签名不同,实例里单独有buildPayParams(),别混用。

3.3 回调验签与解密:顺序不能反

回调处理是翻车重灾区。正确顺序是:先验签(用平台证书验Wechatpay-Signature),再解密resource(用 APIv3 密钥解 AES-256-GCM),最后才处理业务。顺序反了,等于拿未验证的数据做业务,安全上直接失守。

<?php // public/notify.php 核心片段 $rawBody = file_get_contents('php://input'); $headers = array_change_key_case(getallheaders(), CASE_LOWER); // 1. 验签:构造验签串 $message = $headers['wechatpay-timestamp'] . "\n" . $headers['wechatpay-nonce'] . "\n" . $rawBody . "\n"; $serial = $headers['wechatpay-serial']; $pubKey = file_get_contents($cfg['platform_dir'] . '/' . $serial . '.pem'); $ok = openssl_verify($message, base64_decode($headers['wechatpay-signature']), $pubKey, OPENSSL_ALGO_SHA256); if ($ok !== 1) { http_response_code(401); exit('sign fail'); } // 2. 解密 resource $res = json_decode($rawBody, true)['resource']; $plain = decryptAesGcm($cfg['apiv3_key'], $res['nonce'], $res['associated_data'], $res['ciphertext']); $data = json_decode($plain, true); // 3. 幂等处理 + 返回成功 if ($data['trade_state'] === 'SUCCESS') { // 按 out_trade_no 更新订单,注意加唯一索引防重复 } http_response_code(200); echo json_encode(['code' => 'SUCCESS']);

参数说明:getallheaders()在部分 FPM 环境拿不到,实例里做了$_SERVER兜底;验签用的平台证书按Wechatpay-Serial匹配,匹配不到就触发一次refreshPlatformCerts再试;返回体必须是{"code":"SUCCESS"}且 HTTP 200,返回其他内容微信会持续重试,最多重试多次,你的订单表如果没有幂等保护就会被重复更新。这一点我在一个模拟项目里吃过亏,回调重试把库存扣成了负数。

4. 退款、查单与对账单:把边界情况先想好

4.1 退款接口的金额与回调

退款走POST /v3/refund/domestic/refunds,和下单不同,它有自己的notify_url,退款结果异步通知。请求体关键字段:

<?php $refund = [ 'out_trade_no' => $orderNo, 'out_refund_no' => 'R' . $orderNo, // 退款单号,唯一 'amount' => [ 'refund' => 1, // 退款金额,分 'total' => 1, // 原订单金额,分 'currency' => 'CNY', ], ]; $resp = $client->post('/v3/refund/domestic/refunds', json_encode($refund, JSON_UNESCAPED_UNICODE));

逻辑说明:refund不能大于total,否则报「退款金额大于订单金额」;out_refund_no重复提交会返回同一笔退款结果,这其实是幂等特性,可以善用。退款回调的解密流程和支付回调完全一致,只是event_type是REFUND.SUCCESS,实例里复用了同一个WxPayNotify类,按事件类型分发,这个设计比复制两份代码干净。

4.2 查单与对账单的用途区分

查单GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid=xxx用于主动补偿:回调没来、用户说付了但订单没变,就靠它兜底。对账单GET /v3/bill/tradebill是 T+1 的,用于财务对账,别拿它做实时校验。实例里把查单封装成queryOrder(),建议在定时任务里对「待支付超过 5 分钟」的订单跑一遍,比干等回调稳。

4.3 一个容易忽略的时区与时间戳问题

v3 请求头里的timestamp是秒级 Unix 时间,微信允许的偏差是 5 分钟。服务器时间如果没同步,签名会报「时间戳错误」。我一般在上线前跑一次date和ntpdate校验,容器环境尤其注意基础镜像的时区。另外回调验签时也要校验Wechatpay-Timestamp与本地时间差,超过 5 分钟直接拒绝,防重放。

5. 避坑排查:五个真实踩过的坑

5.1 现象:所有请求返回 401「签名错误」

原因:九成是待签名串拼错。常见三种——URL 带了域名、GET 请求 body 没传空串导致少一个\n、openssl_sign没指定 SHA256。解决:把待签名串原样打日志(注意别把私钥打进去),逐行比对;用官方提供的签名校验工具对一遍。我习惯在开发环境把$message输出到独立日志文件,肉眼数换行。

5.2 现象:回调解密得到空字符串

原因:openssl_decrypt的$tag参数没传或传错。AES-256-GCM 的认证标签是密文最后 16 字节,必须单独截出来传给第四个参数。解决:

$ciphertext = base64_decode($res['ciphertext']); $tag = substr($ciphertext, -16); $data = openssl_decrypt(substr($ciphertext, 0, -16), 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $res['associated_data']);

参数说明:$key是 32 位 APIv3 密钥原文,不要做 md5;$nonce是 12 字节;associated_data原样传。少任何一个都解不出来。

5.3 现象:上线一两个月后回调突然全部验签失败

原因:平台证书轮换了,本地缓存的旧证书对不上新的Wechatpay-Serial。解决:验签前先按序列号找本地证书,找不到就调一次证书下载接口刷新,再验;同时加个定时任务每天刷一次。别把平台证书写死在代码里。

5.4 现象:订单被重复更新、库存扣成负数

原因:微信回调会重试,你的处理逻辑没有幂等。解决:给out_trade_no加唯一索引,更新时用UPDATE ... WHERE status = '待支付'带状态条件,影响行数为 0 就直接返回成功,不重复执行业务。这个坑和支付本身无关,但十个项目里八个中招。

5.5 现象:本地能跑,服务器报 cURL 证书错误

原因:服务器 CA 证书过期或没装,v3 请求走 HTTPS 双向校验。解决:更新系统 CA 包,或在 cURL 里显式指定CURLOPT_CAINFO;别图省事关掉CURLOPT_SSL_VERIFYPEER,那是给自己埋雷。

6. 进阶:把签名和证书做成可观测的

真正让这套实例在生产里站住的,不是它能下单,而是出问题时你能快速定位。我的习惯是给WxPayClient加一层轻量日志:每次请求记录method、url、http_code、Wechatpay-Request-ID(响应头里有),失败时把响应体前 500 字符也记下来。微信的报错信息很吝啬,但Request-ID是它侧排查的唯一线索,报给技术支持时带上这个 ID,对方能直接查到那次请求。

再进一步,把平台证书的序列号和过期时间做成一个健康检查接口:

<?php // 健康检查:返回当前缓存的平台证书序列号与到期时间 $certs = glob($cfg['platform_dir'] . '/*.pem'); $out = []; foreach ($certs as $f) { $info = openssl_x509_parse(file_get_contents($f)); $out[] = [ 'serial' => basename($f, '.pem'), 'valid_to' => date('Y-m-d H:i:s', $info['validTo_time_t']), ]; } header('Content-Type: application/json'); echo json_encode($out);

参数说明:openssl_x509_parse返回的validTo_time_t是到期时间戳,提前一周告警,就不会出现「证书过期导致回调全挂」的深夜事故。这个接口别暴露公网,挂在内网或加个简单 token。

还有一个验证技巧:上线前用沙箱环境把「下单→支付→回调→退款→退款回调」整条链路跑一遍,沙箱的商户号和正式环境是分开的,配置里加个env开关切换 base URL 即可。我一般会在沙箱里故意把apiv3_key改错一位,确认解密失败时日志能定位到具体字段,而不是只抛一个空异常。

从那以后我每次接新的支付渠道,都强制先把「验签失败」「解密失败」「证书过期」三种异常各造一次,看日志能不能说清问题,再谈业务逻辑。支付这块没有后悔药,能提前看见的错就别留给线上。希望帮到你。

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

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

PLM选型对比表深度拆解:从架构到BOM的评估清单与避坑指南

简介&#xff1a;面向企业信息化选型人员、PLM/PDM实施顾问与制造业技术管理者的横向评估资料&#xff0c;聚焦达索、西门子、PTC三大国际厂商的产品生命周期管理方案&#xff0c;帮助在立项阶段厘清供应商实力、技术架构与功能覆盖的差异。压缩包内为1份PDF对比表&#xff0c;…

作者头像 李华
网站建设 2026/11/4 2:45:42

基于SpringBoot+Vue的Java宿舍报修管理系统实战:从部署到二次开发

简介&#xff1a;这份资源面向计算机专业学生与Java Web初学者&#xff0c;提供一套基于Java的宿舍报修管理系统完整毕业设计资料&#xff0c;包含论文与配套源码&#xff0c;可用于课程设计、毕业设计参考或校园后勤维修管理场景的技术学习。压缩包共1个docx文件&#xff0c;约…

作者头像 李华
网站建设 2026/11/4 2:45:40

Aras PLM后台配置顺序与权限建模避坑指南

简介&#xff1a;《Aras 学习文档》是一份面向 Aras PLM 初学者、系统管理员及实施顾问的 Word 版系统管理使用手册&#xff0c;主要解决用户与权限配置、基础数据建模以及日常系统管理中的入门问题。压缩包内共 1 个 docx 文件&#xff0c;约 11.06MB&#xff0c;以章节目录组…

作者头像 李华
网站建设 2026/11/4 2:45:09

高校工资系统需求分析模板:模块化功能+权限矩阵+环境约束

简介&#xff1a;本资源是一份面向系统工程与软件工程初学者及项目实践者的《需求分析模板》文档&#xff0c;聚焦高校信息化管理场景&#xff0c;特别适用于工资管理系统等业务型软件的需求梳理与文档编写。文档以西北农林科技大学工资管理系统为典型案例&#xff0c;完整覆盖…

作者头像 李华
网站建设 2026/11/4 2:44:34

Android后台拍照实战:CameraX生命周期解耦与前台服务实现

简介&#xff1a;这份资源面向需要在应用退居后台时仍能调用相机完成拍摄的 Android 开发者&#xff0c;聚焦实时监控、图像记录等场景下的实现思路。内容围绕 Service 后台运行、CAMERA 运行时权限申请、Camera 与 Camera2 两套 API 的取舍、Bitmap 压缩与图片处理、WakeLock …

作者头像 李华
网站建设 2026/11/4 2:44:32

CATIA中文三维文字制作:CAD空心字轮廓导入与凸台拉伸全流程

简介&#xff1a;这份资源是面向CATIA初学者与产品设计人员的三维文字制作教程文档&#xff0c;重点解决在产品模型上添加标识、说明文字时如何生成空心字并拉伸为三维实体的问题&#xff0c;属于入门到进阶之间的实操型技能资料。压缩包共1个文件&#xff0c;为doc格式文档&am…

作者头像 李华