291 lines
10 KiB
PHP
291 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Stripe Gateway — Stripe 支付网关
|
|
*
|
|
* 使用 stripe/stripe-php 实现 Checkout Session 支付、Webhook 回调、退款。
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Stripe_Gateway implements AKSRT_Payment_Gateway_Interface {
|
|
|
|
public function get_id(): string {
|
|
return 'stripe';
|
|
}
|
|
|
|
public function get_name(): string {
|
|
return __('Stripe', 'aksrt');
|
|
}
|
|
|
|
public function is_configured(): bool {
|
|
$config = $this->get_config();
|
|
return !empty($config['secret_key']) && !empty($config['publishable_key']);
|
|
}
|
|
|
|
public function create_payment(array $order): array {
|
|
if (!$this->is_configured()) {
|
|
return ['success' => false, 'message' => __('Stripe 未配置', 'aksrt')];
|
|
}
|
|
|
|
$config = $this->get_config();
|
|
$order_number = $order['order_number'] ?? '';
|
|
$amount = (float) ($order['amount'] ?? 0);
|
|
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
|
|
$currency = strtolower($config['currency'] ?? 'usd');
|
|
|
|
try {
|
|
$stripe = $this->create_client($config);
|
|
|
|
// 创建 Checkout Session
|
|
$session_params = [
|
|
'payment_method_types' => $this->get_payment_methods($config),
|
|
'line_items' => [[
|
|
'price_data' => [
|
|
'currency' => $currency,
|
|
'product_data' => [
|
|
'name' => $subject,
|
|
],
|
|
'unit_amount' => $this->to_smallest_unit($amount, $currency),
|
|
],
|
|
'quantity' => 1,
|
|
]],
|
|
'mode' => 'payment',
|
|
'success_url' => home_url('/wp-json/aksrt/v1/payment/stripe/return?session_id={CHECKOUT_SESSION_ID}&order=' . $order_number),
|
|
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/stripe/cancel?order=' . $order_number),
|
|
'metadata' => [
|
|
'order_number' => $order_number,
|
|
],
|
|
];
|
|
|
|
// 启用自动支付方式(Apple Pay, Google Pay 等)
|
|
if (!empty($config['auto_payment_methods'])) {
|
|
$session_params['automatic_payment_methods'] = [
|
|
'enabled' => true,
|
|
];
|
|
}
|
|
|
|
$session = \Stripe\Checkout\Session::create($session_params, [
|
|
'api_key' => $config['secret_key'],
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'payment_url' => $session->url,
|
|
'payment_data' => [
|
|
'order_number' => $order_number,
|
|
'session_id' => $session->id,
|
|
],
|
|
'message' => __('Stripe 支付页面已创建', 'aksrt'),
|
|
];
|
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
} catch (\Exception $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
public function verify_callback(WP_REST_Request $request): array {
|
|
$config = $this->get_config();
|
|
|
|
try {
|
|
$stripe = $this->create_client($config);
|
|
|
|
$payload = $request->get_body();
|
|
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
|
|
|
|
// 验证 Webhook 签名
|
|
$event = \Stripe\Webhook::constructEvent(
|
|
$payload,
|
|
$sigHeader,
|
|
$config['webhook_secret'] ?? ''
|
|
);
|
|
|
|
if ($event->type !== 'checkout.session.completed') {
|
|
return ['success' => false, 'message' => __('非支付完成事件', 'aksrt')];
|
|
}
|
|
|
|
$session = $event->data->object;
|
|
$orderNumber = $session->metadata->order_number ?? '';
|
|
$amount = ($session->amount_total ?? 0) / 100;
|
|
|
|
return [
|
|
'success' => true,
|
|
'order_number' => $orderNumber,
|
|
'amount' => $amount,
|
|
'transaction_id' => $session->payment_intent ?? $session->id,
|
|
'status' => $session->payment_status === 'paid' ? 'paid' : 'pending',
|
|
];
|
|
} catch (\Stripe\Exception\SignatureVerificationException $e) {
|
|
return ['success' => false, 'message' => __('Webhook 签名验证失败', 'aksrt')];
|
|
} catch (\Exception $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
public function query_status(string $order_number): array {
|
|
if (!$this->is_configured()) {
|
|
return ['success' => false, 'message' => __('Stripe 未配置', 'aksrt')];
|
|
}
|
|
|
|
$config = $this->get_config();
|
|
|
|
try {
|
|
$stripe = $this->create_client($config);
|
|
|
|
// 注意:Stripe API 的 Session::all() 不支持按 metadata 过滤。
|
|
// 此处通过创建时间倒序获取最近的 session,再匹配 metadata。
|
|
// 生产环境建议在 create_payment 时保存 session_id 并用 Session::retrieve() 查询。
|
|
$sessions = \Stripe\Checkout\Session::all([
|
|
'limit' => 100,
|
|
], ['api_key' => $config['secret_key']]);
|
|
|
|
// 手动按 metadata 匹配
|
|
$found = null;
|
|
foreach ($sessions->data as $session) {
|
|
if (($session->metadata->order_number ?? '') === $order_number) {
|
|
$found = $session;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($found) {
|
|
$status = 'pending';
|
|
if ($found->payment_status === 'paid') {
|
|
$status = 'paid';
|
|
} elseif ($found->status === 'expired') {
|
|
$status = 'cancelled';
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'status' => $status,
|
|
'transaction_id' => $found->payment_intent ?? '',
|
|
];
|
|
}
|
|
|
|
return ['success' => true, 'status' => 'pending', 'message' => __('未找到支付记录', 'aksrt')];
|
|
} catch (\Exception $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
public function refund(string $order_number, float $amount): array {
|
|
if (!$this->is_configured()) {
|
|
return ['success' => false, 'message' => __('Stripe 未配置', 'aksrt')];
|
|
}
|
|
|
|
$config = $this->get_config();
|
|
|
|
try {
|
|
$stripe = $this->create_client($config);
|
|
|
|
// 先查询 payment_intent(手动按 metadata 匹配)
|
|
$sessions = \Stripe\Checkout\Session::all([
|
|
'limit' => 100,
|
|
], ['api_key' => $config['secret_key']]);
|
|
|
|
$found = null;
|
|
foreach ($sessions->data as $session) {
|
|
if (($session->metadata->order_number ?? '') === $order_number) {
|
|
$found = $session;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
return ['success' => false, 'message' => __('未找到支付记录', 'aksrt')];
|
|
}
|
|
|
|
$paymentIntentId = $found->payment_intent;
|
|
|
|
if (empty($paymentIntentId)) {
|
|
return ['success' => false, 'message' => __('未找到支付意图', 'aksrt')];
|
|
}
|
|
|
|
$currency = strtolower($config['currency'] ?? 'usd');
|
|
|
|
$refund = \Stripe\Refund::create([
|
|
'payment_intent' => $paymentIntentId,
|
|
'amount' => $this->to_smallest_unit($amount, $currency),
|
|
'reason' => 'requested_by_customer',
|
|
], ['api_key' => $config['secret_key']]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'refund_id' => $refund->id,
|
|
'message' => __('退款成功', 'aksrt'),
|
|
];
|
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
} catch (\Exception $e) {
|
|
return ['success' => false, 'message' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取 Stripe Publishable Key(前端使用)。
|
|
*/
|
|
public function get_publishable_key(): string {
|
|
$config = $this->get_config();
|
|
return $config['publishable_key'] ?? '';
|
|
}
|
|
|
|
private function get_config(): array {
|
|
// 优先从统一设置读取(schema 保存的 key)
|
|
if (class_exists('AKSRT_Settings')) {
|
|
$all = AKSRT_Settings::get_all();
|
|
if (!empty($all['stripe_secret_key'])) {
|
|
return [
|
|
'secret_key' => $all['stripe_secret_key'] ?? '',
|
|
'publishable_key' => $all['stripe_publishable_key'] ?? '',
|
|
'webhook_secret' => $all['stripe_webhook_secret'] ?? '',
|
|
'currency' => $all['stripe_currency'] ?? 'usd',
|
|
'mode' => !empty($all['stripe_test_mode']) ? 'test' : 'live',
|
|
];
|
|
}
|
|
}
|
|
// 回退到独立 option
|
|
$settings = get_option('aksrt_payment_stripe', []);
|
|
return is_array($settings) ? $settings : [];
|
|
}
|
|
|
|
private function create_client(array $config): void {
|
|
\Stripe\Stripe::setApiKey($config['secret_key']);
|
|
|
|
// 设置 API 版本
|
|
\Stripe\Stripe::setApiVersion('2023-10-16');
|
|
|
|
// 启用遥测(可选)
|
|
if (!empty($config['enable_telemetry'])) {
|
|
\Stripe\Stripe::setEnableTelemetry(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取支持的支付方式。
|
|
*/
|
|
private function get_payment_methods(array $config): array {
|
|
$methods = $config['payment_methods'] ?? ['card'];
|
|
|
|
// 验证支付方式
|
|
$valid = ['card', 'alipay', 'wechat_pay', 'sepa_debit', 'ideal', 'bancontact', 'giropay', 'sofort'];
|
|
return array_values(array_intersect($methods, $valid)) ?: ['card'];
|
|
}
|
|
|
|
/**
|
|
* 将金额转换为最小单位(分/cent)。
|
|
*/
|
|
private function to_smallest_unit(float $amount, string $currency): int {
|
|
// 零小数货币(日元、韩元等)
|
|
$zeroDecimal = ['bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'];
|
|
|
|
if (in_array(strtolower($currency), $zeroDecimal, true)) {
|
|
return (int) $amount;
|
|
}
|
|
|
|
return (int) round($amount * 100);
|
|
}
|
|
}
|