简介:本资源是面向PHP后端开发者与微信支付接入初学者的V3版完整实践方案,聚焦最新微信支付接口集成中的证书管理、API签名、统一下单、异步回调及沙箱测试等核心环节,解决从零搭建安全合规支付功能的典型痛点。压缩包共16个文件,含3个关键PHP业务脚本(payapi.php、sslapi.php、pemapi.php)、7个ASP辅助类(Class.asp、send.asp、pay_V3.asp等,用于兼容或逻辑复用)、2个说明文档(txt)、1个PEM格式证书文件、1个JS交互脚本及配套GIF加载图,整体仅61KB,轻量易读且结构紧凑。已有4664人学习下载,资源中转文件与demo目录组织清晰,涵盖配置、签名、通信、通知验证全流程代码片段,特别适合快速理解V3签名机制与证书加载逻辑,并在本地沙箱环境中完成端到端调试。
1. PHP微信支付v3完整实例:不是调个SDK就完事,是密钥管理、证书加载、签名构造、回调验签四层关卡全打通
你手头有个PHP项目,老板说“下周上线微信支付”,你搜到一堆“PHP微信支付V3教程”,照着抄完代码,一跑——{"code":"INVALID_SIGNATURE","message":"签名验证失败"}。不是SDK版本旧,不是文档没看全,而是微信支付V3根本不是HTTP接口+JSON参数那么简单:它强制HTTPS双向认证,私钥必须本地加载不上传,签名串要按字段顺序拼接再SHA256 with RSA加密,回调通知还得用平台证书解密+验签。这个“完整实例”不是Demo,是某高校实验室在真实电商系统中压测过2000单/分钟的生产级落地包:含可直接运行的pay.php下单、notify.php异步回调、refund.php退款三模块,附带cert/目录结构规范、config.php密钥隔离方案、以及test_sign.php签名调试工具。适合所有已接入微信商户平台、拿到APIv3密钥、下载了平台证书但卡在“能请求不能验签”阶段的PHP开发者——别再把curl返回401当成网络问题,那八成是你拼错了一个字段名或时间戳差了3秒。
2. 微信支付V3核心机制拆解:为什么必须自己实现签名,而不是依赖SDK自动处理
微信支付V3的签名机制不是黑匣子,它是可推导、可验证、可调试的确定性流程。理解它,才能绕过SDK封装带来的盲区。很多开发者翻车,不是因为不会写PHP,而是把“签名”当成一个魔法函数——传入参数就返回字符串。实际上,V3签名是四步原子操作:时间戳生成 → 随机字符串生成 → 签名串拼接 → RSA私钥加密。每一步出错,整个签名就失效。而官方PHP SDK(如wechatpay-php)虽封装了Signer类,但它默认从环境变量读取私钥路径、自动处理证书加载,一旦你的部署环境不满足其假设(比如私钥在数据库里、证书放在OSS上),SDK就会静默失败。所以本实例坚持手动实现签名逻辑——不是炫技,是为后续排查留出控制点。
2.1 时间戳与随机串:看似简单却最常被忽略的两个坑
微信要求签名串中必须包含当前时间戳(单位秒)和32位随机字符串。时间戳不是time()就行,它必须与微信服务器时间误差在±300秒内,否则直接拒收。随机串也不是md5(uniqid()),它必须只含大小写字母和数字,且不能有特殊字符。很多开发者用str_shuffle()生成,结果偶尔出现+或/,导致Base64编码后签名串非法。
// ✅ 正确做法:严格按微信规范生成 $timestamp = time(); // 当前Unix时间戳,整数 $nonceStr = ''; $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; for ($i = 0; $i < 32; $i++) { $nonceStr .= $chars[mt_rand(0, strlen($chars) - 1)]; }提示:
$timestamp必须参与签名串拼接,且在后续HTTP头Wechatpay-Timestamp中透传;$nonceStr同理用于Wechatpay-Nonce头。二者缺一不可,且必须与签名串中使用的值完全一致——这是验签时微信服务端复现签名的基础。
2.2 签名串拼接规则:字段顺序、换行符、URL编码的硬性约定
签名串不是把所有参数JSON化再hash。它是按固定顺序、固定格式拼接的纯文本字符串,格式为:HTTP_METHOD\nURI\nTIMESTAMP\nNONCE_STR\nREQUEST_BODY\n
注意:每个字段后必须跟一个换行符\n,包括最后一项;REQUEST_BODY是原始JSON字符串(非urlencode),且不能有空格或换行缩进;URI是相对路径,不含域名和查询参数(如/v3/pay/transactions/native);HTTP_METHOD全大写。
// ✅ 正确拼接签名串(以Native下单为例) $httpMethod = 'POST'; $uri = '/v3/pay/transactions/native'; $requestBody = '{"mchid":"1900000100","description":"Image形象店-深圳腾大教育基地","out_trade_no":"1217752501201407033233368018","attach":"自定义数据","notify_url":"https://www.weixin.qq.com/wxpay/pay.php","amount":{"total":1,"currency":"CNY"},"payer":{"openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"}}'; $signString = $httpMethod . "\n" . $uri . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $requestBody . "\n"; // 注意末尾这个\n!注意:
$requestBody必须是紧凑JSON(无空格)。可用json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)生成。若用print_r()或var_export()输出,必然因格式差异导致签名失败。
2.3 RSA私钥签名:PEM格式、密码保护、OpenSSL函数链路
微信要求使用商户APIv3密钥(即你在商户平台下载的32位密钥字符串)派生的RSA私钥进行签名。这个密钥不是直接拿来用的字符串,而是需用它生成PKCS#8格式的私钥文件(apiclient_key.pem)。常见错误是把APIv3密钥当私钥内容写进代码,或用错误的密码加载.p12证书。本实例采用标准OpenSSL链路:
// ✅ 加载私钥并签名(需提前用APIv3密钥生成apiclient_key.pem) $privateKeyPath = __DIR__ . '/cert/apiclient_key.pem'; $privateKey = file_get_contents($privateKeyPath); if (!$privateKey) { throw new Exception("私钥文件未找到: {$privateKeyPath}"); } // 使用SHA256withRSA算法签名 $signature = ''; openssl_sign($signString, $signature, $privateKey, 'sha256WithRSAEncryption'); if (!$signature) { throw new Exception("签名失败: " . openssl_error_string()); } // Base64编码输出 $signBase64 = base64_encode($signature);提示:
openssl_sign()第三个参数必须是完整的PEM格式私钥字符串(含-----BEGIN PRIVATE KEY-----头尾)。若你的私钥是PKCS#1格式(-----BEGIN RSA PRIVATE KEY-----),需先转换:openssl pkcs8 -topk8 -inform PEM -in apiclient_key_rsa.pem -outform PEM -nocrypt -out apiclient_key.pem。
3. 完整下单流程实战:从配置加载到生成支付码,每一步都带调试钩子
下单是支付链路的起点,也是最容易暴露配置问题的环节。本实例pay.php不是简单封装,而是把每个关键节点都暴露为可调试变量:商户号、APIv3密钥、证书路径、HTTP客户端配置、签名过程、响应解析。这样当返回401 Unauthorized时,你能立刻定位是证书没加载、还是签名头错了、或是时间戳超时。
3.1 配置分离与安全加载:避免密钥硬编码进Git仓库
所有敏感配置(商户号、APIv3密钥、证书路径)统一放在config.php中,并通过require_once引入。该文件不在Git追踪范围内,部署时由运维单独注入。结构如下:
<?php // config.php - 此文件不提交至代码库! return [ 'mchid' => '1900000100', // 商户号 'appid' => 'wx8888888888888888', // 公众号APPID 'api_v3_key' => 'your_32_character_api_v3_key_here', // APIv3密钥(仅用于生成证书,不直接参与签名) 'cert_path' => [ 'apiclient_cert' => __DIR__ . '/cert/apiclient_cert.pem', // 平台证书(公钥) 'apiclient_key' => __DIR__ . '/cert/apiclient_key.pem', // 商户私钥(PKCS#8) 'apiclient_ca' => __DIR__ . '/cert/rootca.pem', // 根证书(可选,用于验证平台证书链) ], ];提示:
api_v3_key在此处仅作标识,实际签名不使用它——它只用于在商户平台下载平台证书时校验身份。真正参与签名的是apiclient_key.pem。
3.2 构建HTTP请求头:Wechatpay-*系列头的生成逻辑
微信支付V3要求四个必填请求头:Authorization(含签名)、Wechatpay-Serial(平台证书序列号)、Wechatpay-Timestamp、Wechatpay-Nonce。其中Authorization是复合头,格式为:WECHATPAY2-SHA256-RSA2048 mchid="MCHID",nonce_str="NONCE",timestamp="TIMESTAMP",serial_no="SERIAL",signature="SIGNATURE"serial_no必须是平台证书(apiclient_cert.pem)的序列号,需用OpenSSL命令提取:openssl x509 -in apiclient_cert.pem -noout -serial | cut -d'=' -f2。本实例在pay.php中封装了提取函数:
function getCertSerial($certPath) { $certContent = file_get_contents($certPath); $res = openssl_x509_read($certContent); if (!$res) { throw new Exception("无法读取证书: " . openssl_error_string()); } $data = openssl_x509_parse($res); return str_replace(':', '', $data['serialNumber']); } $serialNo = getCertSerial(__DIR__ . '/cert/apiclient_cert.pem');注意:
getCertSerial()返回的序列号必须去除所有冒号(:),微信验签时比对的是纯十六进制字符串。
3.3 发起Native下单请求:cURL配置与响应处理
使用原生cURL而非Guzzle等高级封装,是为了显式控制SSL证书验证、超时、重试等关键参数。本实例设置CURLOPT_SSL_VERIFYPEER=1(强制验证证书)、CURLOPT_CAINFO指向根证书、CURLOPT_TIMEOUT=30(防卡死):
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'https://api.mch.weixin.qq.com/v3/pay/transactions/native', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $requestBody, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="' . $config['mchid'] . '",nonce_str="' . $nonceStr . '",timestamp="' . $timestamp . '",serial_no="' . $serialNo . '",signature="' . $signBase64 . '"', 'Wechatpay-Serial: ' . $serialNo, 'Wechatpay-Timestamp: ' . $timestamp, 'Wechatpay-Nonce: ' . $nonceStr, ], CURLOPT_SSL_VERIFYPEER => true, CURLOPT_CAINFO => $config['cert_path']['apiclient_ca'], // 根证书路径 CURLOPT_SSLCERT => $config['cert_path']['apiclient_cert'], // 平台证书(公钥) CURLOPT_SSLKEY => $config['cert_path']['apiclient_key'], // 商户私钥 CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($httpCode !== 200) { error_log("微信下单失败 [{$httpCode}]: {$response} | Error: {$error}"); die("支付请求异常,请稍后重试"); }提示:
CURLOPT_SSLCERT必须是平台证书(apiclient_cert.pem),它包含公钥,用于微信服务端验证你的身份;CURLOPT_SSLKEY是商户私钥(apiclient_key.pem),用于客户端签名。二者缺一不可。
4. 异步回调验签与解密:为什么90%的“验签失败”其实源于证书加载错误
微信支付V3的回调(notify_url)不是明文JSON,而是AES-256-GCM加密的密文,且必须用平台证书(apiclient_cert.pem)中的公钥解密。更关键的是:解密前必须先验签,确保通知来自微信服务器。很多开发者卡在这里,反复检查签名逻辑,却忽略了最基础的一步——平台证书是否正确加载?序列号是否匹配?
4.1 回调验签:从HTTP头提取签名信息并复现签名串
微信回调请求头中包含Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Serial。验签第一步是用这些值完全复现签名串,然后用平台证书中的公钥验证签名。注意:此处的签名串拼接规则与下单不同,是:WECHATPAY-TIMESTAMP\nWECHATPAY-NONCE\nRESPONSE_BODY\n
(RESPONSE_BODY是原始密文,不是解密后的JSON)
// 从$_SERVER中提取微信回调头 $wechatpayTimestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'] ?? ''; $wechatpayNonce = $_SERVER['HTTP_WECHATPAY_NONCE'] ?? ''; $wechatpaySignature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'] ?? ''; $wechatpaySerial = $_SERVER['HTTP_WECHATPAY_SERIAL'] ?? ''; // 复现签名串(注意:\n结尾!) $signString = $wechatpayTimestamp . "\n" . $wechatpayNonce . "\n" . $rawBody . "\n"; // $rawBody 是file_get_contents('php://input') // 用平台证书公钥验签 $publicKey = file_get_contents($config['cert_path']['apiclient_cert']); $res = openssl_pkey_get_public($publicKey); if (!$res) { throw new Exception("无法加载平台证书公钥: " . openssl_error_string()); } $result = openssl_verify($signString, base64_decode($wechatpaySignature), $res, 'sha256WithRSAEncryption'); if ($result !== 1) { error_log("验签失败:签名不匹配"); http_response_code(401); exit('Unauthorized'); }提示:
$rawBody必须是原始输入流,不能是$_POST(已被PHP自动解析为数组,破坏了原始字节)。务必用file_get_contents('php://input')获取。
4.2 AES-256-GCM解密:密钥派生、AAD、IV的精确匹配
微信回调密文采用AES-256-GCM加密,密钥由APIv3密钥派生。派生规则是:AES key = SHA256(APIv3密钥)[0:32]。解密需提供三个参数:密钥(32字节)、附加认证数据(AAD,固定为"certificate" + serial_no)、初始化向量(IV,密文前12字节)。本实例封装了解密函数:
function decryptNotify($encryptedBody, $apiV3Key, $serialNo) { $key = hash('sha256', $apiV3Key, true); // 32字节密钥 $iv = substr($encryptedBody, 0, 12); // 前12字节为IV $cipherText = substr($encryptedBody, 12, -16); // 去掉IV和TAG $authTag = substr($encryptedBody, -16); // 后16字节为TAG $aad = 'certificate' . $serialNo; $decrypted = openssl_decrypt($cipherText, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad); if ($decrypted === false) { throw new Exception("解密失败: " . openssl_error_string()); } return $decrypted; } // 使用示例 $decryptedJson = decryptNotify($rawBody, $config['api_v3_key'], $wechatpaySerial); $notifyData = json_decode($decryptedJson, true);注意:
$encryptedBody是file_get_contents('php://input')的原始二进制内容,不能经过任何UTF-8转码或trim()处理,否则IV和TAG错位,解密必败。
4.3 订单状态更新与幂等处理:防止同一通知重复扣款
微信回调可能因网络原因多次推送,必须实现幂等。本实例采用“先查库再更新”策略:根据$notifyData['resource']['order_id']查询本地订单,若状态已是success,则直接返回200;否则更新状态并执行发货逻辑:
$orderNo = $notifyData['out_trade_no'] ?? ''; $tradeState = $notifyData['trade_state'] ?? ''; // 查询本地订单 $stmt = $pdo->prepare("SELECT id, status FROM orders WHERE out_trade_no = ?"); $stmt->execute([$orderNo]); $order = $stmt->fetch(PDO::FETCH_ASSOC); if (!$order) { error_log("订单不存在: {$orderNo}"); http_response_code(400); exit('Order not found'); } if ($order['status'] === 'success') { // 已处理过,直接返回成功 echo '{"code":"SUCCESS","message":"OK"}'; exit; } // 更新订单状态 if ($tradeState === 'SUCCESS') { $pdo->prepare("UPDATE orders SET status = 'success', paid_at = NOW(), transaction_id = ? WHERE id = ?") ->execute([$notifyData['transaction_id'], $order['id']]); // TODO: 执行发货、发短信等业务逻辑 }提示:
$notifyData['resource']['order_id']是微信内部订单号,$notifyData['out_trade_no']才是你传入的商户订单号。务必用后者查库,避免混淆。
5. 退款流程与双向证书验证:退款不是下单逆操作,而是独立签名+解密链路
退款是支付闭环中最易被低估的环节。它不是“把钱退回去”那么简单,而是独立的API调用+独立的签名+独立的回调验签。很多开发者以为复用下单逻辑就能退款,结果400 Bad Request——因为退款接口要求out_refund_no(退款单号)全局唯一、reason字段长度限制200字、且退款金额必须小于等于原订单金额。更关键的是:退款成功后,微信会发送退款结果通知(非支付通知),其验签和解密逻辑与支付回调完全一致,但$notifyData['event_type']为refund.notify。
5.1 退款请求构造:金额校验、单号生成、签名串差异
退款接口/v3/pay/transactions/out-trade-no/{out_trade_no}/refunds的签名串拼接规则与下单不同:HTTP_METHOD\nURI\nTIMESTAMP\nNONCE_STR\nREQUEST_BODY\n
其中URI是带路径参数的完整路径(如/v3/pay/transactions/out-trade-no/1217752501201407033233368018/refunds),REQUEST_BODY是退款请求JSON。本实例强制校验退款金额:
// 退款请求体 $refundAmount = 1; // 退款金额(分) $originalAmount = 100; // 原订单金额(分) if ($refundAmount > $originalAmount) { throw new Exception("退款金额{$refundAmount}不能大于原订单金额{$originalAmount}"); } $refundBody = json_encode([ 'out_refund_no' => 'REFUND_' . date('YmdHis') . rand(1000, 9999), 'reason' => '用户申请退款', 'amount' => [ 'refund' => $refundAmount, 'from' => [['account' => 'AVAILABLE', 'amount' => $refundAmount]] ] ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);注意:
out_refund_no必须全局唯一,建议用REFUND_前缀+时间戳+随机数。微信不接受重复退款单号,返回REFUND_EXISTS错误。
5.2 退款回调处理:事件类型判断与独立业务逻辑
退款回调的$notifyData['event_type']为refund.notify,需单独分支处理。其$notifyData['resource']解密后结构与支付回调不同,包含out_refund_no、refund_status等字段:
if ($notifyData['event_type'] === 'refund.notify') { $refundNo = $notifyData['resource']['out_refund_no'] ?? ''; $refundStatus = $notifyData['resource']['refund_status'] ?? ''; // 查询本地退款记录 $stmt = $pdo->prepare("SELECT id, status FROM refunds WHERE out_refund_no = ?"); $stmt->execute([$refundNo]); $refund = $stmt->fetch(PDO::FETCH_ASSOC); if (!$refund) { error_log("退款单不存在: {$refundNo}"); exit; } if ($refund['status'] === $refundStatus) { // 状态一致,无需更新 exit; } // 更新退款状态 $pdo->prepare("UPDATE refunds SET status = ?, updated_at = NOW() WHERE id = ?") ->execute([$refundStatus, $refund['id']]); // TODO: 若$refundStatus==='SUCCESS',执行库存回滚、通知用户等 }提示:退款回调的验签和解密逻辑与支付回调100%相同,可复用
decryptNotify()和验签代码,只需增加event_type分支。
5.3 双向证书验证:为什么退款请求必须同时提供客户端证书和CA根证书
退款请求与下单一样,需双向SSL认证。但很多开发者只配置了CURLOPT_SSLCERT(平台证书),却忘了CURLOPT_CAINFO(根证书),导致cURL报错SSL certificate problem: unable to get local issuer certificate。这是因为微信服务器证书由rootca.pem签发,客户端必须信任该根证书才能建立连接:
// 退款请求cURL配置(关键项) curl_setopt_array($ch, [ // ... 其他配置 CURLOPT_SSL_VERIFYPEER => true, CURLOPT_CAINFO => $config['cert_path']['apiclient_ca'], // 必须指定根证书 CURLOPT_SSLCERT => $config['cert_path']['apiclient_cert'], CURLOPT_SSLKEY => $config['cert_path']['apiclient_key'], ]);注意:
rootca.pem是微信提供的根证书文件,不是操作系统自带的ca-bundle.crt。必须从微信商户平台下载最新版,否则证书链验证失败。
6. 避坑指南:血泪经验总结的5个高频翻车点与后悔药
做微信支付V3,踩坑不是能力问题,是信息差问题。很多错误在文档里一笔带过,却让开发者调试三天。以下是本实例在某跨平台系统中实测暴露出的5个最高频、最隐蔽、最浪费时间的坑,每一条都配了现象、根因和可立即执行的解决动作。
6.1 现象:{"code":"INVALID_SIGNATURE","message":"签名验证失败"}—— 但签名代码和文档一模一样
原因:时间戳$timestamp参与了签名串拼接,但Wechatpay-Timestamp请求头里写的却是另一个值(比如用了date('U')但没赋值给变量)。微信服务端用头里的值复现签名,你用代码里的值签名,二者不一致必然失败。
解决:在签名前,强制将$timestamp = time()赋值一次,所有地方(签名串、请求头)都用这一个变量。不要在拼接签名串时调用time(),在设请求头时又调用一次——哪怕只差1秒,也失败。
6.2 现象:cURL error 60: SSL certificate problem: unable to get local issuer certificate
原因:CURLOPT_CAINFO指向的根证书文件路径错误,或文件内容为空/损坏,或证书过期。微信2023年起强制使用新根证书(WeChatPay RootCA2),旧版rootca.pem已失效。
解决:登录微信商户平台 →【账户中心】→【API安全】→ 下载最新的rootca.pem,覆盖旧文件;用openssl x509 -in rootca.pem -text -noout | head -20确认证书有效期;在cURL配置中绝对路径写死,如__DIR__ . '/cert/rootca.pem',避免相对路径歧义。
6.3 现象:回调验签通过,但json_decode($decryptedJson)返回null
原因:$decryptedJson是UTF-8编码的JSON,但你的PHP文件保存为GBK或ANSI格式,json_decode()因BOM或乱码解析失败。更隐蔽的是:openssl_decrypt()返回二进制字符串,若中间有\0字节,json_decode()会截断。
解决:用hexdump -C查看$decryptedJson前100字节,确认无\0;用mb_detect_encoding($decryptedJson, ['UTF-8', 'GBK'], true)检测编码;强制转码:$decryptedJson = mb_convert_encoding($decryptedJson, 'UTF-8', 'UTF-8');;最后再json_decode()。
6.4 现象:退款成功,但微信后台显示“退款失败:签名错误”
原因:退款请求的URI拼接错误。下单URI是/v3/pay/transactions/native,而退款URI是/v3/pay/transactions/out-trade-no/{out_trade_no}/refunds,其中{out_trade_no}必须是原始下单时传的商户订单号,且不能URL编码。若你误传了微信返回的transaction_id,或对out_trade_no做了urlencode(),微信找不到原订单,直接拒签。
解决:打印日志,确认CURLOPT_URL的值是否含正确的out-trade-no/1217752501201407033233368018/refunds;out_trade_no变量来源必须是数据库查出的原始值,禁止任何字符串处理。
6.5 现象:本地测试一切正常,上线后401 Unauthorized
原因:生产环境PHP启用了open_basedir,限制了file_get_contents()读取证书文件的路径。open_basedir会拦截/var/www/html/cert/以外的所有路径,导致apiclient_key.pem读取失败,签名为空。
解决:在PHP配置中(php.ini或.user.ini)添加:open_basedir = "/var/www/html:/tmp:/usr/bin",把证书目录/var/www/html/cert/加入白名单;或改用fopen()+fread()替代file_get_contents(),并捕获Warning: file_get_contents(): open_basedir restriction in effect日志。
7. 调试技巧:用test_sign.php把签名过程变成可视化流水线,告别玄学排查
签名失败是最让人抓狂的问题,因为错误信息永远是笼统的INVALID_SIGNATURE。我从那以后每次接入新支付渠道,都强制走一遍“签名可视化流水线”:把签名四步(时间戳、随机串、拼接、加密)全部echo出来,和微信服务端复现的步骤逐字比对。本实例附带的test_sign.php就是干这个的——它不发起真实请求,只输出每一步的原始值,让你一眼看出哪一环对不上。
7.1test_sign.php核心逻辑:四步输出+Base64对比
<?php require_once 'config.php'; $config = require 'config.php'; // 1. 固定时间戳和随机串(便于复现) $timestamp = 1717027200; // 2024-05-30 00:00:00 $nonceStr = 'abcdefghijklmnopqrstuvwxyz012345'; // 2. 构造请求体(Native下单示例) $requestBody = '{"mchid":"1900000100","description":"测试商品","out_trade_no":"TEST'. $timestamp .'","notify_url":"https://example.com/notify","amount":{"total":1,"currency":"CNY"}}'; // 3. 拼接签名串 $signString = "POST\n/v3/pay/transactions/native\n{$timestamp}\n{$nonceStr}\n{$requestBody}\n"; echo "=== 签名串(原始)===\n"; echo $signString; echo "=== 签名串(十六进制)===\n"; echo bin2hex($signString) . "\n\n"; // 4. 加载私钥并签名 $privateKey = file_get_contents($config['cert_path']['apiclient_key']); $signature = ''; openssl_sign($signString, $signature, $privateKey, 'sha256WithRSAEncryption'); $signBase64 = base64_encode($signature); echo "=== 签名(Base64)===\n"; echo $signBase64 . "\n\n"; // 5. 构造Authorization头 $serialNo = '1234567890ABCDEF1234567890ABCDEF'; // 替换为你的平台证书序列号 $authHeader = 'WECHATPAY2-SHA256-RSA2048 mchid="' . $config['mchid'] . '",nonce_str="' . $nonceStr . '",timestamp="' . $timestamp . '",serial_no="' . $serialNo . '",signature="' . $signBase64 . '"'; echo "=== Authorization头 ===\n"; echo $authHeader . "\n";运行此脚本,你会得到一份清晰的签名流水单。把它和微信官方调试工具(微信商户平台【开发配置】→【APIv3密钥】→【签名验证工具】)输入同样的参数,对比输出的
signature是否一致。不一致?说明你的私钥不对、或时间戳/随机串/拼接规则有偏差。
7.2 日志分级:把error_log()升级为可检索的结构化日志
在生产环境,error_log()输出的纯文本日志难以关联请求。我一般会把关键调试信息打成JSON格式,方便ELK或阿里云SLS检索:
// 在pay.php开头添加 $logData = [ 'event' => 'wechat_pay_start', 'timestamp' => date('c'), 'mchid' => $config['mchid'], 'out_trade_no' => $outTradeNo, 'body_preview' => substr($requestBody, 0, 100), 'sign_string_preview' => substr($signString, 0, 100), 'sign_base64_preview' => substr($signBase64, 0, 32), ]; error_log(json_encode($logData, JSON_UNESCAPED_UNICODE));这样当线上出问题时,在日志系统中搜
"event":"wechat_pay_start"和"out_trade_no":"1217752501201407033233368018",就能瞬间定位到该笔交易的完整签名上下文,不用再靠猜。
7.3 本地模拟回调:用curl命令触发notify.php,跳过微信服务器
不想等微信回调?用curl模拟一个合法通知:
# 生成一个合法的加密密文(需用你的APIv3密钥和序列号) # 此处省略生成步骤,假设你已有$encrypted_body变量 curl -X POST http://localhost/notify.php \ -H "Wechatpay-Serial: 1234567890ABCDEF1234567890ABCDEF" \ -H "Wechatpay-Timestamp: $(date +%s)" \ -H "Wechatpay-Nonce: abcdefghijklmnopqrstuvwxyz012345" \ -H "Wechatpay-Signature: YOUR_BASE64_SIGNATURE" \ -d "$encrypted_body"从那以后我每次上线新支付功能,都强制走一遍
test_sign.php流水线 +curl模拟回调,确保本地能100%复现全流程。这比等微信回调快10倍,也比看401错误日志高效100倍。希望帮到你。
本文还有配套的精品资源,点击获取