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 { // 优先从统一设置读取(schema 保存的 key) if (class_exists('AKSRT_Settings')) { $all = AKSRT_Settings::get_all(); if (!empty($all['paypal_client_id'])) { return [ 'client_id' => $all['paypal_client_id'] ?? '', 'client_secret' => $all['paypal_client_secret'] ?? '', 'currency' => $all['paypal_currency'] ?? 'USD', 'mode' => !empty($all['paypal_sandbox']) ? 'sandbox' : 'live', ]; } } // 回退到独立 option $settings = get_option('aksrt_payment_paypal', []); return is_array($settings) ? $settings : []; } /** * 捕获已批准的 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 ?: [], ]; } }