更新paypalv2
This commit is contained in:
parent
ee39010d5e
commit
dfd308873a
@ -2,123 +2,153 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Pay;
|
namespace App\Http\Controllers\Pay;
|
||||||
|
|
||||||
|
|
||||||
use AmrShawky\LaravelCurrency\Facade\Currency;
|
use AmrShawky\LaravelCurrency\Facade\Currency;
|
||||||
use App\Exceptions\RuleValidationException;
|
use App\Exceptions\RuleValidationException;
|
||||||
use App\Http\Controllers\PayController;
|
use App\Http\Controllers\PayController;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
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
|
class PaypalPayController extends PayController
|
||||||
{
|
{
|
||||||
|
|
||||||
const Currency = 'USD'; //货币单位
|
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)
|
public function gateway(string $payway, string $orderSN)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// 加载网关
|
// 加载网关
|
||||||
$this->loadGateWay($orderSN, $payway);
|
$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()
|
$total = Currency::convert()
|
||||||
->from('CNY')
|
->from('CNY')
|
||||||
->to('USD')
|
->to(self::Currency)
|
||||||
->amount($this->order->actual_price)
|
->amount($this->order->actual_price)
|
||||||
->round(2)
|
->round(2)
|
||||||
->get();
|
->get();
|
||||||
$shipping = 0;
|
|
||||||
$description = $this->order->title;
|
$total = $this->formatPaypalAmount($total, $this->order->actual_price);
|
||||||
$payer = new Payer();
|
$paypalConfig = $this->resolvePaypalConfig($this->payGateway);
|
||||||
$payer->setPaymentMethod('paypal');
|
$accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']);
|
||||||
$item = new Item();
|
|
||||||
$item->setName($product)->setCurrency(self::Currency)->setQuantity(1)->setPrice($total);
|
$paypalOrder = $this->createPaypalOrder($accessToken, $paypalConfig['api_base'], [
|
||||||
$itemList = new ItemList();
|
'intent' => 'CAPTURE',
|
||||||
$itemList->setItems([$item]);
|
'purchase_units' => [
|
||||||
$details = new Details();
|
[
|
||||||
$details->setShipping($shipping)->setSubtotal($total);
|
'reference_id' => $this->order->order_sn,
|
||||||
$amount = new Amount();
|
'description' => $this->order->title,
|
||||||
$amount->setCurrency(self::Currency)->setTotal($total)->setDetails($details);
|
'invoice_id' => $this->order->order_sn,
|
||||||
$transaction = new Transaction();
|
'custom_id' => $this->order->order_sn,
|
||||||
$transaction->setAmount($amount)->setItemList($itemList)->setDescription($description)->setInvoiceNumber($this->order->order_sn);
|
'amount' => [
|
||||||
$redirectUrls = new RedirectUrls();
|
'currency_code' => self::Currency,
|
||||||
$redirectUrls->setReturnUrl(route('paypal-return', ['success' => 'ok', 'orderSN' => $this->order->order_sn]))->setCancelUrl(route('paypal-return', ['success' => 'no', 'orderSN' => $this->order->order_sn]));
|
'value' => $total,
|
||||||
$payment = new Payment();
|
'breakdown' => [
|
||||||
$payment->setIntent('sale')->setPayer($payer)->setRedirectUrls($redirectUrls)->setTransactions([$transaction]);
|
'item_total' => [
|
||||||
$payment->create($paypal);
|
'currency_code' => self::Currency,
|
||||||
$approvalUrl = $payment->getApprovalLink();
|
'value' => $total,
|
||||||
return redirect($approvalUrl);
|
],
|
||||||
} catch (PayPalConnectionException $payPalConnectionException) {
|
],
|
||||||
return $this->err($payPalConnectionException->getMessage());
|
],
|
||||||
|
'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) {
|
} catch (RuleValidationException $exception) {
|
||||||
return $this->err($exception->getMessage());
|
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)
|
public function returnUrl(Request $request)
|
||||||
{
|
{
|
||||||
$success = $request->input('success');
|
$success = $request->input('success');
|
||||||
$paymentId = $request->input('paymentId');
|
$paypalOrderId = $request->input('token');
|
||||||
$payerID = $request->input('PayerID');
|
|
||||||
$orderSN = $request->input('orderSN');
|
$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);
|
$order = $this->orderService->detailOrderSN($orderSN);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
$payGateway = $this->payService->detail($order->pay_id);
|
$payGateway = $this->payService->detail($order->pay_id);
|
||||||
if (!$payGateway) {
|
if (!$payGateway) {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
if($payGateway->pay_handleroute != '/pay/paypal'){
|
|
||||||
|
if ($payGateway->pay_handleroute != '/pay/paypal') {
|
||||||
return 'error';
|
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 {
|
try {
|
||||||
$payment->execute($execute, $paypal);
|
$paypalConfig = $this->resolvePaypalConfig($payGateway);
|
||||||
$this->orderProcessService->completedOrder($orderSN, $order->actual_price, $paymentId);
|
$accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']);
|
||||||
Log::info("paypal支付成功", ['支付成功,支付ID【' . $paymentId . '】,支付人ID【' . $payerID . '】']);
|
$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) {
|
} 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]));
|
return redirect(url('detail-order-sn', ['orderSN' => $orderSN]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步通知
|
* 异步通知
|
||||||
* TODO: 暂未实现,但是好像只实现同步回调即可。这个可以放在后面实现
|
* TODO: 暂未实现,但是好像只实现同步回调即可。这个可以放在后面实现
|
||||||
@ -127,12 +157,145 @@ class PaypalPayController extends PayController
|
|||||||
{
|
{
|
||||||
//获取回调结果
|
//获取回调结果
|
||||||
$json_data = $this->get_JsonData();
|
$json_data = $this->get_JsonData();
|
||||||
if(!empty($json_data)){
|
if (!empty($json_data)) {
|
||||||
Log::debug("paypal notify info:\r\n" . json_encode($json_data));
|
Log::debug("paypal notify info:\r\n" . json_encode($json_data));
|
||||||
}else{
|
} else {
|
||||||
Log::debug("paypal notify fail:参加为空");
|
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()
|
private function get_JsonData()
|
||||||
@ -140,9 +303,8 @@ class PaypalPayController extends PayController
|
|||||||
$json = file_get_contents('php://input');
|
$json = file_get_contents('php://input');
|
||||||
if ($json) {
|
if ($json) {
|
||||||
$json = str_replace("'", '', $json);
|
$json = str_replace("'", '', $json);
|
||||||
$json = json_decode($json,true);
|
$json = json_decode($json, true);
|
||||||
}
|
}
|
||||||
return $json;
|
return $json;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
public/assets/hyper/js/hyper.js
vendored
2
public/assets/hyper/js/hyper.js
vendored
@ -63,7 +63,7 @@ $(function() {
|
|||||||
localStorage.setItem("announcement",setTime);
|
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();
|
console.group("Theme");console.log("Name: Hyper Theme");console.log("Author: Bimoes");console.groupEnd();
|
||||||
});
|
});
|
||||||
// 图片懒加载
|
// 图片懒加载
|
||||||
|
|||||||
BIN
public/vendor/dujiaoka-admin/images/logo.jpg
vendored
Normal file
BIN
public/vendor/dujiaoka-admin/images/logo.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@ -1,6 +1,6 @@
|
|||||||
## 云泽数卡 - Luna模板
|
## 独角数卡 - Luna模板
|
||||||
|
|
||||||
一套简洁的云泽数卡模板
|
一套简洁的独角数卡模板
|
||||||
|
|
||||||
## 特殊用法
|
## 特殊用法
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user