MkSaaS模板的支付模块的方案和流程 — Transcript
Full transcript
- 0:00大家好,MkSaaS模板最近进行了一轮支付模块的重构。
- 0:06这个视频主要介绍新版本支付模块的技术实现方案
- 0:11以及为什么要进行这次重构
- 0:15旧版本有什么问题以及新版本是如何解决这些问题的。
- 0:21首先我们看一下Stripe的最佳实践参考资料
- 0:28也就是这个GitHub仓库,作者是T3 Chat的作者
- 0:32国外非常有名的开发者,他写了一个文档
- 0:38这个文档介绍了如果你要集成Stripe支付的话
- 0:43他推荐了一个接入Stripe的流程。
- 0:49大家有空可以看一下这个文档的内容。
- 0:53它的核心思想就是说,Stripe的Webhook事件有很多
- 0:59而且顺序又比较乱,处理起来会比较麻烦。
- 1:04处理起来会比较麻烦。
- 1:07所以它给了一个实现方案,就是提供了一个叫sync after success的方法。
- 1:15也就是说,当你收到任何一个Stripe事件的时候
- 1:20它就会去调用这个方法,主动请求Stripe的接口
- 1:27然后把用户的一些数据同步到本地
- 1:32以确保用户的订阅状态跟Stripe那边的状态保持一致。
- 1:40它完整的流程是比较麻烦的
- 1:44其实也没有给出完整的真实项目接入源码。
- 1:51所以我根据这个文章
- 1:57在MkSaaS模板上面做了一个实践。
- 2:02首先我们看一下,根据这篇文章的介绍
- 2:08可以毫不夸张地说,基本上市面上你能看到所有开源的SaaS模板都有问题。
- 2:14这个问题是什么呢?
- 2:15就是用户可能没有真正完成支付
- 2:22结果这个SaaS网站认为它完成了支付
- 2:26然后就把它的订阅状态变更了,或者给它发放了积分。
- 2:31最近使用MkSaaS模板的客户
- 2:35也偶尔会有用户反馈类似的问题
- 2:39实际上就是同一个原因。
- 2:43每一个SaaS网站如果要接入Stripe的话
- 2:47我总结了一下,主要就是要解决这三个问题。
- 2:50第一个就是Webhook的事件很多,我们一定要选择正确的事件进行监听和处理
- 2:57如果你监听错误的事件
- 3:01并处理的话其实是没有用的。
- 3:03第二个是Stripe事件的顺序可能是乱的
- 3:07时间也是不确定的。
- 3:09所以SaaS网站的代码必须要能够兼容。
- 3:13而且如果有必要的话,可能要等待某一个事件到来才行。
- 3:20第三个是事件可能重复触发,就是Stripe发给你的事件
- 3:27它可能会重复触发,有可能是Stripe的问题
- 3:31也有可能是你网站上一次处理这个事件失败了
- 3:34然后Stripe隔了一段时间重新触发。
- 3:38所以网站的处理逻辑要考虑幂等性
- 3:41避免同一个事件进行重复处理
- 3:43比如说积分重复发放了。
- 3:47后面我们看一下旧版本的支付方案。
- 3:49至于旧版本的支付流程演示,可以看之前的
- 3:55视频教程以及支付文档。
- 3:59之前的视频演示如何使用MkSaaS模板从零开始配置到部署上线
- 4:09其中包括了支付模块怎么配置
- 4:11怎么在Stripe里面创建产品,然后在MkSaaS模板里面配置这些环境变量。
- 4:21这个流程我就不介绍了
- 4:25原始的流程图的话,之前介绍的是这一个
- 4:28比较简单,就是用户在你这个网页
- 4:32点击了一个支付按钮,然后我们调用Stripe的接口
- 4:35拿到了一个Checkout的URL,然后去打开这个支付的界面。
- 4:42用户在这个支付界面完成了支付之后
- 4:45会发出一些Webhook的事件通知到这个网站
- 4:48这个网站收到这个事件之后,判断用户支付成功还是失败
- 4:52然后进行一些操作。
- 4:54以前的流程就是这么简单。
- 4:56这旧版本的支付方案的话,监听了四个事件
- 4:59第一个是Checkout session completed的,用户在Checkout session完成了支付以及三个订阅相关的事件
- 5:08包括Customer subscription created,updated和deleted。
- 5:13之前的问题在哪里呢?
- 5:15就是如果我们收到了Checkout session completed的事件的时候
- 5:21我们就会去创建一个Payment的记录
- 5:25然后标记这个用户支付完成。
- 5:27但实际上这个事件到达的时候并不总是意味着支付真正的完成了
- 5:33因为什么呢?
- 5:33就是有一些支付方式,它可能是需要一段时间来处理和确认的
- 5:39它只是说用户在那个Checkout的那个页面完成了这个支付的流程
- 5:44然后回到了你的网站。
- 5:47但是这个时候支付可能还在处理和确认当中
- 5:53有可能支付最终是会失败的或者被取消的
- 5:56但是我们之前在这种情况下就已经标记用户支付成功了。
- 6:02那么就已经修改了用户的订阅状态
- 6:07或者给用户发放了积分。
- 6:09所以这种方案是有问题的。
- 6:12这也就是大部分的市面上开源的SaaS模板基本上都是这么处理的
- 6:17所以基本上都有这个问题。
- 6:20下面我们看一下新版本的支付方案是如何解决这些问题的。
- 6:25这是新版本的支付的完整流程图,也就是这张图
- 6:30稍后我们会详细介绍一下这张图里面的流程
- 6:34然后并且在demo的MkSaaS的demo演示站点进行一个功能的演示。
- 6:40我首先简单介绍一下是做了哪几个变更去解决了之前的几个问题。
- 6:49首先第一个变更是监听的事件增加了一个叫Invoice Paid。
- 6:56为什么要监听这个事件呢?
- 6:57因为收到这个事件的时候才表示真正的完成了支付
- 7:01这种情况下我们才能给用户去更新他的订阅状态
- 7:06或者是发放积分。
- 7:09之前的话,我们会在Checkout的Session completed或者是在收到那个
- 7:16Subscription created的时候去创建那个Payment的记录
- 7:21标识用户创建了,标识用户支付完成
- 7:25然后给他创建一条支付成功的记录。
- 7:30但是这个是有问题的,对吧?
- 7:31所以这一次我们改为了收到Invoice paid这个事件的时候
- 7:36才去创建这个Payment的记录,然后把它保存到数据库当中
- 7:40同时再去更新用户的订阅状态,或者是要给他发放积分。
- 7:45这里有一个问题是为什么会选择新增这个事件
- 7:52而不是Invoice payment succeeded的这个事件看起来好像一样
- 7:57一个是支付成功,一个是paid。
- 7:59实际上也是有区别的。
- 8:01paid的这个事件包含了一种特殊情况
- 8:04就比方说零元购,就是说如果你的产品
- 8:10给了一张优惠券给到别人,这个别人可以以零元就可以购买你这个产品的话
- 8:17那么他是不会发送Invoice payment succeeded的事件
- 8:21但是这种情况下还是会发送Invoice paid的事件
- 8:26也就是说Invoice paid的事件的范围比这个事件要更广一些。
- 8:31所以我们选择监听这个事件,然后这个事件的情况下
- 8:34我们就可以给用户发放积分了,这是第一点。
- 8:38第二点,这个第一点的话,实际上解决的就是第一个问题
- 8:44Webhook的事件很多,我们去选择什么
- 8:47哪个正确的事件进行监听和处理。
- 8:49第二个问题就是事件的时机是不确定的。
- 8:56前面说到我们要监听这个Invoice pay的事件
- 9:00但是这个pay的事件是要晚于Checkout的那个Session completed的。
- 9:05也就是说用户在那个支付界面完成了支付之后跳转回到网站的时候
- 9:11这个事件可能还没有到或者是到了还没有处理完
- 9:15这都是有可能的。
- 9:17那么解决方案就是引入了一个支付确认的界面
- 9:20也就是说用户完成了支付之后,不是让他立即跳转到那个网站的那个账单界面或者是积分的界面
- 9:32而是跳转到一个中间页面,这个中间页面叫做支付确认的页面。
- 9:36我们会在这个页面去每隔两秒进行一个轮询
- 9:41检查Payment的记录是否生成。
- 9:45前面有提到,只有收到Invoice pay的事件的时候才会去创建这个Payment的记录。
- 9:52如果我们检查到Payment的记录已经生成了
- 9:55那么也就是说,这个事件收到了
- 9:58用户支付是确认成功的,那么这种情况下
- 10:02我们就可以跳转回网站的那个账单页面
- 10:05或者是积分页面。
- 10:08但是如果这一次检查发现Payment的记录还没有生成
- 10:14那就可能事件还没有处理,还没到或者是还没有处理完
- 10:19那么就等一下,隔两秒再检查一次。
- 10:22同时我们最多限制了一个检查时长是一分钟
- 10:25也就是中间的这个支付确认界面,最多等待一分钟
- 10:30等这个Stripe的这个事件到来并且处理完。
- 10:34如果一分钟还没有等到,那就是超时了。
- 10:38这里面有一个小细节,就是说
- 10:40我怎么确认这个Payment的记录是否已经生成了呢?
- 10:44根据什么东西去查这个Payment的记录呢?
- 10:48这里的话用的是这个Session ID,就是如果是用户手动触发去进行支付的话
- 10:56他要么就是点击那个Price那个页面
- 11:02去按去订阅支付或者是一次性支付
- 11:08也就是购买终身会员。
- 11:10要么就是在那个积分的界面去购买积分包
- 11:15然后跳转到支付页面。
- 11:17这两种情况下都会创建一个Checkout Session
- 11:21所以就会有一个Session的ID。
- 11:24我们在创建Checkout Session的时候,会设置一个
- 11:29支付完成。
- 11:30支付完成之后跳转返回的页面。
- 11:33这个页面的话,我们现在之前设置的是要么是账单页面billing
- 11:38要么就是积分页面Credits。
- 11:41现在我们会让它返回到这个支付确认界面
- 11:44也就是Payment,同时带上两个参数。
- 11:48第一个参数就是Session ID,也就是前面这个Checkout Session ID
- 11:52这个是唯一的。
- 11:53每一次进入到这个Checkout支付页面,就会有一个新的
- 11:57Session ID。
- 11:58同时还有一个callback的地址
- 12:01也就是说如果这个支付确认界面确认完成支付
- 12:05那往哪里返回?
- 12:07自动跳转到哪个页面。
- 12:10好,这里我们有了这个Session ID之后
- 12:16就可以去通过Session ID在数据库里面找payment的这张表
- 12:21看是否有payment的这个对应的记录。
- 12:25OK。
- 12:29第三个变更,第三个主要的变更是新增了一个数据库的字段。
- 12:34我们来说一下为什么要新增这个数据库的字段。
- 12:37根据第三个问题的话,就是事件有可能重复触发
- 12:41所以我们要避免这个事件进行重复处理。
- 12:45比如说我们要避免重复给这个用户发放积分
- 12:51因为如果他发放过了,他这次购买成功
- 12:54然后我们已经发放过了,我们不可能给他重复发放积分的
- 12:59那怎么避免这个重复发放积分呢?
- 13:01这里首先考虑的是能否用Session ID,其实是不可以的
- 13:08因为整个网站,基于MkSaaS模板的功能来看的话
- 13:13大部分情况用Session ID是OK的。
- 13:15但是有一种情况,如果是用户已经订阅了
- 13:19在自动续订的情况下,是不需要用户再去进入到那个支付页面进行支付的
- 13:26那么就不会进入到Checkout session,也就没有Session ID。
- 13:30所以这种情况下,如果自动续订成功了之后
- 13:33收到了Invoice paid的事件,那就是没有Session ID的。
- 13:38在这种情况下,那么payment的记录里面没有Session ID
- 13:43那就没办法保证它是唯一的。
- 13:48但是虽然没有Session ID,但是一定会有一个Invoice ID
- 13:52也就是标识这一次支付成功的发票的编号
- 13:57这发票的编号它是唯一的
- 14:00在数据库层面上,我们保证了数据库里面每一条payment的记录的
- 14:07Invoice ID这个字段是唯一的,那么我们就不可能插入两条相同的Invoice ID的payment的记录。
- 14:17因为如果插入两条相同的话,尝试插入第二条相同Invoice ID的payment记录的时候
- 14:25数据库是会报错的。
- 14:26这种情况下我们就可以返回这个错误给到上层。
- 14:33OK,这个就是新版本的技术
- 14:38支付的解决方案的变更,主要是三点
- 14:42就是新增了一个事件,新增了一个页面
- 14:46新增了一个数据库的字段。
- 14:49下面我们结合新版本的完整的支付流程图
- 14:54然后在MkSaaS的Demo的演示站点上来做一个流程的实践。
- 15:04这个就是新版本的支付模块的完整流程图
- 15:09我们一步步来操作一下。
- 15:11首先是用户要进入到那个支付的页面
- 15:17这边我们先开始注册一个用户开始。
- 15:27我们新建一个账号。
- 15:36收到邮件,然后确认。
- 15:45默认的话,用户注册登录之后会进入到他的Dashboard的界面
- 15:51然后在那个账单界面可以看到它目前是免费计划
- 15:57然后看一下他的积分。
- 15:59目前的话,他积分是有一百个,主要是注册送了五十个
- 16:04还有一个是每个月那个免费用户的话会给他送五十个积分
- 16:09这样子。
- 16:10OK,现在的话我们先让这个用户升级一下他的会员
- 16:17升级到Pro,也就是月订阅的会员。
- 16:23首先进行一个支付,这个就是Stripe的Checkout的界面。
- 16:28这边的话,使用这个Stripe提供的测试卡4242进行一个支付。
- 16:34目前是在测试模式下,所以并不会真的扣钱。
- 16:38MkSaaS的官网的Demo演示站点是就是测试模式下
- 16:43所以可以随便尝试。
- 16:44这个就是支付确认的页面,确认成功之后
- 16:48会重新回到这个账单页面。
- 16:50OK,显示这个用户已经是Pro计划
- 16:54然后有效期是从九月十三号到十月十三号。
- 17:00下面我们再让用户去购买一个积分包。
- 17:05刚才的话用户是有一千一百个积分
- 17:08假设说他接下来30天,这1100个积分不够
- 17:12他又会去购买其他的积分。
- 17:17我们测试一下积分购买的流程,这里也是一样的
- 17:22进入到Stripe的Checkout界面,完成支付。
- 17:26支付完成之后,会跳转到支付确认界面
- 17:31支付确认界面这个URL有个Session ID
- 17:34还有一个Callback,确认成功之后,返回就可以看到他的现在的积分是1300个了。
- 17:42我们可以看一下积分的交易记录。
- 17:46刚刚我们购买了积分,购买了200个积分
- 17:51然后就加上来了。
- 17:54OK,现在我们看一下这个流程图里面是怎么个流程。
- 18:02首先用户点击支付的话会进入到Stripe的支付界面
- 18:07支付完成之后,就会跳转。
- 18:12缩小一点。
- 18:16是会跳转到支付确认界面,但是在这之间
- 18:20这边有一个是什么呢?
- 18:22就是三个事件,是我们监听的Stripe发给网站的三类事件
- 18:32第一个是跟checkout的有关,一个checkout的session completed
- 18:36表示刚刚那个支付的session给完成了
- 18:40然后完成之后,它就会回到刚刚那个支付确认界面。
- 18:45然后第二个事件是我们监听的那个订阅事件subscription create
- 18:51update以及delete三种事件。
- 18:54当收到这个事件的时候做什么样的处理
- 18:57以及这次我们新增的一个invoice paid
- 19:00这个事件。
- 19:02收到这个事件的时候我们怎么处理?
- 19:06下面我们分别看一下以前的话,旧版本在收到checkout的session complete的事件时候
- 19:11我们去创建那个payment,那个支付记录表明用户支付成功了
- 19:17但这一次的话,我们不在这个事件里面做处理了
- 19:21而是挪到了那个invoice paid的事件里面
- 19:24然后以及之前旧版本会在subscription
- 19:27create的事件里面去做创建那个payment的记录
- 19:32现在也不做了,只是输出一个日志记录。
- 19:35所以新版本的话,其实这两个事件你是可以不用监听的。
- 19:40我们这边subscription的事件主要就监听两个
- 19:43一个是update,还有一个delete,因为有可能那个订阅状态的有效期是会发生变化的
- 19:49或者说用户取消了订阅
- 19:52有效期也会变化。
- 19:54这里面的订阅的状态也可能发生变化。
- 19:58然后重点的话就是这次新增了监听
- 20:01invoice pay的事件,然后我们看一下它怎么处理的。
- 20:05这里面分两种,就收到事件之后,我们会解析这个事件
- 20:10看里面是发生了什么样的购买,它是发生了一次性的支付
- 20:16还是订阅性的支付?
- 20:18如果是一次性支付,又分两种情况
- 20:21有可能是用户购买了积分,也有可能是用户购买了终身计划。
- 20:27然后分别调用不同的方法,比如说如果是积分购买
- 20:31那么就调用这个方法去更新用户的积分余额。
- 20:35如果是终身计划购买了终身计划,那么就是更新用户的订阅状态。
- 20:42然后如果是订阅支付的话,那么就会更新用户的
- 20:47跟订阅状态以及有效期这样子。
- 20:51前面我们说到那个用户,在那个checkout的界面完成了支付之后
- 20:56会回到那个支付确认界面,用户在这个界面等待结果。
- 21:00为什么会在等待结果呢?
- 21:02因为这个界面会去那个做一个轮询
- 21:07也就是说它会去检查,每隔两秒去检查一下这一次支付的payment值记录是否生成了
- 21:19所以它查的是数据库里面,这个列就是数据库
- 21:23查数据库里面的这个payment的表是否已生成了对应的这一次session的那个支付记录
- 21:30如果生成了,那么就是说这次支付是确认完成了
- 21:34然后显示着支付成功的状态,然后再跳转回去。
- 21:39但如果等了一分钟还没有查出说这个有一条新的payment的记录给创建了
- 21:47那么的话就是轮询超时,我们就显示一个超时的状态。
- 21:52如果是支付成功的话,刚刚我没有看到
- 21:57如果是支付成功的话,我是第一次演示的是订阅支付成功
- 22:03这边的话是自动重定向到了账单的界面
- 22:07然后显示的用户是Pro计划,然后有效期是多少多少
- 22:11这里面的话就会进入到那个billing的界面
- 22:15然后去调用use current plan的这个hook去查询
- 22:19里面会调用一个server action去查询当前的价格计划和订阅状态
- 22:24并返回这个结果显示在界面上。
- 22:27但如果是用户支付成功并且是购买积分的话
- 22:33那么我们就会把用户重定向那个积分的界面。
- 22:37同样的积分的界面的话,会有一个credits page
- 22:40然后里面有一个credits card那个组件
- 22:44然后那个组件里面会调用use credit balance这个hook
- 22:49里面会触发调用一个server action去查询用户的积分余额
- 22:54查到了之后又返回用户的积分余额显示在界面上。
- 22:58OK,完整流程的话就是这样子,大概就分了三大步
- 23:03第一步是用户在界面上进行一个操作
- 23:08点击去支付,然后进入到Stripe的支付界面
- 23:13等用户支付完成之后,会把用户重定向我们所配置的支付确认界面
- 23:21那这个界面会做什么呢?
- 23:22就是轮询,不停地去查询这一次支付的记录
- 23:28确认是否成功了。
- 23:30这里面实际上就在等Stripe给我们发事件checkout的session completed
- 23:36或者说是subscription事件,或者是invoice paid的事件
- 23:40我们主要等的是这个invoice paid的事件
- 23:42等收到了invoice paid的事件,我们会去解析这个事件里面的信息
- 23:49然后创建一个支付的记录
- 23:53payment的记录。
- 23:54同时更新用户的积分余额或者是他的订阅状态。
- 24:01有了这个支付记录之后,这边查询就查到了结果
- 24:06那么就是支付确认完成。
- 24:08然后就会到第三大步,就是说把用户重定向到对应的界面
- 24:12如果是订阅状态,然后显示为订阅的话
- 24:15那么就重定向账单界面,让用户查看他的订阅状态。
- 24:17如果是购买积分的话,就重定向到积分界面
- 24:21让用户可以查看他的积分余额。
- 24:25完整的流程就是这个样子。
- 24:28整个流程的各个模块其实是比较解耦的
- 24:35然后代码上的话我也保证了命名的一致性。
- 24:41比如说首先是在价格的页面
- 24:45pricing的页面有一个pricing page,page下面有pricing card
- 24:48然后用户点击这个pricing card上面的checkout的button就会去创建一个checkout的session
- 24:53然后就把用户重定向到了支付的页面。
- 25:00然后如果支付完成,那么就会把用户重定向到这个payment page这个页面。
- 25:06然后payment的page页面有一个payment card
- 25:15这个card就会做一些轮询、检查
- 25:18定时检查这个当次的payment的记录是否已经生成。
- 25:22如果payment的记录已经生成,那么就根据那个URL参数里面的callback
- 25:26然后来判断到底是这次是订阅,然后返回到账单界面
- 25:32还是购买积分,返回到积分的界面。
- 25:38如果是订阅的话,就会进入到billing page
- 25:42然后里面有一个组件billing card,这里面会显示用户当前的
- 25:45价格计划以及订阅状态、有效期等等。
- 25:50然后如果是回到了credits界面,积分界面的话
- 25:54就会进入到credits page,里面有一个credits card
- 25:58上面会显示用户当前的积分余额以及积分的状态。
- 26:02就说最近的一个月之内有多少积分是会失效的。
- 26:09大概是这样子,这个代码还是比较清晰易懂的。
- 26:15第五点是模板的局限性,虽然MkSaaS模板已经考虑到了很多的使用场景
- 26:18但是不同产品它的价格方案是不一样的
- 26:23所以不可能考虑到模板,不可能考虑到任何的场景。
- 26:33如果你的产品的价格方案比较复杂的话
- 26:38请一定要阅读一下源码,在你完全理解这个源码的基础之上
- 26:44然后做自己的定制。
- 26:47第二点是关于产品价格计划的变化
- 26:54请务必谨慎去设置产品的价格,也要谨慎对待产品价格的变化
- 26:59不要随便地变更价格,也不要随便地去变更价格计划。
- 27:04为什么?
- 27:14因为你这个东西是会对支付模块造成影响的。
- 27:19如果你确实要改变,要做一下价格的调整的话
- 27:19那建议将原来的价格计划设置为disabled
- 27:25然后再新增一个计划。
- 27:33就是如果你将原来的计划设置为disabled的话
- 27:39它原来的数据是保持一致的,但是
- 27:41它不会在界面上显示,那么新的用户就不可能在上面进行购买了
- 27:45这个就没什么问题。
- 27:51你再新增一个价格计划,那么用户看到的实际上是这个新的计划
- 27:55他只能购买新的这个价格的产品,不会再购买旧的这样的产品。
- 27:58所以这边的话一定要阅读一下源码
- 28:02然后看哪种方式是最符合自己的实际情况的
- 28:09然后再做定夺。
About this transcript
This page contains the full transcript of MkSaaS模板的支付模块的方案和流程 by MkSaaS, generated from the public captions YouTube serves with the video. The transcript has 442 words across 370 segments, with the original timestamps preserved so you can click any line to jump to that moment in the embedded player.
What you can do with it
Use the transcript to take notes, quote the speaker, build a study guide, generate a summary with ChatGPT or Claude via the YouTube Summary tool, or export it as a timed subtitle file with YouTube to SRT. You can also re-open it in the transcriber to translate the transcript into 100+ languages.
Free YouTube transcript tool
YouTube2Text is a free YouTube transcript generator — no signup, no daily limit. Paste any YouTube link and get the full transcript instantly, with timestamps, click-to-jump, translation to 100+ languages, AI prompts for ChatGPT, Claude, and Gemini, and exports to TXT, SRT, VTT, or Markdown.