From dfd308873a361fc5e9d8a522ead14b659eef5098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Fri, 29 May 2026 20:47:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0paypalv2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Pay/PaypalPayController.php | 304 ++++++++++++++---- it | 0 public/assets/hyper/js/hyper.js | 2 +- public/vendor/dujiaoka-admin/images/logo.jpg | Bin 0 -> 18624 bytes resources/views/luna/README.md | 4 +- .../views/luna/layouts/_footer.blade.php | 1 + 6 files changed, 237 insertions(+), 74 deletions(-) create mode 100644 it create mode 100644 public/vendor/dujiaoka-admin/images/logo.jpg diff --git a/app/Http/Controllers/Pay/PaypalPayController.php b/app/Http/Controllers/Pay/PaypalPayController.php index 70451d5..1445321 100644 --- a/app/Http/Controllers/Pay/PaypalPayController.php +++ b/app/Http/Controllers/Pay/PaypalPayController.php @@ -2,123 +2,153 @@ namespace App\Http\Controllers\Pay; - use AmrShawky\LaravelCurrency\Facade\Currency; use App\Exceptions\RuleValidationException; use App\Http\Controllers\PayController; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; -use PayPal\Api\Amount; -use PayPal\Api\Details; -use PayPal\Api\Item; -use PayPal\Api\ItemList; -use PayPal\Api\Payer; -use PayPal\Api\Payment; -use PayPal\Api\PaymentExecution; -use PayPal\Api\RedirectUrls; -use PayPal\Api\Transaction; -use PayPal\Auth\OAuthTokenCredential; -use PayPal\Exception\PayPalConnectionException; -use PayPal\Rest\ApiContext; class PaypalPayController extends PayController { - const Currency = 'USD'; //货币单位 + /** + * PayPal API 地址 + * + * v2 Checkout Orders: + * - 创建订单: POST https://api-m.paypal.com/v2/checkout/orders + * - 捕获订单: POST https://api-m.paypal.com/v2/checkout/orders/{order_id}/capture + */ + const PAYPAL_API_BASE = 'https://api-m.paypal.com'; + + /** + * PayPal 沙盒 API 地址 + */ + const PAYPAL_SANDBOX_API_BASE = 'https://api-m.sandbox.paypal.com'; + public function gateway(string $payway, string $orderSN) { try { // 加载网关 $this->loadGateWay($orderSN, $payway); - $paypal = new ApiContext( - new OAuthTokenCredential( - $this->payGateway->merchant_key, - $this->payGateway->merchant_pem - ) - ); - $paypal->setConfig(['mode' => 'live']); - $product = $this->order->title; + // 得到汇率 $total = Currency::convert() ->from('CNY') - ->to('USD') + ->to(self::Currency) ->amount($this->order->actual_price) ->round(2) ->get(); - $shipping = 0; - $description = $this->order->title; - $payer = new Payer(); - $payer->setPaymentMethod('paypal'); - $item = new Item(); - $item->setName($product)->setCurrency(self::Currency)->setQuantity(1)->setPrice($total); - $itemList = new ItemList(); - $itemList->setItems([$item]); - $details = new Details(); - $details->setShipping($shipping)->setSubtotal($total); - $amount = new Amount(); - $amount->setCurrency(self::Currency)->setTotal($total)->setDetails($details); - $transaction = new Transaction(); - $transaction->setAmount($amount)->setItemList($itemList)->setDescription($description)->setInvoiceNumber($this->order->order_sn); - $redirectUrls = new RedirectUrls(); - $redirectUrls->setReturnUrl(route('paypal-return', ['success' => 'ok', 'orderSN' => $this->order->order_sn]))->setCancelUrl(route('paypal-return', ['success' => 'no', 'orderSN' => $this->order->order_sn])); - $payment = new Payment(); - $payment->setIntent('sale')->setPayer($payer)->setRedirectUrls($redirectUrls)->setTransactions([$transaction]); - $payment->create($paypal); - $approvalUrl = $payment->getApprovalLink(); - return redirect($approvalUrl); - } catch (PayPalConnectionException $payPalConnectionException) { - return $this->err($payPalConnectionException->getMessage()); + + $total = $this->formatPaypalAmount($total, $this->order->actual_price); + $paypalConfig = $this->resolvePaypalConfig($this->payGateway); + $accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']); + + $paypalOrder = $this->createPaypalOrder($accessToken, $paypalConfig['api_base'], [ + 'intent' => 'CAPTURE', + 'purchase_units' => [ + [ + 'reference_id' => $this->order->order_sn, + 'description' => $this->order->title, + 'invoice_id' => $this->order->order_sn, + 'custom_id' => $this->order->order_sn, + 'amount' => [ + 'currency_code' => self::Currency, + 'value' => $total, + 'breakdown' => [ + 'item_total' => [ + 'currency_code' => self::Currency, + 'value' => $total, + ], + ], + ], + 'items' => [ + [ + 'name' => mb_substr($this->order->title, 0, 127), + 'unit_amount' => [ + 'currency_code' => self::Currency, + 'value' => $total, + ], + 'quantity' => '1', + 'category' => 'DIGITAL_GOODS', + ], + ], + ], + ], + 'application_context' => [ + 'brand_name' => config('app.name', 'dujiaoka'), + 'shipping_preference' => 'NO_SHIPPING', + 'user_action' => 'PAY_NOW', + 'return_url' => route('paypal-return', ['success' => 'ok', 'orderSN' => $this->order->order_sn]), + 'cancel_url' => route('paypal-return', ['success' => 'no', 'orderSN' => $this->order->order_sn]), + ], + ]); + + foreach ($paypalOrder['links'] ?? [] as $link) { + if (($link['rel'] ?? '') === 'approve' && !empty($link['href'])) { + return redirect($link['href']); + } + } + + Log::error('paypal创建订单失败', ['response' => $paypalOrder]); + return $this->err('PayPal 创建订单失败:未获取到支付跳转链接'); } catch (RuleValidationException $exception) { return $this->err($exception->getMessage()); + } catch (\Exception $exception) { + Log::error('paypal创建订单异常', ['message' => $exception->getMessage()]); + return $this->err($exception->getMessage()); } } /** - *paypal 同步回调 + * paypal 同步回调 */ public function returnUrl(Request $request) { $success = $request->input('success'); - $paymentId = $request->input('paymentId'); - $payerID = $request->input('PayerID'); + $paypalOrderId = $request->input('token'); $orderSN = $request->input('orderSN'); - if ($success == 'no' || empty($paymentId) || empty($payerID)) { + + if ($success == 'no' || empty($paypalOrderId)) { // 取消支付 - redirect(url('detail-order-sn', ['orderSN' => $payerID])); + return redirect(url('detail-order-sn', ['orderSN' => $orderSN])); } + $order = $this->orderService->detailOrderSN($orderSN); if (!$order) { return 'error'; } + $payGateway = $this->payService->detail($order->pay_id); if (!$payGateway) { return 'error'; } - if($payGateway->pay_handleroute != '/pay/paypal'){ + + if ($payGateway->pay_handleroute != '/pay/paypal') { return 'error'; } - $paypal = new ApiContext( - new OAuthTokenCredential( - $payGateway->merchant_key, - $payGateway->merchant_pem - ) - ); - $paypal->setConfig(['mode' => 'live']); - $payment = Payment::get($paymentId, $paypal); - $execute = new PaymentExecution(); - $execute->setPayerId($payerID); + try { - $payment->execute($execute, $paypal); - $this->orderProcessService->completedOrder($orderSN, $order->actual_price, $paymentId); - Log::info("paypal支付成功", ['支付成功,支付ID【' . $paymentId . '】,支付人ID【' . $payerID . '】']); + $paypalConfig = $this->resolvePaypalConfig($payGateway); + $accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']); + $capture = $this->capturePaypalOrder($accessToken, $paypalConfig['api_base'], $paypalOrderId); + + if (($capture['status'] ?? '') === 'COMPLETED') { + $captureId = $capture['purchase_units'][0]['payments']['captures'][0]['id'] ?? $paypalOrderId; + $this->orderProcessService->completedOrder($orderSN, $order->actual_price, $captureId); + Log::info('paypal支付成功', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, '捕获ID' => $captureId]); + } else { + Log::error('paypal支付未完成', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, 'response' => $capture]); + } } catch (\Exception $e) { - Log::error("paypal支付失败", ['支付失败,支付ID【' . $paymentId . '】,支付人ID【' . $payerID . '】']); + Log::error('paypal支付失败', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, '错误' => $e->getMessage()]); } + return redirect(url('detail-order-sn', ['orderSN' => $orderSN])); } - /** * 异步通知 * TODO: 暂未实现,但是好像只实现同步回调即可。这个可以放在后面实现 @@ -127,12 +157,145 @@ class PaypalPayController extends PayController { //获取回调结果 $json_data = $this->get_JsonData(); - if(!empty($json_data)){ + if (!empty($json_data)) { Log::debug("paypal notify info:\r\n" . json_encode($json_data)); - }else{ + } else { Log::debug("paypal notify fail:参加为空"); } + } + /** + * 格式化 PayPal 金额 + * + * PayPal v2 Orders API 要求金额必须大于 0,且最多保留两位小数。 + * 如果 CNY 转 USD 后因为订单金额过小或汇率服务异常得到 0.00,则使用 PayPal 最小可支付金额 0.01。 + */ + private function formatPaypalAmount($convertedAmount, $originalAmount): string + { + $amount = round((float)$convertedAmount, 2); + + if ($amount <= 0) { + Log::warning('paypal订单金额转换后小于等于0,已使用最小金额0.01', [ + 'original_amount_cny' => $originalAmount, + 'converted_amount_usd' => $convertedAmount, + ]); + + $amount = 0.01; + } + + return number_format($amount, 2, '.', ''); + } + + /** + * 获取 PayPal OAuth2 Access Token + */ + private function getAccessToken(string $clientId, string $clientSecret, string $apiBase): string + { + $response = $this->paypalClient($apiBase)->post('/v1/oauth2/token', [ + 'auth' => [$clientId, $clientSecret], + 'form_params' => [ + 'grant_type' => 'client_credentials', + ], + 'headers' => [ + 'Accept' => 'application/json', + 'Accept-Language' => 'en_US', + ], + ]); + + $data = json_decode((string)$response->getBody(), true); + + if (empty($data['access_token'])) { + throw new \RuntimeException('PayPal access_token 获取失败'); + } + + return $data['access_token']; + } + + /** + * 创建 PayPal v2 Checkout Order + */ + private function createPaypalOrder(string $accessToken, string $apiBase, array $payload): array + { + return $this->paypalRequest('POST', '/v2/checkout/orders', $accessToken, $apiBase, $payload); + } + + /** + * 捕获 PayPal v2 Checkout Order + */ + private function capturePaypalOrder(string $accessToken, string $apiBase, string $paypalOrderId): array + { + return $this->paypalRequest('POST', '/v2/checkout/orders/' . urlencode($paypalOrderId) . '/capture', $accessToken, $apiBase); + } + + /** + * 请求 PayPal API + */ + private function paypalRequest(string $method, string $uri, string $accessToken, string $apiBase, array $payload = null): array + { + $options = [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + ]; + + if ($payload !== null) { + $options['json'] = $payload; + } + + try { + $response = $this->paypalClient($apiBase)->request($method, $uri, $options); + return json_decode((string)$response->getBody(), true) ?: []; + } catch (RequestException $exception) { + $responseBody = $exception->hasResponse() ? (string)$exception->getResponse()->getBody() : ''; + Log::error('paypal api request error', [ + 'method' => $method, + 'uri' => $uri, + 'status' => $exception->hasResponse() ? $exception->getResponse()->getStatusCode() : null, + 'response' => $responseBody, + ]); + + throw new \RuntimeException($responseBody ?: $exception->getMessage(), $exception->getCode(), $exception); + } + } + + /** + * 解析 PayPal 配置 + * + * 后台支付配置建议: + * - 商户号 merchant_id:PayPal Client ID + * - 商户密钥 merchant_pem:PayPal Secret + * - 商户 KEY merchant_key:可选,填 sandbox/test 则使用沙盒;填 live/prod 或留空则使用生产 + * + * 兼容旧配置:如果 merchant_id 为空,则继续使用 merchant_key 作为 Client ID。 + */ + private function resolvePaypalConfig($payGateway): array + { + $merchantId = trim((string)$payGateway->merchant_id); + $merchantKey = trim((string)$payGateway->merchant_key); + $merchantPem = trim((string)$payGateway->merchant_pem); + + $mode = strtolower($merchantKey); + $apiBase = in_array($mode, ['sandbox', 'test'], true) ? self::PAYPAL_SANDBOX_API_BASE : self::PAYPAL_API_BASE; + + return [ + 'client_id' => $merchantId ?: $merchantKey, + 'client_secret' => $merchantPem, + 'api_base' => $apiBase, + ]; + } + + /** + * PayPal HTTP Client + */ + private function paypalClient(string $apiBase): Client + { + return new Client([ + 'base_uri' => $apiBase, + 'timeout' => 30, + 'http_errors' => true, + ]); } private function get_JsonData() @@ -140,9 +303,8 @@ class PaypalPayController extends PayController $json = file_get_contents('php://input'); if ($json) { $json = str_replace("'", '', $json); - $json = json_decode($json,true); + $json = json_decode($json, true); } return $json; } - } diff --git a/it b/it new file mode 100644 index 0000000..e69de29 diff --git a/public/assets/hyper/js/hyper.js b/public/assets/hyper/js/hyper.js index 3b80b2a..c90b49f 100644 --- a/public/assets/hyper/js/hyper.js +++ b/public/assets/hyper/js/hyper.js @@ -63,7 +63,7 @@ $(function() { localStorage.setItem("announcement",setTime); } // 版权 - console.group("Faka");console.log("Name: 云泽数卡");console.log("Github: https://github.com/assimon/dujiaoka");console.groupEnd(); + console.group("Faka");console.log("Name: 独角数卡");console.log("Github: https://github.com/assimon/dujiaoka");console.groupEnd(); console.group("Theme");console.log("Name: Hyper Theme");console.log("Author: Bimoes");console.groupEnd(); }); // 图片懒加载 diff --git a/public/vendor/dujiaoka-admin/images/logo.jpg b/public/vendor/dujiaoka-admin/images/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ada4fb65d7044c3b1a31fd9037d24a3b2213cad7 GIT binary patch literal 18624 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 + \ No newline at end of file