文章目录
如果 Shopify 应用只收取固定月费,传统的订阅计费逻辑通常比较简单。
但当应用需要根据实际使用量收费时,事情就会复杂很多。例如:
- 每生成一个折扣码收费一次。
- 每处理一张图片收费一次。
- 每次调用 AI 功能收费一次。
- 根据客户使用量分级收费。
- 采用固定月费加额外使用费。
- 为不同商家提供不同套餐。
Shopify 现在提供了统一的 Shopify App Pricing,用于配置固定订阅、按用量计费和组合套餐。App Events API 则负责接收应用上报的使用事件,并将这些事件用于应用分析或按用量计费。
本文使用 Gadget 的折扣码生成器作为基础项目,完成一个带计费功能的 Shopify 嵌入式应用。
应用的示例逻辑是:
商家打开应用
→ 检查当前是否有有效套餐
→ 没有套餐则跳转到定价页
→ 有套餐则进入应用
→ 商家生成折扣码
→ 应用上报一次 generate-discount 使用事件
→ Shopify 根据套餐规则计算费用
本项目实际演示了什么
从折扣码生成器开始
字幕中的项目不是从空白应用开始,而是 Fork 了一个 Gadget 折扣码生成器模板。
这个模板已经包含:
- Shopify 嵌入式应用。
- React 前端。
- Gadget Node.js 后端。
- Discount Code 数据模型。
- 生成折扣码的 Action。
- Shopify 应用连接。
- Development Store 安装流程。
应用的原始功能很简单:商家点击按钮后,应用生成一条折扣码,并将它保存到 Gadget 数据库。
为生成折扣码增加计费
在计费版本中,每当商家成功生成一条折扣码,应用就会上报一次使用事件:
generate-discount
这个事件可以被 Shopify App Pricing 中的按用量套餐用作计量单位。
例如:
每生成一条折扣码,收费 1 美分
实际价格只是教程示例,生产应用应根据目标客户、服务成本和 Shopify 应用定价策略进行设计。
当前 Shopify App Pricing 的计费模式
固定周期订阅
固定订阅适合:
- 月费套餐。
- 年费套餐。
- 不限制使用量的基础服务。
- 按功能划分的标准套餐。
例如:
Monthly Plan:每月 10 美元
Yearly Plan:每年 100 美元
Shopify App Pricing 支持月度和年度订阅,也可以为年度套餐设置折扣。
按用量计费
按用量计费适合:
- 按生成次数收费。
- 按处理数量收费。
- 按 AI 调用次数收费。
- 按存储量收费。
- 按导入商品数量收费。
本项目中,计量单位是:
generate-discount
每上报一次这个事件,就代表应用完成了一次可计费操作。
组合套餐
组合套餐由两部分组成:
固定月费 + 按用量收费
例如:
每月 5 美元基础费用
+
每生成一条折扣码收费 1 美分
这种模式适合既需要稳定基础收入,又需要根据实际使用量收费的应用。
免费试用和零美元测试套餐
Shopify App Pricing 支持免费试用和零美元测试套餐。
开发阶段可以创建一个只允许测试店铺使用的私有套餐:
Monthly Test Plan:0 美元
这样可以测试:
- 套餐选择页面。
- 商家授权流程。
- 订阅状态读取。
- 应用重定向。
- 应用事件上报。
不需要让开发者为自己的测试操作支付真实费用。
创建 Gadget Shopify 应用
Fork 折扣码生成器模板
打开 Gadget,使用折扣码生成器模板创建项目。
项目名称可以使用:
billing-tutorial
如果名称已经被使用,可以换成:
billing-tutorial-dev
创建完成后,检查项目结构。
查看 Discount Code 模型
折扣码模型通常包含:
Discount Code
├── Shop
└── Code
其中:
Shop表示折扣码属于哪个 Shopify 店铺。Code保存生成的折扣码文本。
应用的核心 Action 会生成折扣码,并将它写入 Gadget 数据库。
连接 Shopify Development Store
打开 Gadget 项目的 Shopify 连接设置,创建或连接 Shopify 应用。
然后:
- 登录 Shopify 账户。
- 创建开发应用。
- 选择 Shopify Development Store。
- 安装应用。
- 在 Shopify Admin 中打开嵌入式应用。
- 点击生成折扣码。
- 确认折扣码能够正常创建。
完成这一步后,先确认原始应用工作正常,再开始添加计费逻辑。
配置开发和生产环境
Gadget 的两个环境
Gadget 项目通常包含:
- Development Environment。
- Production Environment。
开发环境用于:
- 测试计费。
- 测试应用事件。
- 测试套餐跳转。
- 测试 Shopify API。
- 测试数据库和前端。
生产环境用于正式商家和正式计费。
开发和生产应用要分开
生产应用不应该直接使用开发应用的配置。
建议分别使用:
Shopify Development App
→ Gadget Development Environment
以及:
Shopify Production App
→ Gadget Production Environment
生产环境还需要单独配置:
- Shopify Client Secret。
- 应用 URL。
- Webhook。
- API 权限。
- App Pricing 套餐。
- App Events 配置。
配置 Shopify App Pricing
打开应用分发设置
进入 Shopify Partner 或 Dev Dashboard,打开目标应用的分发和定价设置。
当前 Shopify App Pricing 用于配置应用套餐、订阅和按用量计费。
不同版本的 Shopify 管理界面名称可能略有变化,但核心流程是:
打开应用
→ 进入 Distribution 或 App Pricing
→ 创建应用定价方案
→ 配置套餐
→ 保存并发布配置
Shopify App Pricing 已经取代 Managed Pricing,今后的应用计费应优先使用 Shopify App Pricing 流程。
创建私有测试套餐
开发阶段先创建一个 Private Plan。
例如:
Plan Name:Monthly Test
Display Name:Monthly Test
Price:0
为私有套餐添加允许访问的测试店铺域名,例如:
testing-development-store.myshopify.com
私有套餐只对指定测试店铺可见,适合:
- 开发阶段测试。
- 内部演示。
- QA 验证。
- 不希望真实收费的应用测试。
如果没有设置测试店铺域名,应用的定价页可能不会显示这个私有套餐。
创建固定订阅套餐
创建一个月度和年度固定订阅套餐。
示例:
Monthly:每月 10 美元
Yearly:每年 100 美元
年度套餐可以通过较低的年度总价提供折扣。
生产应用还可以增加:
- 免费试用期。
- 不同功能等级。
- 不同商家规模的套餐。
- 更高额度的高级套餐。
创建组合计费套餐
创建一个同时包含固定费用和用量费用的套餐。
示例:
基础费用:每月 5 美元
用量费用:每生成一条折扣码收费 1 美分
创建用量收费部分时,需要填写一个内部 Event Handle:
generate-discount
这个值非常重要。
它必须与代码发送给 App Events API 的 Event Handle 完全一致。
以下两个值不一致,就无法正确计量:
套餐中:generate-discount
代码中:generate_discount
推荐在代码和定价配置中统一使用小写短横线格式。
创建应用定价页面
使用 Shopify 提供的定价页面
Shopify App Pricing 会为应用提供定价选择页面。
页面中可以显示:
- 套餐名称。
- 月费或年费。
- 免费试用。
- 按用量收费规则。
- 当前测试店铺可以使用的私有套餐。
- 商家选择和批准套餐的入口。
应用不应该自己伪造一套付款流程,而应使用 Shopify 的应用定价和订阅批准流程。
测试店铺域名很重要
如果私有测试套餐没有绑定正确的店铺域名,测试店铺可能看不到这个套餐。
检查:
- Shopify 店铺的
.myshopify.com域名。 - Partner Dashboard 中填写的域名。
- 应用当前连接的店铺。
- Development App 是否与测试店铺匹配。
不要只使用店铺显示名称。定价配置通常需要使用准确的店铺域名或 Shopify 要求的店铺标识。
在应用中检查订阅状态
为什么需要检查当前套餐
应用不能只把定价页放在那里,却允许所有用户直接使用收费功能。
应用打开时,应该先检查当前安装是否存在有效订阅。
基本流程是:
应用加载
→ 获取当前 Shopify 店铺
→ 查询当前 App Installation
→ 读取 active subscriptions
→ 找到有效订阅
→ 进入应用
如果没有有效订阅:
跳转到 Shopify 应用定价页
查询当前应用安装
可以通过 Shopify Admin GraphQL API 查询当前应用安装信息和有效订阅。
需要关注的信息包括:
- 当前应用安装。
- 当前有效订阅。
- 订阅状态。
- 套餐名称。
应用可能同时存在:
- 已取消的旧订阅。
- 当前有效订阅。
- 切换套餐中的临时记录。
因此,不能只检查订阅列表是否为空,而要检查订阅状态是否为有效状态。
判断是否有有效套餐
后端加载逻辑可以按照以下思路处理:
读取 active subscriptions
→ 遍历订阅列表
→ 找到状态为 active 的订阅
→ 获取当前套餐名称
→ 返回套餐信息
如果找到了有效订阅:
允许进入应用
如果没有找到有效订阅:
返回定价页面地址
不要只根据套餐名称判断权限。套餐名称可能被商家或管理员调整,真正重要的是订阅状态和应用定价配置。
将未订阅用户重定向到定价页
嵌入式应用的重定向问题
Shopify 嵌入式应用运行在 iframe 中。
如果直接在 iframe 内打开定价页面,可能会导致定价页面仍然嵌在应用内部,用户体验不正确。
因此,需要让重定向跳出 iframe,在 Shopify Admin 的完整页面中打开定价流程。
处理三种页面状态
应用加载时可以处理:
正在加载
显示全页面 Loading 状态。
正在检查订阅状态
没有订阅
显示:
正在跳转到定价页面
然后跳转到 Shopify App Pricing 页面。
已有订阅
显示正常的应用界面。
显示当前套餐
当用户有有效订阅后,可以在应用顶部显示当前套餐:
Current plan: Monthly Test
同时添加一个按钮:
Pricing Plans
点击后可以回到 Shopify 应用定价页面,方便商家升级或切换套餐。
使用 App Events API 记录用量
什么是 App Events API
App Events API 用于向 Shopify 上报应用中发生的事件。
它可以记录:
- 可计费事件。
- 普通应用活动。
- 功能使用次数。
- 应用指标。
- 商家行为数据。
如果使用 Shopify App Pricing 的按用量套餐,App Events API 就可以作为自动计量入口。
创建计量事件
本项目将“生成折扣码”定义为一个事件:
generate-discount
每当商家成功生成一条折扣码,应用就上报一次事件。
如果一次操作生成多条折扣码,也可以通过事件属性中的数量值记录实际数量。
App Event 与套餐 Event Handle 必须一致
定价套餐中的计量句柄:
generate-discount
代码上报的事件句柄也必须是:
generate-discount
它们必须完全一致,包括:
- 字母大小写。
- 连字符。
- 空格。
- 下划线。
- 拼写。
建议将事件句柄定义为共享常量,避免在不同文件中手动重复输入。
在 Gadget 中配置 Shopify Client Secret
创建环境变量
在 Gadget 项目的 Environment Variables 中添加:
SHOPIFY_CLIENT_SECRET
将它设置为 Secret。
不要把 Shopify Client Secret 写在:
- React 前端。
- Git 仓库。
- 浏览器代码。
- 日志输出。
- 公开的配置文件。
获取 Client Secret
在 Shopify Dev Dashboard 中打开对应应用的设置,复制 Client Secret。
将它保存到 Gadget 的服务器端环境变量中。
Development 和 Production 应分别配置各自的 Secret。
为什么需要 Client Secret
App Events API 的服务端请求需要使用 Shopify 应用凭据获取访问令牌。
这个流程必须在 Gadget 后端完成,因为 Client Secret 不能暴露给浏览器。
编写上报计量事件的后端工具
创建工具文件
在 Gadget API 目录下创建工具文件,例如:
api/
utils/
reportBillingEvent.ts
这个工具负责:
- 获取 Shopify 应用访问令牌。
- 读取当前 Shopify 店铺 ID。
- 构造 App Event。
- 发送事件到 Shopify。
- 记录响应状态。
- 处理错误。
- 设置幂等键。
获取 Shopify 访问令牌
后端首先使用 Shopify 应用凭据获取用于发送事件的访问令牌。
请求需要在服务器端完成,并使用环境变量中的 Client Secret。
示意流程如下:
Gadget 后端
→ Shopify Token Endpoint
→ 获取访问令牌
→ 使用 Bearer Token 调用 App Events API
不要在 React 前端直接请求 Token Endpoint。
构造 App Event
事件通常需要包括:
- Shopify 店铺 ID。
- Event Handle。
- 事件发生时间。
- 幂等键。
- 用量值或其他属性。
示意结构如下:
{
shopId: currentShopId,
eventHandle: "generate-discount",
timestamp: new Date().toISOString(),
idempotencyKey: uniqueEventId,
attributes: {
value: 1
}
}
具体请求格式应以当前 Shopify App Events API 文档为准。
使用幂等键
幂等键用于避免同一事件因为网络重试而被重复计费。
例如:
同一条折扣码生成操作
→ 请求发送一次
→ 网络超时
→ 应用再次发送
→ Shopify 根据相同幂等键只计量一次
不要简单使用:
shopId + 当前日期
作为所有事件的幂等键,因为同一个店铺一天可能生成很多条折扣码。
更安全的做法是为每一次成功的业务操作生成唯一事件 ID,例如:
discountCodeRecordId
或者使用:
shopId + discountCodeId + eventType
确保同一项业务操作始终使用同一个幂等键,而不同操作不会意外共用一个键。
在折扣码 Action 中上报用量
在成功回调中上报
折扣码只有在成功创建后,才应该上报用量事件。
推荐流程:
创建折扣码
→ Shopify 或 Gadget 操作成功
→ 保存 Discount Code 记录
→ 上报 generate-discount 事件
不要在折扣码创建失败时上报计费事件。
使用当前 Shopify 连接
折扣码创建 Action 需要获得当前 Shopify 店铺的连接信息。
上报工具至少需要知道:
- 当前 Shopify 店铺 ID。
- 当前环境。
- 计量事件句柄。
- 本次操作的唯一 ID。
示意流程如下:
await createDiscountCode();
await reportBillingEvent({
shopId: connections.shopify.currentShopId,
eventHandle: "generate-discount",
value: 1,
idempotencyKey: discountCodeId
});
实际代码应根据 Gadget 当前生成的 Shopify Connection 对象和 Action 参数进行调整。
是否应该等待计量请求完成
如果计量事件是收费依据,应用需要设计清楚:
- 计量请求失败时是否重试。
- 折扣码已经创建但计量失败时怎么办。
- 是否将事件放入后台队列。
- 是否保存待上报状态。
- 是否允许商家继续使用功能。
对于重要的计费事件,不建议简单忽略错误。
可以在 Gadget 数据库中保存:
billingEventPending
billingEventSent
billingEventFailed
这样后续可以重试未成功上报的事件。
在 Dev Dashboard 查看 App Events
查看应用日志
打开 Shopify Dev Dashboard 中的应用日志。
在日志筛选中查看:
- App Event。
- App Billing Event。
刚发送的事件可能需要一点时间才会显示。
检查事件内容
确认以下内容正确:
- Event Handle 是
generate-discount。 - Shop ID 正确。
- 用量值正确。
- 时间戳正确。
- 幂等键存在。
- Shopify 返回成功状态。
如果事件没有出现,检查:
- Client Secret 是否正确。
- Token 是否成功获取。
- Event Handle 是否与套餐配置一致。
- 请求是否从后端发送。
- 当前店铺是否已经安装应用。
- Gadget 环境是否使用了正确的 Shopify 应用配置。
App Events 不只用于计费
即使应用没有使用按用量计费,也可以使用 App Events API 记录功能使用情况。
例如:
discount-created
product-imported
ai-search-used
image-processed
report-exported
这些事件可以帮助开发者了解:
- 商家最常使用什么功能。
- 哪些功能很少被使用。
- 不同套餐的使用差异。
- 应用的真实使用量。
- 计费方案是否合理。
订阅检查与用量计费的关系
没有套餐时不要直接使用收费功能
应用可以根据当前订阅状态决定是否允许生成折扣码。
例如:
没有有效套餐
→ 跳转定价页
或者采用免费额度:
免费用户每月允许生成 10 条折扣码
超过后才需要订阅
具体策略取决于应用业务,不一定所有功能都必须完全锁定。
只对有效套餐上报计费事件
如果事件是实际收费依据,后端应确认:
- 当前应用安装有效。
- 当前套餐处于 active 状态。
- 当前套餐包含这个计量事件。
- 当前操作确实完成。
- 当前事件尚未上报。
不要仅根据前端显示的套餐名称决定是否计费。
处理取消和套餐切换
商家可能会:
- 取消订阅。
- 从月费切换到年费。
- 从基础套餐升级到用量套餐。
- 从用量套餐降级到免费套餐。
- 在旧订阅结束前批准新订阅。
因此,应用需要根据 Shopify 返回的订阅状态动态判断,而不是永久缓存一次套餐名称。
端到端测试流程
测试免费私有套餐
- 创建只允许开发店铺使用的零美元私有套餐。
- 打开 Shopify 应用定价页。
- 选择测试套餐。
- 批准安装或订阅。
- 返回嵌入式应用。
- 确认可以进入应用主页。
测试无订阅重定向
- 取消或移除测试订阅。
- 重新打开应用。
- 检查应用是否查询到没有有效订阅。
- 确认页面跳转到 Shopify App Pricing。
- 确认跳转发生在完整 Shopify Admin 页面,而不是被困在 iframe 中。
测试固定套餐
- 创建月费套餐。
- 创建年费套餐。
- 检查定价页面显示。
- 测试套餐批准流程。
- 确认应用显示当前套餐名称。
- 点击 Pricing Plans,确认可以返回套餐选择页面。
测试按用量套餐
- 创建基础月费加用量收费套餐。
- 配置 Event Handle 为
generate-discount。 - 订阅这个测试套餐。
- 返回应用并生成一条折扣码。
- 确认折扣码成功创建。
- 确认 App Events 日志出现一条事件。
- 检查事件用量值是否为 1。
- 重复发送相同幂等键,确认不会重复计量。
发布前检查
不要把测试套餐直接当作生产套餐
私有套餐适合测试,不应代替正式的 Public Plan。
发布前检查:
- Public Plan 是否已经配置。
- 月费和年费价格是否正确。
- 用量价格是否正确。
- 免费试用是否符合预期。
- Event Handle 是否稳定。
- 计费描述是否清楚。
- 套餐名称是否适合商家理解。
检查用量事件
确认每次收费操作都满足:
- 操作确实成功。
- 事件只发送一次。
- 幂等键稳定且唯一。
- 用量数值正确。
- 失败后可以重试。
- 不会因页面刷新重复收费。
- 不会因后台任务重复执行而重复收费。
检查安全配置
确认以下敏感数据只存在后端:
- Shopify Client Secret。
- Shopify Token。
- App Events API 访问令牌。
- Gadget 生产环境变量。
- 计费事件内部配置。
React 前端只负责显示状态和发起业务请求,不能直接持有 Shopify 应用密钥。
总结
这份字幕实际演示的是一个基于 Gadget 的 Shopify 应用计费项目。
项目从折扣码生成器开始,逐步加入:
- Shopify App Pricing。
- 月费订阅。
- 年费订阅。
- 零美元私有测试套餐。
- 固定费用加按用量收费。
- App Events API。
- 应用订阅状态检查。
- 未订阅用户重定向。
- 当前套餐显示。
- 生成折扣码用量计量。
- 幂等键防止重复计费。
完整流程可以概括为:
创建 Gadget Shopify 应用
→ 安装到 Development Store
→ 创建 Shopify App Pricing 套餐
→ 配置私有测试套餐
→ 查询当前有效订阅
→ 没有订阅时跳转定价页
→ 有订阅时进入应用
→ 完成可计费操作
→ 通过 App Events API 上报用量
→ Shopify 根据套餐进行计量和结算
Shopify App Pricing 现在支持固定周期订阅、按用量计费和组合套餐。App Events API 则把应用中的功能使用记录统一发送到 Shopify,既可以用于应用分析,也可以作为用量计费的计量来源。
对于 Gadget 应用来说,最重要的实现原则是:
- 在后端检查订阅状态。
- 只在业务操作成功后上报事件。
- 使用稳定的 Event Handle。
- 为每个业务操作设置唯一幂等键。
- 将 Client Secret 和 Token 保存在服务器端。
- 为失败的计费事件设计重试机制。
- 在 Development Store 中完成完整测试后,再配置 Public Plan。
参考资料
citation:构建 Shopify 应用与 Shopify App Pricing




