关于分析Ecshop自带PayPal标准支付模块支付失败的原因

2016-09-11 20:38 来源:www.chinab4c.com 作者:ecshop专家

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

/**
* 生成支付代码
* @param   array   $order  订单信息
* @param   array   $payment    支付方式信息
*/
function get_code($order, $payment)
{
$data_order_id      = $order['log_id'];
$data_amount        = $order['order_amount'];
$data_return_url    = return_url(basename(__FILE__, '.php'));
$data_pay_account   = $payment['paypal_account'];
$currency_code      = $payment['paypal_currency'];
$data_notify_url    = return_url(basename(__FILE__, '.php'));
$cancel_return      = $GLOBALS['ecs']->url();

$def_url  = '
' .   // 不能省略
"" .                             // 不能省略
"" .                 // 贝宝帐号
"" .                 // payment for
"" .                        // 订单金额
"" .            // 货币
"" .                    // 付款后页面
"" .                      // 订单号
"" .                              // 字符集
"" .                              // 不要求客户提供收货地址
"" .                                  // 付款说明
"" .
"" .
"" .
"" .                      // 按钮
"
";

return $def_url;
}
当该表单提交到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中用来处理返回信息的代码。代码如下:

/**
* 响应操作
*/
function respond()
{
$payment        = get_payment('paypal');
$merchant_id    = $payment['paypal_account'];               ///获取商户编号

// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value)
{
$value = urlencode(stripslashes($value));
$req .= "&$key=$value";
}

// post back to PayPal system to validate
$header = "POST /cgi-bin/webscr HTTP/1.0\\r\\n";
$header .= "Content-Type: application/x-www-form-urlencoded\\r\\n";
$header .= "Content-Length: " . strlen($req) ."\\r\\n\\r\\n";
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);

// assign posted variables to local variables
$item_name = $_POST['item_name'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_currency = $_POST['mc_currency'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$payer_email = $_POST['payer_email'];
$order_sn = $_POST['invoice'];
$memo = !empty($_POST['memo']) ? $_POST['memo'] : '';
$action_note = $txn_id . '(' . $GLOBALS['_LANG']['paypal_txn_id'] . ')' . $memo;

if (!$fp)
{
fclose($fp);

return false;
}
else
{
fputs($fp, $header . $req);
while (!feof($fp))
{
$res = fgets($fp, 1024);
if (strcmp($res, 'VERIFIED') == 0)
{
// check the payment_status is Completed
if ($payment_status != 'Completed' && $payment_status != 'Pending')
{
fclose($fp);

return false;
}

// check that txn_id has not been previously processed
/*$sql = "SELECT COUNT(*) FROM " . $GLOBALS['ecs']->table('order_action') . " WHERE action_note LIKE '" . mysql_like_quote($txn_id) . "%'";
if ($GLOBALS['db']->getOne($sql) > 0)
{
fclose($fp);

return false;
}*/

// check that receiver_email is your Primary PayPal email
if ($receiver_email != $merchant_id)
{
fclose($fp);

return false;
}

// check that payment_amount/payment_currency are correct
$sql = "SELECT order_amount FROM " . $GLOBALS['ecs']->table('pay_log') . " WHERE log_id = '$order_sn'";
if ($GLOBALS['db']->getOne($sql) != $payment_amount)
{
fclose($fp);

return false;
}
if ($payment['paypal_currency'] != $payment_currency)
{
fclose($fp);

return false;
}

// process payment
order_paid($order_sn, PS_PAYED, $action_note);
fclose($fp);

return true;
}
elseif (strcmp($res, 'INVALID') == 0)
{
// log for manual investigation
fclose($fp);

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