Stripe 接入指南

前言

Stripe 是一家全球领先的支付处理平台,适合各种规模和行业的企业,相当于国内的微信、支付宝。

Stripe 功能非常强大,但其官方文档以平铺式为主,对于新手接入来说,可能缺少一个完整的流程指引。因此,本篇旨在为首次接入 Stripe 的开发者提供一份简单易懂的指南。尽管官方提供了丰富且灵活的事件和 API,组合方式多种多样,玩法也十分多样化,但本文的重点在于梳理支付的基本流程,帮助大家以最短路径实现接入。

准备工作

以下均为测试环境,也建议大家在测试环境准备

大概分为两部分:

  • Stripe 商家端管理后台
  • 初始化 sdk

Stripe 商家端管理后台

  1. 注册 Stripe 账户:前往[Stripe 官网]((https://dashboard.stripe.com/register)注册一个开发者账户。
  2. 配置公私钥(复制下来后面会用到)
    upload successful
  3. 配置 Webhook
    • URL 是 Stripe 平台回调己方服务器端的 API 地址
    • 事件至少配置checkout.session.completed 和 charge.refuned,其他的可以根据自己需要配置,具体事件说明后面会介绍
      upload successful

初始化 Sdk

  1. 安装 Stripe sdk:npm install stripe
  2. 初始化 Stripe:
1
2
3
4
5
const STRIPE_SECRET_KEY = 使用上面的密钥
export const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15' /* 最好填上图中对应 API 版本 */,
typescript: true
})

接入流程

业务一般有 3到4 种:

  • 一次性付款
  • 订阅/取消订阅
  • 退款

一次性付款

大致流程如下图

upload successful

  1. 用户访问己方购买页面,选择要付费的产品
  2. 己方客户端/浏览器通知己方服务端去创建一个购买的会话(session),己方需要存一下自己的订单,关联上 sessionId,后面会用到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name,
description,
},
unit_amount: 41.9
},
quantity: 1,
},
],
client_reference_id: userId,
mode: 'payment', // mode 是一次性支付
success_url: 'https://baidu.com',
cancel_url: 'https://google.com',
})
// 创建订单,和 sessionId 关联起来
await createOrder({ userId, productId, sessionId })
res.send({url: session.url })
  1. 服务端使用 stripe sdk 调用 checkout.session.create,stripe 会返回一个支付链接,最后将支付链接再返回给己方客户端
  2. 客户端访问此链接会自动跳转到第三方的支付页面
    upload successful
  3. 用户填完信息,购买产品
  4. stripe 收完款同时回调己方服务端 webhook 事件,checkout.session.completed,己方服务端处理业务,比如更新用户权益,创建订单等等…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.post('/stripe/webhook', async(req,res)=> {
const event = stripe.webhooks.constructEvent(
rawBody,
req.headers['stripe-signature'],
STRIPE_WEBHOOK_SECRET
) as Stripe.DiscriminatedEvent

switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object
if(session.status === 'complete' && session.payment_status === 'paid') {
// 根据 sessionId 去取第 2 步关联的订单
const order = getOrderBySessionId(session.id)
// 更新订单状态
await updateOrder(orderId, { status: 'success' })
// 处理其他任务,如给用户发放权益等
......
}
break
})

订阅

订阅根一次性购买流程差不多,只是订阅要复杂一些,因为订阅是周期性的购买,业务方需要管理这些订阅信息(订阅开始时间,订阅结束时间,订阅的什么类型产品),来处理相应的会员权益。

流程与一次性购买不同的是,第 2 步要创建周期信息:

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
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name,
description,
},
unit_amount: 41.99,
recurring: {
interval: 'month',// 可选:'day' | 'month' | 'week' | 'year',取决于自身业务
},
},
quantity: 1,
},
],
client_reference_id: userId,
mode: 'subscription',
success_url: 'https://baidu.com',
cancel_url: 'https://google.com',
})
// 创建订单
const order = await createOrder({ userId, productId, sessionId: session.id } })
// 更新订阅信息,注意把session相关的subscription暂存到用户订阅表,在后续订阅和退款中会需要
await updateUserWallet(userId, { subscribedAt: xxx, subscriptionExpireAt: xxx, subscriptionId: session.subscription })

最后也是在 收到 webhook 事件 checkout.sessions.complete,根据 sessionId 找到己方订单,处理相关业务。

当然,这不是唯一的解法/方案,但是最简单的。也有另外的一些方案是,在 stripe.checkout.sessions.create 时,添加 metadata 信息,stripe 发现是要创建订阅 session,创建完之后以 webhook 的方式推给我们,己方需要在 webhook 监听 customer.subscription.created 事件,推过来的 event.data.object 会携带创建时的 metadata 信息,此时,己方把订阅信息存起来也是可以的,后面消费这个订阅信息即可,是否这样做,取决于后面是否有更复杂业务,这里暂不考虑

推荐使用前者方案,与一次性购买保持一致

取消订阅

取消订阅一个点要说明的是取消订阅并不是退款,取消订阅之后依旧可以享受的当次订阅的权益只是下一次不会自动续订,仅此而已,举个例子:1月1日订阅了一个月的会员,1月15日取消订阅,那么在2月1日之前依旧属于订阅期,可以使用订阅的权益,但是2月1日之后就会结束,但如果不取消订阅,那么在2月1日会自动续订。

用户取消订阅 2 种手段,一种是在 stripe 平台用户端,另一种是己方平台,区别在于如果使用己方平台取消,需要主动使用 stripe sdk 取消,而用户直接使用 stripe ,需要己方在 webhook 监听 customer.subscription.updated 事件。

这里就以用户使用己方取消为例,流程如下:

upload successful

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const subscriptionId = await getUserWallet(userId, req.subscriptionId)
const response = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
})
if (response.cancel_at_period_end) {
logger.info('Cancel Stripe subscription successfully')
// 处理取消订阅相关信息,最好给一个状态,幂等防止重复取消
await updateWallet(userId ,{ status: 'cancel' })
res.send({ data: 'success' })
} else {
logger.error(
`Cancel Stripe subscription fail, userId: ${userId}, sessionSubscriptionId: ${sessionSubscriptionId}`
)
res.send({ data: 'fail' })
}

如果使用 stripe 平台取消,那么监听 webhook customer.subscription.updated 事件,事件数据会提供 subscriptionId,然后后续业务跟上面一样,不再赘述

退款

退款为2种:

  • 一次性购买的退款
  • 订阅的退款
    退款跟取消流程类似,如果退款的是订阅,代表是立即退订,暂停和移除用户的会员权益,这与取消订阅则不同。

这里以用户在 stripe 平台退款为例,己方需要监听退款事件,这里的一个关键点/难点是,退款事件没有提供订阅信息(id),但提供了发票id,发票是支付成功后 stripe 保留的,发票中会提供购买/订阅的具体信息,这样我们需要通过退款事件的发票id换一下订阅信息,最后取消用户订阅的相关权益

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
const invoiceId = getStripeInvoiceId(chargeData.invoice)
if (!invoiceId) {
logger.error('Stripe refund fail: not found invoiceId')
return
}
const invoiceInfo = await stripe.invoices.retrieve(invoiceId)
const subscriptionId = invoiceInfo.subscription
const userWallet = await getUserWallet(userId, { subscriptionId })
// verify some logic
await updateUserWallet(userId, { subscribedAt: null, subscriptionExpired: null, xxx, status: 'refund' })
// other service
......

最后

建议你在测试环境中充分验证你的接入逻辑,确保在生产环境中能够稳定运行。如果你有更多复杂的业务场景或疑问,可以随时参考 Stripe 官方文档,或根据业务需要进一步扩展你的实现。

Stripe 的集成没有固定的最佳实践,适合你的业务需求的方案才是最好的方案。祝你的项目接入 Stripe 一切顺利!