Stripe支付平台对接技术方案
Madman 数据分析师

我接触过的几个支付管理平台 AdyenBraintreePayPalStripe

最近新接触的Stripe支付平台,相比前几个我感觉Stripe更专业一些,后台管理功能清晰,操作方便,体验好。
开发文档也很清晰,相对前之前接触的支付平台开发文档,Stripe这个我觉得更容易快速了解其所有API,以及功能。API参数及参数说明很清晰,返回数据结构很合理,对应的API返回对应对象属性很容易理解。不像PayPal返回结果特别不好用,夹着各种键值对,结构凌乱。

下面记录我集成Stripe支付平台过程。

前端SDK集成: AndroidIOSJS

IOS SDK集成(前端相关这里只做粗略讲解,PHP后端再详细讲解。

建议用pods 安装SDK,如果不了解Pods 请前往CocoaPods 了解更多。
在Pods/Podfile文件下添加如下:

1
pod 'Stripe', '13.0.0'

然后终端执行命令

1
pod update

之后就根据 Stripe IOS相关文档编写相关代码

在集成过程中前端SDK会需要一个Publishable key ,这个Publishable key需要注册Stripe账户后登陆到Stripe管理后台,进入到 Dashboard Developers->API keys模块页面上获取。

Stripe 的API keys有 Publishable key(公钥) 和 Secret key(私钥)

1
2
3
4
5
6
//Publishable key 大概格式
测试环境:pk_test_***********************
正式环境:pk_live_***********************
//Secret key 大概格式
测试环境:sk_test_***********************
正式环境:sk_live_***********************
  • Publishable key 可以公开在前端代码里面,就是给前端SDK用的,在前端输入银行卡信息后,前端需要用Publishable key和卡信息生成一个token传给后端,由于我们公司的PCI认证还在办理中,所以用户的卡信息是不能直接传到我们后台服务器,Stripe的SDK是符合PCI规范的我们主要通过Stripe的SDK来生成一个token,后端拿这个token可以调用Stripe的API去添加卡或者付款,前端用银行卡信息生成的这个token只能使用一次并且两个小时内有效。
  • Secret key 相当于拥有你Secret账户的数据的新增、修改、删除权限。所以这个是要严格秘密的保存在服务端配置文件里面。

Android SDK集成也简单讲一下

使用Android StudioIntelliJ来安装Stripe Android库类似。您不需要克隆回购站或下载任何文件。只需将以下内容添加到项目build.gradle文件中,位于依赖项部分。

1
implementation 'com.stripe:stripe-android:6.1.2'

Android 的前端代码集成逻辑也基本是一致的。都是最后通过用户输入的卡信息和Publishable key生成一个token传给后台。

网站 js SDK集成也简单讲一下

Card Element Quickstart
使用我们预先构建的UI组件的Elements,安全地收集敏感卡的详细信息。
Elements是Stripe.js的一部分。要开始,请在您的页面上包含此脚本 - 它应始终直接从https://js.stripe.com加载:

1
<script src="https://js.stripe.com/v3/"></script>

js SDK的集成跟App有比较大的差别,具体细节查阅 Stripe.js & Elements
不过最后都是前端 用 卡信息和Publishable key生成一个token传给后台。

Stripe PHP后台集成详细说明(编辑中)

1. 安装Stripe PHP Composer包

首先说明下后台环境:nginx服务器,PHP+Laravel5.2, mySql5.6
其次对Composer包有疑问的请前往Packagist了解更多。
在PHP项目根目录下的composer.json 的 require下面添加:

"stripe/stripe-php": "^6.7.1"

然后执行命令:

composer update --no-scripts

安装完后,PHP项目的Vendor下面会有Stripe的源码, 在使用一个代码库之前简单阅读以下他的源码是有必要的,看源码,你会更清楚这个代码库有什么,你需要什么。

2. 配置Stripe API业务及异常处理类

在项目中新建一个XXStripe.php,这个类专门处理Stripe API调用前后相关业务逻辑,以及API错误异常catch

PHP环境变量配置 .env
在 .env下配置Secret key, 就是前面说到的秘密的保存在服务端的私钥。

1
2
;STRIPE payment
STRIPE_API_KEY=sk_test_***********************

配置PHP支付相关config文件 payment.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
return [
'PAYMENT_ENV' => env('PAYMENT_ENV', 'sandbox'),

'braintree' => [
'merchantId' => env('BRAINTREE_MERCHANT_ID', ''),
'publicKey' => env('BRAINTREE_PUBLIC_KEY', ''),
'privateKey' => env('BRAINTREE_PRIVATE_KEY', ''),
'CESKey' => env('BRAINTREE_CES_KEY', '')
],

'adyen' => [

'app_name' => env('ADYEN_APP_NAME', ''),
'username' => env('ADYEN_USERNAME',''),
'password' => env('ADYEN_PASSWORD', ''),
'env' => env('ADYEN_ENV', ''),
'merchant_account' => env('ADYEN_MERCHANT_ACCOUNT', ''),
'shopper_interaction' => env('ADYEN_SHOPPER_INTERACTION', ''),
],

'payPal' => [
'user' => env('PAYPAL_USER', ''),
'password' => env( 'PAYPAL_PASSWORD', ''),
'signature' => env( 'PAYPAL_SIGNATURE', ''),
'new_user' => env('NEW_PAYPAL_USER', ''),
'new_password' => env( 'NEW_PAYPAL_PASSWORD', ''),
'new_signature' => env( 'NEW_PAYPAL_SIGNATURE', ''),
],

'stripe' => [
'api_key' => env('STRIPE_API_KEY', 'sk_test_***'),
]
];

新建XXStripe类 处理Stripe API类参数及返回结果异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
namespace App\Modules\Checkout\Payment\Stripe;

use Carbon\Carbon;
use Stripe\Card; //👇这里可以看到可以引入Stripe各种API类,用来下面调用。
use Stripe\Charge;
use Stripe\Customer;
use Stripe\Dispute;
use Stripe\Error\Base;
use Stripe\Refund;
use Stripe\Source;
use Stripe\Stripe;

class XXStripe
{

public function __construct()
{
//这里取到上面👆config里面的配置的stripe的私钥配置,初始化到Stripe对象里面。
$stripeConfig = Config::get('payment.stripe');
Stripe::setApiKey($stripeConfig['api_key']);
//更多相关初始化配置阅读Stripe源码。
// Stripe::setAccountId("");
}

public function addCard()
{
//如果Customer已经create 则直接拿之前存customer_id,往这个customer下面添加卡。
if ($customerPayment) { //$customerPayment用$user_id去自己的业务库查是否创建过stripe customer的记录,没有则执行else逻辑
$customer = Customer::retrieve($customerPayment->braintree_token);
$card = $customer->sources->create(["source" => $token]);
} else { //如何没有记录则在Stripe平台上Customer::create
$customer = Customer::create([
'source' => $token,
'email' => $email,
'metadata' => ['patpat_customer_id' => $customerId, 'firstname' => $user->customers_firstname, 'lastname' => $user->customers_lastname, 'phone' => $phone]
]);
//添加卡后台拿到返回的$card对象存到对应业务库
$card = current($customer->sources->data);
}

//你的业务代码,把Stripe 返回$customer,$card的觉得有用的对象参数 存到对应的业务表里。
}

public function setDefaultCard()
{
//$stripeCustomerId需要根据userId查到之前创建用户数存的stripeCustomerId
//stripeCustomerId大概格式:cus_CvpoGuZa0BnJEz
$customer = Customer::retrieve($stripeCustomerId);
$customer->default_source = $cardId; //$cardId也是之前存的用户的$cards列表,用户可能选一张卡来做完默认卡,$cardId大概格式:card_1CWwT1G5LmXuczdEAZo0lB2E
$customer->save();
return $customer->default_source;
}

public function deleteCard()
{
//用$stripeCustomerId查找到Stripe支付的对应用户
$customer = Customer::retrieve($stripeCustomerId);
//删除用户下对应的银行卡
$customer->sources->retrieve($cardId)->delete();
}

public function pay()
{
try {
$charge = Charge::create([
'amount' => $totalPay*100,
'currency' => 'usd',
'description' => 'Example charge',
'capture' => false,
'customer' => $stripeCustomerId, //$stripeCustomerId是之前添加卡的时候存的
'source' => $cardId, //这个参数可不传,传这个参数就会用这个指定的card付款,$cardId也是添加卡的时候存的,
'metadata' =>['order_id'=>$orderId]
]);
} catch (\Stripe\Error\Base $e){ //catch Stripe的API基本异常
// Display a very generic error to the user, and maybe send
// yourself an email
Log::info($e->getStripeCode());
Log::info($e->getMessage());
} catch (Exception $e){
// Something else happened, completely unrelated to Stripe
}
}

}

3. 使用前端传过来的token添加Stripe Customers 或直接付款 Payments

通过前端调用后端API传过来 user_id 和 token。
前端通过Stripe SDK生成的token可以用来调用任何需要token参数的 Stripe API, 可以用来在Stripe后台创建Customer, 用token创建Customer是会把token关联的银行卡信息自动添加到这个Customer下面。创建Customer会返回创建的customer_id,这个customer_id可以用来以后长期支付。
所以我这边的处理是拿到token后并不是去直接支付,因为这个token只能使用一次并且两个小时内有效。
用来创建Customer并自动在这个Customer下面添加一张银行卡,这样能拿到返回的Customer信息,Customer信息里面有customer_id card_id等信息,可以用来以后长期支付。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function addCard($token, $email, $user_id)
{
//如果Customer已经create 则直接拿之前存customer_id,往这个customer下面添加卡。
if ($customerPayment) { //$customerPayment用$user_id去自己的业务库查是否创建过stripe customer的记录,没有则执行else逻辑
$customer = Customer::retrieve($customerPayment->braintree_token);
$card = $customer->sources->create(["source" => $token]);
} else { //如何没有记录则在Stripe平台上Customer::create
$customer = Customer::create([
'source' => $token,
'email' => $email,
'metadata' => ['patpat_customer_id' => $customerId, 'firstname' => $user->customers_firstname, 'lastname' => $user->customers_lastname, 'phone' => $phone]
]);
//添加卡后台拿到返回的$card对象存到对应业务库
$card = current($customer->sources->data);
}

//你的业务代码,把Stripe 返回$customer,$card的觉得有用的对象参数 存到对应的业务表里。
}

4. Stripe默认卡设置,及删除卡

Strip跟AdyenBraintree支付平台有点不一样的地方

  • Strip是创建Customer,创建的Customer对应你们的需要付款的用户,Strip的Customer下面可以添加Card或者是在Customer下面添加Sources,Sources是支付源,比如支付宝,微信,PayPal,微软支付等本地化支付。
    然后Customer下面的这些多张Cards和多个Sources只有个一个被设置为默认支付方式,如下图

所以业务方去调用付款API的时候可以只传一个customer_id的参数和付款相关金额货币code,就可以完成付款,因为在Stripe上的Customer已经设置了一个默认付款方式

  • Braintree和Adyen平台是给card授权后生成一个长期有效的可用于付款的card对应的付款token。
1
2
3
4
5
6
7
8
9
public function setDefaultCard($userId, $cardId)
{
//$stripeCustomerId需要根据userId查到之前创建用户数存的stripeCustomerId
//stripeCustomerId大概格式:cus_CvpoGuZa0BnJEz
$customer = Customer::retrieve($stripeCustomerId);
$customer->default_source = $cardId; //$cardId也是之前存的用户的$cards列表,用户可能选一张卡来做完默认卡,$cardId大概格式:card_1CWwT1G5LmXuczdEAZo0lB2E
$customer->save();
return $customer->default_source;
}

删除卡

1
2
3
4
5
6
7
public function deleteCard($stripeCustomerId, $cardId)
{
//用$stripeCustomerId查找到 对应用户
$customer = Customer::retrieve($stripeCustomerId);
//删除用户下对应的银行卡
$customer->sources->retrieve($cardId)->delete();
}

5. 使用Stripe创建的Customers ID支付或者指定Customers的某张卡支付

Stripe的Charge类用于支付等API调用,下面我们看看使用 customer_id 付款代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function pay()
{
try {
$charge = Charge::create([
'amount' => $totalPay*100,
'currency' => 'usd',
'description' => 'Example charge',
'capture' => false,
'customer' => $stripeCustomerId, //$stripeCustomerId是之前添加卡的时候存的
'source' => $cardId, //这个参数可不传,传这个参数就会用这个指定的card付款,$cardId也是添加卡的时候存的,
'metadata' =>['order_id'=>$orderId]
]);
} catch (\Stripe\Error\Base $e){ //catch Stripe的API基本异常
// Display a very generic error to the user, and maybe send
// yourself an email
Log::info($e->getStripeCode());
Log::info($e->getMessage());
} catch (Exception $e){
// Something else happened, completely unrelated to Stripe
}
}

调用Stripe每个API都建议 try catch以为Stripe的错误信息不会在api结果中放回,Stripe的api一般只返回成功后的对象,错误信息都是通过异常抛出,更多Error信息了解 进入Stripe API Error

6. 支付交易Capture(扣款), Released(放弃扣款), Refund(扣款后退款)

从上面付款可以看到参数 ‘capture’ => false, 这个参数主要是支付只授权,不立马扣款,给一个时间段给用户取消订单,这个时间段交易未capture,交易取消是不扣手续的,如果直接Capture了,用户取消订单,对业务方会产生额外手续费用。所以我们选择支付动作时只授权,不Capture。一般是一天批量Capture一次交易。
Capture代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function capture($transactionId)
{
//$transactionId 是在付款API Charge::create的时候返回的Charge的id,大概格式:ch_1CWgDWGmkmBuczdEnIVX1PXo
try{
$charge = Charge::retrieve($transactionId);
$charge = $charge->capture();
if ($charge->status == 'succeeded') {
return ['status' => true, 'payment_type'=>PaymentType::STRIPE, 'content'=>['braintree_transaction_id'=>$charge->id], 'message'=>"Settled Succeed!"];
} else {
return ['status' => false, 'payment_type'=>PaymentType::STRIPE, 'content'=>['braintree_transaction_id'=>$charge->id], 'message'=>"Settled Failure!"];
}
}catch (\Exception $e) {
return ['status' => false, 'payment_type'=>PaymentType::STRIPE, 'content'=>['braintree_transaction_id'=>$transactionId], 'message'=>$e->getMessage()];
}
}

Released、Refund 代码是一样的,这个也是Stripe跟我之前接触的AdyenBraintree支付平台不一样的地方。
Adyen平台未扣款交易 调用 cancel() API, 扣款后调用的是refund() API
Braintree平台未扣款交易 调用的是 void() API, 扣款后调用的是 refund()API
Stripe Released、Refund代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function refund($transactionId, $amount, $refundId)
{
//$transactionId 是在付款API Charge::create的时候返回的Charge的id,大概格式:ch_1CWgDWGmkmBuczdEnIVX1PXo
try{
$refund = Refund::create(['charge' => $transactionId, 'amount' => $amount, 'metadata'=> ['refund_id'=>$refundId]]);
if ($refund->status == 'succeeded') {
return ['status' => true, 'payment_type'=>PaymentType::STRIPE, 'content'=>['stripe_refund_id'=>$refund->id], 'message'=>"Refund Succeed!"];
} else {
return ['status' => false, 'payment_type'=>PaymentType::STRIPE, 'content'=>['stripe_refund_id'=>$refund->id], 'message'=>"Refund Failure!"];
}
}catch (\Exception $e) {
return ['status' => false, 'payment_type'=>PaymentType::STRIPE, 'content'=>['braintree_transaction_id'=>$transactionId], 'message'=>$e->getMessage()];
}

}

更多Stripe API对象详细请查阅 Stripe API 文档
对应Customers对象API文档
对应Charges(付款)对象API文档
对应Cards对象API文档
对应Refunds对象API文档

7. 交易风控相关,Stripe各种状态变更通知接收并更新到数据库

最后介绍一下Stripe的 Webhooks

Webhooks 主要跟Adyen的通知类似,把用户及交易的所有事件状态变更通知到我们服务器,我们接受到通知时对不同情况做出相应处理。
比如Stripe检测到某笔交易是诈骗订单,会把订单状态及信息通知到我们,我们风控得知后可以对交易进行处理,已经后期风控规则的控制。

Webhooks的配置需要在Stripe 的 Dashboard 中 的 Developers->Webhooks中添加业务服务器回调utl。

然后在业务回调url中获取通知数据,把有用的数据存到业务库中。以及可以对相应特殊类型进行业务逻辑处理。

这样有了通知数据后可以很方便的从通知数据中查询到任何的交易状态变更,以及Customer状态变更。

  • 本文标题:Stripe支付平台对接技术方案
  • 本文作者:Madman
  • 创建时间:2018-05-28 16:01:49
  • 本文链接:https://www.patpat.site/开发/后端/Stripe支付平台对接技术方案.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论