ThinkPHP框架接入Stripe支付
我的框架是ThinkPHP5.0,后台是fastadmin
1.安装composer包,包名是 stripe/stripe-php
2.下单代码
<?php
namespace app\common\service;
use app\common\controller\Api;
use Stripe\Stripe;
use Stripe\Checkout\Session;
use think\Config;class StripeService extends Api {protected $noNeedLogin = ['*'];protected $noNeedRight = ['*'];/*** 创建支付会话(APP调用)* @param string $orderNo 你的业务订单号* @param float $amount 金额(英镑)* @return array*/public function createPayment($orderNo, $amount,$type) {Stripe::setApiKey(Config::get('stripe.secret_key'));try {$paymentIntent = \Stripe\PaymentIntent::create(['amount' => $amount * 100,'currency' => 'gbp','payment_method_types' => ['card'], // 指定卡支付方式,包含Apple Pay'metadata' => ['order_no' => $orderNo,'business_type' => $type]]);return ['paymentIntent' => $paymentIntent->client_secret, // 核心参数'merchantName' => Config::get('stripe.merchantName'),'publishKey'=>Config::get('stripe.publishKey')];} catch (\Exception $e) {throw new \Exception("支付创建失败: " . $e->getMessage());}}/*** 创建H5支付会话(网页端调用)* @param string $orderNo 业务订单号* @param float $amount 金额(英镑)* @param string $type 业务类型* @return array*/public function createH5Payment($orderNo, $amount, $type) {Stripe::setApiKey(Config::get('stripe.secret_key'));try {$session = Session::create(['payment_method_types' => ['card'],'line_items' => [['price_data' => ['currency' => 'gbp','product_data' => ['name' => 'Order ' . $orderNo, // 可根据业务需求修改商品名称],'unit_amount' => $amount * 100, // 转换为分],'quantity' => 1,]],'mode' => 'payment','success_url' => request()->domain().'/api/notify/activity_stripe?session_id={CHECKOUT_SESSION_ID}','cancel_url' => request()->domain().'/api/notify/activity_cancel_stripe?session_id={CHECKOUT_SESSION_ID}','metadata' => ['order_no' => $orderNo,'business_type' => $type]]);return ['sessionId' => $session->id,'redirectUrl' => $session->url, // 前端需跳转到这个URL完成支付'publishKey' => Config::get('stripe.publishKey')];} catch (\Exception $e) {throw new \Exception('H5支付创建失败: ' . $e->getMessage());}}
}
2.处理webhook异步通知修改订单的支付状态。需要去Stripe后台设置接收webhook通知的url,Stripe的通知是所有的事件通过一个接口通知,所以在处理的时候要判断事件类型.
<?phpnamespace app\api\controller;use app\common\controller\Api;use app\common\model\ActivityOrder;
use app\common\model\GroupOrder;
use app\common\model\GroupSettle;
use think\Config;
use think\Log;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;
class Stripenotify extends Api
{protected $noNeedLogin = '*';protected $noNeedRight = '*';public function handle() {$payload = file_get_contents('php://input');$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';try {// 验证事件签名$event = Webhook::constructEvent($payload,$sigHeader,Config::get('stripe.webhook_secret'));// 只处理checkout.session.completed事件if ($event->type !== 'checkout.session.completed') {Log::info("忽略非支付完成事件: " . $event->type);return json(['code' => 200, 'msg' => 'Event ignored']);}$session = $event->data->object;Log::info('____Stripe Session Data: ' . json_encode($session));// 提取业务类型$businessType = $session->metadata['business_type'] ?? 'unknown';Log::info("业务类型识别: " . $businessType);switch ($businessType) {case 'activity':$this->handleActivityPayment($session);break;case 'group':$this->handleGroupPayment($session);break;case 'settle':$this->handleSettlePayment($session);break;default:Log::warning("未知业务类型: " . $businessType);return json(['code' => 400, 'msg' => 'Unknown business type']);}return json(['code' => 200, 'msg' => '处理成功']);} catch (SignatureVerificationException $e) {Log::error("Webhook签名验证失败: " . $e->getMessage());return json(['code' => 403, 'msg' => 'Invalid signature'], 403);} catch (\Exception $e) {Log::error("Webhook处理异常: " . $e->getMessage());return json(['code' => 500, 'msg' => 'Server error'], 500);}}/*** 处理活动订单支付* @param \Stripe\Checkout\Session $session*/protected function handleActivityPayment($session) {try {// 1. 提取关键数据$metadata = $session->metadata;$orderNo = $metadata['order_no'] ?? null;$paidAmount = $session->amount_total / 100; // GBP$paymentId = $session->payment_intent;Log::info("开始处理活动订单 | 订单号: {$orderNo} | 支付金额: {$paidAmount}");// 2. 基础验证if (empty($orderNo)) {throw new \Exception("订单号缺失");}// 3. 获取订单数据$order = ActivityOrder::where('order_num', $orderNo)->find();if (!$order) {throw new \Exception("订单不存在: {$orderNo}");}// 4. 金额验证(防止篡改)
// if (bccomp($paidAmount, $order->total_price, 2) !== 0) {
// throw new \Exception("金额不一致 | 订单: {$orderNo} | 支付金额: {$paidAmount} | 订单金额: {$order->total_price}");
// }// 5. 状态验证(防止重复处理)if ($order->status == 2) {Log::info("订单已支付,跳过处理: {$orderNo}");return;}// 6. 开启事务$order->startTrans();// 7. 更新订单状态$updateData = ['status' => '2', // 假设2表示已支付'pay_order_num' => $paymentId,'pay_time' => time(),'pay_way' => '7' // 支付方式标识];if (!$order->save($updateData)) {throw new \Exception("订单状态更新失败: {$orderNo}");}// 8. 提交事务$order->commit();Log::info("活动订单支付成功 | 订单号: {$orderNo}");} catch (\Exception $e) {// 回滚事务isset($order) && $order->rollback();Log::error("活动订单处理失败 | 订单号: {$orderNo} | 错误: " . $e->getMessage());throw $e; // 向上抛出以触发500响应}}/*** 团购订单逻辑* @param $session* @return void*/protected function handleGroupPayment($session){try {// 1. 提取关键数据$metadata = $session->metadata;$orderNo = $metadata['order_no'] ?? null;$paidAmount = $session->amount_total / 100; // GBP$paymentId = $session->payment_intent;Log::info("开始处理团购订单 | 订单号: {$orderNo} | 支付金额: {$paidAmount}");// 2. 基础验证if (empty($orderNo)) {throw new \Exception("团购订单号缺失");}// 3. 获取订单数据$order = GroupOrder::where('order_num', $orderNo)->find();if (!$order) {throw new \Exception("团购订单不存在: {$orderNo}");}// 4. 金额验证(防止篡改)
// if (bccomp($paidAmount, $order->total_price, 2) !== 0) {
// throw new \Exception("金额不一致 | 订单: {$orderNo} | 支付金额: {$paidAmount} | 订单金额: {$order->total_price}");
// }// 5. 状态验证(防止重复处理)if ($order->status == 2) {Log::info("团购订单已支付,跳过处理: {$orderNo}");return;}// 6. 开启事务$order->startTrans();// 7. 更新订单状态$updateData = ['status' => '2', // 假设2表示已支付'pay_order_num' => $paymentId,'pay_time' => time(),'pay_way' => '7' // 支付方式标识];if (!$order->save($updateData)) {throw new \Exception("团购订单状态更新失败: {$orderNo}");}// 8. 提交事务$order->commit();Log::info("团购订单支付成功 | 订单号: {$orderNo}");} catch (\Exception $e) {// 回滚事务isset($order) && $order->rollback();Log::error("团购订单处理失败 | 订单号: {$orderNo} | 错误: " . $e->getMessage());throw $e; // 向上抛出以触发500响应}}/*** 团购订单逻辑* @param $session* @return void*/protected function handleSettlePayment($session){try {// 1. 提取关键数据$metadata = $session->metadata;$orderNo = $metadata['order_no'] ?? null;$paidAmount = $session->amount_total / 100; // GBP$paymentId = $session->payment_intent;Log::info("开始处理线下买单订单 | 订单号: {$orderNo} | 支付金额: {$paidAmount}");// 2. 基础验证if (empty($orderNo)) {throw new \Exception("线下买单订单号缺失");}// 3. 获取订单数据$order = GroupSettle::where('order_num', $orderNo)->find();if (!$order) {throw new \Exception("线下买单不存在: {$orderNo}");}// 4. 金额验证(防止篡改)
// if (bccomp($paidAmount, $order->total_price, 2) !== 0) {
// throw new \Exception("金额不一致 | 订单: {$orderNo} | 支付金额: {$paidAmount} | 订单金额: {$order->total_price}");
// }// 5. 状态验证(防止重复处理)if ($order->status == 2) {Log::info("线下买单已支付,跳过处理: {$orderNo}");return;}// 6. 开启事务$order->startTrans();// 7. 更新订单状态$updateData = ['status' => '2', // 假设2表示已支付'pay_order_num' => $paymentId,'pay_time' => time(),'pay_way' => '7' // 支付方式标识];if (!$order->save($updateData)) {throw new \Exception("订单状态更新失败: {$orderNo}");}// 8. 提交事务$order->commit();Log::info("线下买单订单支付成功 | 订单号: {$orderNo}");} catch (\Exception $e) {// 回滚事务isset($order) && $order->rollback();Log::error("线下买单订单处理失败 | 订单号: {$orderNo} | 错误: " . $e->getMessage());throw $e; // 向上抛出以触发500响应}}
}
3.相关配置文件。建议测试的时候使用测试模式,换成测试的密钥,用测试银行卡模拟付款。
<?php
return ['secret_key' => 'sk_test_51QTOQRGaWuPD3************IMFFHEyAzCFdPRlgUG007tpHxjwo', // 替换为你的测试密钥'webhook_secret' => 'whsec_9m***********md9', // Webhook 密钥'currency' => 'gbp', // 默认货币 英镑'merchantName' => 'g***********y', // 商户号'publishKey'=>'pk_test_51Q**************KpiYR'
];
4.调试程序的时候可以通过后台的控制台手动发布webhook推送