恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

PHP微信支付V3完整实战:解决401签名失败与验签问题

  • 首页
  • 资讯中心
  • /
  • PHP微信支付V3完整实战:解决401签名失败与验签问题

相关资讯

C语言数据结构与算法:程序员的底层交通规划图 2026/10/18 8:31:42
C语言数据结构实战:从链表崩溃到算法面试硬通货 2026/10/18 8:31:42
eNSP综合配置实战:从VLAN划分到NAT转换的完整网络搭建 2026/10/18 8:31:42

最新资讯

拆解 openCode 源码:用 TaoToken 统一 Key 打通全平台 AI 编程助手
HTML效果图还原实战:布局切块、响应式与常见坑位排查
Scrapy爬虫+SpringBoot+Vue构建高考志愿智能推荐系统
机房搬迁与网络割接实施方案:从物理搬迁到业务割接的工程化指南
System x3850 x6 装 Windows Server 全流程避坑指南
用AI Agent聊聊天就能完成数据抓取与报告生成?一文讲透原理与实战

今日推荐

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

PHP微信支付V3完整实战:解决401签名失败与验签问题

发布时间:2026/10/18 8:31:42
PHP微信支付V3完整实战:解决401签名失败与验签问题 简介本资源是一份面向PHP后端开发者与微信支付接入初学者的V3版完整实践方案聚焦最新微信支付接口集成解决商户系统快速对接、安全签名、证书管理及异步通知处理等核心难点。压缩包共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轻量易部署。已有4657人学习下载内容结构清晰从Config.asp配置、统一下单到notify.asp回调验证覆盖沙箱测试、签名生成、证书加载、订单查询与异常退款全流程。读者可直接复用核心代码模块结合说明文档理解V3签名机制与HTTPS双向认证逻辑快速构建安全合规的微信支付能力。1. PHP微信支付v3完整实例为什么你照着官方文档写完还是返回401或验签失败很多同学在做毕设、课设或内部系统对接时一上来就猛啃微信支付V3官方文档——密钥管理、证书下载、签名生成、回调验签、异步通知解密……看起来条理清晰但真正跑起来90%的人卡在第一步{code:INVALID_REQUEST,message:invalid signature}。不是签名算法写错了而是微信V3的签名机制根本不是纯PHP函数调用就能搞定的黑匣子它强依赖证书文件路径权限、HTTP头字段顺序、时间戳精度、body原始字节流编码、甚至curl版本对TLS1.2握手的支持程度。这套PHP微信支付v3完整实例就是为解决这个“明明代码逻辑对但就是过不了网关”的典型翻车场景而生。它不讲抽象原理只交付可直接运行、带日志追踪、含异常兜底、覆盖下单→支付→回调→查询→退款全链路的最小可行方案。适合正在赶毕设 deadline 的学生、需要快速上线支付模块的中小项目开发者以及被微信沙箱环境反复教育过的“老油条”——它不是教学Demo是一份能进生产环境经脱敏改造后的工程级参考。2. 总体架构设计三层结构与四个核心模块的职责划分微信支付V3接口不是单个API调用而是一套有状态、有时序、有安全边界的业务流程。本方案采用清晰的三层分层结构接入层Router→ 服务层Service→ 基础设施层SDK Core避免把签名逻辑、证书加载、HTTP客户端耦合在控制器里。四个核心模块分别承担明确职责WechatPayConfig负责密钥/证书/商户号等静态配置的集中管理与校验Signer模块封装签名生成与验签逻辑严格遵循RFC 7515 JWS Compact Serialization规范HttpClient模块定制化封装cURL行为强制启用TLS1.2、禁用重定向、统一超时策略并记录完整请求/响应原始字节流用于排查NotifyHandler模块专责处理异步通知包含自动解密、验签、幂等性校验、事务回滚钩子等生产必备能力。这种设计让每个模块可独立单元测试也方便在后续扩展支付宝、银联等其他渠道时复用基础设施层。2.1 配置中心WechatPayConfig类的设计与初始化约束配置不是简单地把APPID、MCHID、APIv3KEY塞进数组。微信V3要求所有敏感信息必须通过环境变量或加密配置文件加载禁止硬编码。本方案中WechatPayConfig类强制执行三项校验商户证书apiclient_cert.pem与私钥apiclient_key.pem必须成对存在且私钥需为PKCS#1格式非PKCS#8否则openssl_pkey_get_private()会静默失败APIv3密钥长度必须为32位ASCII字符少一位或多一位都会导致HMAC-SHA256计算结果错位沙箱环境与正式环境的域名、端口、证书路径必须完全隔离切换时不能仅改URL。// src/Config/WechatPayConfig.php class WechatPayConfig { private string $mchId; private string $appId; private string $apiV3Key; private string $certPath; // apiclient_cert.pem 绝对路径 private string $keyPath; // apiclient_key.pem 绝对路径 private string $baseUrl; public function __construct(array $config) { $this-mchId $config[mch_id] ?? ; $this-appId $config[app_id] ?? ; $this-apiV3Key $config[api_v3_key] ?? ; $this-certPath $config[cert_path] ?? ; $this-keyPath $config[key_path] ?? ; $this-baseUrl $config[base_url] ?? https://api.mch.weixin.qq.com/v3; $this-validate(); } private function validate(): void { if (strlen($this-apiV3Key) ! 32) { throw new InvalidArgumentException(APIv3 key must be exactly 32 ASCII characters); } if (!is_readable($this-certPath)) { throw new RuntimeException(Certificate file not readable: {$this-certPath}); } if (!is_readable($this-keyPath)) { throw new RuntimeException(Private key file not readable: {$this-keyPath}); } // 验证证书与私钥是否匹配关键 $cert file_get_contents($this-certPath); $key file_get_contents($this-keyPath); $pubKey openssl_pkey_get_public($cert); $privKey openssl_pkey_get_private($key); if (!$pubKey || !$privKey) { throw new RuntimeException(Invalid certificate or private key format); } openssl_free_key($pubKey); openssl_free_key($privKey); } }提示openssl_pkey_get_private()对私钥格式极其敏感。若你从微信商户平台下载的apiclient_key.pem打开后首行是-----BEGIN ENCRYPTED PRIVATE KEY-----说明它是PKCS#8加密格式必须用OpenSSL命令转成PKCS#1openssl rsa -in apiclient_key.pem -out apiclient_key_pkcs1.pem否则PHP会返回false且无错误提示签名永远失败。2.2 签名引擎Signer类如何严格还原微信V3签名规范微信V3签名不是简单的“拼接字符串HMAC”而是JWS Compact Serialization的子集{HTTP_METHOD}\n{RELATIVE_URL}\n{TIMESTAMP}\n{NONCE_STR}\n{BODY_SHA256}\n六段换行拼接后用商户私钥RSA-SHA256签名再Base64UrlEncode。Signer类必须保证时间戳使用UTC秒级时间非毫秒且与微信服务器时间差不能超过±300秒nonce_str必须每次请求唯一推荐使用bin2hex(random_bytes(16))生成BODY_SHA256必须是对原始HTTP body字节流非JSON字符串计算SHA256空body也要计算hash(sha256, )签名结果必须用strtr()替换/为-_ 否则微信验签失败。// src/Signer/Signer.php class Signer { private string $privateKey; public function __construct(string $privateKeyPem) { $this-privateKey $privateKeyPem; } public function sign(string $method, string $url, int $timestamp, string $nonceStr, string $body): string { // 提取相对URL去掉协议、域名、端口 $parsed parse_url($url); $relativeUrl $parsed[path] . ($parsed[query] ?? ); // 计算body SHA256注意是原始字节不是json_encode后的字符串 $bodyHash hash(sha256, $body, true); // raw_outputtrue // 拼接签名原文 $message implode(\n, [ strtoupper($method), $relativeUrl, (string)$timestamp, $nonceStr, base64_encode($bodyHash), ]); // RSA-SHA256签名 $signature ; $result openssl_sign($message, $signature, $this-privateKey, OPENSSL_ALGO_SHA256); if (!$result) { throw new RuntimeException(Failed to generate signature: . openssl_error_string()); } // Base64UrlEncode return strtr(base64_encode($signature), /, -_); } public function verify(string $signature, string $timestamp, string $nonceStr, string $body, string $certPem): bool { $bodyHash hash(sha256, $body, true); $message implode(\n, [ GET, // 回调验签固定为GET /v3/notify/payments/notify, $timestamp, $nonceStr, base64_encode($bodyHash), ]); $pubKey openssl_pkey_get_public($certPem); if (!$pubKey) { return false; } $rawSig strtr($signature, -_, /); $rawSig base64_decode($rawSig); $result openssl_verify($message, $rawSig, $pubKey, OPENSSL_ALGO_SHA256); openssl_free_key($pubKey); return $result 1; } }这段代码的关键在于$body参数传入的是原始HTTP请求体如{mchid:1900000109,out_trade_no:ORDER_XXXXX,...}的UTF-8字节流不是PHP数组。很多同学用json_encode($data)后直接传给sign()却忘了json_encode()默认加空格、转义中文导致SHA256值与微信服务器计算的不一致——这是验签失败最隐蔽的原因之一。2.3 HTTP客户端HttpClient如何规避curl常见陷阱微信V3强制要求TLS1.2且对HTTP头字段顺序、大小写、空格有严格校验。原生curl_exec()极易踩坑比如CURLOPT_SSLVERSION设为CURL_SSLVERSION_TLSv1_2在旧版cURL中无效CURLOPT_HTTPHEADER若未按微信要求的顺序Authorization,Content-Type,Accept,User-Agent排列会导致401CURLOPT_POSTFIELDS若传数组而非字符串cURL会自动转为multipart/form-data而微信只接受application/json。本方案HttpClient强制使用CURL_SSLVERSION_TLSv1_2并检测curl_version()[features] CURL_VERSION_SSL所有Header手动拼接为字符串数组按微信文档指定顺序POST请求CURLOPT_POSTFIELDS必须为JSON字符串且Content-Type必须为application/json启用CURLOPT_VERBOSE到临时文件便于抓包比对。// src/Http/HttpClient.php class HttpClient { private array $options; public function __construct(array $options []) { $this-options array_merge([ CURLOPT_TIMEOUT 30, CURLOPT_CONNECTTIMEOUT 10, CURLOPT_RETURNTRANSFER true, CURLOPT_HEADER false, CURLOPT_SSL_VERIFYPEER true, CURLOPT_SSL_VERIFYHOST 2, CURLOPT_SSLVERSION CURL_SSLVERSION_TLSv1_2, CURLOPT_CAINFO __DIR__ . /cacert.pem, // 自带最新根证书 ], $options); } public function request(string $method, string $url, array $headers [], ?string $body null): array { $ch curl_init(); curl_setopt_array($ch, $this-options); // 强制设置Header顺序微信要求 $standardHeaders [ Content-Type: application/json, Accept: application/json, User-Agent: PHP-WechatPay-V3/1.0, ]; $allHeaders array_merge($standardHeaders, $headers); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($ch, CURLOPT_HTTPHEADER, $allHeaders); if ($body ! null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $headerSize curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header substr($response, 0, $headerSize); $bodyResponse substr($response, $headerSize); $error curl_error($ch); curl_close($ch); if ($error) { throw new RuntimeException(cURL error: {$error}); } return [ http_code $httpCode, headers $header, body $bodyResponse, content_type $contentType, ]; } }注意CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST必须为true禁用证书校验等于裸奔。若你遇到SSL certificate problem: unable to get local issuer certificate不要设为false而是下载最新cacert.pem curl.se/docs/caextract.html 并用CURLOPT_CAINFO指向它。这是微信V3上线前必须完成的安全基线。2.4 异步通知处理器NotifyHandler的幂等性与解密闭环微信支付成功后会向你的notify_url发起HTTPS POST请求body是AES-256-GCM加密的JSON。这不是简单file_get_contents(php://input)就能处理的——它要求先从HTTP头Wechatpay-Serial获取证书序列号动态加载对应证书用Wechatpay-Timestamp和Wechatpay-Nonce参与验签解密时additional_data必须为Wechatpay-TimestampWechatpay-NonceWechatpay-Serial三段拼接解密后JSON中的resource.associated_data必须与resource.nonce、resource.ciphertext一起验证GCM tag。NotifyHandler将整个流程封装为原子操作验签 → 解密 → JSON解析 → 业务处理 → 返回200。任何一步失败都返回401或500微信会重试最多5次。// src/Notify/NotifyHandler.php class NotifyHandler { private Signer $signer; private string $apiV3Key; public function __construct(Signer $signer, string $apiV3Key) { $this-signer $signer; $this-apiV3Key $apiV3Key; } public function handle(string $rawBody, array $headers): array { $timestamp $headers[Wechatpay-Timestamp] ?? ; $nonce $headers[Wechatpay-Nonce] ?? ; $serial $headers[Wechatpay-Serial] ?? ; $signature $headers[Wechatpay-Signature] ?? ; // 步骤1验签 if (!$this-signer-verify($signature, $timestamp, $nonce, $rawBody, $this-getCertBySerial($serial))) { throw new RuntimeException(Signature verification failed); } // 步骤2解密 $notifyData json_decode($rawBody, true); $resource $notifyData[resource] ?? []; $ciphertext $resource[ciphertext] ?? ; $nonceStr $resource[nonce] ?? ; $associatedData $resource[associated_data] ?? ; $decrypted $this-decryptAesGcm($ciphertext, $this-apiV3Key, $nonceStr, $associatedData); if (!$decrypted) { throw new RuntimeException(AES-GCM decryption failed); } $event json_decode($decrypted, true); if (json_last_error() ! JSON_ERROR_NONE) { throw new RuntimeException(Invalid decrypted JSON); } // 步骤3幂等性检查查数据库是否存在相同out_trade_no event_type $outTradeNo $event[resource][out_trade_no] ?? ; $eventType $notifyData[event_type] ?? ; if ($this-isDuplicateNotification($outTradeNo, $eventType)) { return [status duplicate, message Already processed]; } // 步骤4执行业务逻辑更新订单状态、发短信、扣库存... $this-processPaymentSuccess($event); return [status success, message Processed]; } private function decryptAesGcm(string $ciphertext, string $key, string $nonce, string $associatedData): ?string { $keyBytes hex2bin(hash(sha256, $key, true)); // APIv3Key先SHA256再取前32字节 $ciphertextBytes base64_decode($ciphertext); $ivLen 12; $iv substr($ciphertextBytes, 0, $ivLen); $cipherText substr($ciphertextBytes, $ivLen, -16); $authTag substr($ciphertextBytes, -16); $decrypted openssl_decrypt( $cipherText, aes-256-gcm, $keyBytes, OPENSSL_RAW_DATA, $iv, $authTag, $associatedData ); return $decrypted ?: null; } private function getCertBySerial(string $serial): string { // 实际项目中这里应根据serial号从证书池中加载对应证书 // 本例简化为返回预置证书内容 return file_get_contents(__DIR__ . /../certs/wechatpay_cert.pem); } private function isDuplicateNotification(string $outTradeNo, string $eventType): bool { // 示例查数据库trade_notify_log表 // SELECT COUNT(*) FROM trade_notify_log WHERE out_trade_no ? AND event_type ? AND status success return false; // 真实实现需补充 } private function processPaymentSuccess(array $event): void { // 更新订单状态为paid、记录流水、触发后续动作... // 此处为占位实际业务逻辑填充点 } }这段代码揭示了一个关键细节APIv3Key不是直接当AES密钥用而是先hash(sha256, $key, true)取32字节二进制值再作为AES-256密钥。很多同学直接拿32位字符串当密钥导致解密永远失败——这是微信文档里埋得最深的“玄学”点。3. 核心功能模块实现下单、支付、查询、退款四步闭环一个完整的支付流程绝不是只实现“用户点击付款”这一个动作。本方案覆盖从创建订单、跳转支付、接收回调、到主动查询与异常退款的全生命周期。每个接口都附带真实可运行的参数示例、错误码映射、以及业务侧必须做的兜底动作。例如unifiedorder接口返回prepay_id后前端需用该ID再次调起JSAPI支付而query_order接口在用户关闭页面后必须由后台定时轮询确保状态最终一致refund接口则要求退款单号全局唯一且金额不能超过原订单——这些都不是微信的限制而是金融合规的刚性要求。3.1 JSAPI下单接口生成prepay_id并返回签名参数给前端JSAPI支付是公众号/H5场景的核心。调用/v3/pay/transactions/jsapi接口传入appid、mchid、description、amount、payer用户openid、notify_url等微信返回prepay_id。但注意prepay_id本身不能直接给前端调起支付必须由后端用它二次签名生成timeStamp、nonceStr、package、signType、paySign五参数前端再传给WeixinJSBridge.invoke(getBrandWCPayRequest, ...)。本方案JsapiOrderService类封装了这两步。// src/Service/JsapiOrderService.php class JsapiOrderService { private HttpClient $httpClient; private Signer $signer; private WechatPayConfig $config; public function __construct(HttpClient $httpClient, Signer $signer, WechatPayConfig $config) { $this-httpClient $httpClient; $this-signer $signer; $this-config $config; } public function createOrder(string $openId, string $outTradeNo, int $amount, string $description): array { $timestamp time(); $nonceStr bin2hex(random_bytes(16)); $body json_encode([ appid $this-config-getAppId(), mchid $this-config-getMchId(), description $description, out_trade_no $outTradeNo, attach PHP-V3-Demo, amount [ total $amount, currency CNY, ], payer [ openid $openId, ], notify_url https://yourdomain.com/api/wechat/notify, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $url $this-config-getBaseUrl() . /v3/pay/transactions/jsapi; $signature $this-signer-sign(POST, $url, $timestamp, $nonceStr, $body); $authHeader sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-config-getMchId(), $nonceStr, $signature, $timestamp, $this-config-getCertSerial() ); $response $this-httpClient-request(POST, $url, [Authorization $authHeader], $body); if ($response[http_code] ! 200) { throw new RuntimeException(Wechat API Error: {$response[http_code]} - {$response[body]}); } $result json_decode($response[body], true); $prepayId $result[prepay_id] ?? ; // 第二步为前端生成支付参数 $jsApiParams $this-generateJsApiParameters($prepayId, $timestamp, $nonceStr); return [ prepay_id $prepayId, jsapi_parameters $jsApiParams, timestamp $timestamp, ]; } private function generateJsApiParameters(string $prepayId, int $timestamp, string $nonceStr): array { $package prepay_id{$prepayId}; $paySign $this-signJsApi($package, $timestamp, $nonceStr); return [ appId $this-config-getAppId(), timeStamp (string)$timestamp, nonceStr $nonceStr, package $package, signType RSA, paySign $paySign, ]; } private function signJsApi(string $package, int $timestamp, string $nonceStr): string { $message implode(\n, [ $this-config-getAppId(), (string)$timestamp, $nonceStr, $package, ]); $signature ; openssl_sign($message, $signature, $this-config-getPrivateKey(), OPENSSL_ALGO_SHA256); return base64_encode($signature); } }血泪经验signJsApi()里的签名用的是商户私钥即apiclient_key.pem不是APIv3密钥这是两个完全不同的密钥体系APIv3密钥用于HTTP接口签名和AES解密而JSAPI支付签名用RSA私钥。混淆这两者是新手最常写的“后悔药”代码。3.2 主动查询订单query_order接口的健壮性设计用户可能在支付过程中关闭页面或网络超时未收到回调。此时必须由后台主动查询订单状态确保数据最终一致。/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid{mchid}接口返回trade_stateSUCCESS/REFUND/NOTPAY/CLOSED等。本方案OrderQueryService做了三重保障设置指数退避重试首次1s第二次2s第三次4s最多3次对trade_state为NOTPAY或PROCESSING的订单不立即返回失败而是标记为“待确认”供人工介入查询结果存入本地缓存如Redis避免高频重复查询。// src/Service/OrderQueryService.php class OrderQueryService { private HttpClient $httpClient; private Signer $signer; private WechatPayConfig $config; private \Redis $redis; // 假设已连接Redis public function __construct(HttpClient $httpClient, Signer $signer, WechatPayConfig $config, \Redis $redis) { $this-httpClient $httpClient; $this-signer $signer; $this-config $config; $this-redis $redis; } public function queryByOutTradeNo(string $outTradeNo, int $retryTimes 3): array { $url sprintf( %s/v3/pay/transactions/out-trade-no/%s?mchid%s, $this-config-getBaseUrl(), urlencode($outTradeNo), $this-config-getMchId() ); for ($i 0; $i $retryTimes; $i) { $timestamp time(); $nonceStr bin2hex(random_bytes(16)); $signature $this-signer-sign(GET, $url, $timestamp, $nonceStr, ); $authHeader sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-config-getMchId(), $nonceStr, $signature, $timestamp, $this-config-getCertSerial() ); $response $this-httpClient-request(GET, $url, [Authorization $authHeader]); if ($response[http_code] 200) { $result json_decode($response[body], true); $tradeState $result[trade_state] ?? UNKNOWN; // 缓存查询结果有效期10分钟 $this-redis-setex(wechat:order:{$outTradeNo}, 600, json_encode($result)); return [ status success, trade_state $tradeState, data $result, ]; } if ($response[http_code] 404) { // 订单不存在可能是未创建或已过期 return [status not_found, message Order not found]; } // 指数退避1s, 2s, 4s usleep((int)pow(2, $i) * 1000000); } return [status failed, message Query failed after retries]; } }3.3 申请退款refund接口的金额校验与幂等控制退款不是简单调接口。微信要求out_refund_no商户退款单号必须全局唯一amount不能超过原订单total同一笔订单多次退款累计amount不能超过total且必须传reason退款原因。本方案RefundService在调用前强制校验这四点并将out_refund_no写入数据库做唯一索引从源头杜绝重复退款。// src/Service/RefundService.php class RefundService { private HttpClient $httpClient; private Signer $signer; private WechatPayConfig $config; private \PDO $pdo; // 假设已连接数据库 public function __construct(HttpClient $httpClient, Signer $signer, WechatPayConfig $config, \PDO $pdo) { $this-httpClient $httpClient; $this-signer $signer; $this-config $config; $this-pdo $pdo; } public function applyRefund(string $outTradeNo, string $outRefundNo, int $amount, string $reason): array { // 步骤1查原订单金额 $stmt $this-pdo-prepare(SELECT total_amount FROM trade_orders WHERE out_trade_no ?); $stmt-execute([$outTradeNo]); $original $stmt-fetch(PDO::FETCH_ASSOC); if (!$original) { throw new RuntimeException(Original order not found); } // 步骤2查已退款总额 $stmt $this-pdo-prepare(SELECT SUM(refund_amount) as sum FROM trade_refunds WHERE out_trade_no ?); $stmt-execute([$outTradeNo]); $refunded $stmt-fetch(PDO::FETCH_ASSOC); $sumRefunded (int)($refunded[sum] ?? 0); if ($amount $sumRefunded $original[total_amount]) { throw new RuntimeException(Refund amount exceeds original order total); } // 步骤3检查out_refund_no是否已存在幂等 $stmt $this-pdo-prepare(SELECT COUNT(*) FROM trade_refunds WHERE out_refund_no ?); $stmt-execute([$outRefundNo]); if ($stmt-fetchColumn() 0) { throw new RuntimeException(Duplicate out_refund_no); } // 步骤4调用微信退款接口 $timestamp time(); $nonceStr bin2hex(random_bytes(16)); $url $this-config-getBaseUrl() . /v3/pay/transactions/out-trade-no/ . urlencode($outTradeNo) . /refunds; $body json_encode([ out_refund_no $outRefundNo, amount [ refund $amount, total $original[total_amount], currency CNY, ], reason $reason, ], JSON_UNESCAPED_UNICODE); $signature $this-signer-sign(POST, $url, $timestamp, $nonceStr, $body); $authHeader sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-config-getMchId(), $nonceStr, $signature, $timestamp, $this-config-getCertSerial() ); $response $this-httpClient-request(POST, $url, [Authorization $authHeader], $body); if ($response[http_code] ! 200) { throw new RuntimeException(Wechat refund API Error: {$response[http_code]} - {$response[body]}); } $result json_decode($response[body], true); // 步骤5落库关键 $this-pdo-prepare(INSERT INTO trade_refunds (out_trade_no, out_refund_no, refund_amount, status, wechat_resp) VALUES (?, ?, ?, ?, ?)) -execute([$outTradeNo, $outRefundNo, $amount, $result[status] ?? UNKNOWN, json_encode($result)]); return [ refund_id $result[refund_id] ?? , status $result[status] ?? UNKNOWN, receipt_url $result[receipt_url] ?? , ]; } }3.4 退款结果查询refund_query接口与状态同步退款是异步过程applyRefund返回ACCEPTED不代表退款成功只是受理。必须调用/v3/pay/transactions/out-trade-no/{out_trade_no}/refunds/{out_refund_no}查询最终状态。本方案RefundQueryService与OrderQueryService类似但增加了对refund_statusSUCCESS/ABNORMAL/CHANGE的精细化处理并支持回调式通知当状态变为SUCCESS时推送消息给运营后台。// src/Service/RefundQueryService.php class RefundQueryService { private HttpClient $httpClient; private Signer $signer; private WechatPayConfig $config; private \Redis $redis; public function __construct(HttpClient $httpClient, Signer $signer, WechatPayConfig $config, \Redis $redis) { $this-httpClient $httpClient; $this-signer $signer; $this-config $config; $this-redis $redis; } public function queryRefund(string $outTradeNo, string $outRefundNo): array { $url sprintf( %s/v3/pay/transactions/out-trade-no/%s/refunds/%s, $this-config-getBaseUrl(), urlencode($outTradeNo), urlencode($outRefundNo) ); $timestamp time(); $nonceStr bin2hex(random_bytes(16)); $signature $this-signer-sign(GET, $url, $timestamp, $nonceStr, ); $authHeader sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-config-getMchId(), $nonceStr, $signature, $timestamp, $this-config-getCertSerial() ); $response $this-httpClient-request(GET, $url, [Authorization $authHeader]); if ($response[http_code] ! 200) { return [status failed, http_code $response[http_code], body $response[body]]; } $result json_decode($response[body], true); $refundStatus $result[refund_status] ?? UNKNOWN; // 若为SUCCESS触发通知 if ($refundStatus SUCCESS) { $this-notifyRefundSuccess($outTradeNo, $outRefundNo, $result); } return [ status success, refund_status $refundStatus, data $result, ]; } p a hrefhttps://download.csdn.net/download/qi50000/9254549 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号