Ecshop自带有一个PayPal标准支付模块,只需要在后台安装并设置PayPal帐号即可使用。但是这个Ecshop v2.7.2这个版本的PayPal标准支付是有bug的,当在PayPal设置自动返回后,就会出现订单在返回自己网站后明明已支付成功,却显示支付失败的问题。但只要是手动从PayPal返回的,却一切正常。
刚开始一直不知道是哪里出了问题。仔细分析了一下PayPal的标准支付流程后就可以很容易的找到问题的症结所在了。首先在自己的网站需要生成一个包含购物车信息的表单,用来提交到PayPal。在includes/modules/payment/paypal.php文件中的如下代码:

  1. /**
  2.      * 生成支付代码
  3.      * @param   array   $order  订单信息
  4.      * @param   array   $payment    支付方式信息
  5.      */
  6.     function get_code($order, $payment)
  7.     {
  8.         $data_order_id      = $order['log_id'];
  9.         $data_amount        = $order['order_amount'];
  10.         $data_return_url    = return_url(basename(__FILE__, '.php'));
  11.         $data_pay_account   = $payment['paypal_account'];
  12.         $currency_code      = $payment['paypal_currency'];
  13.         $data_notify_url    = return_url(basename(__FILE__, '.php'));
  14.         $cancel_return      = $GLOBALS['ecs']->url();
  15.  
  16.         $def_url  = '<br /><form style="text-align:center;" action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">' .   // 不能省略
  17.             "<input type='hidden' name='cmd' value='_xclick'>" .                             // 不能省略
  18.             "<input type='hidden' name='business' value='$data_pay_account'>" .                 // 贝宝帐号
  19.             "<input type='hidden' name='item_name' value='$order[order_sn]'>" .                 // payment for
  20.             "<input type='hidden' name='amount' value='$data_amount'>" .                        // 订单金额
  21.             "<input type='hidden' name='currency_code' value='$currency_code'>" .            // 货币
  22.             "<input type='hidden' name='return' value='$data_return_url'>" .                    // 付款后页面
  23.             "<input type='hidden' name='invoice' value='$data_order_id'>" .                      // 订单号
  24.             "<input type='hidden' name='charset' value='utf-8'>" .                              // 字符集
  25.             "<input type='hidden' name='no_shipping' value='1'>" .                              // 不要求客户提供收货地址
  26.             "<input type='hidden' name='no_note' value=''>" .                                  // 付款说明
  27.             "<input type='hidden' name='notify_url' value='$data_notify_url'>" .
  28.             "<input type='hidden' name='rm' value='2'>" .
  29.             "<input type='hidden' name='cancel_return' value='$cancel_return'>" .
  30.             "<input type='submit' value='" . $GLOBALS['_LANG']['paypal_button'] . "'>" .                      // 按钮
  31.             "</form><br />";
  32.  
  33.         return $def_url;
  34.     }

当该表单提交到PayPal后,客户可在PayPal平台完成支付。当客户完成支付,PayPal会立即post一个表单到购物的站点,具体的返回地址就是刚才那个表单中的notify_url的值。而客户在返回购物网站的时候有两种可能,如果卖家的PayPal帐号设置了自动返回,那么支付完成后将在10秒内自动跳转到购物网站,而这个跳转是没有post那些必要的返回信息的。另一种情况,就是卖家没有设置自动返回,这是在客户点击跳转会购物网站的页面如果用firebug查看一下,是可以看到一个post的表单的,里面包含了所以必须的信息。而Ecshop的PayPal标准支付模块的bug恰恰就是没有考虑到返回的这个差异。
再回过头来看看paypal.php的相关代码,问题就一目了然了,首先上述表单中的return_url是用户完成支付后返回网站时显示给用户看的页面,notify_url的值是用户完成支付时,PayPal用来post相关信息的地址。那么在get_code($order, $payment)这个方法中,不难发现这两个地址是相同的。
那么在继续看paypal.php中用来处理返回信息的代码。代码如下:

  1.   /**
  2.      * 响应操作
  3.      */
  4.     function respond()
  5.     {
  6.         $payment        = get_payment('paypal');
  7.         $merchant_id    = $payment['paypal_account'];               ///获取商户编号
  8.  
  9.         // read the post from PayPal system and add 'cmd'
  10.         $req = 'cmd=_notify-validate';
  11.         foreach ($_POST as $key => $value)
  12.         {
  13.             $value = urlencode(stripslashes($value));
  14.             $req .= "&$key=$value";
  15.         }
  16.  
  17.         // post back to PayPal system to validate
  18.         $header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
  19.         $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
  20.         $header .= "Content-Length: " . strlen($req) ."\r\n\r\n";
  21.         $fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
  22.  
  23.         // assign posted variables to local variables
  24.         $item_name = $_POST['item_name'];
  25.         $item_number = $_POST['item_number'];
  26.         $payment_status = $_POST['payment_status'];
  27.         $payment_amount = $_POST['mc_gross'];
  28.         $payment_currency = $_POST['mc_currency'];
  29.         $txn_id = $_POST['txn_id'];
  30.         $receiver_email = $_POST['receiver_email'];
  31.         $payer_email = $_POST['payer_email'];
  32.         $order_sn = $_POST['invoice'];
  33.         $memo = !empty($_POST['memo']) ? $_POST['memo'] : '';
  34.         $action_note = $txn_id . '(' . $GLOBALS['_LANG']['paypal_txn_id'] . ')' . $memo;
  35.  
  36.         if (!$fp)
  37.         {
  38.             fclose($fp);
  39.  
  40.             return false;
  41.         }
  42.         else
  43.         {
  44.             fputs($fp, $header . $req);
  45.             while (!feof($fp))
  46.             {
  47.                 $res = fgets($fp, 1024);
  48.                 if (strcmp($res, 'VERIFIED') == 0)
  49.                 {
  50.                     // check the payment_status is Completed
  51.                     if ($payment_status != 'Completed' && $payment_status != 'Pending')
  52.                     {
  53.                         fclose($fp);
  54.                         return false;
  55.                     }
  56.  
  57.                     // check that txn_id has not been previously processed
  58.                     /*$sql = "SELECT COUNT(*) FROM " . $GLOBALS['ecs']->table('order_action') . " WHERE action_note LIKE '" . mysql_like_quote($txn_id) . "%'";
  59.                     if ($GLOBALS['db']->getOne($sql) > 0)
  60.                     {
  61.                         fclose($fp);
  62.  
  63.                         return false;
  64.                     }*/
  65.  
  66.                     // check that receiver_email is your Primary PayPal email
  67.                     if ($receiver_email != $merchant_id)
  68.                     {
  69.                         fclose($fp);
  70.                         return false;
  71.                     }
  72.                     // check that payment_amount/payment_currency are correct
  73.                     $sql = "SELECT order_amount FROM " . $GLOBALS['ecs']->table('pay_log') . " WHERE log_id = '$order_sn'";
  74.                     if ($GLOBALS['db']->getOne($sql) != $payment_amount)
  75.                     {
  76.                         fclose($fp);
  77.  
  78.                         return false;
  79.                     }
  80.                     if ($payment['paypal_currency'] != $payment_currency)
  81.                     {
  82.                         fclose($fp);
  83.  
  84.                         return false;
  85.                     }
  86.  
  87.                     // process payment
  88.                     order_paid($order_sn, PS_PAYED, $action_note);
  89.                     fclose($fp);
  90.  
  91.                     return true;
  92.                 }
  93.                 elseif (strcmp($res, 'INVALID') == 0)
  94.                 {
  95.                     // log for manual investigation
  96.                     fclose($fp);
  97.                     return false;
  98.                 }
  99.             }
  100.         }
  101.     }

接下来的流程是在接到PayPal post过来的表单后,直接在拼接上cmd=_notify-validate,然后在发回PayPal的服务器进行验证,再对PayPal返回的信息进行逐行匹配,如果发现有’VERIFIED’就表示整个支付完成。所以整个流程并不复杂,当客户完成支付,PayPal会立即post一个表单到notify_url的这个地址,也就会先执行一次respond()这个方法,在这个时候其实网站后台对于订单数据的操作已经完成了。当客户在手动返回的时候又要同样有一个post表单过来,所以会重复执行一遍respond(),当然结果会显示successfully,而当客户是自动返回购物网站的,由于自动返回并没有post过来任何表单,那么拼接上cmd=_notify-validate再发回PayPal服务器验证一定是失败的,所以会显示支付失败,而实际情况是支付成功的。
至此,问题已经很清楚了,具体的解决方案就不赘述了

发表评论

电子邮件地址不会被公开。 必填项已用*标注