// +---------------------------------------------------------------------- declare (strict_types=1); namespace app\common\library\paypal; use Alipay\EasySDK\Kernel\Config; use Alipay\EasySDK\Kernel\Factory; use Alipay\EasySDK\Kernel\Util\ResponseChecker; use app\common\enum\Client as ClientEnum; use app\common\library\Log; use app\common\library\payment\gateway\Driver; use cores\exception\BaseException; use cores\Request; use PayPal\Api\Amount; use PayPal\Api\Order; use PayPal\Api\Payer; use PayPal\Api\Payment; use PayPal\Api\PaymentExecution; use PayPal\Api\RedirectUrls; use PayPal\Api\Transaction; use PayPal\Api\VerifyWebhookSignature; use PayPal\Auth\OAuthTokenCredential; use PayPal\Exception\PayPalConnectionException; use PayPal\Rest\ApiContext; use think\Exception; use think\exception\HttpException; /** * paypal驱动 * Class PayPal * @package app\common\library\payment\gateway\driver */ class PayPal { // 统一下单API的返回结果 private $result; // 异步通知的请求参数 (由第三方支付发送) private $notifyParams; protected $config; protected $notifyWebHookId;// 3NP026061E6858914 // 异步通知的验证结果 private $notifyResult; public $apiContext; public function __construct($config) { // 秘钥配置 $this->config = $config; $this->notifyWebHookId = $this->config['web_hook_id']; $this->apiContext = new ApiContext( new OAuthTokenCredential( $this->config['client_id'], $this->config['secret'] ) ); $this->apiContext->setConfig([ 'mode' => $this->config['mode'],//sandbox, live 'log.LogEnabled' => true, 'log.FileName' => app()->getRootPath() . 'runtime/log/PayPal.log', // 记录日志 'log.LogLevel' => 'debug', // 在live上用info 'cache.enable' => true, ]); } /** * 统一下单API * @param string $outTradeNo 交易订单号 * @param string $totalFee 实际付款金额 * @param array $extra 附加的数据 (需要携带H5端支付成功后跳转的url) * @return bool|array * @throws BaseException */ public function unify(string $outTradeNo, string $totalFee, array $extra = [], $currency = 'USD') { $apiContext = new ApiContext( new OAuthTokenCredential( $this->config['client_id'], // ClientID $this->config['secret'] // ClientSecret ) ); // After Step 2 $payer = new Payer(); $payer->setPaymentMethod('paypal'); $amount = new Amount(); $amount->setTotal($totalFee); $amount->setCurrency($currency); $transaction = new Transaction(); $transaction->setAmount($amount); $redirectUrls = new RedirectUrls(); //live //$return_url = config('app.app_host') . $this->config['return_url'] . $outTradeNo; //$cancel_Url = config('app.app_host').$this->config['cancel_url']; //sandbox $return_url = 'https://lar.lmm.gold/api/index/index'; $cancel_url = 'https://lar.lmm.gold/store/index.html'; $redirectUrls->setReturnUrl($return_url) ->setCancelUrl($cancel_url); $payment = new Payment(); $payment->setIntent('sale') ->setPayer($payer) ->setTransactions(array($transaction)) ->setRedirectUrls($redirectUrls); $this->result = $payment->create($apiContext);// This will print the detailed information on the exception. //REALLY HELPFUL FOR DEBUGGING //echo "\n\nRedirect user to approval_url: " . $payment->getApprovalLink() . "\n"; return ['approval_link' => $payment->getApprovalLink()]; } /** * 交易查询 (主动查询订单支付状态) * @param string $outTradeNo 交易订单号 * @return array|null * @throws BaseException */ public function tradeQuery(string $outTradeNo): ?array { try { $payment = Payment::get($outTradeNo, $this->apiContext); // 记录日志 Log::append('Paypal-tradeQuery', ['outTradeNo' => $outTradeNo, 'result' => json_encode($result)]); // 处理响应或异常 //$this->throwError($result->msg . "," . $result->subMsg); // 返回查询成功的结果 return $result->toArray(); } catch (\Throwable $e) { $this->throwError('支付宝API交易查询失败:' . $e->getMessage(), true, 'tradeQuery'); } return null; } public function executePayment($paymentId) { try { $payment = Payment::get($paymentId, $this->apiContext); $execution = new PaymentExecution(); $execution->setPayerId($payment->getPayer()->getPayerInfo()->getPayerId()); // 执行付款 $payment->execute($execution, $this->apiContext); $payment::get($payment->getId(), $this->apiContext); $transactions = $payment->getTransactions(); \think\facade\Log::error('$transactions::' . json_encode($transactions)); if ($payment->getState() == 'approved' && $payment->getId() == $paymentId) { //related_resources->sale->id return true; } return false; } catch (\Exception $e) { \think\facade\Log::error('executePayment', ['paymentId' => $paymentId, 'errMsg' => $e->getMessage()]); $this->throwError('执行失败:' . $e->getMessage(), true, 'tradeQuery'); return false; } } /** * 支付成功后的异步通知 * @return bool */ public function notify(Request $request, $webHookId): bool { // 接收表单数据 try { $headers = $request->header(); $headers = array_change_key_case($headers, CASE_UPPER); $content = $request->getContent(); \think\facade\Log::error('notify::' . json_encode($headers)); // 如果是laravel,这里获请求头的方法可能要变,现在是$headers['PAYPAL-AUTH-ALGO'],去到laravel的话可能要$headers['PAYPAL-AUTH-ALGO'][0],到时试试就知道了,实在不行打日志看看数据结构再确定如何获取 $signatureVerification = new VerifyWebhookSignature(); $signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']); $signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']); $signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']); $signatureVerification->setWebhookId($webHookId ?: $this->notifyWebHookId); $signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']); $signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']); $signatureVerification->setRequestBody($content); $result = clone $signatureVerification; $output = $signatureVerification->post($this->apiContext); \think\facade\Log::error('notify' . json_encode($output)); if ($output->getVerificationStatus() == 'SUCCESS') { return true; } throw new HttpException(400, 'Verify Failed.'); } catch (HttpException $exception) { \think\facade\Log::error('PayPal Notification Verify Failed' . $exception->getMessage()); return false; } } /** * PAYPAL退款API * @param string $outTradeNo 第三方交易单号 * @param string $refundAmount 退款金额 * @param array $extra 附加的数据 * @return bool * @throws BaseException */ public function refund(string $outTradeNo, string $refundAmount, array $extra = []): bool { try { // 发起API调用 $outRequestNo = (string)time(); return true; } catch (\Throwable $e) { $this->throwError('支付宝API退款请求:' . $e->getMessage(), true, 'refund'); } return false; } /** * 单笔转账接口 * @param string $outTradeNo 交易订单号 * @param string $totalFee 实际付款金额 * @param array $extra 附加的数据 (ALIPAY_LOGON_ID支付宝登录号,支持邮箱和手机号格式; name参与方真实姓名) * @return bool */ public function transfers(string $outTradeNo, string $totalFee, array $extra = []): bool { return false; } /** * 获取异步回调的请求参数 * @return array */ public function getNotifyParams(): array { return [ // 第三方交易流水号 'buyerId' => $this->notifyParams['PayerID'], 'paymentId' => $this->notifyParams['paymentId'] ]; } /** * 返回异步通知结果的输出内容 * @return string */ public function getNotifyResponse(): string { return $this->notifyResult ? 'success' : 'FAIL'; } public function getUnifyResult(): array { if (empty($this->result->getApprovalLink())) { $this->throwError('paypal当前没有unify结果', true, 'getUnifyResult'); return []; } // 整理返回的数据 return ['approval_link' => $this->result->getApprovalLink(), 'id' => $this->result->getId()]; } /** * 设置支付宝配置信息(全局只需设置一次) * @param array $options 支付宝配置信息 * @param string $client 下单客户端 * @return null */ public function setOptions(array $options, string $client) { return $this; } /** * 输出错误信息 * @param string $errMessage 错误信息 * @param bool $isLog 是否记录日志 * @param string $action 当前的操作 * @throws BaseException */ private function throwError(string $errMessage, bool $isLog = false, string $action = '') { $this->error = $errMessage; $isLog && Log::append("Alipay-{$action}", ['errMessage' => $errMessage]); throwError($errMessage); } /** * 获取和验证下单接口所需的附加数据 * @param array $extra * @return array * @throws BaseException */ private function extraAsUnify(array $extra): array { if (!array_key_exists('returnUrl', $extra)) { $this->throwError('returnUrl参数不存在'); } return $extra; } /** * 异步回调地址 * @return string */ private function notifyUrl(): string { // 例如:https://www.xxxx.com/alipayNotice.php return base_url() . 'alipayNotice.php'; } }