当前位置: 首页 > news >正文

ThinkPHP6集成yansongda/pay实现微信支付宝聚合支付与退款实战

1. 项目概述与核心价值

最近在做一个基于ThinkPHP6的移动应用后端项目,支付模块是绕不开的核心功能。老板的要求很明确:既要支持微信支付,也要支持支付宝支付,而且得覆盖APP支付和退款这两个最常用的场景。一开始我也考虑过自己分别去对接两家的官方SDK,但一想到那动辄几十页的文档、复杂的签名验签流程、以及不同支付渠道迥异的回调处理逻辑,头就大了。更别提后续维护,任何一家的接口变动都可能意味着大量的代码重写。

正是在这种背景下,我发现了 yansongda/pay 这个PHP支付SDK扩展包。它本质上是一个聚合支付解决方案,用一套统一的API封装了微信支付和支付宝支付的复杂逻辑。对于使用ThinkPHP6这类现代PHP框架的开发者来说,集成它意味着可以用一种更优雅、更高效的方式来完成支付功能开发,把精力从“如何对接支付渠道”转移到“如何设计更好的业务流”上。

这个Demo项目的目的,就是基于ThinkPHP6框架,完整地走通使用yansongda/pay扩展包实现微信APP支付、支付宝APP支付以及对应退款功能的全部流程。我会从环境配置、SDK集成、参数配置、支付下单、异步回调处理到发起退款,一步步拆解,并分享我在实际对接中踩过的坑和总结的经验。无论你是刚接触支付模块的新手,还是正在寻找更优聚合支付方案的同行,相信这份实录都能给你提供直接的参考。

2. 环境准备与扩展包集成

2.1 基础环境与ThinkPHP6项目初始化

首先,确保你的开发环境满足基本要求。我使用的是PHP 7.4+,Composer进行依赖管理,数据库选用MySQL。ThinkPHP6要求PHP版本>=7.1,但为了更好的性能和语法支持,建议直接上7.4或8.0。

通过Composer创建一个新的ThinkPHP6项目:

composer create-project topthink/think tp6-pay-demo cd tp6-pay-demo

项目创建后,基础的目录结构就生成了。我们需要重点关注的是config目录(存放配置)、app目录(业务逻辑)和route目录(路由定义)。

注意:ThinkPHP6默认采用了多应用模式,但为了Demo的简洁性,我们就在单应用(app目录下)下进行开发。如果你的项目是多应用,只需将下面的控制器、服务类等放置到对应应用目录下即可。

2.2 安装与配置 yansongda/pay 扩展包

接下来是核心步骤,安装支付SDK。在项目根目录下执行:

composer require yansongda/pay

这个命令会拉取 yansongda/pay 及其依赖(如 Guzzle HTTP客户端、各种加密解密库)。安装过程通常很顺利。

安装完成后,我们需要发布扩展包的配置文件到ThinkPHP6项目中。yansongda/pay 提供了一个Artisan命令(如果它提供了ThinkPHP6的专用服务,可能需要手动处理)。更通用的方式是,我们直接在config目录下创建一个pay.php配置文件。

不过,yansongda/pay 的官方文档可能更倾向于使用其自带的Pay门面。为了更好融入ThinkPHP6的配置体系,我推荐采用依赖注入和自定义配置的方式。首先,在config目录下新建pay.php

<?php // config/pay.php return [ 'default' => 'alipay', // 默认使用的支付渠道,可按需修改 'log' => [ 'enable' => true, 'file' => runtime_path('logs/pay.log'), // 支付日志路径 'level' => 'info', // 日志级别 ], // 支付宝配置 'alipay' => [ 'default' => [ // 必填-支付宝分配的 app_id 'app_id' => env('ALIPAY_APP_ID', ''), // 必填-应用私钥 字符串或路径 'app_secret_cert' => env('ALIPAY_APP_SECRET_CERT', ''), // 必填-应用公钥证书 路径 'app_public_cert_path' => env('ALIPAY_APP_PUBLIC_CERT_PATH', ''), // 必填-支付宝公钥证书 路径 'alipay_public_cert_path' => env('ALIPAY_PUBLIC_CERT_PATH', ''), // 必填-支付宝根证书 路径 'alipay_root_cert_path' => env('ALIPAY_ROOT_CERT_PATH', ''), 'mode' => 'normal', // 可选,设置使用证书模式:normal / certificate 'notify_url' => env('ALIPAY_NOTIFY_URL', ''), // 异步通知地址 'return_url' => env('ALIPAY_RETURN_URL', ''), // 同步通知地址 'sandbox' => env('ALIPAY_SANDBOX', false), // 是否沙箱环境 ], ], // 微信支付配置 'wechat' => [ 'default' => [ // 必填-商户号 'mch_id' => env('WECHAT_PAY_MCH_ID', ''), // 必填-商户API v3密钥 'mch_secret_v3' => env('WECHAT_PAY_SECRET_V3', ''), // 必填-商户私钥 字符串或路径 'mch_secret_key' => env('WECHAT_PAY_SECRET_KEY', ''), // 必填-商户API证书序列号 'mch_certificate_serial' => env('WECHAT_PAY_CERT_SERIAL', ''), // 必填-商户API证书 路径 (`.pem`格式) 'mch_certificate_path' => env('WECHAT_PAY_CERT_PATH', ''), // 必填-微信支付平台证书 路径 (`.pem`格式) 'wechat_public_cert_path' => env('WECHAT_PUBLIC_CERT_PATH', ''), 'mode' => 'normal', // 可选,设置使用证书模式:normal / certificate 'notify_url' => env('WECHAT_PAY_NOTIFY_URL', ''), // 支付通知地址 'app_id' => env('WECHAT_PAY_APP_ID', ''), // APP支付对应的APPID 'sandbox' => env('WECHAT_PAY_SANDBOX', false), // 是否沙箱环境 ], ], ];

这个配置文件将支付宝和微信支付的配置分离,并通过env()函数读取环境变量,这样可以将敏感的密钥信息放在.env文件中,避免硬编码和泄露。你需要根据自己在支付宝开放平台和微信支付商户平台申请的信息来填充.env文件。

实操心得:关于证书路径。支付宝和微信支付(V3)现在都强制要求使用证书。app_secret_certmch_secret_key是你的应用私钥商户私钥的字符串内容或文件路径。而*_public_cert_path*_cert_path是对应平台颁发的公钥证书文件的物理路径。务必分清“私钥”和“公钥证书”。建议将证书文件放在项目根目录的cert目录下(并加入.gitignore),然后在.env中配置类似ALIPAY_APP_PUBLIC_CERT_PATH=/path/to/your/project/cert/appCertPublicKey.crt的绝对路径。

2.3 创建支付服务类

为了业务解耦和代码复用,我们创建一个支付服务类。在app目录下新建service目录(如果不存在),然后创建PayService.php

<?php // app/service/PayService.php declare(strict_types=1); namespace app\service; use Yansongda\Pay\Pay; use think\facade\Config; use think\facade\Log; class PayService { // 支付配置实例缓存 protected static $configInstances = []; /** * 获取支付配置 * @param string $channel 支付渠道:alipay, wechat * @return array */ protected static function getConfig(string $channel): array { if (!isset(self::$configInstances[$channel])) { $config = Config::get('pay'); if ($channel == 'wechat') { // 微信支付配置可能需要根据具体支付场景(APP、小程序等)微调app_id // 这里我们使用config中配置的通用APP支付app_id $channelConfig = $config[$channel]['default'] ?? []; } else { $channelConfig = $config[$channel]['default'] ?? []; } self::$configInstances[$channel] = $channelConfig; } return self::$configInstances[$channel]; } /** * 创建支付宝APP支付订单 * @param array $orderData 订单数据 * @return \Yansongda\Supports\Collection */ public static function alipayApp(array $orderData) { $config = self::getConfig('alipay'); $pay = Pay::alipay($config); $order = [ 'out_trade_no' => $orderData['out_trade_no'], // 商户订单号 'total_amount' => $orderData['total_amount'], // 支付金额,单位元 'subject' => $orderData['subject'], // 订单标题 // APP支付固定传 'app' 'method' => 'alipay.trade.app.pay', ]; // 可选:设置商品详情等扩展参数 if (!empty($orderData['body'])) { $order['body'] = $orderData['body']; } try { $response = $pay->app($order); // $response 是一个 Collection 对象,包含了支付宝返回的支付串等信息 return $response; } catch (\Exception $e) { Log::error('支付宝APP支付下单失败:' . $e->getMessage()); throw $e; } } /** * 创建微信APP支付订单 * @param array $orderData 订单数据 * @return \Yansongda\Supports\Collection */ public static function wechatApp(array $orderData) { $config = self::getConfig('wechat'); $pay = Pay::wechat($config); $order = [ 'out_trade_no' => $orderData['out_trade_no'], // 商户订单号 'amount' => [ 'total' => intval($orderData['total_amount'] * 100), // 微信支付金额单位是分,需要转换 ], 'description' => $orderData['description'], // 商品描述 'notify_url' => $config['notify_url'], // 回调地址,可从配置读取或传入 ]; // APP支付需要指定 $order['appid'] = $config['app_id']; try { $response = $pay->app($order); // 微信APP支付返回的是一个包含 prepay_id 等信息的数组,SDK已处理好 return $response; } catch (\Exception $e) { Log::error('微信APP支付下单失败:' . $e->getMessage()); throw $e; } } /** * 处理支付异步通知(回调) * @param string $channel 支付渠道 * @return \Yansongda\Supports\Collection */ public static function handleNotify(string $channel) { $config = self::getConfig($channel); if ($channel == 'alipay') { $pay = Pay::alipay($config); } else { $pay = Pay::wechat($config); } // 框架的请求对象 $request = request(); try { // 验签并解析通知数据 $data = $pay->callback($request->getContent() ?: $request->post()); // $data 是验签通过后的通知数据集合 return $data; } catch (\Exception $e) { Log::error($channel . '支付回调验签失败:' . $e->getMessage()); // 必须返回失败响应给支付平台 if ($channel == 'alipay') { // 支付宝需要返回 ‘success’ 或 ‘failure’ throw $e; } else { // 微信支付V3需要返回特定的失败JSON throw $e; } } } /** * 发起退款 * @param string $channel 支付渠道 * @param array $refundData 退款数据 * @return \Yansongda\Supports\Collection */ public static function refund(string $channel, array $refundData) { $config = self::getConfig($channel); if ($channel == 'alipay') { $pay = Pay::alipay($config); $order = [ 'out_trade_no' => $refundData['out_trade_no'], // 原商户订单号 'refund_amount' => $refundData['refund_amount'], // 退款金额,单位元 'out_request_no' => $refundData['out_request_no'], // 退款请求号,同一笔交易多次退款需要唯一 ]; } else { $pay = Pay::wechat($config); $order = [ 'out_trade_no' => $refundData['out_trade_no'], // 原商户订单号 'amount' => [ 'refund' => intval($refundData['refund_amount'] * 100), // 退款金额,分 'total' => intval($refundData['total_amount'] * 100), // 原订单金额,分 ], 'out_refund_no' => $refundData['out_refund_no'], // 商户退款单号 ]; } try { $response = $pay->refund($order); return $response; } catch (\Exception $e) { Log::error($channel . '退款失败:' . $e->getMessage()); throw $e; } } }

这个服务类封装了支付下单、回调处理和退款的核心方法。使用静态方法是为了调用方便,你也可以根据项目需要改为依赖注入的单例模式。

3. 支付下单接口实现详解

有了服务类,我们就可以在控制器中创建具体的支付接口了。这里我们创建两个控制器方法,分别对应支付宝APP支付和微信APP支付。

3.1 创建支付控制器与路由

首先,创建一个支付控制器:

php think make:controller Pay

编辑app/controller/Pay.php

<?php // app/controller/Pay.php declare(strict_types=1); namespace app\controller; use app\BaseController; use app\service\PayService; use think\facade\Log; class Pay extends BaseController { /** * 创建支付宝APP支付订单 * @return \think\Response */ public function createAlipayOrder() { // 1. 接收前端传入的订单信息(实际项目中应从数据库或Session获取) $request = request(); $orderSn = $request->param('order_sn', date('YmdHis') . mt_rand(1000, 9999)); $amount = $request->param('amount', 0.01); // 单位元 $subject = $request->param('subject', '测试商品'); // 2. 业务校验(例如检查订单状态、金额等) // ... 这里省略你的业务逻辑校验 ... // 3. 调用支付服务 try { $orderData = [ 'out_trade_no' => $orderSn, 'total_amount' => $amount, 'subject' => $subject, 'body' => $subject . '-详细描述', // 可选 ]; $response = PayService::alipayApp($orderData); // 4. 返回给客户端的数据 // 支付宝APP支付返回的是一个 order_string,客户端SDK需要这个字符串发起支付 $data = [ 'code' => 0, 'msg' => 'success', 'data' => [ 'order_string' => $response->get('order_string'), // 关键支付参数 'out_trade_no' => $orderSn, ] ]; return json($data); } catch (\Exception $e) { Log::error('支付宝下单接口异常:' . $e->getMessage()); return json(['code' => 500, 'msg' => '支付订单创建失败:' . $e->getMessage()]); } } /** * 创建微信APP支付订单 * @return \think\Response */ public function createWechatOrder() { $request = request(); $orderSn = $request->param('order_sn', 'WX' . date('YmdHis') . mt_rand(1000, 9999)); $amount = $request->param('amount', 1); // 单位元 $description = $request->param('description', '测试商品'); // 业务校验... try { $orderData = [ 'out_trade_no' => $orderSn, 'total_amount' => $amount, 'description' => $description, ]; $response = PayService::wechatApp($orderData); // 微信APP支付返回的数据结构已包含APP端调起支付所需的所有参数(如prepay_id等) // SDK通常已处理好,直接返回给客户端即可 $data = [ 'code' => 0, 'msg' => 'success', 'data' => $response->all() // 返回所有参数 ]; return json($data); } catch (\Exception $e) { Log::error('微信下单接口异常:' . $e->getMessage()); return json(['code' => 500, 'msg' => '支付订单创建失败:' . $e->getMessage()]); } } }

然后,我们需要在route/app.php中定义路由,让这些接口可以被访问:

<?php // route/app.php use think\facade\Route; // 支付宝APP支付下单 Route::post('pay/alipay/app', 'Pay/createAlipayOrder'); // 微信APP支付下单 Route::post('pay/wechat/app', 'Pay/createWechatOrder');

3.2 关键参数解析与客户端对接

现在,后端接口已经准备好了。前端(APP)调用这些接口后,会拿到不同的数据,需要用各自平台的SDK来调起支付。

对于支付宝APP支付: 后端返回的order_string是一个完整的、签名后的订单信息字符串。Android和iOS的支付宝SDK都提供了一个方法,直接传入这个字符串即可调起支付。例如,在Android原生开发中,你会这样调用:

// 伪代码 PayTask alipay = new PayTask(activity); String result = alipay.pay(orderString, true);

这个order_string包含了所有必要信息(如商户ID、订单号、金额等),并且已经由我们的后端使用支付宝私钥签好名了,客户端无需再做任何处理。

对于微信APP支付: yansongda/pay SDK在调用app()方法后,返回的数据集合里包含了appid,partnerid,prepayid,package,noncestr,timestamp,sign等字段。这些字段正是微信APP支付调起所需的参数。你需要将这些参数原样返回给APP端。APP端(无论是Android还是iOS)的微信SDK需要这些参数来生成最终的支付请求。

注意事项:微信支付的签名(sign)是由SDK在服务端生成好的。绝对不要在客户端重新计算签名,直接使用服务端返回的sign值。客户端的任务只是把这些参数按微信SDK要求的格式组装并调用。

金额单位的坑: 这是一个非常容易出错的地方。支付宝支付的金额单位是,支持两位小数(例如0.01代表1分钱)。而微信支付的金额单位是,且必须是整数(例如1代表1分钱)。在我们的服务类中,已经做了转换:intval($orderData['total_amount'] * 100)。务必确保传入微信支付接口的amount.total是整数分。

4. 异步通知(回调)处理实战

支付成功后,支付宝和微信支付服务器会主动向我们配置的notify_url发送一个POST请求,通知我们支付结果。这是确保订单状态最终一致性的关键,必须可靠处理。

4.1 创建回调控制器与路由

创建一个专门的控制器来处理回调:

php think make:controller Notify

编辑app/controller/Notify.php

<?php // app/controller/Notify.php declare(strict_types=1); namespace app\controller; use app\BaseController; use app\service\PayService; use think\facade\Db; use think\facade\Log; class Notify extends BaseController { /** * 支付宝支付异步通知 * @return string */ public function alipay() { // 支付宝回调是POST请求,参数在$_POST中,但SDK推荐用getContent() Log::info('收到支付宝异步通知,原始数据:' . file_get_contents('php://input')); try { // 1. 使用PayService处理通知,进行验签并解析数据 $data = PayService::handleNotify('alipay'); // $data 是验签通过后的数据集合 // 2. 获取关键业务参数 $outTradeNo = $data->get('out_trade_no'); // 商户订单号 $tradeNo = $data->get('trade_no'); // 支付宝交易号 $tradeStatus = $data->get('trade_status'); // 交易状态 $totalAmount = $data->get('total_amount'); // 订单金额 Log::info("支付宝回调验签成功,订单号:{$outTradeNo}, 状态:{$tradeStatus}"); // 3. 检查交易状态 if ($tradeStatus == 'TRADE_SUCCESS' || $tradeStatus == 'TRADE_FINISHED') { // 支付成功,处理你的业务逻辑(如更新订单状态、记录支付日志等) $this->updateOrderStatus($outTradeNo, 'paid', $tradeNo, 'alipay', $totalAmount); // 4. 处理完成后,必须返回 'success' 给支付宝,否则支付宝会认为通知失败并重复发送 return 'success'; } else { // 其他状态,如 TRADE_CLOSED(交易关闭),根据业务处理 Log::info("支付宝订单 {$outTradeNo} 状态为 {$tradeStatus},非成功状态"); return 'success'; // 即使非成功状态,也返回success告知支付宝已收到,避免重复通知 } } catch (\Exception $e) { Log::error('支付宝回调处理异常:' . $e->getMessage()); // 验签失败或处理异常,返回 'failure' 告知支付宝 return 'failure'; } } /** * 微信支付异步通知 * @return \think\Response\Json */ public function wechat() { Log::info('收到微信支付异步通知,原始数据:' . file_get_contents('php://input')); try { $data = PayService::handleNotify('wechat'); $outTradeNo = $data->get('out_trade_no'); $transactionId = $data->get('transaction_id'); $tradeState = $data->get('trade_state'); // 微信是 trade_state $totalFee = $data->get('amount.total') / 100; // 转换回元 Log::info("微信支付回调验签成功,订单号:{$outTradeNo}, 状态:{$tradeState}"); if ($tradeState == 'SUCCESS') { $this->updateOrderStatus($outTradeNo, 'paid', $transactionId, 'wechat', $totalFee); // 微信支付V3要求返回特定的JSON响应 return json(['code' => 'SUCCESS', 'message' => '成功']); } else { Log::info("微信支付订单 {$outTradeNo} 状态为 {$tradeState}"); // 即使失败,也要返回成功应答,否则微信会重试 return json(['code' => 'SUCCESS', 'message' => '成功']); } } catch (\Exception $e) { Log::error('微信支付回调处理异常:' . $e->getMessage()); // 微信支付V3失败应答 return json(['code' => 'FAIL', 'message' => $e->getMessage()]); } } /** * 更新订单状态(示例方法,需根据你的业务实现) * @param string $outTradeNo 商户订单号 * @param string $status 状态 * @param string $platformTradeNo 平台交易号 * @param string $channel 支付渠道 * @param float $amount 金额 * @return bool */ protected function updateOrderStatus(string $outTradeNo, string $status, string $platformTradeNo, string $channel, float $amount): bool { // 这里应该使用数据库事务,确保数据一致性 Db::startTrans(); try { // 1. 根据 outTradeNo 查询你的本地订单 // $order = Db::name('order')->where('order_sn', $outTradeNo)->lock(true)->find(); // if (!$order) { // throw new \Exception('订单不存在'); // } // if ($order['status'] == 'paid') { // // 已支付,避免重复处理 // Db::commit(); // return true; // } // 2. 更新订单状态为已支付,记录平台交易号、支付渠道、支付时间等 // $updateData = [ // 'status' => $status, // 'pay_channel' => $channel, // 'platform_trade_no' => $platformTradeNo, // 'pay_time' => date('Y-m-d H:i:s'), // 'updated_at' => date('Y-m-d H:i:s'), // ]; // Db::name('order')->where('id', $order['id'])->update($updateData); // 3. 记录支付日志 // Db::name('pay_log')->insert([...]); // 模拟成功 Log::info("更新订单状态成功:{$outTradeNo} -> {$status}, 平台单号:{$platformTradeNo}"); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); Log::error("更新订单状态失败:{$outTradeNo}, 错误:" . $e->getMessage()); throw $e; // 重新抛出,让上层捕获并返回失败给支付平台 } } }

然后在路由文件中添加回调路由:

// route/app.php // 支付宝支付回调 Route::post('notify/alipay', 'Notify/alipay'); // 微信支付回调 Route::post('notify/wechat', 'Notify/wechat');

4.2 回调处理的注意事项与避坑指南

回调处理是支付系统中最容易出问题也最关键的一环,以下是几个核心注意事项:

  1. 幂等性处理:支付平台可能会因为网络等原因多次发送相同的通知。你的回调逻辑必须保证同一笔订单,即使被通知多次,也只会成功处理一次。在上面的updateOrderStatus方法中,检查订单是否已支付就是幂等性处理。务必在更新订单前加锁(如使用SELECT ... FOR UPDATE或框架的锁机制),防止并发回调导致重复更新。

  2. 验签至关重要PayService::handleNotify内部调用了SDK的callback方法,这个方法会自动验证签名。永远不要跳过验签直接处理业务逻辑,否则可能会被伪造的请求攻击。验签失败必须记录日志并返回失败响应。

  3. 响应格式必须正确

    • 支付宝:处理成功后,必须输出纯文本的success(不能有多余的空格、换行或任何其他字符)。失败则输出failure
    • 微信支付V3:处理成功后,必须返回JSON格式{"code": "SUCCESS", "message": "成功"}。失败则返回{"code": "FAIL", "message": "错误原因"}。返回错误的格式会导致微信不断重发通知(通常最多24小时)。
  4. 日志记录要详尽:在回调入口处,务必记录原始的请求数据(file_get_contents('php://input'))。这是排查问题的第一手资料。同时,记录验签结果、业务处理的关键步骤和结果。

  5. 网络超时与异常处理:你的回调接口应该在收到通知后尽快处理并返回,建议超时时间设置得短一些(如3秒)。如果业务处理非常耗时(比如要调用其他外部服务),应该先返回成功应答给支付平台,然后将实际业务逻辑放入消息队列异步处理,避免因超时导致支付平台认为通知失败。

  6. 确保公网可访问notify_url必须是公网能直接访问的URL,不能是localhost或内网地址。开发测试时可以使用内网穿透工具(如ngrok、花生壳)将本地服务暴露到公网。

5. 退款功能实现与联调

退款流程在业务上通常是手动触发的(例如管理员在后台操作)。其核心也是调用SDK的退款接口。

5.1 创建退款接口

Pay控制器中增加一个退款方法:

// app/controller/Pay.php 追加方法 /** * 统一退款接口 * @return \think\Response */ public function refund() { $request = request(); $channel = $request->param('channel'); // alipay 或 wechat $outTradeNo = $request->param('out_trade_no'); // 原商户订单号 $refundAmount = $request->param('refund_amount'); // 退款金额,单位元 $reason = $request->param('reason', ''); // 退款原因 // 业务校验:检查订单是否存在、是否已支付、退款金额是否合理等 // ... try { // 生成退款单号(同一笔订单多次退款,此号需唯一) $refundSn = 'R' . date('YmdHis') . mt_rand(1000, 9999); $refundData = [ 'out_trade_no' => $outTradeNo, 'refund_amount' => floatval($refundAmount), 'out_request_no' => $refundSn, // 支付宝参数名 'out_refund_no' => $refundSn, // 微信参数名 'refund_reason' => $reason, // 可选 ]; // 对于微信支付,还需要原订单总金额(用于校验) // 这里假设从数据库查询到了原订单金额 // $order = Db::name('order')->where('order_sn', $outTradeNo)->find(); // $refundData['total_amount'] = $order['amount']; $response = PayService::refund($channel, $refundData); // 退款申请成功,更新本地退款单状态为“处理中”或“已提交” // 实际退款结果以异步通知为准 // Db::name('refund_order')->insert([...]); Log::info("{$channel}退款申请提交成功,退款单号:{$refundSn},响应:" . json_encode($response->all())); return json([ 'code' => 0, 'msg' => '退款申请已提交', 'data' => [ 'refund_sn' => $refundSn, 'platform_response' => $response->all(), ] ]); } catch (\Exception $e) { Log::error("{$channel}退款申请失败:{$outTradeNo}, " . $e->getMessage()); return json(['code' => 500, 'msg' => '退款申请失败:' . $e->getMessage()]); } }

添加路由:

// route/app.php // 退款接口 Route::post('pay/refund', 'Pay/refund');

5.2 退款异步通知处理

退款的结果也是通过异步通知告知的。支付宝和微信支付的退款通知,使用的URL可以和支付通知URL相同,也可以单独配置。SDK的callback方法能自动区分是支付通知还是退款通知。

你需要在之前的Notify控制器的alipaywechat方法中,增加对退款通知的处理逻辑。可以通过通知参数中的字段来区分,例如支付宝的refund_fee字段存在通常表示退款通知,微信的refund_status字段。

一个更清晰的实践是,在支付平台商户后台,为退款单独配置一个notify_url,比如https://yourdomain.com/notify/refund/alipay,然后创建一个新的控制器方法来专门处理退款回调。这样逻辑更分离。

退款回调的处理逻辑与支付回调类似:

  1. 验签。
  2. 解析退款结果(成功/失败)。
  3. 更新本地退款单状态(如“退款成功”或“退款失败”)。
  4. 如果是部分退款,还需要考虑更新原订单的剩余可退款金额等状态
  5. 返回正确的应答。

5.3 沙箱环境与联调技巧

在开发阶段,强烈建议使用支付平台提供的沙箱环境

  • 支付宝沙箱:在支付宝开放平台可以很方便地创建沙箱应用和沙箱账号。配置config/pay.php中的sandboxtrue,并使用沙箱环境的app_id和对应的密钥证书。沙箱环境有专门的支付模拟器,可以模拟各种支付场景(成功、失败、输入密码等)。
  • 微信支付沙箱:微信支付的沙箱环境配置稍复杂,需要调用专门的接口获取沙箱签名密钥。yansongda/pay SDK也支持沙箱模式,配置sandboxtrue并可能需要调整部分配置。

联调工具

  • Postman/API Fox:用于测试你的下单、退款接口。
  • 内网穿透工具:用于将本地回调地址暴露给支付平台的沙箱环境。这是测试回调功能的必备工具。
  • 日志:开启config/pay.php中的日志功能,将日志级别设为debug,所有与支付平台的请求和响应细节都会记录下来,是排查问题的利器。

踩坑实录:微信支付证书序列号。微信支付V3要求上传商户API证书,并在请求中携带证书序列号(mch_certificate_serial)。这个序列号不是文件名,而是从证书中解析出来的。你可以使用OpenSSL命令获取:openssl x509 -in apiclient_cert.pem -noout -serial | cut -d'=' -f2 | awk '{print toupper($0)}'。务必确保配置中的序列号与使用的证书匹配,否则会报“证书序列号错误”。

6. 部署上线与安全加固

当Demo测试通过,准备上线时,有几个关键点需要切换和加固。

  1. 切换为生产环境配置

    • .env文件中的*_SANDBOX配置改为false
    • 替换所有密钥和证书为生产环境的正式密钥证书。沙箱和生产的密钥证书绝对不能混用。
    • 再次确认notify_urlreturn_url是正式环境的域名。
  2. 密钥证书安全管理

    • 永远不要将生产环境的私钥和证书上传到Git等版本控制系统。.env文件和cert/目录必须加入.gitignore
    • 在服务器上,将证书文件放在Web根目录之外(如/etc/yourproject/certs/),然后在.env中配置绝对路径。
    • 设置证书文件的权限为600(仅所有者可读可写)。
  3. 监控与告警

    • 监控支付回调接口的访问日志和错误日志。任何验签失败或处理异常都应该触发告警(如发送邮件、钉钉消息)。
    • 监控订单表,对于“已支付”但长时间未收到回调的订单,需要有对账或主动查询的补偿机制。
  4. 对账: 支付系统稳定运行后,每天或定期(如凌晨)需要运行对账作业。调用支付宝和微信支付的“下载账单”接口,获取平台侧的交易流水,与你数据库中的订单流水进行核对,确保每一笔交易状态和金额都一致。这是发现“掉单”(支付成功但回调失败)等问题的最终保障。

通过以上六个部分的拆解,从环境搭建、SDK集成、支付下单、回调处理到退款功能,一个基于ThinkPHP6和yansongda/pay的完整支付Demo就构建完成了。这套方案的优势在于用统一的代码风格处理了双渠道支付,大大降低了开发和维护成本。在实际项目中,你还需要围绕这个核心,构建更健壮的业务订单系统、更完善的日志监控和更强大的对账能力。

http://www.jsqmd.com/news/1159235/

相关文章:

  • 数据 Pipeline(Pandas+Celery):10万条数据处理的自动化流程
  • 3小时用LangChain+RAG+Streamlit搭建本地智能文档助手
  • 沈阳不错的电镀定制厂家:上新 - 品牌推广大师
  • 企业级AI Agent生产实践:从架构设计到部署运维的完整指南
  • 陶艺博客SEO实战:月入3万美金的内容策略与变现模式
  • WordPress迁移实战:从性能瓶颈到现代化技术栈的完整方案
  • 特征值与特征向量:从矩阵变换到空间结构的深度解析(38)
  • SSD系统迁移实战:克隆/重装/激活全链路避坑指南
  • AI Agent开发实战:从零构建大模型应用的核心技能与学习路径
  • BurpSuite保姆级安装配置指南:从Java环境到HTTPS抓包全解析
  • 技术栈一览
  • AI Agent 一句话部署静态页面到云上并分享
  • 2026年洛阳新房装修:婚房装修方案优选 | 情感表达、颜值实用评判标准
  • 亨得利官方名表服务中心|详细地址及服务电话权威信息通告(2026年7月最新) - 亨得利钟表维修中心
  • 当2000个浏览器扩展同时运行,你的安全基线还在靠Excel统计吗?
  • 财报分析实战:3个Python脚本自动计算ROE、毛利率等10项核心财务比率
  • 运放与三极管压控恒流源:5个关键参数选型与PCB布局避坑指南
  • Codex实战指南:从零部署到15种核心应用场景详解
  • AWS S3 预签名URL 安全与权限配置详解:IAM策略与签名时效的5个关键点
  • Unity URP卡通渲染管线全攻略:从色阶化光照到描边实现
  • 亨得利官方名表服务中心|详细地址与24小时客服电话权威信息声明(2026年7月最新) - 亨得利钟表维修中心
  • FreeRTOS 11.3.0 在 Keil 中编译报错 L6235E:2 种重复 Startup 文件的根因与修复
  • 匿名通信协议 V7 与 V8 差异解析:从 2 种校验算法到代码移植要点
  • 2026年7月偏高岭土品牌推荐,白刚玉/氧化铝粉/白糊精/型煤球团粘合剂/磷酸二氢铝/氧化铝空心球,偏高岭土品牌怎么选择 - 品牌推荐师
  • RBAC与MAC访问控制对比:3种模型在Linux/Windows中的实现差异
  • 基于51单片机太阳能路灯台灯无线蓝牙物联网光照控制设计DIY122
  • ANSYS 2025 R2安装实战:跨系统集成与许可证深度配置指南
  • Gemma 4B手机端部署实战:ARM量化+JNI集成全链路教程
  • 中国地址生成器终极指南:告别手动填写的烦恼
  • STM32 HAL库与标准库DMA串口对比:3种printf重定向方案性能实测