PayPal.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\common\library\payment\gateway\driver;
  13. use Alipay\EasySDK\Kernel\Config;
  14. use Alipay\EasySDK\Kernel\Factory;
  15. use Alipay\EasySDK\Kernel\Util\ResponseChecker;
  16. use app\common\enum\Client as ClientEnum;
  17. use app\common\library\Log;
  18. use app\common\library\payment\gateway\Driver;
  19. use cores\exception\BaseException;
  20. use cores\Request;
  21. use PayPal\Api\Amount;
  22. use PayPal\Api\Order;
  23. use PayPal\Api\Payer;
  24. use PayPal\Api\Payment;
  25. use PayPal\Api\PaymentExecution;
  26. use PayPal\Api\RedirectUrls;
  27. use PayPal\Api\Transaction;
  28. use PayPal\Api\VerifyWebhookSignature;
  29. use PayPal\Auth\OAuthTokenCredential;
  30. use PayPal\Exception\PayPalConnectionException;
  31. use PayPal\Rest\ApiContext;
  32. use think\exception\HttpException;
  33. /**
  34. * paypal驱动
  35. * Class Alipay
  36. * @package app\common\library\payment\gateway\driver
  37. */
  38. class PayPal extends Driver
  39. {
  40. // 统一下单API的返回结果
  41. private $result;
  42. // 异步通知的请求参数 (由第三方支付发送)
  43. private $notifyParams;
  44. protected $config;
  45. protected $notifyWebHookId;// 3NP026061E6858914
  46. // 异步通知的验证结果
  47. private $notifyResult;
  48. public $apiContext;
  49. public function __construct($config)
  50. {
  51. // 秘钥配置
  52. $this->config = $config;
  53. $this->notifyWebHookId = $this->config['web_hook_id'];
  54. $this->apiContext = new ApiContext(
  55. new OAuthTokenCredential(
  56. $this->config['client_id'],
  57. $this->config['secret']
  58. )
  59. );
  60. $this->apiContext->setConfig([
  61. 'mode' => $this->config['mode'],
  62. 'log.LogEnabled' => true,
  63. 'log.FileName' => app()->getRootPath() . 'runtime/log/PayPal.log', // 记录日志
  64. 'log.LogLevel' => 'debug', // 在live上用info
  65. 'cache.enable' => true,
  66. ]);
  67. }
  68. /**
  69. * 统一下单API
  70. * @param string $outTradeNo 交易订单号
  71. * @param string $totalFee 实际付款金额
  72. * @param array $extra 附加的数据 (需要携带H5端支付成功后跳转的url)
  73. * @return bool
  74. * @throws BaseException
  75. */
  76. public function unify(string $outTradeNo, string $totalFee, array $extra = [], $currency = 'USD'): bool
  77. {
  78. $apiContext = new ApiContext(
  79. new OAuthTokenCredential(
  80. 'AS0FH780ZGtSAdpT1NTjwkFzryCPf69rZb_FR9Rt_rZdasB80cmjqTQ6CQELWiFVh_MU9e31CSnyz7Ai', // ClientID
  81. 'EDqRQhgLNHCb5bxld98T8-JJJZKvMIeqxudO7lMwDFOxBfy138PjM5A21FnDNyb3q4yYUh8r7Qr2BnVi' // ClientSecret
  82. )
  83. );
  84. // After Step 2
  85. $payer = new Payer();
  86. $payer->setPaymentMethod('paypal');
  87. $amount = new Amount();
  88. $amount->setTotal($totalFee);
  89. $amount->setCurrency($currency);
  90. $transaction = new Transaction();
  91. $transaction->setAmount($amount);
  92. $redirectUrls = new RedirectUrls();
  93. $redirectUrls->setReturnUrl("https://lar.lmm.gold/api/index/index")
  94. ->setCancelUrl("https://lar.lmm.gold/store/index.html");
  95. $payment = new Payment();
  96. $payment->setIntent('sale')
  97. ->setPayer($payer)
  98. ->setTransactions(array($transaction))
  99. ->setRedirectUrls($redirectUrls);
  100. //$payment->create($apiContext);
  101. try {
  102. $this->result = $payment->create($apiContext);
  103. } catch (\PayPal\Exception\PayPalConnectionException $ex) {
  104. // This will print the detailed information on the exception.
  105. //REALLY HELPFUL FOR DEBUGGING
  106. echo $ex->getData();
  107. $this->throwError('支付宝API交易查询失败:' . $ex->getMessage(), true, 'tradeQuery');
  108. return false;
  109. }
  110. echo "\n\nRedirect user to approval_url: " . $payment->getApprovalLink() . "\n";
  111. // 记录日志
  112. Log::append('PayPal-unify', ['client' => $this->client, 'result' => $this->result]);
  113. redirect($payment->getApprovalLink());
  114. // 请求成功
  115. return true;
  116. }
  117. /**
  118. * 交易查询 (主动查询订单支付状态)
  119. * @param string $outTradeNo 交易订单号
  120. * @return array|null
  121. * @throws BaseException
  122. */
  123. public function tradeQuery(string $outTradeNo): ?array
  124. {
  125. try {
  126. $result = Order::get($outTradeNo, $this->apiContext);
  127. // 记录日志
  128. Log::append('Paypal-tradeQuery', ['outTradeNo' => $outTradeNo, 'result' => json_encode($result)]);
  129. // 处理响应或异常
  130. //$this->throwError($result->msg . "," . $result->subMsg);
  131. // 返回查询成功的结果
  132. return $result->toArray();
  133. } catch (\Throwable $e) {
  134. $this->throwError('支付宝API交易查询失败:' . $e->getMessage(), true, 'tradeQuery');
  135. }
  136. return null;
  137. }
  138. public function executePayment($paymentId)
  139. {
  140. try {
  141. $payment = Payment::get($paymentId, $this->apiContext);
  142. $execution = new PaymentExecution();
  143. $execution->setPayerId($payment->getPayer()->getPayerInfo()->getPayerId());
  144. // 执行付款
  145. $payment->execute($execution, $this->apiContext);
  146. return $payment::get($payment->getId(), $this->apiContext);
  147. } catch (\Exception $e) {
  148. \think\facade\Log::error('executePayment', ['paymentId' => $paymentId, 'errMsg' => $e->getMessage()]);
  149. $this->throwError('执行失败:' . $e->getMessage(), true, 'tradeQuery');
  150. return false;
  151. }
  152. }
  153. /**
  154. * 支付成功后的异步通知
  155. * @return bool
  156. */
  157. public function notify(Request $request, $webHookId): bool
  158. {
  159. // 接收表单数据
  160. try {
  161. $headers = $request->header();
  162. $headers = array_change_key_case($headers, CASE_UPPER);
  163. $content = $request->getContent();
  164. \think\facade\Log::error('notify::' . json_encode($headers));
  165. // 如果是laravel,这里获请求头的方法可能要变,现在是$headers['PAYPAL-AUTH-ALGO'],去到laravel的话可能要$headers['PAYPAL-AUTH-ALGO'][0],到时试试就知道了,实在不行打日志看看数据结构再确定如何获取
  166. $signatureVerification = new VerifyWebhookSignature();
  167. $signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
  168. $signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
  169. $signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']);
  170. $signatureVerification->setWebhookId($webHookId ?: $this->notifyWebHookId);
  171. $signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
  172. $signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);
  173. $signatureVerification->setRequestBody($content);
  174. $result = clone $signatureVerification;
  175. $output = $signatureVerification->post($this->apiContext);
  176. \think\facade\Log::error('notify' . json_encode($output));
  177. if ($output->getVerificationStatus() == 'SUCCESS') {
  178. return true;
  179. }
  180. throw new HttpException(400, 'Verify Failed.');
  181. } catch (HttpException $exception) {
  182. \think\facade\Log::error('PayPal Notification Verify Failed' . $exception->getMessage());
  183. return false;
  184. }
  185. // 记录日志
  186. // Log::append('PayPal-notify', [
  187. // 'params' => $this->notifyParams,
  188. // 'verifyNotify' => $verifyNotify,
  189. // 'response' => $this->getNotifyResponse(),
  190. // 'result' => $this->notifyResult,
  191. // 'message' => '支付宝异步回调验证' . ($this->notifyResult ? '成功' : '失败')
  192. // ]);
  193. return $this->notifyResult;
  194. }
  195. /**
  196. * PAYPAL退款API
  197. * @param string $outTradeNo 第三方交易单号
  198. * @param string $refundAmount 退款金额
  199. * @param array $extra 附加的数据
  200. * @return bool
  201. * @throws BaseException
  202. */
  203. public function refund(string $outTradeNo, string $refundAmount, array $extra = []): bool
  204. {
  205. try {
  206. // 发起API调用
  207. $outRequestNo = (string)time();
  208. return true;
  209. } catch (\Throwable $e) {
  210. $this->throwError('支付宝API退款请求:' . $e->getMessage(), true, 'refund');
  211. }
  212. return false;
  213. }
  214. /**
  215. * 单笔转账接口
  216. * @param string $outTradeNo 交易订单号
  217. * @param string $totalFee 实际付款金额
  218. * @param array $extra 附加的数据 (ALIPAY_LOGON_ID支付宝登录号,支持邮箱和手机号格式; name参与方真实姓名)
  219. * @return bool
  220. */
  221. public function transfers(string $outTradeNo, string $totalFee, array $extra = []): bool
  222. {
  223. // https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer
  224. return false;
  225. }
  226. /**
  227. * 获取异步回调的请求参数
  228. * @return array
  229. */
  230. public function getNotifyParams(): array
  231. {
  232. return [
  233. // 第三方交易流水号
  234. 'buyerId' => $this->notifyParams['PayerID'],
  235. 'paymentId' => $this->notifyParams['paymentId']
  236. ];
  237. }
  238. /**
  239. * 返回异步通知结果的输出内容
  240. * @return string
  241. */
  242. public function getNotifyResponse(): string
  243. {
  244. return $this->notifyResult ? 'success' : 'FAIL';
  245. }
  246. public function getUnifyResult(): array
  247. {
  248. if (empty($this->result->getApprovalLink())) {
  249. $this->throwError('paypal当前没有unify结果', true, 'getUnifyResult');
  250. return [];
  251. }
  252. // 整理返回的数据
  253. return ['approval_link' => $this->result->getApprovalLink(), 'id' => $this->result->getId()];
  254. }
  255. /**
  256. * 设置支付宝配置信息(全局只需设置一次)
  257. * @param array $options 支付宝配置信息
  258. * @param string $client 下单客户端
  259. * @return Driver|null
  260. */
  261. public function setOptions(array $options, string $client): ?Driver
  262. {
  263. return $this;
  264. }
  265. /**
  266. * 输出错误信息
  267. * @param string $errMessage 错误信息
  268. * @param bool $isLog 是否记录日志
  269. * @param string $action 当前的操作
  270. * @throws BaseException
  271. */
  272. private function throwError(string $errMessage, bool $isLog = false, string $action = '')
  273. {
  274. $this->error = $errMessage;
  275. $isLog && Log::append("Alipay-{$action}", ['errMessage' => $errMessage]);
  276. throwError($errMessage);
  277. }
  278. /**
  279. * 获取和验证下单接口所需的附加数据
  280. * @param array $extra
  281. * @return array
  282. * @throws BaseException
  283. */
  284. private function extraAsUnify(array $extra): array
  285. {
  286. if ($this->client === ClientEnum::H5) {
  287. if (!array_key_exists('returnUrl', $extra)) {
  288. $this->throwError('returnUrl参数不存在');
  289. }
  290. }
  291. return $extra;
  292. }
  293. /**
  294. * 删除HTML中的指定标签
  295. * @param array $tags
  296. * @param $string
  297. * @return array|string|string[]|null
  298. */
  299. private function deleteHtmlTags(array $tags, $string)
  300. {
  301. $preg = [];
  302. foreach ($tags as $key => $value) {
  303. $preg[$key] = "/<({$value}.*?)>(.*?)<(\/{$value}.*?)>/si";
  304. }
  305. return preg_replace($preg, '', $string);
  306. }
  307. /**
  308. * 异步回调地址
  309. * @return string
  310. */
  311. private function notifyUrl(): string
  312. {
  313. // 例如:https://www.xxxx.com/alipayNotice.php
  314. return base_url() . 'alipayNotice.php';
  315. }
  316. }