|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* @remark :
|
|
|
|
* @name :PayStripeApi.php
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:35
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace App\Helper;
|
|
|
|
|
|
|
|
use App\Models\ExtentModule\ExtensionModuleValue;
|
|
|
|
|
|
|
|
class PayStripeApi
|
|
|
|
{
|
|
|
|
private $secretKey;
|
|
|
|
//币种对应支付方式
|
|
|
|
public $currency_types = [
|
|
|
|
'usd' => ['card', 'alipay', 'wechat_pay', 'cashapp', 'link', 'afterpay_clearpay'],
|
|
|
|
'eur' => ['card', 'ideal', 'giropay', 'sofort', 'bancontact', 'klarna', 'link'],
|
|
|
|
'gbp' => ['card', 'apple_pay', 'google_pay', 'klarna', 'link', 'afterpay_clearpay'],
|
|
|
|
'aud' => ['card', 'afterpay_clearpay', 'apple_pay', 'google_pay'],
|
|
|
|
'cad' => ['card', 'apple_pay', 'google_pay', 'link'],
|
|
|
|
'sgd' => ['card', 'grabpay', 'fpx', 'wechat_pay', 'apple_pay', 'google_pay'],
|
|
|
|
'jpy' => ['card', 'apple_pay', 'google_pay'],
|
|
|
|
'cny' => ['alipay', 'wechat_pay'],
|
|
|
|
'brl' => ['card', 'boleto', 'pix'],
|
|
|
|
'mxn' => ['card', 'oxxo'],
|
|
|
|
'inr' => ['card', 'upi', 'netbanking'],
|
|
|
|
'php' => ['card', 'paymaya', 'gcash'],
|
|
|
|
'myr' => ['card', 'fpx'],
|
|
|
|
'thb' => ['card', 'promptpay'],
|
|
|
|
'idr' => ['card', 'bank_transfer'],
|
|
|
|
'zar' => ['card'],
|
|
|
|
'ngn' => ['card'],
|
|
|
|
'aed' => ['card', 'apple_pay', 'google_pay']
|
|
|
|
];
|
|
|
|
|
|
|
|
// 构造函数设置密钥
|
|
|
|
public function __construct()
|
|
|
|
{
|
|
|
|
$this->secretKey = 'sk_test_51MyseXIWCYVeLww1tbPZzRe1Qk4lS5d2VLiDjpju7G0ToiX1RJcFinQXNlftq9eCjZE0n2gjaz1mfy1g0mxTusdf00m636Gv62';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :通用的 cURL 请求方法
|
|
|
|
* @name :sendRequest
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:38
|
|
|
|
*/
|
|
|
|
private function sendRequest($url, $method = 'POST', $data = [])
|
|
|
|
{
|
|
|
|
$ch = curl_init();
|
|
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
|
|
"Authorization: Bearer {$this->secretKey}",
|
|
|
|
"Content-Type: application/x-www-form-urlencoded"
|
|
|
|
]);
|
|
|
|
if ($method === 'POST') {
|
|
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
|
|
|
} elseif ($method === 'GET') {
|
|
|
|
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
|
|
|
} elseif ($method === 'DELETE') {
|
|
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
|
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if (curl_errno($ch)) {
|
|
|
|
throw new Exception('cURL Error: ' . curl_error($ch));
|
|
|
|
}
|
|
|
|
curl_close($ch);
|
|
|
|
return json_decode($response, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :创建支付意图
|
|
|
|
* @name :createPaymentIntent
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:38
|
|
|
|
*/
|
|
|
|
public function createPaymentIntent($amount, $currency = 'usd', $paymentMethodTypes = 'card')
|
|
|
|
{
|
|
|
|
$url = "https://api.stripe.com/v1/payment_intents";
|
|
|
|
$data = [
|
|
|
|
'amount' => $amount,
|
|
|
|
'currency' => $currency,
|
|
|
|
'payment_method_types[]' => $paymentMethodTypes,
|
|
|
|
];
|
|
|
|
return $this->sendRequest($url, 'POST', $data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :查询支付意图
|
|
|
|
* @name :retrievePaymentIntent
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:38
|
|
|
|
*/
|
|
|
|
public function retrievePaymentIntent($paymentIntentId)
|
|
|
|
{
|
|
|
|
$url = "https://api.stripe.com/v1/payment_intents/{$paymentIntentId}";
|
|
|
|
return $this->sendRequest($url, 'GET');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :创建退款
|
|
|
|
* @name :createRefund
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:42
|
|
|
|
*/
|
|
|
|
public function createRefund($chargeId, $amount = null)
|
|
|
|
{
|
|
|
|
$url = "https://api.stripe.com/v1/refunds";
|
|
|
|
$data = ['charge' => $chargeId];
|
|
|
|
if ($amount) {
|
|
|
|
$data['amount'] = $amount;
|
|
|
|
}
|
|
|
|
return $this->sendRequest($url, 'POST', $data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :查询退款
|
|
|
|
* @name :retrieveRefund
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:42
|
|
|
|
*/
|
|
|
|
public function retrieveRefund($refundId)
|
|
|
|
{
|
|
|
|
$url = "https://api.stripe.com/v1/refunds/{$refundId}";
|
|
|
|
return $this->sendRequest($url, 'GET');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :处理 Webhook
|
|
|
|
* @name :handleWebhook
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 10:43
|
|
|
|
*/
|
|
|
|
public static function handleWebhook()
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
// Webhook 签名密钥(从 Stripe 仪表盘获取)
|
|
|
|
$endpointSecret = 'whsec_garhW2TrCIrduyM3rve9mFS2sn69B9Yt';
|
|
|
|
// 获取原始请求内容
|
|
|
|
$payload = request()->getContent();
|
|
|
|
// 获取 Stripe 签名头
|
|
|
|
$sigHeader = request()->header('Stripe-Signature');
|
|
|
|
// 验证签名
|
|
|
|
if (!self::verifySignature($payload, $sigHeader, $endpointSecret)) {
|
|
|
|
return [
|
|
|
|
'code' => '201',
|
|
|
|
'message' => 'Invalid signature',
|
|
|
|
'details' => [
|
|
|
|
'payload' => $payload,
|
|
|
|
'sigHeader' => $sigHeader,
|
|
|
|
'endpointSecret' => $endpointSecret,
|
|
|
|
],
|
|
|
|
]; // 返回 400 Bad Request 状态码;
|
|
|
|
}
|
|
|
|
$event = json_decode($payload, true);
|
|
|
|
// 获取事件类型
|
|
|
|
$eventType = $event['type'];
|
|
|
|
$eventData = $event['data']['object'];
|
|
|
|
// 根据事件类型处理
|
|
|
|
switch ($eventType) {
|
|
|
|
case 'payment_intent.succeeded':
|
|
|
|
// 处理支付成功逻辑
|
|
|
|
$paymentIntentId = $eventData['id'];
|
|
|
|
self::getExtensionInfo($paymentIntentId,$eventData);
|
|
|
|
break;
|
|
|
|
case 'payment_intent.payment_failed':
|
|
|
|
// 处理支付失败逻辑
|
|
|
|
@file_put_contents(storage_path('logs/lyh_error.log'), var_export($eventData, true) . PHP_EOL, FILE_APPEND);
|
|
|
|
$error = $eventData['last_payment_error'];
|
|
|
|
break;
|
|
|
|
case 'charge.refunded':
|
|
|
|
// 处理退款逻辑
|
|
|
|
$chargeId = $eventData['id'];
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new \Exception('Unhandled event type: ' . $eventType);
|
|
|
|
}
|
|
|
|
return $event;
|
|
|
|
} catch (Exception $e) {
|
|
|
|
throw new \Exception('Webhook Error: ' . $e->getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :根据id获取当前数据详情
|
|
|
|
* @name :getExtensionInfo
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/25 14:43
|
|
|
|
*/
|
|
|
|
public static function getExtensionInfo($id,$eventData){
|
|
|
|
$extensionModel = new ExtensionModuleValue();
|
|
|
|
$info = $extensionModel->read(['value'=>$id]);
|
|
|
|
if($info === false){
|
|
|
|
@file_put_contents(storage_path('logs/lyh_3059_error.log'), var_export($id.':当前数据错误', true) . PHP_EOL, FILE_APPEND);
|
|
|
|
}
|
|
|
|
//组装数据保存
|
|
|
|
$data = [
|
|
|
|
['uuid'=>$info['uuid'],'module_id'=>$info['module_id'],'field_id'=>6,'value'=>'success'],
|
|
|
|
['uuid'=>$info['uuid'],'module_id'=>$info['module_id'],'field_id'=>7,'value'=>json_encode($eventData)],
|
|
|
|
];
|
|
|
|
$moduleValueModel = new ExtensionModuleValue();
|
|
|
|
$moduleValueModel->insertAll($data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @remark :验证签名
|
|
|
|
* @name :verifySignature
|
|
|
|
* @author :lyh
|
|
|
|
* @method :post
|
|
|
|
* @time :2024/12/24 15:55
|
|
|
|
*/
|
|
|
|
public static function verifySignature($payload, $sigHeader, $endpointSecret)
|
|
|
|
{
|
|
|
|
// 解析 Signature Header,获取 timestamp 和签名
|
|
|
|
if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', $sigHeader, $matches)) {
|
|
|
|
return false; // 签名格式错误
|
|
|
|
}
|
|
|
|
$timestamp = $matches[1]; // 提取时间戳
|
|
|
|
$receivedSignature = $matches[2]; // 提取签名
|
|
|
|
// 防止重放攻击:检查时间戳是否在 5 分钟以内
|
|
|
|
$currentTimestamp = time();
|
|
|
|
if (abs($currentTimestamp - $timestamp) > 300) {
|
|
|
|
return false; // 签名过期
|
|
|
|
}
|
|
|
|
// 计算预期签名
|
|
|
|
$signedPayload = "{$timestamp}.{$payload}";
|
|
|
|
$expectedSignature = hash_hmac('sha256', $signedPayload, $endpointSecret);
|
|
|
|
// 比较签名是否匹配
|
|
|
|
return hash_equals($expectedSignature, $receivedSignature);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} |
...
|
...
|
|