Skip to content

Tutorial (Chinese)

Matthew Lee edited this page Aug 24, 2023 · 155 revisions

背景

对于一个全球化的小型 SaaS 付费产品而言,通常会涉及到账号和支付这两个功能。我们需要知道一个用户是否购买了某项服务、该服务是否到期等等,以便动态开启或关闭该用户的付费权益。

具体到 Web App 层面,在笔者有限的职业生涯当中,并没有发现一个好用的同时支持账号和支付的管理系统。许多开发者不得不自己开发一套类似的系统,或者考虑到实现难度和工期而放弃接入支付(就像笔者之前只会开发免费 App 那样)。从另一个角度可能也说明,类似的实现可能都是业务逻辑强相关,不便于分享。

所以笔者期望能实现一个业务无关的、支持账号和支付的管理系统,从而在今后的独立开发过程中能快速接入,一劳永逸 💰

幸运的是,我们不用从零实现一个这样的系统:

我们只需要将上述服务组合即可。相信读者已经知道,笔者最终选择了 Google 登录 + Lemon Squeezy 的组合方式。

为什么选择 Google 登录

从生态建设(Web、Android)来看,Google 的账号系统是最为强势的,可以覆盖尽可能多的人群。

为什么选择 Lemon Squeezy

尽管笔者给出了 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 商店

如果你之前没有 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 凭证

接着我们要在 这个链接 创建 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 同意屏幕

接着我们要在 这个链接 创建 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 git@github.com: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
  • 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

现如今几乎所有浏览器和操作系统都强制使用 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 的使用方法可以参见 官方文档

APIs

目前我们已经支持如下场景:

  • 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 时间戳,单位为秒
}

Google 登录

该方法将返回完整的用户信息,后续校验订单或订阅是否可用的方法均依赖于这些信息。

// 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 页面反馈。

Q & A

问:该项目可以同时支持多个商店吗?

答:可以,只需要部署一次,在调用 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 里实现相似的兼容逻辑。