带你设计一套会员功能并开发它
引言
相信每一位程序员都梦想着有一天开发出自己的产品,而且有人愿意为自己的产品买单。用户买单的方式可能是一次性购买产品的使用权、购买产品会员享受高级功能等等。本文和下一篇文章将进行会员与支付功能的设计开发分享,希望可以给正在开发会员功能的开发者提供一点帮助。
本文我们先把支付的逻辑放一边,只关注会员功能的设计和开发,抽丝剥茧,为接入支付功能打好基础。
「会员功能系列文章」第二篇:《👉基于Lemon Squeezy开发你的全球可用的会员功能》
设计思考与开发
当我们开始思考设计一个会员功能的时候,它包含了用户类别的设计、数据存储的设计、后端API的设计、前端界面的设计和预防风险的设计。现在我们就一个个理清思路。
用户类别设计
我正在开发一个工具类网站,为了给用户提供更多的灵活性并满足不同的使用需求,我决定将用户分为三类:
- 免费用户:每天可以使用10次,用户角色值设为1
- 月度会员:每天可以使用500次,用户角色值设为2
- 加油包用户:每次购买可增加100次可用次数,免费用户和月度会员均可成为加油包用户,所以不用单独设置角色值
付费功能核心逻辑是这样:
- 免费用户可以升级为月度会员,一个会员周期是31天
- 会员可以提前续费,会员有效期会顺延
- 当用户的使用次数达到限制,可通过加油包获取更多次数,每次购买加油包获得100次使用次数,有效期为7天,如果多次购买,加油包有效期会顺延
我认为这种设计可以兼顾轻度用户、重度用户和临时重度用户,再通过定价策略,就可以激励潜在的重度需求的用户购买月度会员。当然这套设计未经验证,如果效果不好,到时候再调整就好了。
数据存储设计
因为会员和加油包有过期时间,如果把相关数据记录在像 MySQL 或者 Postgres 里,我还需要定时去更改用户角色,无形中给自己增加了开发量。为了让开发流程更丝滑,我选择了 Redis 作为数据存储的解决方案,我只需要给 Redis key 设置过期时间,key 过期就查不到了,等同于会员或加油包到期。
Redis 我仍然沿用 Upstash 的免费 Redis 资源,关于 Upstash 的介绍,可以看我的这篇文章:👉《用 Upstash 作为你的 Redis 服务器》。
本文默认你对 Redis 的基础类型和命令有一定的认识,所以不会对 Redis 的用法进行介绍。
对于数据存储的设计,我首先需要设计几个关键的 Redis key:
- 用户每日已使用次数:
userId:<userID>::date:<date>::user_date_uses
- 用户每日第一次使用功能时,通过自增命令,Redis 会自动创建一个与日期相关的 key;
- 有了每日使用的次数,我就可以通过用户角色对应的日上限(普通用户10次,会员500次)算出该用户每日剩余次数
- 为什么我记录已使用次数,而不是记录剩余次数?这是因为,如果记录剩余次数,当剩余0次时会查询到0,而当天第一次使用前,查询剩余次数会因为 key 不存在而查到
null
,在区分0和 null
的时候,可能会出现混淆,所以我认为记录已使用次数是更好的做法。
- 会员状态:
userId:<userID>::membership
- 用户升级会员后,服务端会在 Redis 里创建这个用户的会员状态 key,value 设置为2,表示用户是有效的会员。
- key 的过期时间是会员的剩余有效期(秒为单位)。
- 加油包余额:
userId:<userID>:boost_pack_uses
- 用户购买加油包后,会创建一个初始 value 为100的加油包余额 key
- key 的过期时间是加油包的有效期(秒为单位)
- 当用户日限额用完后,开始扣加油包的余额,用
desc
进行自减
服务端设计与开发
在真实的生产环境中,我们一般是通过查询订单状态,然后内部调用升级、续费、获得加油包的方法,但本文一方面是剥离了支付功能,另一方面出于方便测试和演示,所有相关功能全部以暴露接口、接口再调用内部方法的方式来展开说明。
参数类型和常量定义
首先把用户角色类型和重要的常量定义清楚。
用户角色类型定义:
为了更方便地维护 Redis 相关的 key 和其它设置,我们还可以创建一个文件用来记录这些信息,如:
API设计开发
1、升级/续费会员:/api/mambership/fake/upgrade
upgrade
功能设计:
- 判断当前用户角色
- 如果是普通用户,则升级会员,设置
userId:<userID>::membership
的value为2,过期时间31天
- 如果是会员用户,则延长会员期,更新
userId:<userID>::membership
的过期时间。
- 每次购买都清空当日已使用次数,这是出于用户体验的考虑,可以不要
代码实现如下:
这样升级/续费的接口就完成了,可以通过postman进行逻辑测试。完整源码和线上演示地址放在文末。
2、购买加油包:/api/mambership/fake/bugBoostPack
boostPack
功能设计:
- 判断当前加油包剩余次数
- 如果剩余0次,则设置
userId:<userID>:boost_pack_uses
的值为100,过期时间7天。
- 如果剩余大于0次,则增加100次剩余次数,增加7天过期时间
代码实现如下:
这样购买加油包的接口也完成了,可以通过postman进行逻辑测试。完整源码和线上演示地址放在文末。
3、检查使用次数和会员状态:/api/mambership/fake/checkStatus
checkStatus
功能设计:
- 获取
userId:<userID>::date:<date>::user_date_uses
、userId:<userID>:boost_pack_uses
和userId:<userID>::membership
的值,返回当前可用次数、加油包余额、加油包过期时间及会员过期时间。
核心代码实现如下:
这样购买获取会员状态和使用次数的接口也完成了,可以通过postman进行逻辑测试。完整源码和线上演示地址放在文末。
4、使用功能:/api/fake/useFunction
核心代码设计如下:
- 服务端调用工具方法前,先查询剩余次数,如果默认次数+加油包次数>0,则可以调用,否则返回错误提示
- 服务端调用工具方法后,修改 redis 统计的使用次数,这里要先判断日限额剩余次数,然后再判断加油包剩余次数
- 如果【默认使用次数 - 日使用次数】> 0,则自增一个日使用次数;
- 如果【默认使用次数 - 日使用次数】<= 0,则判断加油包次数
userId:<userID>:boost_pack_uses
的值
完整源码和线上演示地址放在文末。
前端设计
- 用户界面:
- 显示当前用户类型、剩余使用次数、加油包余额(包括到期日期)和会员到期日期(可以通过检查Redis键的TTL得到)。
- 提供购买额外次数的选项。
- 提供续费选项。
- 提示和警告:
- 当用户达到使用限制时,提示购买加油包或者升级会员
- 普通用户:Become a member to enjoy 500 uses every day.
- 会员用户:Purchase a Boost Pack to get more uses right now.
- 当加油包即将到期或会员即将到期时,显示相应的提醒。
演示截图如下:
风险应对策略
涉及到金钱的功能一定要做好风险应对策略,否则出了生产事故就会对品牌产生很大影响。本文核心逻辑都是在操作 redis,所以主要考虑 redis 的连接和操作失败的问题。因为这是一块很大的专题,所以不在本文进行详细叙述,仅抛砖引玉提供一些应对策略:
- 重试策略:由于网络波动或短暂的服务中断,Redis 操作可能会偶尔失败。这种情况下,实施一个自动重试策略是有益的。
- 错误日志:确保记录所有 Redis 相关的错误,这样你可以追踪、分析并修复它们。
- 用户反馈:如果 Redis 操作失败,并且你已尝试了所有自动重试,那么应该给用户一个明确的错误消息。这样,用户会知道发生了什么,并且可以稍后重试。
- 后备策略:考虑创建一个后端队列或延迟任务系统。当 Redis 操作失败并且重试不起作用时,你可以将操作的详细信息放入队列中,并在后台持续尝试,直到成功。
- 监控:使用 Upstash 提供的监控工具或其他第三方服务,如 Datadog 或 Sentry 来实时监控 Redis 的性能和错误率。这样,如果出现问题,你可以迅速得知并采取行动。
结语
把本文的代码块去掉,就是一份会员功能设计和代码设计的文档,希望本文对会员功能的设计思考和代码的实现都能对你有所启发。
「会员功能系列文章」第二篇:《👉基于Lemon Squeezy开发你的全球可用的会员功能》
源码与演示
源码:👉membership
在线演示:👉模拟会员功能
专栏资源
专栏介绍:以实战的角度进行Next.js生态圈的技术栈分享,内容包括但不限于:Next.js理论知识、功能模块设计思路、实战中使用到的技术栈。这是一个长期更新的专栏,我会持续把自己的思考和经验提炼分享出来,欢迎关注我的专栏👇
专栏地址:👉Next.js生态圈实战
专栏演示站:👉Next.js Demos
专栏源码仓库:👉Github - Source Code
交个朋友:👉加入「独立全栈交流群」