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

相关新闻

Agent时代企业数据安全新挑战:目标对齐、工具污染与语义蒸馏

Agent时代企业数据安全新挑战:目标对齐、工具污染与语义蒸馏

1. 项目概述:当大模型从“工具”变成“同事”,安全边界正在悄然瓦解“企业大模型数据安全不能只盯着防泄露”——这句话我第一次在某次内部安全评审会上听到时,下意识皱了眉头。不是因为它错,而是因为它太对了,对得让人…

2026/10/19 18:57:48 阅读更多 →
PaddleOCR 2.7 + Qt5 桌面中文识别工具开发实践

PaddleOCR 2.7 + Qt5 桌面中文识别工具开发实践

简介:将百度PaddleOCR 2.7与Qt5框架结合,面向需要构建桌面OCR工具并借助GPU加速识别流程的开发者。项目以光学字符识别技术为核心,使用C封装PaddleOCR预训练模型,完整包含文字检测与识别两个子模块,可在Qt图形界面完成…

2026/10/19 18:07:39 阅读更多 →
AI周报:推理模型降价、开源权重与Agent落地趋势

AI周报:推理模型降价、开源权重与Agent落地趋势

这周的AI圈,比国庆的景区还热闹。我原本以为长假期间大家会消停几天,结果从10月1日到8日,各大发布通道几乎没断过:新一代推理模型、开源权重、GPU实例降价、Agent框架2.0更新……每一条单独看都像常规操作,串在一起就很…

2026/10/19 10:17:32 阅读更多 →

最新新闻

Flutter 库鸿蒙适配实战:m_package 从崩溃到跑通的完整流程

Flutter 库鸿蒙适配实战:m_package 从崩溃到跑通的完整流程

没有哪次推送失败比“鸿蒙上跑不起来”更让人头大了。Flutter 项目本来一套代码走天下,结果到了鸿蒙设备上,编译能过,一打开就直接崩了,日志指向m_package里的某个工具函数。这个库说大不大,却几乎每个页面都引用它&am…

2026/10/19 23:03:33 阅读更多 →
智能体Agent实战:从原理到代码,打造会做事的AI

智能体Agent实战:从原理到代码,打造会做事的AI

智能体(Agent)到底是个啥东西?我直接说结论:智能体不是一个神秘的技术黑盒,而是“大模型 记忆 工具 自主决策循环”的组合体。它跟普通的聊天机器人最大的区别就三个字——会做事。你问ChatGPT“明天深圳天气怎么样…

2026/10/19 23:00:29 阅读更多 →
校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

1. 校招面试官到底想在算法题里看到什么很多人准备校招的时候,第一反应是去刷题网站按标签刷个三五百道,觉得数量上去了自然就稳了。我带过几届校招的技术面,也跟不少做校招的朋友聊过,实际情况是:面试官出算法题&…

2026/10/19 22:50:12 阅读更多 →
【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

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

2026/10/19 22:46:04 阅读更多 →
open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

1. 什么是 open-code-review:一个被严重低估的开发者协作新范式open-code-review 不是某个具体软件的名字,也不是某家公司的私有产品,而是一类正在快速成型的技术实践范式——它指代的是基于开源理念、命令行界面(CLI)…

2026/10/19 22:40:54 阅读更多 →
二轮平衡车全套源码:卡尔曼滤波与PID三环控制实战

二轮平衡车全套源码:卡尔曼滤波与PID三环控制实战

简介:这是一套面向电子爱好者与嵌入式初学者的二轮平衡车DIY完整资料包,涵盖从硬件搭建到软件调试的全链路内容。资源定位为STM32平台平衡车的开源学习套件,包含互补滤波、卡尔曼滤波、DMP三种姿态解算方案的源码,三套工程均编译通…

2026/10/19 22:39:52 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

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

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/18 12:16:16 阅读更多 →