type
status
date
slug
summary
category
tags
password
icon

引言

相信每一位程序员都梦想着有一天开发出自己的产品,而且有人愿意为自己的产品买单。用户买单的方式可能是一次性购买产品的使用权、购买产品会员享受高级功能等等。本文和下一篇文章将进行会员与支付功能的设计开发分享,希望可以给正在开发会员功能的开发者提供一点帮助。
本文我们先把支付的逻辑放一边,只关注会员功能的设计和开发,抽丝剥茧,为接入支付功能打好基础。
「会员功能系列文章」第二篇:《👉基于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_usesuserId:<userID>:boost_pack_usesuserId:<userID>::membership的值,返回当前可用次数、加油包余额、加油包过期时间及会员过期时间。
核心代码实现如下:
这样购买获取会员状态和使用次数的接口也完成了,可以通过postman进行逻辑测试。完整源码和线上演示地址放在文末。
4、使用功能:/api/fake/useFunction
核心代码设计如下:
  • 服务端调用工具方法前,先查询剩余次数,如果默认次数+加油包次数>0,则可以调用,否则返回错误提示
  • 服务端调用工具方法后,修改 redis 统计的使用次数,这里要先判断日限额剩余次数,然后再判断加油包剩余次数
    • 如果【默认使用次数 - 日使用次数】> 0,则自增一个日使用次数;
    • 如果【默认使用次数 - 日使用次数】<= 0,则判断加油包次数userId:<userID>:boost_pack_uses的值
      • 如果大于0,则自减1
完整源码和线上演示地址放在文末。

前端设计

  • 用户界面
    • 显示当前用户类型、剩余使用次数、加油包余额(包括到期日期)和会员到期日期(可以通过检查Redis键的TTL得到)。
    • 提供购买额外次数的选项。
    • 提供续费选项。
  • 提示和警告
    • 当用户达到使用限制时,提示购买加油包或者升级会员
      • 普通用户:Become a member to enjoy 500 uses every day.
      • 会员用户:Purchase a Boost Pack to get more uses right now.
    • 当加油包即将到期或会员即将到期时,显示相应的提醒。
演示截图如下:
notion image

风险应对策略

涉及到金钱的功能一定要做好风险应对策略,否则出了生产事故就会对品牌产生很大影响。本文核心逻辑都是在操作 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
国内镜像仓库:👉Gitee — Source Code
 
基于Lemon Squeezy开发你的全球可用的会员功能精读React hooks(十六):一个为代码优雅而生的hook——use