Drop dual-read payment runtime fallbacks, align PHP schema to Vue keys, read gateway credentials only from aksrt_settings with enable toggles, expose configured adapters at checkout, fix credits pay signatures, and apply order expiry settings. Wire orphan order/card-pass/withdrawal schema pages; delete unused Payment* wrappers.
252 lines
9.0 KiB
PHP
252 lines
9.0 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT PayPal Gateway — PayPal 支付网关
|
||
*
|
||
* 使用 WordPress HTTP API 直接调用 PayPal REST API,无需第三方 SDK。
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class AKSRT_PayPal_Gateway implements AKSRT_Payment_Gateway_Interface {
|
||
|
||
public function get_id(): string {
|
||
return 'paypal';
|
||
}
|
||
|
||
public function get_name(): string {
|
||
return __('PayPal', 'aksrt');
|
||
}
|
||
|
||
public function is_configured(): bool {
|
||
if (class_exists('AKSRT_Settings_Payment_Runtime')
|
||
&& !AKSRT_Settings_Payment_Runtime::is_gateway_enabled('paypal')) {
|
||
return false;
|
||
}
|
||
$config = $this->get_config();
|
||
return !empty($config['client_id']) && !empty($config['client_secret']);
|
||
}
|
||
|
||
public function create_payment(array $order): array {
|
||
if (!$this->is_configured()) {
|
||
return ['success' => false, 'message' => __('PayPal 未配置', 'aksrt')];
|
||
}
|
||
|
||
$config = $this->get_config();
|
||
$order_number = $order['order_number'] ?? '';
|
||
$amount = (float) ($order['amount'] ?? 0);
|
||
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
|
||
$currency = $config['currency'] ?? 'USD';
|
||
|
||
try {
|
||
$response = $this->api_request('POST', '/v2/checkout/orders', [
|
||
'intent' => 'CAPTURE',
|
||
'purchase_units' => [[
|
||
'reference_id' => $order_number,
|
||
'description' => $subject,
|
||
'amount' => [
|
||
'currency_code' => $currency,
|
||
'value' => number_format($amount, 2, '.', ''),
|
||
],
|
||
]],
|
||
'application_context' => [
|
||
'return_url' => home_url('/wp-json/aksrt/v1/payment/paypal/return?order=' . $order_number),
|
||
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/paypal/cancel?order=' . $order_number),
|
||
'brand_name' => get_bloginfo('name'),
|
||
'user_action' => 'PAY_NOW',
|
||
],
|
||
]);
|
||
|
||
if ($response['status'] === 201) {
|
||
$data = $response['body'];
|
||
$approveUrl = '';
|
||
foreach ($data['links'] ?? [] as $link) {
|
||
if ($link['rel'] === 'approve') {
|
||
$approveUrl = $link['href'];
|
||
break;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'payment_url' => $approveUrl,
|
||
'payment_data' => [
|
||
'order_number' => $order_number,
|
||
'paypal_id' => $data['id'] ?? '',
|
||
],
|
||
'message' => __('PayPal 订单已创建', 'aksrt'),
|
||
];
|
||
}
|
||
|
||
return ['success' => false, 'message' => $response['body']['message'] ?? __('PayPal 创建失败', 'aksrt')];
|
||
} catch (\Exception $e) {
|
||
return ['success' => false, 'message' => $e->getMessage()];
|
||
}
|
||
}
|
||
|
||
public function verify_callback(WP_REST_Request $request): array {
|
||
try {
|
||
$body = json_decode($request->get_body(), true);
|
||
$eventType = $body['event_type'] ?? '';
|
||
|
||
if ($eventType !== 'CHECKOUT.ORDER.APPROVED') {
|
||
return ['success' => false, 'message' => __('非支付完成事件', 'aksrt')];
|
||
}
|
||
|
||
$resource = $body['resource'] ?? [];
|
||
$orderNumber = '';
|
||
$amount = 0;
|
||
|
||
foreach ($resource['purchase_units'] ?? [] as $unit) {
|
||
$orderNumber = $unit['reference_id'] ?? '';
|
||
$amount = (float) ($unit['amount']['value'] ?? 0);
|
||
break;
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'order_number' => $orderNumber,
|
||
'amount' => $amount,
|
||
'transaction_id' => $resource['id'] ?? '',
|
||
'status' => 'paid',
|
||
];
|
||
} catch (\Exception $e) {
|
||
return ['success' => false, 'message' => $e->getMessage()];
|
||
}
|
||
}
|
||
|
||
public function query_status(string $order_number): array {
|
||
return [
|
||
'success' => true,
|
||
'status' => 'pending',
|
||
'message' => __('PayPal 状态通过 Webhook 更新', 'aksrt'),
|
||
];
|
||
}
|
||
|
||
public function refund(string $order_number, float $amount): array {
|
||
return [
|
||
'success' => false,
|
||
'message' => __('PayPal 退款需要通过后台手动处理', 'aksrt'),
|
||
];
|
||
}
|
||
|
||
private function get_config(): array {
|
||
if (!class_exists('AKSRT_Settings')) {
|
||
return [];
|
||
}
|
||
$all = AKSRT_Settings::get_all();
|
||
return [
|
||
'client_id' => (string) ($all['paypal_client_id'] ?? ''),
|
||
'client_secret' => (string) ($all['paypal_client_secret'] ?? ''),
|
||
'currency' => (string) ($all['paypal_currency'] ?? 'USD'),
|
||
'mode' => !empty($all['paypal_sandbox']) ? 'sandbox' : 'live',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 捕获已批准的 PayPal 订单(v2 CAPTURE 必需步骤)。
|
||
* 用户在 return_url 返回后调用,确认真正扣款。
|
||
*
|
||
* @param string $paypal_order_id PayPal 订单 ID(create 时返回的 id)
|
||
* @return array{success:bool, order_number?:string, amount?:float, transaction_id?:string, status?:string, message?:string}
|
||
*/
|
||
public function capture_order(string $paypal_order_id): array {
|
||
if ($paypal_order_id === '') {
|
||
return ['success' => false, 'message' => __('缺少 PayPal 订单号', 'aksrt')];
|
||
}
|
||
|
||
try {
|
||
$response = $this->api_request('POST', '/v2/checkout/orders/' . rawurlencode($paypal_order_id) . '/capture');
|
||
$body = $response['body'];
|
||
|
||
if (in_array($response['status'], [200, 201], true) && ($body['status'] ?? '') === 'COMPLETED') {
|
||
$unit = $body['purchase_units'][0] ?? [];
|
||
$capture = $unit['payments']['captures'][0] ?? [];
|
||
return [
|
||
'success' => true,
|
||
'order_number' => $unit['reference_id'] ?? '',
|
||
'amount' => (float) ($capture['amount']['value'] ?? 0),
|
||
'transaction_id' => $capture['id'] ?? ($body['id'] ?? ''),
|
||
'status' => 'paid',
|
||
];
|
||
}
|
||
|
||
return ['success' => false, 'message' => $body['message'] ?? __('PayPal 捕获失败', 'aksrt')];
|
||
} catch (\Exception $e) {
|
||
return ['success' => false, 'message' => $e->getMessage()];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 OAuth2 access token(v2 REST 必需)。
|
||
*/
|
||
private function get_access_token(): string {
|
||
$config = $this->get_config();
|
||
$isSandbox = ($config['mode'] ?? 'live') === 'sandbox';
|
||
$baseUrl = $isSandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||
|
||
$response = wp_remote_post($baseUrl . '/v1/oauth2/token', [
|
||
'headers' => [
|
||
'Authorization' => 'Basic ' . base64_encode(($config['client_id'] ?? '') . ':' . ($config['client_secret'] ?? '')),
|
||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||
],
|
||
'body' => 'grant_type=client_credentials',
|
||
'timeout' => 30,
|
||
]);
|
||
|
||
if (is_wp_error($response)) {
|
||
throw new \Exception($response->get_error_message());
|
||
}
|
||
|
||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||
$token = $body['access_token'] ?? '';
|
||
if ($token === '') {
|
||
throw new \Exception($body['error_description'] ?? __('PayPal 鉴权失败', 'aksrt'));
|
||
}
|
||
|
||
return (string) $token;
|
||
}
|
||
|
||
/**
|
||
* 调用 PayPal REST API(带 OAuth2 Bearer token)
|
||
*/
|
||
private function api_request(string $method, string $endpoint, array $data = []): array {
|
||
$config = $this->get_config();
|
||
$isSandbox = ($config['mode'] ?? 'live') === 'sandbox';
|
||
$baseUrl = $isSandbox
|
||
? 'https://api-m.sandbox.paypal.com'
|
||
: 'https://api-m.paypal.com';
|
||
|
||
$url = $baseUrl . $endpoint;
|
||
|
||
$headers = [
|
||
'Content-Type' => 'application/json',
|
||
'Authorization' => 'Bearer ' . $this->get_access_token(),
|
||
];
|
||
|
||
$args = [
|
||
'method' => $method,
|
||
'headers' => $headers,
|
||
'timeout' => 30,
|
||
];
|
||
if (!empty($data)) {
|
||
$args['body'] = wp_json_encode($data);
|
||
}
|
||
|
||
$response = wp_remote_request($url, $args);
|
||
|
||
if (is_wp_error($response)) {
|
||
throw new \Exception($response->get_error_message());
|
||
}
|
||
|
||
$status = wp_remote_retrieve_response_code($response);
|
||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||
|
||
return [
|
||
'status' => $status,
|
||
'body' => $body ?: [],
|
||
];
|
||
}
|
||
}
|