Nova Kwok's Awesome Blog

在 PHP 应用中整合 Stripe 并接受支付宝付款

除了生病和亲朋离世的痛苦是真实的外,其余世间所有痛苦都是价值观带来的。

最近在某超级大佬的帮助下有机会接触到 Stripe 的工作流程,事情很简单,对于优秀的服务,我们应该付出使用他们的成本(这样他们可以继续提供优质的服务),对于商户来说收钱就是一个比较有意思的部分了,鉴于大多数网友都是付钱,本文决定分享一下 Stripe 整合支付宝来收钱的方法,且本文不是网上很多出现的那种引用 checkout.js 的过期的方法(许多人都互相转来转去,看了一圈下来都是这个),而是使用 Stripe.js 来完成。

Stripe 目前收款方式有两种,简单来说,我们分为 Easy 难度和 Hard 难度,前者只支持信用卡,储蓄卡和 Apple Pay,而后者则支持多种支付方式,Stripe 支持的支付方式一览表如下:

FLOWSPAYMENT METHODS WITH PAYMENT INTENTS APITOKENS OR SOURCES WITH CHARGES API
CARDSSupportedSupported on Tokens Not recommended on Sources
DYNAMIC 3D SECURESupportedNot supported
CARD PRESENTSupportedNot supported
ALIPAYPlannedSupported
ACH DEBITPlannedSupported on Tokens Not supported on Sources
ACH CREDIT TRANSFERPlannedBeta
BANCONTACTPlannedSupported
EPSPlannedBeta
GIROPAYPlannedSupported
IDEALPlannedSupported
MULTIBANCOPlannedBeta
PRZELEWY24PlannedBeta
SEPA DIRECT DEBITPlannedSupported
SOFORTPlannedSupported
WECHAT PAYPlannedBeta

Easy 模式——使用 「Checkout」

Easy 模式即使用他们写好的页面,被称为「Checkout」,对于商户来说需要在后台定义好产品(Products),生成 sku 后写一个按钮触发脚本自动跳转过去,页面上需要写的内容如下:

<!-- Load Stripe.js on your website. -->
<script src="https://js.stripe.com/v3"></script>

<!-- Create a button that your customers click to complete their purchase. Customize the styling to suit your branding. -->
<button
  style="background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em"
  id="checkout-button-sku_xxxxxxxxxxx"
  role="link"
>
  Checkout
</button>

<div id="error-message"></div>

<script>
(function() {
  var stripe = Stripe('pk_test_xxxxxxxxxxxx');

  var checkoutButton = document.getElementById('checkout-button-sku_G40GQYkIX4a8c4');
  checkoutButton.addEventListener('click', function () {
    // When the customer clicks on the button, redirect
    // them to Checkout.
    stripe.redirectToCheckout({
      items: [{sku: 'sku_xxxxxxxxxxx', quantity: 1}],

      // Do not rely on the redirect to the successUrl for fulfilling
      // purchases, customers may not always reach the success_url after
      // a successful payment.
      // Instead use one of the strategies described in
      // https://stripe.com/docs/payments/checkout/fulfillment
      successUrl: 'https://xxx.xxx.xx/success',
      cancelUrl: 'https://xxx.xxx.xx/canceled',
    })
    .then(function (result) {
      if (result.error) {
        // If `redirectToCheckout` fails due to a browser or network
        // error, display the localized error message to your customer.
        var displayError = document.getElementById('error-message');
        displayError.textContent = result.error.message;
      }
    });
  });
})();
</script>

这样在用户点了按钮之后就会出现一个 Stripe 的支付页面:

这样就可以用了,用户在付款完成之后就会跳转回到 successUrl,同时 Stripe 可以给你预先定义好的接口(WebHook)发一个 POST 请求告知,大致逻辑如下(其实官方有示范):

 \Stripe\Stripe::setApiKey('sk_test_xxxxxxxxxxxxxx');

// You can find your endpoint's secret in your webhook settings
$endpoint_secret = 'whsec_xxxxxxxxxxxxxxx';

$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;

try {
    $event = \Stripe\Webhook::constructEvent(
        $payload, $sig_header, $endpoint_secret
    );
} catch(\UnexpectedValueException $e) {
    // Invalid payload
    http_response_code(400);
    exit();
} catch(\Stripe\Exception\SignatureVerificationException $e) {
    // Invalid signature
    http_response_code(400);
    exit();
}

// Handle the checkout.session.completed event
if ($event->type == 'checkout.session.completed') {
    $session = $event->data->object;
    // 授权用户
    $target_customer = \Stripe\Customer::retrieve($session['customer']);
    $target_email = $target_customer['email'];
    // 然后这里自己根据 email 找到对应用户完成接下来的步骤,比如把文件通过邮件发给用户,给用户头像加个 Buff 啥的~
}

这样就可以获取到用户的信息并且给用户提供/升级服务了,很方便是不是?

不过呢,「Checkout」只支持卡和 Apple Pay,对于喜欢见到付钱就想扫一扫的用户来说并不友好,所以我们需要使用一些别的方法。

Hard 模式——使用「STRIPE ELEMENTS」

为了照顾没有信用卡,遇见码就开始掏手机准备打开或绿或蓝应用准备开始扫一扫的用户来说,我们需要加入支付宝的支持功能。

首先确认你的账户中 Alipay 是连接上并且处于激活状态的,没有这一点等于没戏(也就不用继续往下看了)。

如果你的 Stripe 已经连接上了支付宝,接下来我们就可以开始整合了。

首先我们明白一下对于商户来说,逻辑是怎么样的:

首先由于 Stripe 并不是原生支持支付宝,所以所有这种非信用卡交易都被挂了称为「Source」的东西下,可以理解为一个插件或者一个临时的钱包,以下一段是具体的逻辑,请仔细阅读:

当用户需要付款的时候,用户会先通过 JS 创建一个 「Source」对象,并指定类型为「Alipay」,这个时候 Stripe.js 会带领用户去支付宝的付款页面进行支付,如果付款成功了,那么这个「Source」的状态会从 charge.pending 变成 source.chargeable ,可以理解为用户给临时钱包付了钱,在有了这个状态之后我们可以调用 Stripe 对这个 Source 扣款(Charge),把临时钱包的钱扣到自己 Stripe 账户上,然后就完成了付款的过程。

用户逻辑

我们先来看用户的逻辑部分:

用户的逻辑是,在对应的购买页面上应该有一个 Button,上面写上「立即购买」,这样用户只要一摸那个按钮,就可以看到支付宝的付款页面了,为了满足这个需要,我们需要这么做,在对应的页面上放个 Button:

<button id="checkout-button">
    立即购买
</button>

然后引用 stripe.js 并写一点 JS 来完成接下来的事情:

<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
    (function() {
      var stripe = Stripe('pk_xxxxxxxxxxxxxx');

      var checkout-button = document.getElementById('checkout-button');

      checkout-button.addEventListener('click', function () {
        stripe.createSource({
          type: 'alipay',
          amount: 1988,
          currency: 'hkd',
          // 这里你需要渲染出一些用户的信息,不然后期没法知道是谁在付钱
          owner: {
            email: '{$user_email}',
          },
          redirect: {
            return_url: 'https://xxx.xxx.xx/buy',
          },
        }).then(function(result) {
          window.location.replace(result.source.redirect.url);
        });
      });

    })();
</script>

其中,ownerowner 下的 email 建议填写,不然付款后可能不好找到究竟是哪个用户付了钱,如果正巧你们不用 email 来标识用户,那也可以写点别的,对于 owner 来说有以下字段可供选择:

  "owner": {
    "address": null,
    "email": "[email protected]",
    "name": null,
    "phone": null,
    "verified_address": null,
    "verified_email": null,
    "verified_name": null,
    "verified_phone": null
  },

此外,如果你还希望在 Source 中包含一些其他的内容的话,可以自由地使用 metadata ,并在内部包含一系列键值对。由于 createSource 执行完成后会返回一个包含 Source 对象,类似如下:

{
  "id": "src_16xhynE8WzK49JbAs9M21jaR",
  "object": "source",
  "amount": 1099,
  "client_secret": "src_client_secret_UfwvW2WHpZ0s3QEn9g5x7waU",
  "created": 1445277809,
  "currency": "usd",
  "flow": "redirect",
  "livemode": true,
  "owner": {
    "address": null,
    "email": null,
    "name": "null",
    "phone": null,
    "verified_address": null,
    "verified_email": null,
    "verified_name": "null",
    "verified_phone": null
  },
  "redirect": {
    "return_url": "https://shop.example.com/crtA6B28E1",
    "status": "pending",
    "url": "https://hooks.stripe.com/redirect/src_16xhynE8WzK49JbAs9M21jaR?client_secret=src_client_secret_UfwvW2WHpZ0s3QEn9g5x7waU"
  },
  "statement_descriptor": null,
  "status": "pending",
  "type": "alipay",
  "usage": "single_use",
  "alipay": {
    "statement_descriptor": null,
    "native_url": null
  }
}

其中的 redirect[url] 只要访问了就会自动被 Stripe 跳转到支付宝家的支付页面上,所以我们最后会有一行:

window.location.replace(result.source.redirect.url);

将用户跳转过去,然后用户扫码付钱:

用户这边的事情就结束了。

服务器逻辑

用户的事情结束了,服务器端就需要开始处理用户的请求了,一个简单的方法如下,在用户付款完成后 Stripe 会跳转回我们 JS 中定义的 return_url 并附带一些参数,类似如下:

https://xxx.xxx.xx/buy?client_secret=src_client_secret_xxxxxxxxx&source=src_xxxxxxxxx

这个时候我们可以通过服务端来解析 src_xxxxxxxxx 得知是谁在付钱,并完成后续的操作:

\Stripe\Stripe::setApiKey('sk_xxxxxxxxxxxxxx');

// 获取 URL 中 source 字段
$source_id = filter_input(INPUT_GET, 'source', FILTER_SANITIZE_URL);
$source_object = \Stripe\Source::retrieve($source_id);

// 先确认一下用户付了钱,别有 Object 就直接开始整...
$status = $source_object->redirect->status;
if($status == "failed")
{
   	// 如果用户没有付钱,我们该怎么做?
}
else {
    // 从临时钱包从把钱扣了~
    \Stripe\Charge::create([
        'amount' => 1988,
        'currency' => 'hkd',
        'source' => $source_id,
    ]);
    // 有了 Object 之后我们可以提取出对应的用户邮件地址或者别的信息,比如邮件地址可以这样提取
    $user_email = $source_object->owner->email;
    // 然后这里自己根据 email 找到对应用户完成接下来的步骤,比如把文件通过邮件发给用户,给用户头像加个 Buff 啥的~
}

顺便可以登录 Stripe 后台看看~

不过这种方法只是说可以用而已,最好的方法可以参考 Best Practices for Using Sources 来接受 Webhook 多次验证,但这个就不在本文的范围内了。

由于是第一次接触支付领域,上述步骤中可能还是会有不少坑或者啥的(所以别直接在生产环境照抄,写完之后一定要多 Review 几遍逻辑漏洞),不过这个至少是一个可用最小模型了,还有不少可以改进的地方,比如浏览器端的函数其实可以异步拉起,这样可以在网页上弄一个 Modal 弹窗,看上去更加用户友好一些。

如果还有啥需要注意的话,那就是,别熬夜写代码,不然就会和我一样:

Happy Hacking && Happy Halloween !

References

  1. Checkout Overview
  2. Stripe API Reference——Source
  3. Best Practices for Using Sources
  4. Alipay Payments with Sources

#Chinese #Stripe #PHP