Better Late Than Never.

PayPal支付、同步、异步回调以及退款的实现

因为工作需要,需要对网站接入PayPal的支付方式。再整个过程中碰到的坑已经是无力吐槽了。但到最后实现了整个流程之后发现步骤其实并不繁琐,只是想埋怨下PayPal的官方文档(对外国开发者这么不友好吗,~_~怪自己英文不好,阅读障碍…)和他们提供的沙箱环境(访问慢,尽管已经是科学上网)。好啦,不说那么多了。接下来就和大家说说如何以最快的方式将PayPal集成到网站中,实现支付、回调、退款以及验证。
  

已写成packages,github

step1 获取数据

注册PayPal的账户,升级为商户账户(不需要任何审核)。进入开发者平台,创建APP应用

此时需要用到的数据:用于收款的账号、Client ID、Secret

step2 支付

好了,终于拿到我们需要的数据了,现在进行我们的支付
创建HTML表单提交数据到PayPal进行支付

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Paypal订单支付</title>
</head>
<body>
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="POST"  name="form_starPay"> <!-- // Live https://www.paypal.com/cgi-bin/webscr -->
    <input type='hidden' name='cmd' value='{{ $parameter['cmd'] }}'>  <!-- //告诉paypal该表单是立即购买 -->
    <input type='hidden' name='business' value='{{ $parameter['business'] }}'> <!-- //卖家帐号 也就是收钱的帐号 -->
    <input type='hidden' name='item_name' value='{{ uniqid() }}'> <!-- //商品名称 item_number -->
    <input type='hidden' name='item_number' value='{{ uniqid() }}'> <!-- //物品号 item_number -->
    <input type='hidden' name='amount' value='{{ $parameter['amount'] }}'> <!-- .// 订单金额 -->
    <input type='hidden' name='currency_code' value='{{ $parameter['currency_code'] }}'> <!-- .// 货币 -->
    <input type='hidden' name='return' value='{{ $parameter['return'] }}'> <!-- .// 支付成功后网页跳转地址 -->
    <input type='hidden' name='notify_url' value='http://hezhizheng.com'> <!-- .//支付成功后paypal后台发送订单通知地址 -->
    <input type='hidden' name='cancel_return' value='http://hezhizheng.com'> <!-- .//用户取消交易返回地址 -->
    <input type='hidden' name='invoice' value='111111111'> <!-- .//自定义订单号 -->
    <input type='hidden' name='charset' value='utf-8'> <!-- .// 字符集 -->
    <input type='hidden' name='no_shipping' value='1'> <!-- .// 不要求客户提供收货地址 -->
    <input type='hidden' name='no_note' value='1'> <!-- .// 付款说明 -->
    <input type='hidden' name='rm' value='{{ $parameter['rm'] }}'> <!-- 不知道是什么 -->
    <input type="image" name="submit"   src="https://www.paypal.com/en_US/i/btn/btn_buynow_LG.gif" />
</form>
正在跳转Paypal支付,请稍等。。。
<script>
    function sub(){
        document.form_starPay.submit();
    }
    onload(sub())
</script>
</body>
</html>
变量说明:
business 您的PayPal账户上的电子邮件地址
quantity 物品数量。大于 1 时,会与金额相乘
item_name 物品名称(或购物车名称)。必须是字母数字字符,最多为 127 个字符
item_number 用于跟踪付款的可选传递变量。必须是字母数字字符,最多为 127 个字符
amount 物品的价格(购物车中所有物品的总价格)
shipping 该物品的运送成本
shipping2 每增加一件物品所需的运送成本
handling 手续费
tax 基于交易的税额。如果使用该变量,传递值将覆盖所有用户信息税收设置(不管买家所在位置)。
no_shipping 送货地址。如果设为 "1",则不会要求您的客户提供送货地址。该变量为可选项;如果省略或设为 "0",将提示您的客户输入送货地址
cn 可选标签,会在提示栏上显示(最多 40 个字符)
no_note 为付款加入提示。如果设为 "1",则不会提示您的客户输入提示。该变量为可选项;如果省略或设为 "0",将提示您的客户输入提示。
on0 第一选项栏名称。最多 64 个字符
os0 第一组选项值。最多 200 个字符。"on0" 必须定义,以便识别 "os0"。
on1 第二选项栏名称。最多 64 个字符
os1 第二组选项值。最多 200 个字符。"on1" 必须定义,以便识别 "os1"。
custom 决不会向您的客户显示的可选转递变量。可用于跟踪存货
invoice 决不会向您的客户显示的可选转递变量。可用于跟踪账单号
notify_url 仅与 IPN 一起使用。发送 IPN Form Post 的互联网 URL
return 您的客户完成付款后将返回的互联网 URL
cancel_return 您的客户取消付款后将返回的互联网 URL
image_url 您要用作图标的图片的互联网 URL,图片大小为 150 X 50 像素
cs 设置您的付款页面的背景色。如果设为 "1",背景色将为黑色。该变量为可选项;如果省略或设为 "0",背景色将为白色

在游览器中打开上述代码,如无意外会跳转到PayPal界面,登录你沙箱账户中的买家账号你会看到如下界面

点击Pay Now完成支付。

step3 异步回调

这里不对同步回调做说明,一般用户支付完成直接返回到回我们设置的“return”值(如需验证,参照异步回调即可),这里不对IPN的设置做说明(亲测不设置IPN,回调一样能成功,前提必须是外网可以访问的到)。

    /**
     * 异步回调处理
     * @return string
     */
    public function payPalNotifyCallback()
    {
        $verify = $this->verifyNotify();
        if ($verify) {
            return 'success';
        } else {
            return 'fail';
        }
    }

    /**
     * 验证异步回调数据
     * @return bool
     */
    private function verifyNotify()
    {
        if (!request()->isMethod('POST')) die();
        $post = request()->all();

        $order_sn = $post['invoice']; //收取订单号

        $url = (new PayPalFactory)->getRequestUrl();

        $data['cmd'] = '_notify-validate'; //增加cmd参数,
        foreach ($post as $key => $item) $data[$key] = ($item);  //如果数据验证失败,请在这里将参数urlencode

        $res = $this->http($url, $data, 'POST');

        if (!empty($res)) {
            if (strcmp($res, "VERIFIED") == 0) {

                if ($post['payment_status'] == 'Completed') {
                    //付款完成,这里修改订单状态
                    return true;
                }
            } elseif (strcmp($res, "INVALID") == 0) {
                //未通过认证,有可能是编码错误或非法的 POST 信息
                return false;
            }
        } else {
            //未通过认证,有可能是编码错误或非法的 POST 信息

            return false;

        }
        return false;
    }

    /**
     * 模拟提交参数,支持https提交 可用于各类api请求
     * @param string $url : 提交的地址
     * @param array $data : POST数组
     * @param string $method : POST/GET,默认GET方式
     * @return mixed
     */
    private function http($url, $data = [], $method = 'GET')
    {
        $curl = curl_init(); // 启动一个CURL会话
        curl_setopt($curl, CURLOPT_URL, $url); // 要访问的地址
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 对认证证书来源的检查
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
        curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); // 模拟用户使用的浏览器
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 使用自动跳转
        curl_setopt($curl, CURLOPT_AUTOREFERER, 1); // 自动设置Referer
        if ($method == 'POST') {
            curl_setopt($curl, CURLOPT_POST, 1); // 发送一个常规的Post请求
            if ($data != '') {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data); // Post提交的数据包
            }
        }
        curl_setopt($curl, CURLOPT_TIMEOUT, 30); // 设置超时限制防止死循环
        curl_setopt($curl, CURLOPT_HEADER, 0); // 显示返回的Header区域内容
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 获取的信息以文件流的形式返回
        $tmpInfo = curl_exec($curl); // 执行操作
        curl_close($curl); // 关闭CURL会话
        return $tmpInfo; // 返回数据
    }

到这里,完成了我们的回调处理。

step4 退款

这里使用PayPal的官方的一个composer包 "paypal/rest-api-sdk-php": "1.7.4",这里不做安装说明。

    /**
     *
     * 根据异步回调的txn_id进行退款操作
     * @param Order $order
     * @return mixed
     */
    public function payPalRefundForTxnId()
    {
        try {
                    $txn_id = ‘这个值在回调的时候获取到’;
            $apiContext = new ApiContext(
                new OAuthTokenCredential(
                    ‘client_id’,
                    ‘client_secret’
                ));  // 这里是我们第一步拿到的数据
            // $apiContext->setConfig(['mode' => 'live']);  // live下设置

            $amt = new Amount();
            $amt->setCurrency('USD')
                ->setTotal(100);  // 退款的费用

            $refund = new Refund();
            $refund->setAmount($amt);

            $sale = new Sale();
            $sale->setId($txn_id);
            $refundedSale = $sale->refund($refund, $apiContext);

        } catch (\Exception $e) {
            // PayPal无效退款
            return json_decode(json_encode(['message' => $e->getMessage(), 'code' => $e->getCode(), 'state' => $e->getMessage()]));  // to object
        }
       // 退款完成
        return $refundedSale; 
    }

到此,PayPal的支付->回调->退款都已实现,有疑问可在下方留言。希望可以帮到大家。:smiley:

-- END

写的不错,赞助一下主机费

扫一扫,用支付宝赞赏
扫一扫,用微信赞赏
2018-02-08 15:39

没明白过来呀,大哥

hezhizheng #2
2018-02-08 16:11

@jerry 哪个部分没明白,不然我也没法回答你

2018-02-23 16:11

大哥看不懂。。。。你那些key在哪里设置

2018-02-26 11:46

@hezhizheng 博主好,退款的时候报 Request was refused.You can not refund this type of transaction 错误

hezhizheng #5
2018-02-26 22:02

遇到到问题的请描述好你的问题,不然也没法回答你们:relieved:
请尽量留下你们真实的邮箱地址,以便接受回复通知,谢谢来访 :smiley:

2018-03-14 15:04

楼主,最近我也遇到一个商城需要对接到paypal,能加个好友交流一下吗

2018-03-14 16:45

@hezhizheng 可以加一下好友交流一下吗,最近有个项目需要用到PayPal

hezhizheng #8
2018-03-17 07:02

@qianyu1024 貌似大家都喜欢直接加QQ:joy: 博客的联系邮箱即是本人QQ 添加时请备注 谢谢!

2018-03-29 16:13

我想问一下,上线后,Client ID、Secret应该在哪里获取?

hezhizheng #10
2018-03-29 21:01

@lsj 自己创建的APP中有Live 跟 Sandbox 两种 , 切换到Live 就可以看到“上线“ Client ID、Secret

2018-07-02 16:09

想问下,我做的_xclick-subscriptions,订阅类,假如用户取消了订阅,我的后台如何去验证他的会员到期。

hezhizheng #12
2018-07-02 20:54

@Jarvis 不好意思,这块没了解过

2018-07-10 15:51

(new PayPalFactory)->getRequestUrl()
这块 没问题吗???

2018-08-11 00:24

政哥666啊

hezhizheng #15
2018-08-11 22:40

@吴智 尴尬了:cold_sweat:

2018-08-27 15:13

我想问下如果集成html那种paypal支付是不是支付回调再html那里处理啊 需要后台设置支付回调吗 还有楼主这种应该是服务器模式吧 这种支付回调地址是每一次支付传过去的 是不是后台不需要像支付宝微信那样设置支付回调呢

hezhizheng #17
2018-08-27 21:29

@bug 回调地址直接在html中设置,不需要像支付宝/微信那样在后台设置

2018-08-28 17:13

我按照你这写法支付异步 验证接收不到返回值啊 不知道为什么

hezhizheng #19
2018-10-20 22:51

@bug 保证异步的地址是外网可以访问到的

2018-12-18 18:36

你好,在回调中我请求'https://www.paypal.com/cgi-bin/webscr';进行IPN,校验数据,返回
Access Denied
You don\'t have permission to access "http://www.paypal.com/cgi-bin/webscr" on this server.
Reference #18.8680317.1545128201.18d81072
是什么原因呢

hezhizheng #21
2018-12-19 23:03

@zane 你这个不是上面方法中出现的问题吧,暂时没时间测试 :neutral_face:

2018-12-20 16:55

哥们,我是使用paypal的php sdk,退款的时候,我看文档说的是如果部分退款就要写金额,如果全额退款,就不用Amont对象,我两种情况都试过,都不行,

  $validator = Validator::make($request->all(), [
            // 'sale_id' => 'required|string|min:2|max:64',
            'currency' => 'nullable|in:EUR,USD',
            // 'total' => 'required|numeric|min:0',
        ]);
        if ($validator->fails()) {
            $error = $validator->errors()->first();
            return $this->outPutJson('', 201, $error);
        }

        $currency = request('currency', 'EUR');
        $total = request('total', 0.01);
        $sale_id = request('sale_id', '4EY4029291028401A');
       #全额退款,无需Amount对象
        // $amt = new Amount();
        // $amt->setTotal($total)
        //     ->setCurrency($currency);

        $refund = new Refund();
        // $refund->setAmount($amt);

        $sale = new Sale();
        $sale->setId($sale_id);
        try {
            $refundedSale = $sale->refund($refund, $this->paypal);
            dd($refundedSale);
        } catch (PayPalConnectionException $ex) {
            dd($ex->getCode(), $ex->getData());
            // die($ex);
        } catch (\Exception $ex) {
            // die($ex);
        }

然后报错:

401
"{"name":"PERMISSION_DENIED","message":"Permission denied.","information_link":"https://developer.paypal.com/docs/api/payments/#errors","debug_id":"b1eab9efd8409"}
2019-03-25 14:26

博主你好,请教下,测试了一波,异步通知好像是在付款完成后点击“返回商户页面”后才触发的,同步比异步稍微晚了几秒。 请问这个有什么办法在用户付款完成后就能收到异步回调吗?

2019-03-25 14:40

请无视上面的提问,支付完成后,等待一段时间就可以收到异步回调了。点击“返回商户页面”只触发同步回调

2019-04-24 16:53

对接到退款上遇到了点问题,能联系一下吗

2019-04-28 10:19

可否加QQ联系?

2019-04-28 10:53

cmd是什么数据?

hezhizheng #28
2019-04-29 13:06

@pingtingniaonuo 请看注释,声明表单是立即购买

2019-04-29 18:36

那个PayPalFactory是怎麽来的? 找遍整个sdk都没有这东西

2019-07-26 13:59

大佬,退款接口报错"message" -> "Authentication failed due to invalid authentication credentials or a missing Authorization header.",请问下是什么原因?可以加下我的扣扣吗?3306428634,万分感谢

2019-07-30 15:00

在paypal支付后,订单状态为approved,但是我的网站一直没接收到paypal的回调信息,但是paypal那边订单记录我已经扣钱了,在我的网站还没充上钱,偶发现象不知道为什么

2020-03-04 20:32

你好,我想问一下 step3 异步回调

是paypal回调我服务器上面脚本

还是 step2 支付 里面的return 网址呀?

hezhizheng #33
2020-03-05 10:05

@Obama 异步回调是 notify_url 对应的地址,需要支持外网访问

2020-03-19 07:15

博主大大好,没找到您的邮箱和QQ号,自己摸索快半个月了,对照您的教程也试过N遍,一直是付款收到,回调没动静的状态.QQ3306168,能否与您联系,不胜感激