-
Notifications
You must be signed in to change notification settings - Fork 23
Tutorial (Chinese)
对于一个全球化的小型 SaaS 付费产品而言,通常会涉及到账号和支付这两个功能。我们需要知道一个用户是否购买了某项服务、该服务是否到期等等,以便动态开启或关闭该用户的付费权益。
具体到 Web App 层面,在笔者有限的职业生涯当中,并没有发现一个好用的同时支持账号和支付的管理系统。许多开发者不得不自己开发一套类似的系统,或者考虑到实现难度和工期而放弃接入支付(就像笔者之前只会开发免费 App 那样)。从另一个角度可能也说明,类似的实现可能都是业务逻辑强相关,不便于分享。
所以笔者期望能实现一个业务无关的、支持账号和支付的管理系统,从而在今后的独立开发过程中能快速接入,一劳永逸 💰
幸运的是,我们不用从零实现一个这样的系统:
我们只需要将上述服务组合即可。相信读者已经知道,笔者最终选择了 Google 登录 + Lemon Squeezy 的组合方式。
从生态建设(Web、Android)来看,Google 的账号系统是最为强势的,可以覆盖尽可能多的人群。
尽管笔者给出了 Stripe、Gumroad、Paddle、Lemon Squeezy 四个选项,但其实只有两个选项:Stripe 和其他。
不选择 Stripe 的原因简单而直接:因为笔者生活在中国大陆,获取一个合法有效的 Stripe 收款账户难度较高,有潜在的跨境税务合规问题,因此优先考虑其他选项。其实可以使用 Stripe Atlas 注册一家海外公司,感兴趣的读者可以参阅这篇文章 Stripe Atlas Review: How we started a US Company as non-US residents。
Gumroad、Paddle、Lemon Squeezy 三家均帮助商家解决了跨境税务合规核问题,不同点在于:
平台 | 手续费 | 支付方式 | 出金方式 | 备注 |
---|---|---|---|---|
Gumroad | 10% | 借记卡、PayPal、Apple Pay、Google Pay | 借记卡(香港)、PayPal、Stripe | 最低售满 $10 才能出金 |
Paddle | 5% + 50¢ | 借记卡、PayPal、Apple Pay、Google Pay、支付宝 | PayPal、Payoneer、Wire | 若产品售价低于 $10,需要联系客服自定义价格 |
Lemon Squeezy | 5% + 50¢ | 借记卡、PayPal、Apple Pay、Google Pay、支付宝、微信支付 | 借记卡(香港)、PayPal | 最低售满 $50 才能出金 |
可以看到,Lemon Squeezy 手续费较低,且支持支付宝和微信支付,综合来看是(中国大陆地区收款的)最佳选择。
你只需要准备好一张非大陆地区的借记卡(Debit Card)用于收款。如果你没有,建议想办法去办一张。
如果没有借记卡,非大陆地区的 PayPal 账号应该也可以,但是 PayPal 电汇回国内银行手续费很高,不划算。
首先你需要在 Lemon Squeezy 上创建商店(Store),然后再通过 Google OAuth 同意屏幕审核。
如果你之前没有 Lemon Squeezy 账号,可以在这里注册。
注册完毕以后,你需要在 Dashboard 里配置好你的商店信息,如图所示:
完成图中的第一步和第二步之后,点击第三步的「Activate Store」按钮,填写其中的 *
必选项。
在这一步,Lemon Squeezy 不要求你已经开发出线上可用的产品,但你必须准确描述即将售卖的商品的信息和计划:
- Tell us about you and your business.
- What types of products do you plan to sell on Lemon Squeezy?
- How do you plan to use Lemon Squeezy to sell your products?
注意阅读 Lemon Squeezy 可以售卖的商品种类。
你可以使用 ChatGPT 润色你的想法并填写之,提交后大概两个工作日会有审核结果。
通过审核以后,你可以继续完成 Setup 中的其他步骤。我们也可以在商品即将上架时再处理这些步骤。
这里简单介绍一下在 Lemon Squeezy 中商店和商品的对应关系:
- 一个 Lemon Squeezy 账号可以创建多个商店(Store),但大多数人只需要创建一个商店即可
- 在一个商店内,可以售卖多种商品(Product),可以是完全没有关系的商品,就像杂货铺那样
- 对于一个商品,可以创建不同的变种(Variant),比如可以单次购买、也可以持续订阅等等
以笔者创建的 Demo App 为例:
- https://mthli.lemonsqueezy.com 是笔者创建的商店
- 在笔者创建的商店中,售卖了 Lemon Tree 这一种商品
- 在 Lemon Tree 这个商品中,存在单次付费(Order)和持续订阅(Subscription)两个变种
- 今后笔者的商店中将会出现更多商品 ...
理解这种对应关系有助于我们上架和管理商品。
接着我们要在 这个链接 创建 Google API 凭证。
点击顶部栏的「创建凭证」,选择「OAuth 客户端 ID」,应用类型选择「Web 应用」,然后按照如下截图示例填写:
- 名称 - 填写你的 Web App 名称
-
已授权的 JavaScript 来源
- URL 1 - 填写你的 Web App 的线上域名
-
URL 2 - 必须填写为
http://localhost
-
URL 3 - 要带上你本地 Web App 的调试端口,比如
http://localhost:3000
- 已获授权的重定向 URI - 我们用不到,所以不填写
至此我们已经创建好凭证了。可以看到右边的「客户端 ID」,拷贝之,接下来会用到。
接着我们要在 这个链接 创建 Google OAuth 同意屏幕。
第一步按照如下截图示例填写:
个中字段不与赘述。唯一需要注意的是「应用隐私权政策链接」,审核较为严格:
- 你需要在 Web App 首页添加隐私政策链接,方便用户(和审核人员)查看
- 隐私政策内容可以参考 笔者的示例,或者自己用 ChatGPT 写一份 😆
第二步按照如下截图示例填写:
主要是要在「您的非敏感范围」中选上 /auth/userinfo.email
和 /auth/userinfo.profile
,其他可以不填。
第三步全是可选字段,可以不填;第四步是最终预览效果。
至此我们已经填写完了所有信息,已经可以在测试环境使用 Google 登录了(需要把自己的邮箱添加到测试人员名单中)。
如果你使用的是 React,推荐使用 @react-oauth/google 这个库接入 Google 登录,非常傻瓜。
如果你的线上 Web App 完成了隐私政策的配置,可以将「发布状态」转为「正式版」等待审核,一般三到五个工作日即可通过。如果审核不通过,按照邮件整改即可。不过按照笔者的教程,应该可以一次通过(笔者整改了两次)。
更多 Google 登录的信息可以参见 官方文档。
假设你的服务器操作系统是 Debian GNU/Linux 11 (bullseye)
请务必执行完以下每一条命令。
# 安装 NGINX
sudo apt-get install nginx
sudo systemd enable nginx
sudo systemd start nginx
# 安装 Redis
sudo apt-get install redis
sudo systemd enable redis
sudo systemd start redis
# 安装 MongoDB
# https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-debian/
sudo apt-get install gnupg curl
curl -fsSL https://pgp.mongodb.com/server-6.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg --dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg] http://repo.mongodb.org/apt/debian bullseye/mongodb-org/6.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo systemctl enable mongod
sudo systemctl start mongod
# 安装 Certbot
sudo apt-get install certbot
sudo apt-get install python3-certbot-nginx
# 安装 PM2
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
nvm install node # 这里需要重启 Bash
npm install -g pm2
pm2 install pm2-logrotate
# 安装 Python3 和 Pip3
sudo apt-get install python3
sudo apt-get install python3-pip
# 安装 Pyenv
# https://github.com/pyenv/pyenv#automatic-installer
curl https://pyenv.run | bash
# 安装 Pipenv
pip install --user pipenv
# 安装本项目的所有依赖
git clone [email protected]:mthli/lemonsqueepy.git
cd ./lemonsqueepy/
pipenv install
pipenv install --dev
首先我们需要将之前 创建 Google API 凭证 获取到的「客户端 ID」通过 redis-cli
添加到 Redis 中:
# SADD 可以同时支持多个项目(商品)使用 Google 登录
SADD google_oauth_client_ids "YOUR_GOOGLE_API_CLIENT_ID"
接着前往 Lemon Squeezy API 页面 创建一个新的 API Key,通过 redis-cli
添加到 Redis 中:
SET lemonsqueezy_api_key "YOUR_LEMONSQUEEZY_API_KEY"
然后前往 Lemon Squeezy Webhooks 页面 创建一个新的 Webhook,需要进行如下配置:
-
Callback URL - 请务必按照
https://YOUR_DOMAIN/api/webhooks/lemonsqueezy
的格式填写- 比如笔者的服务器 URL 是
https://lemon.mthli.com/api/webhooks/lemonsqueezy
- 比如笔者的服务器 URL 是
-
Signing secret - 请务必填写一个长度为 16 的字符串,比如
0123456789abcdef
- 我们将会使用这个字符串进行 AES-128 加解密,所以字符串长度必须为 16
- 请不要在正式环境使用
0123456789abcdef
这个显而易见的例子
- What updates should we send? - 勾选全部事件(目前有 14 个事件)
最后将 Signing secret 通过 redis-cli
添加到 Redis 中:
SET lemonsqueezy_signing_secret "YOUR_LEMONSQUEEZY_SIGNING_SECRET"
现如今几乎所有浏览器和操作系统都强制使用 HTTPS 了,所以我们也要配置一下。
假设你已经成功执行了 安装依赖 中的所有命令,那么接下来只需要进行如下操作:
- 将项目文件
lemon.mthli.com.conf
中的server_name
替换为你自己的域名YOUR_DOMAIN
- 将项目文件
lemon.mthli.com.conf
重命名为你自己的域名YOUR_DOMAIN.conf
- 将
YOUR_DOMAIN.conf
拷贝到/etc/nginx/conf.d/
目录下 - 执行
sudo certbot --nginx -d YOUR_DOMAIN
生成 HTTPS 证书,或者 - 定期执行
sudo certbot renew
更新证书,避免其 90 天后过期
直接在仓库目录运行如下命令即可:
# 请确保你不是在 pipenv shell 中运行该命令
pm2 start ./pm2.json
你可以在 ~/.pm2/logs/
目录下看到所有项目运行日志,以及在 /var/log/nginx/
目录下看到所有 NGINX 日志。
更多 PM2 的使用方法可以参见 官方文档。
目前我们已经支持如下场景:
- Google 登录
- 校验订单是否可用
- 校验订阅是否可用
- 校验证书是否可用
- 激活证书
你可以将这个项目用于:
- 一个独立的服务(推荐),并从你的前端应用中直接发起 RESTful 请求
- 一个独立的服务(推荐),并从 Node.js 或者 Go 等语言实现的后端服务中发起 RPC 请求
- 一个 Python 网络框架,并在其中添加自己的业务逻辑
具体 API 的使用方法可以参见笔者的示例代码 mthli/lemontree。
如果你的 Web App 允许用户在非登录状态下体验部分功能,可以使用这个接口的返回值作为用户标记。
// Request Method
POST /api/user/register
// Response Body
// Content-Type: application/json
{
"id": "...",
"token": "...", // 建议使用 token 作为用户标识符
"email": "",
"name": "",
"avatar": "",
"create_timestamp": 1692784307, // UNIX 时间戳,单位为秒
"update_timestamp": 1692784307 // UNIX 时间戳,单位为秒
}
该方法将返回完整的用户信息,后续校验订单或订阅是否可用的方法均依赖于这些信息。
// Request Method
POST /api/user/oauth/google
// Request Body
// Content-Type: application/json
{
"credential": "...", // 必需;传入 Google 登录组件返回的 credential 字段 (凭证)
"user_token": "...", // 可选;如果之前使用了预注册标记用户,在这里传入预注册获取的 token
"verify_exp": false // 可选;检查 Google 登录凭证是否过期,默认为 false,不检查
}
// Response Body
// Content-Type: application/json
{
"id": "...", // 将用于创建 Lemon Squeezy 订单或订阅
"token": "...", // 将用于校验 Lemon Squeezy 订单或订阅是否可用
"email": "...",
"name": "...",
"avatar": "...",
"create_timestamp": 1692784307, // UNIX 时间戳,单位为秒
"update_timestamp": 1692784720 // UNIX 时间戳,单位为秒
}
更多 Google 登录的信息可以参见 官方文档。
首先你需要在 Lemon Squeezy Products 页面 点击「Share」按钮,生成一个 Checkout Link,
随后需要在 Web App 中按照以下规则在 Checkout Link 中添加自定义字段:
const checkoutUrl = 'https://YOUR_CHECKOUT_LINK'
+ `?checkout[custom][user_id]=${userId}` // 必需;填写之前 Google 登录获取到的 id 字段,否则无法校验订单或订阅
+ `&checkout[email]=${email}` // 可选;直接将 Gmail 邮箱填写到 Checkout 页面,免得用户再手动填写一次
完整示例可参见笔者的示例代码 lemontree/src/Order.tsx。
请务必理解在 创建 Lemon Squeezy 商店 章节中提到的商店和商品的关系。
// Request Method
GET /api/orders/check
?user_token=... // 必需;当前用户的 token
&store_id=... // 必需;商店 ID,在 Lemon Squeezy 的 Settings - Stores 页面获取
&product_id=... // 必需;产品 ID,在 Lemon Squeezy 的 Product Details 页面获取
&variant_id=... // 必需;变种 ID,在 Lemon Squeezy 的 Product Details 页面获取
&test_mode=false // 可选;默认为 false,即线上环境
// Response Body
// Content-Type: application/json
{
"available": true, // true 表示可用(已支付),false 表示其它不可用状态
"status": "paid", // 当前订单状态;枚举:
// "pending" - 等待支付
// "paid" - 已支付
// "failed" - 支付失败
// "refunded" - 已退款
"created_at": "2023-08-08T10:02:20", // 创建时间,格式为符合 ISO 8601 的字符串
"updated_at": "2023-08-09T10:04:28" // 更新时间,格式为符合 ISO 8601 的字符串
}
如果想在测试环境测试,即 &test_mode=true
,需要在 Stripe 申请一张测试信用卡。
请务必理解在 创建 Lemon Squeezy 商店 章节中提到的商店和商品的关系。
// Request Method
GET /api/subscriptions/check
?user_token=... // 必需;当前用户的 token
&store_id=... // 必需;商店 ID,在 Lemon Squeezy 的 Settings - Stores 页面获取
&product_id=... // 必需;产品 ID,在 Lemon Squeezy 的 Product Details 页面获取
&variant_id=... // 必需;变种 ID,在 Lemon Squeezy 的 Product Details 页面获取
&test_mode=false // 可选;默认为 false,即线上环境
// Response Body
// Content-Type: application/json
{
"available": true, // true 表示可用(试用期或活跃期),false 表示其它不可用状态
"status": "paid", // 当前订阅状态;枚举:
// "on_trial" - 试用期
// "active" - 活跃期
// "paused"
// "past_due"
// "unpaid"
// "cancelled"
// "expired"
"created_at": "2023-08-08T10:02:20", // 创建时间,格式为符合 ISO 8601 的字符串
"updated_at": "2023-08-09T10:04:28" // 更新时间,格式为符合 ISO 8601 的字符串
}
当订阅不可用时 status
较为复杂,具体可以参见 Lemon Squeezy API 文档。
如果想在测试环境测试,即 &test_mode=true
,需要在 Stripe 申请一张测试信用卡。
在创建订单或订阅时,你可以配置是否同时生成证书(License)。
对于证书而言,其实是不需要与账号绑定的,只要证书有效即可校验通过。
所以证书适用于一些不需要登录功能的场景,比如 Windows 激活码。
// Request Method
GET /api/licenses/check
?license_key=... // 必需;填写对应订单或订阅的 license key(用户可以在自己的邮件中获取到)
&test_mode=false // 可选;默认为 false,即线上环境
// Response Body
// Content-Type: application/json
{
"available": true, // true 表示可用(活跃期),false 表示其它不可用状态
"status": "active", // 当前证书状态;枚举:
// "inactive" - 非活跃期
// "active" - 活跃期
// "expired" - 已过期
// "disabled" - 被禁止
"activation_limit": 5, // 该证书的激活次数限制
"instances_count": 4, // 该证书已经被激活次数
"created_at": "2023-08-08T10:02:20", // 创建时间,格式为符合 ISO 8601 的字符串
"updated_at": "2023-08-09T10:04:28" // 更新时间,格式为符合 ISO 8601 的字符串
}
如果想在测试环境测试,即 &test_mode=true
,需要在 Stripe 申请一张测试信用卡。
请务必使用该接口激活证书,而不是自己请求 Lemon Squeezy API,
// Request Method
POST /api/licenses/activate
// Request Body
// Content-Type: application/json
{
"license_key": "...", // 必需;用户手动填写的 license key
"instance_name": "..." // 必需;给这次激活操作起个名字,比如在什么设备上激活的
}
// Response Body
// Content-Type: application/json
{
"available": true, // true 表示可用(活跃期),false 表示其它不可用状态
"status": "active", // 当前证书状态;枚举:
// "inactive" - 非活跃期
// "active" - 活跃期
// "expired" - 已过期
// "disabled" - 被禁止
"activation_limit": 5, // 该证书的激活次数限制
"instances_count": 4, // 该证书已经被激活次数
"created_at": "2023-08-08T10:02:20", // 创建时间,格式为符合 ISO 8601 的字符串
"updated_at": "2023-08-09T10:04:28" // 更新时间,格式为符合 ISO 8601 的字符串
}
以上就是目前支持的所有 API 了,应该能满足大部分账号和支付场景。
如有新需求或者 Bugs 可以在本项目的 Issues 页面反馈。
问:该项目可以同时支持多个商店吗?
答:可以,只需要部署一次,在调用 API 时设置对应的 store_id
即可。
问:该项目可以同时支持多个 Web App 吗?
答:可以,只需要部署一次,通过 redis-cli
添加不同 Web App 的 Google API 凭证即可。
问:该项目可以给非 Web App 使用吗?
答:可以,任何不上架 App Store 或者 Google Play 的软件都可以使用。
问:如何手动管理订单、订阅和证书,比如给用户退款?
答:在 Lemon Squeezy Dashboard 直接操作即可,系统会根据 Webhooks 回调自动更新订单、订阅或证书状态。
问:该项目与直接请求 Lemon Squeezy API 有何不同吗?
答:如果你试图在前端直接接入 Lemon Squeezy API,你将暴露你的 API Key;且 Lemon Squeezy API 设计并不合理,该项目解决了不同 API 之间的差异性,否则你将不得不在每个 App 里实现相似的兼容逻辑。