Paypal.php 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <?php
  2. /*
  3. * FecShop file.
  4. *
  5. * @link http://www.fecshop.com/
  6. * @copyright Copyright (c) 2016 FecShop Software LLC
  7. * @license http://www.fecshop.com/license/
  8. */
  9. namespace fecshop\services\payment;
  10. //use fecshop\models\mysqldb\IpnMessage;
  11. use fecshop\services\Service;
  12. use Yii;
  13. /**
  14. * Payment Paypal services.
  15. * @author Terry Zhao <2358269014@qq.com>
  16. * @since 1.0
  17. */
  18. class Paypal extends Service
  19. {
  20. /**
  21. * paypal支付状态 详细参看:https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/
  22. * 打开url后,浏览器查找:PAYMENTINFO_n_PAYMENTSTATUS , 即可找到下面各个状态对应的含义
  23. */
  24. public $payment_status_none = 'none';
  25. public $payment_status_completed = 'completed';
  26. public $payment_status_denied = 'denied';
  27. public $payment_status_expired = 'expired';
  28. public $payment_status_failed = 'failed';
  29. public $payment_status_in_progress = 'in_progress';
  30. public $payment_status_pending = 'pending';
  31. public $payment_status_refunded = 'refunded';
  32. public $payment_status_refunded_part= 'partially_refunded';
  33. public $payment_status_reversed = 'reversed';
  34. public $payment_status_unreversed = 'canceled_reversal';
  35. public $payment_status_processed = 'processed';
  36. public $payment_status_voided = 'voided';
  37. public $seller_email ;
  38. // 是否使用证书的方式(https)
  39. public $use_local_certs = false;
  40. // 在payment中 express paypal 的配置值
  41. public $express_payment_method;
  42. public $standard_payment_method;
  43. public $version = '109.0';
  44. public $crt_file;
  45. protected $_postData;
  46. protected $_order;
  47. const EXPRESS_TOKEN = 'paypal_express_token';
  48. const EXPRESS_PAYER_ID = 'paypal_express_payer_id';
  49. protected $payerID;
  50. protected $token;
  51. // 允许更改的订单状态,不存在这里面的订单状态不允许修改
  52. protected $_allowChangOrderStatus;
  53. protected $_ipnMessageModelName = '\fecshop\models\mysqldb\IpnMessage';
  54. protected $_ipnMessageModel;
  55. protected $_account;
  56. protected $_password;
  57. protected $_signature;
  58. protected $_env;
  59. public function init()
  60. {
  61. parent::init();
  62. $this->_account = Yii::$app->store->get('payment_paypal', 'paypal_account');
  63. $this->_password = Yii::$app->store->get('payment_paypal', 'paypal_password');
  64. $this->_signature = Yii::$app->store->get('payment_paypal', 'paypal_signature');
  65. $this->_env = Yii::$app->store->get('payment_paypal', 'paypal_env');
  66. list($this->_ipnMessageModelName, $this->_ipnMessageModel) = \Yii::mapGet($this->_ipnMessageModelName);
  67. $this->_allowChangOrderStatus = [
  68. Yii::$service->order->payment_status_pending,
  69. Yii::$service->order->payment_status_processing,
  70. ];
  71. }
  72. /**
  73. * @param $domain | string
  74. * @return 得到证书crt文件的绝对路径
  75. */
  76. public function getCrtFile($domain)
  77. {
  78. if (isset($this->crt_file[$domain]) && !empty($this->crt_file[$domain])) {
  79. return Yii::getAlias($this->crt_file[$domain]);
  80. }
  81. }
  82. /**
  83. * 在paypal 标准支付中,paypal会向网站发送IPN消息,告知fecshop订单支付状态,
  84. * 进而fecshop更改订单状态。
  85. * fecshop一方面验证消息是否由paypal发出,另一方面要验证订单是否和后台的一致。
  86. */
  87. public function receiveIpn($post)
  88. {
  89. \Yii::info('receiveIpn', 'fecshop_debug');
  90. if ($this->verifySecurity($post)) {
  91. \Yii::info('verifySecurity', 'fecshop_debug');
  92. // 验证数据是否已经发送
  93. //if ($this->isNotDuplicate()) {
  94. // 验证数据是否被篡改。
  95. if ($this->isNotDistort()) {
  96. \Yii::info('updateOrderStatusByIpn', 'fecshop_debug');
  97. $this->updateOrderStatusByIpn();
  98. } else {
  99. // 如果数据和订单数据不一致,而且,支付状态为成功,则此订单
  100. // 标记为可疑的。
  101. $suspected_fraud = Yii::$service->order->payment_status_suspected_fraud;
  102. $this->updateOrderStatusByIpn($suspected_fraud);
  103. }
  104. // }
  105. }
  106. }
  107. /**
  108. * 该函数是为了验证IPN是否是由paypal发出,
  109. * 当paypal发送IPN消息给fecshop,fecshop不知道是否是伪造的支付消息,
  110. * 因此,fecshop将接收到的参数传递给paypal,询问paypal是否是paypal
  111. * 发送的IPN消息,如果是,则返回VERIFIED。
  112. */
  113. protected function verifySecurity($post)
  114. {
  115. $this->_postData = $post;
  116. $verifyUrl = $this->getVerifyUrl();
  117. \Yii::info('verifyUrl:'.$verifyUrl, 'fecshop_debug');
  118. $verifyReturn = $this->curlGet($verifyUrl);
  119. \Yii::info('verifyReturn:'.$verifyReturn, 'fecshop_debug');
  120. if ($verifyReturn == 'VERIFIED') {
  121. return true;
  122. }
  123. }
  124. /**
  125. * paypal发送的IPN,需要进行验证是否IPN是由paypal发出
  126. * 因此需要请求paypal确认,此函数返回请求paypal的url。
  127. */
  128. protected function getVerifyUrl()
  129. {
  130. $urlParamStr = '';
  131. if ($this->_postData) {
  132. foreach ($this->_postData as $k => $v) {
  133. $urlParamStr .= '&'.$k.'='.urlencode($v);
  134. }
  135. }
  136. $urlParamStr .= '&cmd=_notify-validate';
  137. $urlParamStr = substr($urlParamStr, 1);
  138. $current_payment_method = Yii::$service->payment->getPaymentMethod();
  139. //if ($current_payment_method == $this->standard_payment_method) {
  140. // $verifyUrl = Yii::$service->payment->getStandardWebscrUrl($this->standard_payment_method);
  141. //} else {
  142. // $verifyUrl = Yii::$service->payment->getExpressWebscrUrl($this->express_payment_method);
  143. //}
  144. if ($this->_env == Yii::$service->payment->env_sanbox) {
  145. $verifyUrl = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  146. } else {
  147. $verifyUrl = 'https://www.paypal.com/cgi-bin/webscr';
  148. }
  149. $verifyUrl = $verifyUrl.'?'.$urlParamStr;
  150. return $verifyUrl;
  151. }
  152. /**
  153. * @param $url | string, 请求的url
  154. * @param $i | 请求的次数,因为curl可能存在失败的可能,当
  155. * 失败后,就会通过递归的方式进行多次请求,这里设置的最大请求5次。
  156. * @return 返回请求url的return信息。
  157. */
  158. protected function curlGet($url, $i = 0)
  159. {
  160. $ch = curl_init($url);
  161. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  162. curl_setopt($ch, CURLOPT_POST, 1);
  163. curl_setopt($ch, CURLOPT_VERBOSE, 1);
  164. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  165. curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
  166. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  167. curl_setopt($ch, CURLOPT_SSLVERSION, 6);
  168. if ($this->use_local_certs) {
  169. $crtFile = $this->getCrtFile('www.paypal.com');
  170. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
  171. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  172. curl_setopt($ch, CURLOPT_CAINFO, $crtFile);
  173. } else {
  174. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  175. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  176. }
  177. curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
  178. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
  179. curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']);
  180. $httpResponse = curl_exec($ch);
  181. if (!$httpResponse) {
  182. $i++;
  183. if ($i <= 5) {
  184. return $this->curlGet($url, $i);
  185. } else {
  186. return $httpResponse;
  187. }
  188. }
  189. return $httpResponse;
  190. }
  191. /**
  192. * paypal 可能发送多次IPN消息
  193. * 判断是否重复,如果不重复,把当前的插入。
  194. */
  195. /*
  196. protected function isNotDuplicate()
  197. {
  198. $ipn = $this->_ipnMessageModel->find()
  199. ->asArray()
  200. ->where([
  201. 'txn_id'=>$this->_postData['txn_id'],
  202. 'payment_status'=>$this->_postData['payment_status'],
  203. ])
  204. ->one();
  205. if (is_array($ipn) && !empty($ipn)) {
  206. return false;
  207. } else {
  208. $IpnMessage = new $this->_ipnMessageModelName();
  209. $IpnMessage->txn_id = $this->_postData['txn_id'];
  210. $IpnMessage->payment_status = $this->_postData['payment_status'];
  211. $IpnMessage->updated_at = time();
  212. $IpnMessage->save();
  213. return true;
  214. }
  215. }
  216. */
  217. /**
  218. * 验证订单数据是否被篡改。
  219. * 通过订单号找到订单,查看是否存在
  220. * 验证邮件地址,订单金额是否准确。
  221. */
  222. protected function isNotDistort()
  223. {
  224. //Yii::$app->mylog->log("begin isNotDistort..");
  225. $increment_id = $this->_postData['invoice'];
  226. $mc_gross = $this->_postData['mc_gross'];
  227. $mc_currency = $this->_postData['mc_currency'];
  228. if ($increment_id && $mc_gross && $mc_currency) {
  229. $this->_order = Yii::$service->order->getByIncrementId($increment_id);
  230. if ($this->_order) {
  231. $order_currency_code = $this->_order['order_currency_code'];
  232. if ($order_currency_code == $mc_currency) {
  233. // 核对订单总额
  234. $currentCurrencyGrandTotal = $this->_order['grand_total'];
  235. // if (round($currentCurrencyGrandTotal, 2) == round($mc_gross, 2)) {
  236. // 因为float精度问题,使用高精度函数进行比较,精度到2位小数
  237. if(bccomp($currentCurrencyGrandTotal, $mc_gross, 2) == 0){
  238. return true;
  239. } else {
  240. }
  241. } else {
  242. }
  243. }
  244. }
  245. return false;
  246. }
  247. /**
  248. * @param $orderstatus | String 订单状态
  249. * 根据接收的ipn消息,更改订单状态。
  250. */
  251. protected function updateOrderStatusByIpn($orderstatus = '')
  252. {
  253. $order_cancel_status = Yii::$service->order->payment_status_canceled;
  254. // 如果订单状态被取消,那么不能进行支付。
  255. if ($this->_order->order_status == $order_cancel_status) {
  256. Yii::$service->helper->error->add('The order status has been canceled and you can not pay for item ,you can create a new order to pay');
  257. return;
  258. }
  259. $updateArr = [];
  260. if ($this->_postData['txn_type']) {
  261. $updateArr['txn_type'] = $this->_postData['txn_type'];
  262. }
  263. if ($this->_postData['txn_id']) {
  264. $updateArr['txn_id'] = $this->_postData['txn_id'];
  265. }
  266. if ($this->_postData['payer_id']) {
  267. $updateArr['payer_id'] = $this->_postData['payer_id'];
  268. }
  269. if ($this->_postData['ipn_track_id']) {
  270. $updateArr['ipn_track_id'] = $this->_postData['ipn_track_id'];
  271. }
  272. if ($this->_postData['receiver_id']) {
  273. $updateArr['receiver_id'] = $this->_postData['receiver_id'];
  274. }
  275. if ($this->_postData['verify_sign']) {
  276. $updateArr['verify_sign'] = $this->_postData['verify_sign'];
  277. }
  278. if ($this->_postData['charset']) {
  279. $updateArr['charset'] = $this->_postData['charset'];
  280. }
  281. if ($this->_postData['mc_fee']) {
  282. $updateArr['payment_fee'] = $this->_postData['mc_fee'];
  283. $currency = $this->_postData['mc_currency'];
  284. $updateArr['base_payment_fee'] = Yii::$service->page->currency->getBaseCurrencyPrice($this->_postData['mc_fee'], $currency);
  285. }
  286. if ($this->_postData['payment_type']) {
  287. $updateArr['payment_type'] = $this->_postData['payment_type'];
  288. }
  289. if ($this->_postData['payment_date']) {
  290. $updateArr['paypal_order_datetime'] = date('Y-m-d H:i:s', $this->_postData['payment_date']);
  291. }
  292. if ($this->_postData['protection_eligibility']) {
  293. $updateArr['protection_eligibility'] = $this->_postData['protection_eligibility'];
  294. }
  295. $updateArr['updated_at'] = time();
  296. //$this->_order->updated_at = time();
  297. // 在service中不要出现事务代码,如果添加事务,请在调用层使用。
  298. //$innerTransaction = Yii::$app->db->beginTransaction();
  299. //try {
  300. // 可以更改的订单状态
  301. if ($orderstatus) {
  302. $updateArr['order_status'] = $orderstatus;
  303. $this->_order->updateAll(
  304. $updateArr,
  305. [
  306. 'and',
  307. ['order_id' => $this->_order['order_id']],
  308. ['in','order_status',$this->_allowChangOrderStatus]
  309. ]
  310. );
  311. // 指定了订单状态
  312. // $this->_order->order_status = $orderstatus;
  313. // $this->_order->save();
  314. // $payment_status = strtolower($this->_postData['payment_status']);
  315. // Yii::$app->mylog->log('save_'.$orderstatus);
  316. } else {
  317. $payment_status = strtolower($this->_postData['payment_status']);
  318. if ($payment_status == $this->payment_status_completed) {
  319. // paypal支付完成,将订单状态改成:收款已确认。
  320. // 只有存在于 $this->_allowChangOrderStatus 数组的状态,才允许更改,按照目前的设置,取消了的订单是不允许更改的
  321. $orderstatus = Yii::$service->order->payment_status_confirmed;
  322. $updateArr['order_status'] = $orderstatus;
  323. $updateColumn = $this->_order->updateAll(
  324. $updateArr,
  325. [
  326. 'and',
  327. ['order_id' => $this->_order['order_id']],
  328. ['in','order_status',$this->_allowChangOrderStatus]
  329. ]
  330. );
  331. //$this->_order->order_status = Yii::$service->order->payment_status_processing;
  332. // 更新订单信息
  333. //$this->_order->save();
  334. // 因为IPN消息可能不止发送一次,但是这里只允许一次,
  335. // 如果重复发送,$updateColumn 的更新返回值将为0
  336. if (!empty($updateColumn)) {
  337. Yii::$service->order->orderPaymentCompleteEvent($this->_order['increment_id']);
  338. // 上面的函数已经执行下面的代码,因此注释掉。
  339. // $orderInfo = Yii::$service->order->getOrderInfoByIncrementId($this->_order['increment_id']);
  340. // 发送新订单邮件
  341. // Yii::$service->email->order->sendCreateEmail($orderInfo);
  342. }
  343. } elseif ($payment_status == $this->payment_status_pending) {
  344. // pending 代表信用卡预付方式,需要等待paypal从信用卡中把钱扣除,因此订单状态是processing
  345. $orderstatus = Yii::$service->order->payment_status_processing;
  346. $updateArr['order_status'] = $orderstatus;
  347. $updateColumn = $this->_order->updateAll(
  348. $updateArr,
  349. [
  350. 'and',
  351. ['order_id' => $this->_order['order_id']],
  352. ['order_status' => Yii::$service->order->payment_status_pending]
  353. ]
  354. );
  355. } elseif ($payment_status == $this->payment_status_failed) {
  356. // 暂不处理
  357. } elseif ($payment_status == $this->payment_status_refunded) {
  358. // 暂不处理
  359. } else {
  360. // 暂不处理
  361. }
  362. }
  363. //$innerTransaction->commit();
  364. return true;
  365. //} catch (\Exception $e) {
  366. // $innerTransaction->rollBack();
  367. //}
  368. //return false;
  369. }
  370. // express 部分
  371. /**
  372. * @param $token | String , 通过 下面的 PPHttpPost5 方法返回的paypal express的token
  373. * @return String,通过token得到跳转的 paypal url,通过这个url跳转到paypal登录页面,进行支付的开始
  374. */
  375. public function getExpressCheckoutUrl($token)
  376. {
  377. if ($token) {
  378. //$webscrUrl = Yii::$service->payment->getExpressWebscrUrl($this->express_payment_method);
  379. if ($this->_env == Yii::$service->payment->env_sanbox) {
  380. $webscrUrl = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  381. } else {
  382. $webscrUrl = 'https://www.paypal.com/cgi-bin/webscr';
  383. }
  384. return $webscrUrl.'?cmd=_express-checkout&token='.urlencode($token);
  385. }
  386. }
  387. /**
  388. * @param $token | String , 通过 下面的 PPHttpPost5 方法返回的paypal standard的token
  389. * @return String,通过token得到跳转的 paypal url,通过这个url跳转到paypal登录页面,进行支付的开始
  390. */
  391. public function getStandardCheckoutUrl($token)
  392. {
  393. if ($token) {
  394. // $webscrUrl = Yii::$service->payment->getStandardWebscrUrl($this->standard_payment_method);
  395. if ($this->_env == Yii::$service->payment->env_sanbox) {
  396. $webscrUrl = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  397. } else {
  398. $webscrUrl = 'https://www.paypal.com/cgi-bin/webscr';
  399. }
  400. return $webscrUrl.'?useraction=commit&cmd=_express-checkout&token='.urlencode($token);
  401. }
  402. }
  403. /**
  404. * @param $methodName_ | String,请求的方法,譬如: $methodName_ = "SetExpressCheckout";
  405. * @param $nvpStr_ | String ,请求传递的购物车中的产品和总额部分的数据,组合成字符串的格式。
  406. * @param $i | Int , 限制递归次数的变量,当api获取失败的时候,可以通过递归的方式多次尝试,直至超过最大失败次数,才会返回失败
  407. * 此方法为获取token。返回的数据为数组,里面含有 ACK TOKEN 等值。
  408. * 也就是和paypal进行初次的api账号密码验证,成功后返回token等信息。
  409. */
  410. public function PPHttpPost5($methodName_, $nvpStr_, $i = 1)
  411. {
  412. $current_payment_method = Yii::$service->payment->getPaymentMethod();
  413. $API_NvpUrl = Yii::$service->payment->getStandardNvpUrl($this->standard_payment_method);
  414. $API_Signature = $this->_signature;
  415. $API_UserName = $this->_account;
  416. $API_Password = $this->_password;
  417. if ($this->_env == Yii::$service->payment->env_sanbox) {
  418. $API_NvpUrl = 'https://api-3t.sandbox.paypal.com/nvp';
  419. } else {
  420. $API_NvpUrl = 'https://api-3t.paypal.com/nvp';
  421. }
  422. if ($current_payment_method == $this->standard_payment_method) {
  423. $ipn_url = Yii::$service->payment->getStandardIpnUrl($this->standard_payment_method);
  424. } else {
  425. $ipn_url = Yii::$service->payment->getExpressIpnUrl($this->express_payment_method);
  426. }
  427. // Set the API operation, version, and API signature in the request.
  428. $nvpreq = "METHOD=$methodName_&PWD=$API_Password&USER=$API_UserName&SIGNATURE=$API_Signature$nvpStr_";
  429. $nvpreq .= "&PAYMENTREQUEST_0_NOTIFYURL=".urlencode($ipn_url);
  430. //echo $nvpreq;
  431. //\Yii::info($nvpreq, 'fecshop_debug');
  432. //exit;
  433. $ch = curl_init();
  434. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  435. curl_setopt($ch, CURLOPT_URL, $API_NvpUrl);
  436. curl_setopt($ch, CURLOPT_VERBOSE, 1);
  437. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  438. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  439. curl_setopt($ch, CURLOPT_POST, 1);
  440. curl_setopt($ch, CURLOPT_SSLVERSION, 6);
  441. if ($this->use_local_certs) {
  442. $crtFile = $this->getCrtFile('api-3t.paypal.com');
  443. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
  444. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  445. curl_setopt($ch, CURLOPT_CAINFO, $crtFile);
  446. } else {
  447. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  448. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  449. }
  450. // Set the request as a POST FIELD for curl.
  451. curl_setopt($ch, CURLOPT_POSTFIELDS, $nvpreq);
  452. curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
  453. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
  454. curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']);
  455. // Get response from the server.
  456. $httpResponse = curl_exec($ch);
  457. //echo "<br><br>%%%%%".$httpResponse."%%%%%<br><br>";
  458. if (!$httpResponse) {
  459. $i++;
  460. if ($i > 5) {
  461. //获取三次失败后,则推出。
  462. exit("$methodName_ failed: ".curl_error($ch).'('.curl_errno($ch).')');
  463. } else {
  464. $httpResponse = $this->PPHttpPost5($methodName_, $nvpStr_, $i);
  465. }
  466. } else {
  467. //第一次获取数据失败,则再次获取。并返回、
  468. if ($i > 0) {
  469. //return $httpResponse;
  470. }
  471. }
  472. //paypal返回的是一系列的字符串,譬如:L_TIMESTAMP0=2014-11-08T01:51:13Z&L_TIMESTAMP1=2014-11-08T01:40:41Z&L_TIMESTAMP2=2014-11-08T01:40:40Z&
  473. //下面要做的是先把字符串通过&字符打碎成数组
  474. //
  475. //echo "***************<br>";
  476. //echo urldecode($httpResponse);
  477. //echo "<br>***************<br>";
  478. $httpResponseAr = explode('&', urldecode($httpResponse));
  479. $httpParsedResponseAr = [];
  480. foreach ($httpResponseAr as $i => $value) {
  481. $tmpAr = explode('=', $value);
  482. if (sizeof($tmpAr) > 1) {
  483. $httpParsedResponseAr[$tmpAr[0]] = $tmpAr[1];
  484. }
  485. }
  486. if ((0 == sizeof($httpParsedResponseAr)) || !array_key_exists('ACK', $httpParsedResponseAr)) {
  487. exit("Invalid HTTP Response for POST request($nvpreq) to $API_NvpUrl.");
  488. }
  489. return $httpParsedResponseAr;
  490. }
  491. /**
  492. * @param $nvp_array | Array, 各个配置参数
  493. * 将数组里面的key和value,组合成url的字符串,生成nvp url
  494. */
  495. public function getRequestUrlStrByArray($nvp_array)
  496. {
  497. $str = '';
  498. if (is_array($nvp_array) && !empty($nvp_array)) {
  499. foreach ($nvp_array as $k=>$v) {
  500. $str .= '&'.urlencode($k).'='.urlencode($v);
  501. }
  502. }
  503. //echo $str;exit;
  504. return $str;
  505. }
  506. /**
  507. * 【paypal支付部分】api发送付款请求的参数部分
  508. * 通过该函数,将参数组合成字符串,为下一步api发送给paypal进行付款做准备
  509. */
  510. public function getCheckoutPaymentNvpStr($token)
  511. {
  512. $nvp_array = [];
  513. $nvp_array['PAYERID'] = $this->getPayerID();
  514. $nvp_array['TOKEN'] = $this->getToken();
  515. $nvp_array['PAYMENTREQUEST_0_PAYMENTACTION'] = 'Sale';
  516. $nvp_array['VERSION'] = $this->version;
  517. // https://developer.paypal.com/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/
  518. // 检查地址
  519. $nvp_array['ADDROVERRIDE'] = 0;
  520. //ADDROVERRIDE
  521. // 得到购物车的信息,通过购物车信息填写。
  522. $orderInfo = Yii::$service->order->getInfoByPaymentToken($token);
  523. //$cartInfo = Yii::$service->cart->getCartInfo(true);
  524. $currency = Yii::$service->page->currency->getCurrentCurrency();
  525. $grand_total = Yii::$service->helper->format->number_format($orderInfo['grand_total']);
  526. $subtotal = Yii::$service->helper->format->number_format($orderInfo['subtotal']);
  527. $shipping_total = Yii::$service->helper->format->number_format($orderInfo['shipping_total']);
  528. $discount_amount= Yii::$service->helper->format->number_format($orderInfo['subtotal_with_discount']);
  529. $subtotal = $subtotal - $discount_amount;
  530. $nvp_array['PAYMENTREQUEST_0_SHIPTOSTREET'] = $orderInfo['customer_address_street1'].' '.$orderInfo['customer_address_street2'];
  531. $nvp_array['PAYMENTREQUEST_0_SHIPTOCITY'] = $orderInfo['customer_address_city'];
  532. $nvp_array['PAYMENTREQUEST_0_SHIPTOSTATE'] = $orderInfo['customer_address_state_name'];
  533. $nvp_array['PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE'] = $orderInfo['customer_address_country'];
  534. $nvp_array['PAYMENTREQUEST_0_SHIPTOZIP'] = $orderInfo['customer_address_zip'];
  535. $nvp_array['PAYMENTREQUEST_0_SHIPTONAME'] = $orderInfo['customer_firstname'].' '.$orderInfo['customer_lastname'];
  536. $nvp_array['PAYMENTREQUEST_0_INVNUM'] = $orderInfo['increment_id'];
  537. $nvp_array['PAYMENTREQUEST_0_CURRENCYCODE'] = $currency;
  538. $nvp_array['PAYMENTREQUEST_0_AMT'] = $grand_total;
  539. $nvp_array['PAYMENTREQUEST_0_ITEMAMT'] = $subtotal;
  540. $nvp_array['PAYMENTREQUEST_0_SHIPPINGAMT'] = $shipping_total;
  541. if ($this->seller_email) {
  542. $nvp_array['PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID'] = $this->seller_email;
  543. }
  544. $i = 0;
  545. foreach ($orderInfo['products'] as $item) {
  546. $nvp_array['L_PAYMENTREQUEST_0_QTY'.$i] = $item['qty'];
  547. $nvp_array['L_PAYMENTREQUEST_0_NUMBER'.$i] = $item['sku'];
  548. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = Yii::$service->helper->format->number_format($item['price']);
  549. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = $item['name'];
  550. $nvp_array['L_PAYMENTREQUEST_0_CURRENCYCODE'.$i] = $currency;
  551. $i++;
  552. }
  553. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = 'Discount';
  554. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = '-'.$discount_amount;
  555. //var_dump($nvp_array);
  556. $nvpStr = $this->getRequestUrlStrByArray($nvp_array);
  557. //var_dump($nvpStr);
  558. return $nvpStr;
  559. }
  560. /**
  561. * 【paypal快捷支付部分】将参数组合成字符串。
  562. * 通过api token,从paypal获取用户在paypal保存的货运地址。
  563. */
  564. public function getExpressAddressNvpStr()
  565. {
  566. $nvp_array = [];
  567. $nvp_array['VERSION'] = Yii::$service->payment->paypal->version;
  568. $nvp_array['token'] = Yii::$service->payment->paypal->getToken();
  569. return $this->getRequestUrlStrByArray($nvp_array);
  570. }
  571. /**
  572. * @param $landingPage | String ,访问api的类型,譬如login
  573. * 【paypal快捷支付部分】通过购物车中的数据,组合成访问paypal express api的url
  574. * 这里返回的的字符串,是快捷支付部分获取token和payerId的参数。
  575. * 将返回的参数,传递给Yii::$service->payment->paypal->PPHttpPost5($methodName_, $nvpStr_)
  576. * 最终得到paypal express的token和payerId
  577. */
  578. public function getExpressTokenNvpStr($landingPage = 'Login', $return_url='', $cancel_url='')
  579. {
  580. $nvp_array = [];
  581. $nvp_array['LANDINGPAGE'] = $landingPage;
  582. if ($return_url) {
  583. $nvp_array['RETURNURL'] = $return_url;
  584. } else {
  585. $nvp_array['RETURNURL'] = Yii::$service->payment->getExpressReturnUrl($this->express_payment_method);
  586. }
  587. if ($cancel_url) {
  588. $nvp_array['CANCELURL'] = $cancel_url;
  589. } else {
  590. $nvp_array['CANCELURL'] = Yii::$service->payment->getExpressCancelUrl($this->express_payment_method);
  591. }
  592. $nvp_array['PAYMENTREQUEST_0_PAYMENTACTION'] = 'Sale';
  593. $nvp_array['VERSION'] = $this->version;
  594. // 得到购物车的信息,通过购物车信息填写。
  595. $cartInfo = Yii::$service->cart->getCartInfo(true);
  596. $currency = Yii::$service->page->currency->getCurrentCurrency();
  597. $grand_total = $cartInfo['grand_total'];
  598. $subtotal = $cartInfo['product_total'];
  599. $shipping_total = $cartInfo['shipping_cost'];
  600. $discount_amount = $cartInfo['coupon_cost'];
  601. $subtotal = $subtotal - $discount_amount;
  602. $nvp_array['PAYMENTREQUEST_0_CURRENCYCODE'] = $currency;
  603. $nvp_array['PAYMENTREQUEST_0_AMT'] = $grand_total;
  604. $nvp_array['PAYMENTREQUEST_0_ITEMAMT'] = $subtotal;
  605. $nvp_array['PAYMENTREQUEST_0_SHIPPINGAMT'] = $shipping_total;
  606. if ($this->seller_email) {
  607. $nvp_array['PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID'] = $this->seller_email;
  608. }
  609. $i = 0;
  610. foreach ($cartInfo['products'] as $item) {
  611. $nvp_array['L_PAYMENTREQUEST_0_QTY'.$i] = $item['qty'];
  612. $nvp_array['L_PAYMENTREQUEST_0_NUMBER'.$i] = $item['sku'];
  613. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = $item['product_price'];
  614. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = Yii::$service->store->getStoreAttrVal($item['name'], 'name');
  615. $nvp_array['L_PAYMENTREQUEST_0_CURRENCYCODE'.$i] = $currency;
  616. $i++;
  617. }
  618. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = 'Discount';
  619. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = '-'.$discount_amount;
  620. return $this->getRequestUrlStrByArray($nvp_array);
  621. }
  622. /**
  623. * @param $landingPage | String ,访问api的类型,譬如login
  624. * 【paypal标准支付部分】通过订单中的数据,组合成访问paypal api的url
  625. * 这里返回的的字符串,是标准支付部分获取token和payerId的参数。
  626. * 通过 $checkoutReturn = Yii::$service->payment->paypal->PPHttpPost5($methodName_, $nvpStr_);
  627. * 获取token和payerId的参数。($nvpStr_ 就是本函数的返回值)
  628. */
  629. public function getStandardTokenNvpStr($landingPage = 'Login', $return_url='', $cancel_url='')
  630. {
  631. $nvp_array = [];
  632. $nvp_array['LANDINGPAGE'] = $landingPage;
  633. if ($return_url) {
  634. $nvp_array['RETURNURL'] = $return_url;
  635. } else {
  636. $nvp_array['RETURNURL'] = Yii::$service->payment->getStandardReturnUrl('paypal_standard');
  637. }
  638. if ($cancel_url) {
  639. $nvp_array['CANCELURL'] = $cancel_url;
  640. } else {
  641. $nvp_array['CANCELURL'] = Yii::$service->payment->getStandardCancelUrl('paypal_standard');
  642. }
  643. $nvp_array['PAYMENTREQUEST_0_PAYMENTACTION'] = 'Sale';
  644. $nvp_array['VERSION'] = $this->version;
  645. // 得到购物车的信息,通过购物车信息填写。
  646. $orderInfo = Yii::$service->order->getCurrentOrderInfo();
  647. //var_dump($orderInfo);
  648. $currency = $orderInfo['order_currency_code'];
  649. $grand_total = Yii::$service->helper->format->number_format($orderInfo['grand_total']);
  650. $subtotal = Yii::$service->helper->format->number_format($orderInfo['subtotal']);
  651. $shipping_total = Yii::$service->helper->format->number_format($orderInfo['shipping_total']);
  652. $discount_amount= $orderInfo['subtotal_with_discount'] ? $orderInfo['subtotal_with_discount'] : 0;
  653. $subtotal = $subtotal - $discount_amount;
  654. $nvp_array['PAYMENTREQUEST_0_CURRENCYCODE'] = $currency;
  655. $nvp_array['PAYMENTREQUEST_0_AMT'] = $grand_total;
  656. $nvp_array['PAYMENTREQUEST_0_ITEMAMT'] = $subtotal;
  657. $nvp_array['PAYMENTREQUEST_0_SHIPPINGAMT'] = $shipping_total;
  658. if ($this->seller_email) {
  659. $nvp_array['PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID'] = $this->seller_email;
  660. }
  661. $i = 0;
  662. foreach ($orderInfo['products'] as $item) {
  663. $nvp_array['L_PAYMENTREQUEST_0_QTY'.$i] = $item['qty'];
  664. $nvp_array['L_PAYMENTREQUEST_0_NUMBER'.$i] = $item['sku'];
  665. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = Yii::$service->helper->format->number_format($item['price']);
  666. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = $item['name'];
  667. ;
  668. $nvp_array['L_PAYMENTREQUEST_0_CURRENCYCODE'.$i] = $currency;
  669. $i++;
  670. }
  671. $nvp_array['L_PAYMENTREQUEST_0_NAME'.$i] = 'Discount';
  672. $nvp_array['L_PAYMENTREQUEST_0_AMT'.$i] = '-'.$discount_amount;
  673. //var_dump($nvp_array);
  674. //exit;
  675. return $this->getRequestUrlStrByArray($nvp_array);
  676. }
  677. /**
  678. * 从get参数里得到paypal支付的token
  679. */
  680. public function getToken()
  681. {
  682. if (!$this->token) {
  683. $token = Yii::$app->request->get('token');
  684. if (!$token) {
  685. $token = Yii::$app->request->post('token');
  686. }
  687. $token = \Yii::$service->helper->htmlEncode($token);
  688. if ($token) {
  689. $this->token = $token;
  690. }
  691. }
  692. return $this->token;
  693. }
  694. /**
  695. * 从get参数里得到paypal支付的PayerID
  696. */
  697. public function getPayerID()
  698. {
  699. if (!$this->payerID) {
  700. $payerID = Yii::$app->request->get('PayerID');
  701. if (!$payerID) {
  702. $payerID = Yii::$app->request->post('PayerID');
  703. }
  704. $payerID = \Yii::$service->helper->htmlEncode($payerID);
  705. if ($payerID) {
  706. $this->payerID = $payerID;
  707. }
  708. }
  709. return $this->payerID;
  710. }
  711. /**
  712. * @param $doCheckoutReturn | Array ,
  713. * paypal付款状态提交后,更新订单的支付部分的信息。
  714. */
  715. public function updateOrderPayment($doCheckoutReturn, $token)
  716. {
  717. if ($doCheckoutReturn) {
  718. $order = Yii::$service->order->getByPaymentToken($token);
  719. $order_cancel_status = Yii::$service->order->payment_status_canceled;
  720. // 如果订单状态被取消,那么不能进行支付。
  721. if ($order['order_status'] == $order_cancel_status) {
  722. Yii::$service->helper->errors->add('The order status has been canceled and you can not pay for item ,you can create a new order to pay');
  723. return false;
  724. }
  725. $updateArr = [];
  726. if ($order['increment_id']) {
  727. //echo 'bbb';
  728. $updateArr['txn_id'] = $doCheckoutReturn['PAYMENTINFO_0_TRANSACTIONID'];
  729. $updateArr['txn_type'] = $doCheckoutReturn['PAYMENTINFO_0_TRANSACTIONTYPE'];
  730. $PAYMENTINFO_0_AMT = $doCheckoutReturn['PAYMENTINFO_0_AMT'];
  731. $updateArr['payment_fee'] = $doCheckoutReturn['PAYMENTINFO_0_FEEAMT'];
  732. $currency = $doCheckoutReturn['PAYMENTINFO_0_CURRENCYCODE'];
  733. $updateArr['base_payment_fee'] = Yii::$service->page->currency->getBaseCurrencyPrice($updateArr['payment_fee'], $currency);
  734. $updateArr['payer_id'] = $this->getPayerID();
  735. $updateArr['correlation_id'] = $doCheckoutReturn['CORRELATIONID'];
  736. $updateArr['protection_eligibility'] = $doCheckoutReturn['PAYMENTINFO_0_PROTECTIONELIGIBILITY'];
  737. $updateArr['protection_eligibility_type'] = $doCheckoutReturn['PAYMENTINFO_0_PROTECTIONELIGIBILITYTYPE'];
  738. $updateArr['secure_merchant_account_id'] = $doCheckoutReturn['PAYMENTINFO_0_SECUREMERCHANTACCOUNTID'];
  739. $updateArr['build'] = $doCheckoutReturn['BUILD'];
  740. $updateArr['payment_type'] = $doCheckoutReturn['PAYMENTINFO_0_PAYMENTTYPE'];
  741. $updateArr['paypal_order_datetime'] = date('Y-m-d H:i:s', $doCheckoutReturn['PAYMENTINFO_0_ORDERTIME']);
  742. $PAYMENTINFO_0_PAYMENTSTATUS = $doCheckoutReturn['PAYMENTINFO_0_PAYMENTSTATUS'];
  743. $updateArr['updated_at'] = time();
  744. if (
  745. strtolower($PAYMENTINFO_0_PAYMENTSTATUS) == $this->payment_status_completed
  746. ||
  747. strtolower($PAYMENTINFO_0_PAYMENTSTATUS) == $this->payment_status_processed
  748. ) {
  749. $order_status = Yii::$service->order->payment_status_confirmed;
  750. if ($currency == $order['order_currency_code'] && $PAYMENTINFO_0_AMT == $order['grand_total']) {
  751. $updateArr['order_status'] = $order_status;
  752. $updateColumn = $order->updateAll(
  753. $updateArr,
  754. [
  755. 'and',
  756. ['order_id' => $order['order_id']],
  757. ['in','order_status',$this->_allowChangOrderStatus]
  758. ]
  759. );
  760. // 因为IPN消息可能不止发送一次,但是这里只允许一次,
  761. // 如果重复发送,$updateColumn 的更新返回值将为0
  762. if (!empty($updateColumn)) {
  763. // 执行订单支付成功后的事情。
  764. Yii::$service->order->orderPaymentCompleteEvent($order['increment_id']);
  765. // 上面的函数已经执行下面的代码,因此注释掉。
  766. // $orderInfo = Yii::$service->order->getOrderInfoByIncrementId($order['increment_id']);
  767. // Yii::$service->email->order->sendCreateEmail($orderInfo);
  768. }
  769. return true;
  770. } else {
  771. // 金额不一致,判定为欺诈
  772. Yii::$service->helper->errors->add('The amount of payment is inconsistent with the amount of the order');
  773. $order_status = Yii::$service->order->payment_status_suspected_fraud;
  774. $updateArr['order_status'] = $order_status;
  775. $updateColumn = $order->updateAll(
  776. $updateArr,
  777. [
  778. 'and',
  779. ['order_id' => $order['order_id']],
  780. ['in','order_status',$this->_allowChangOrderStatus]
  781. ]
  782. );
  783. }
  784. } elseif (strtolower($PAYMENTINFO_0_PAYMENTSTATUS) == $this->payment_status_pending) {
  785. // 这种情况代表paypal 信用卡预售,需要等待一段时间才知道是否收到钱
  786. $order_status = Yii::$service->order->payment_status_processing;
  787. if ($currency == $order['order_currency_code'] && $PAYMENTINFO_0_AMT == $order['grand_total']) {
  788. $updateArr['order_status'] = $order_status;
  789. $updateColumn = $order->updateAll(
  790. $updateArr,
  791. [
  792. 'and',
  793. ['order_id' => $order['order_id']],
  794. ['order_status' => Yii::$service->order->payment_status_pending]
  795. ]
  796. );
  797. // 这种情况并没有接收到paypal的钱,只是一种支付等待状态,
  798. // 因此,对于这种支付状态,视为正常订单,但是没有支付成功,需要延迟等待,如果支付成功,paypal会继续发送IPN消息。
  799. return true;
  800. } else {
  801. // 金额不一致,判定为欺诈
  802. Yii::$service->helper->errors->add('The amount of payment is inconsistent with the amount of the order');
  803. $order_status = Yii::$service->order->payment_status_suspected_fraud;
  804. $updateArr['order_status'] = $order_status;
  805. $updateColumn = $order->updateAll(
  806. $updateArr,
  807. [
  808. 'and',
  809. ['order_id' => $order['order_id']],
  810. ['in','order_status',$this->_allowChangOrderStatus]
  811. ]
  812. );
  813. }
  814. } else {
  815. Yii::$service->helper->errors->add('paypal payment is not complete , current payment status is {payment_status}', ['payment_status' => $PAYMENTINFO_0_PAYMENTSTATUS]);
  816. }
  817. } else {
  818. Yii::$service->helper->errors->add('current order is not exist');
  819. }
  820. } else {
  821. Yii::$service->helper->errors->add('CheckoutReturn is empty');
  822. }
  823. return false;
  824. }
  825. }