Files
wordpress-theme-aksrt/inc/classes/payment/gateways/class-aksrt-stripe-gateway.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

290 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 {
if (class_exists('AKSRT_Settings_Payment_Runtime')
&& !AKSRT_Settings_Payment_Runtime::is_gateway_enabled('stripe')) {
return false;
}
$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 {
if (!class_exists('AKSRT_Settings')) {
return [];
}
$all = AKSRT_Settings::get_all();
return [
'secret_key' => (string) ($all['stripe_secret_key'] ?? ''),
'publishable_key' => (string) ($all['stripe_publishable_key'] ?? ''),
'webhook_secret' => (string) ($all['stripe_webhook_secret'] ?? ''),
'currency' => (string) ($all['stripe_currency'] ?? 'usd'),
'mode' => !empty($all['stripe_test_mode']) ? 'test' : 'live',
];
}
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);
}
}