文件目录
Shopify Webhook 可以在产品、订单或客户发生变化时通知应用。
传统 Webhook 的使用方式比较直接:
Shopify 数据发生变化
→ Shopify 发送完整事件数据
→ 应用接收 Webhook
→ 应用更新自己的数据库
这种方式在小规模应用中没有太大问题。
但当应用连接了几百甚至几千个 Shopify 店铺时,情况会复杂很多。应用可能会收到大量产品变化事件,而每个事件都携带很多暂时用不到的数据。
新的 Shopify 事件订阅机制尝试解决这个问题。开发者可以通过 GraphQL 查询指定自己真正需要的字段,让 Shopify 只发送应用需要的数据。
本文使用 Gadget 构建一个产品同步示例:
- 监听产品创建事件。
- 监听产品更新事件。
- 使用 GraphQL 查询指定 Webhook 数据字段。
- 使用 Gadget POST Route 接收事件。
- 使用 HMAC 验证请求确实来自 Shopify。
- 使用 Shopify Client Secret 进行签名校验。
- 使用 Gadget API 将产品 Upsert 到数据库。
- 保留传统产品删除 Webhook。
- 测试 Shopify 事件是否成功到达 Gadget。
重要说明:该功能仍处于开发者预览
字幕中的新事件系统处于 Shopify Developer Preview 阶段。
这意味着:
- 功能可能存在问题。
- 配置格式可能发生变化。
- API 版本可能不稳定。
- 事件 Payload 结构可能调整。
- 当前不建议直接用于关键生产业务。
- 上线前必须重新核对 Shopify 最新文档。
本文会按照字幕实际演示的流程介绍技术原理和开发步骤,但生产环境仍建议优先使用当前稳定的 Shopify Webhook 方案,等新机制正式稳定后再迁移关键业务。
这个项目实际演示了什么
使用 Shopify 产品事件同步外部数据库
项目模拟一个常见场景:
Shopify 店铺中创建或更新产品
→ Shopify 发送事件
→ Gadget 接收产品数据
→ Gadget 将产品写入自己的数据库
应用自己的数据库可以用于:
- 建立产品搜索索引。
- 创建外部数据仓库。
- 生成报表。
- 同步到其他系统。
- 触发后续业务逻辑。
- 为 AI 推荐准备数据。
同时使用新旧两套机制
字幕中的项目没有完全删除传统 Webhook,而是同时使用两种方式:
- 产品创建和更新使用新的事件订阅机制。
- 产品删除继续使用传统 Shopify Webhook。
这样做可以展示两套机制可以在同一个应用中并存。
实际项目中,也可以先只将一部分低风险事件迁移到新机制,保留核心业务继续使用稳定 Webhook。
项目使用的技术栈
Shopify 应用
项目是一个 Shopify 应用,使用 Shopify 应用配置文件管理:
- API 版本。
- Webhook。
- 事件订阅。
- 接收地址。
- GraphQL 数据查询。
- 开发环境设置。
Gadget 后端
Gadget 提供:
- Node.js 后端。
- PostgreSQL 数据库。
- Shopify 应用连接。
- HTTP Route。
- API 和 Action。
- 数据模型。
- 日志系统。
- 开发和生产环境。
- Shopify Client Secret 环境变量。
GraphQL 查询
新的事件订阅允许开发者使用 GraphQL 查询描述想接收的数据。
例如,只需要产品 ID、标题和更新时间时,可以使用类似的字段选择:
{
id
title
handle
updatedAt
}
如果还需要产品状态,可以加入:
{
id
title
handle
status
updatedAt
}
查询中只应该保留应用真正需要的数据。
HMAC 签名校验
Gadget 接收 Shopify POST 请求后,需要使用 Shopify Client Secret 验证 HMAC 签名。
HMAC 校验可以帮助应用确认:
这个请求确实由 Shopify 发送
不要只因为请求来自一个公开 URL,就直接相信请求内容。
创建 Gadget 产品同步项目
使用已有模板
字幕中的代码基于一个已经配置好的 Gadget 项目。
项目包含:
- Shopify 产品数据模型。
- 产品同步逻辑。
- 产品删除 Webhook。
- 产品 POST Route。
- 数据库写入逻辑。
- 开发环境配置。
如果从零开始,也可以先创建一个 Gadget Shopify 应用,再添加 Product 数据模型和自定义 POST Route。
准备 Product 数据模型
在 Gadget 中创建或确认 Product 模型。
至少需要保存:
- Shopify 产品 ID。
- 产品标题。
- 产品 Handle。
- 产品状态。
- 产品更新时间。
如果应用只需要同步产品标题,也不必在数据库中保存整个产品对象。
保存 Shopify 店铺关系
产品记录还应该关联所属 Shopify 店铺。
示意结构:
Product
├── shop
├── shopifyProductId
├── title
├── handle
└── updatedAt
同一个 Shopify 产品 ID 可能在不同店铺中出现,因此建议使用:
shopId + shopifyProductId
作为业务上的唯一组合,而不是只使用产品 ID。
配置 Shopify 事件订阅
打开开发环境应用配置
打开 Shopify 应用的开发环境配置文件,例如:
shopify.app.development.toml
字幕中的事件配置位于 Shopify 应用配置文件中。
正式项目应区分:
Development App
Production App
开发环境先使用 Development Store 测试,不要直接连接生产店铺。
使用不稳定 API 版本
因为该功能处于开发者预览,字幕中使用了:
unstable
API 版本。
这只是为了测试开发者预览功能,不代表生产环境应该永久使用不稳定版本。
正式发布前应:
- 检查 Shopify 当前支持的稳定 API 版本。
- 查看新事件机制是否已经正式发布。
- 更新应用配置。
- 重新验证 Payload 结构。
- 重新测试签名校验和事件投递。
配置产品创建事件
添加一个产品创建事件,并为它设置:
- 事件类型。
- Gadget POST Route 地址。
- GraphQL 查询。
- API 版本。
事件接收地址就是 Shopify 向 Gadget 发送 POST 请求的目标。
例如:
https://your-gadget-domain.com/api/products
实际 URL 应以 Gadget 当前环境生成的公开 Route 地址为准。
配置产品更新事件
产品更新事件使用同样的结构:
- 事件类型改为产品更新。
- 接收地址仍然指向产品 Route。
- 使用相同或不同的 GraphQL 查询。
- 根据应用需要选择字段。
创建和更新可以共用一个处理入口,也可以使用不同的 Route。
共用一个入口时,需要从事件 Payload 或请求头中判断事件类型。
配置产品删除事件
字幕中的产品删除仍然使用传统 Shopify Webhook。
这样做是因为:
- 新事件机制仍在开发者预览。
- 删除事件可能需要单独处理。
- 传统 Webhook 已经可以稳定工作。
- 可以逐步迁移,而不是一次切换所有事件。
产品删除事件通常只需要产品 ID,因此传统 Webhook 对这个场景已经足够。
用 GraphQL 查询控制 Payload
默认接收大量数据的问题
如果应用接收完整产品对象,Payload 可能包含:
- 产品基本信息。
- 产品描述。
- 媒体。
- 产品选项。
- 变体。
- Metafields。
- 时间字段。
- 状态字段。
- 其他嵌套数据。
但很多应用只需要:
产品 ID
产品标题
更新时间
接收多余字段会增加:
- 网络传输量。
- 请求解析时间。
- 应用计算量。
- 数据库处理量。
- 存储成本。
- 事件处理复杂度。
只请求产品标题
如果应用只需要同步产品标题,可以将查询缩小为:
{
id
title
}
Shopify 发送的事件数据就只包含这些应用需要的字段。
请求更多产品字段
如果应用还需要 Handle、状态和更新时间,可以使用:
{
id
title
handle
status
updatedAt
}
每增加一个字段,都应确认应用确实会使用它。
不要为了方便请求完整对象
如果应用需要完整产品对象,可以继续使用传统 Webhook。
新事件订阅最有价值的场景不是“把所有字段换一种方式接收”,而是:
只监听特定字段
只传输真正需要的数据
只处理相关业务
创建 Gadget POST Route
设置公开 POST 入口
在 Gadget 中创建一个用于接收 Shopify 事件的 POST Route。
字幕中的 Route 类似:
products
这个 Route 要求:
- 可以接收 Shopify POST 请求。
- 使用 HTTPS。
- 可以访问生产环境。
- 能够读取原始请求体。
- 能够读取 Shopify HMAC 请求头。
- 能快速返回成功响应。
不要在 Shopify 配置中使用本地 localhost 地址作为生产事件接收地址。
设置 Shopify Client Secret
在 Gadget 的环境变量中添加 Shopify Client Secret:
SHOPIFY_API_SECRET
具体变量名可以根据项目规范命名,但必须保证代码和环境变量名称一致。
将变量设置为 Secret。
不要将 Client Secret 写入:
- 前端代码。
- Git 仓库。
- Shopify 配置公开文件。
- 日志。
- 客户端请求。
Shopify Client Secret 应只在 Gadget 后端使用。
验证 Shopify HMAC
读取请求签名
Shopify 会在请求头中提供 HMAC 签名。
Gadget Route 收到请求后,先读取:
- 原始请求体。
- HMAC 请求头。
- Shopify Client Secret。
使用原始请求体计算签名
HMAC 校验必须基于 Shopify 发送过来的原始请求体进行。
基本流程是:
读取原始 POST Body
→ 使用 Shopify Client Secret 计算 HMAC
→ 与请求头中的 HMAC 比较
→ 一致才继续处理
不要先随意修改 JSON,再使用修改后的内容进行签名校验。
如果框架已经自动解析了请求体,需要确认仍然可以获取原始 Body。
校验失败时拒绝请求
如果 HMAC 不匹配:
- 不要写入数据库。
- 不要执行产品更新。
- 不要信任请求中的产品 ID。
- 返回未授权或签名错误响应。
- 记录必要的安全日志。
不要把 Client Secret 写入错误日志。
解析事件 Payload
获取事件主体
签名验证通过后,再解析 JSON Body。
字幕中的处理逻辑会从请求中提取:
- 事件 ID。
- 产品数据。
- Shopify 产品 ID。
- 查询中配置的字段。
概念上可以理解为:
event body
├── event metadata
├── product id
└── product fields
只依赖查询中请求的字段
如果配置的 GraphQL 查询只返回:
{
id
title
}
那么 Route 就不应该假设 handle、status 或 variants 一定存在。
处理代码必须与实际查询保持一致。
使用 Upsert 保存产品
为什么不能只用 Create
字幕中的 Shopify 事件在测试时出现了多次投递。
如果每次收到事件都执行 Create,就可能在数据库中产生重复产品。
因此,产品同步应该使用 Upsert:
产品已存在
→ 更新产品
产品不存在
→ 创建产品
设计唯一标识
建议为产品记录使用唯一标识:
shopId + Shopify Product ID
这样可以区分不同店铺中的同一个 Shopify 资源 ID。
示意逻辑如下:
查找当前店铺和 Shopify Product ID
→ 找到记录则更新
→ 找不到记录则创建
为什么 Shopify 可能多次发送事件
字幕中测试创建产品时,同一事件似乎被发送了多次。
Webhook 和事件投递系统通常不能简单假设“每个事件只到达一次”。
重复投递可能来自:
- 网络重试。
- Shopify 投递重试。
- 接收端响应太慢。
- 处理过程暂时失败。
- 开发环境测试行为。
- 事件系统本身的投递机制。
所以,接收端必须具备幂等能力。
幂等不只是为了避免重复产品
Upsert 还可以避免:
- 重复创建数据库记录。
- 重复触发下游任务。
- 重复发送通知。
- 重复建立外部系统关系。
- 重复处理相同事件。
如果后续逻辑具有副作用,还需要基于事件 ID 或业务唯一键单独去重。
Gadget Public API 与 Internal API
使用 Public API
字幕中使用的是 Gadget Public API。
Public API 会:
- 执行对应的数据操作。
- 运行相关 Action 代码。
- 触发业务逻辑。
- 参与权限控制。
如果产品同步时还需要执行其他业务规则,Public API 更适合。
使用 Internal API
Gadget Internal API 更接近直接修改数据库。
它通常不会运行完整的 Action 代码。
Internal API 适合:
- 只需要直接写入数据。
- 已经在 Route 中完成所有校验。
- 不需要触发额外业务 Action。
- 追求简单的数据保存流程。
应该如何选择
| 场景 | 推荐方式 |
|---|---|
| 需要执行 Action 业务逻辑 | Public API |
| 只想直接更新数据库 | Internal API |
| 需要访问权限控制 | Public API |
| Route 已完成全部校验 | 可考虑 Internal API |
| 需要触发关联操作 | Public API |
无论使用哪一种 API,HMAC 验证都应该在写入前完成。
测试产品创建事件
在开发店铺创建产品
打开 Shopify Development Store,创建一个测试产品:
Test Product
填写产品标题并保存。
保存后,Shopify 应该向 Gadget 产品 Route 发送事件。
查看 Gadget 日志
进入 Gadget 日志页面,检查:
- Route 是否收到请求。
- HMAC 校验是否成功。
- 事件主体是否能够解析。
- 产品 ID 是否存在。
- Product Upsert 是否成功。
- 数据库中是否生成或更新产品。
检查字段是否按查询返回
如果 GraphQL 查询中只请求了:
{
id
title
}
日志和数据库中就不应该期待完整的产品字段。
如果需要新的字段,应:
- 更新 GraphQL 查询。
- 更新产品数据模型。
- 更新 Route 处理代码。
- 重新测试事件 Payload。
测试产品更新事件
修改产品标题
在 Shopify Admin 中修改测试产品标题并保存。
检查 Gadget 是否收到产品更新事件。
检查 Upsert 结果
数据库中应该仍然只有一条对应产品记录,但标题已经更新。
正确结果是:
一条产品记录
标题已更新
而不是:
两条相同产品记录
如果出现重复记录,说明 Upsert 唯一条件设计不正确。
传统 Webhook 与新事件订阅并存
删除事件继续使用传统 Webhook
字幕中只为产品创建和更新使用新机制,删除仍然使用传统 Shopify Webhook。
产品删除时,应用可以根据 Shopify 产品 ID删除本地记录。
为什么删除事件可以暂时保留传统方式
原因包括:
- 新机制仍处于预览。
- 删除场景通常只需要少量字段。
- 传统删除 Webhook 已经可以工作。
- 逐步迁移更容易排查问题。
- 不需要同时改动所有业务。
迁移策略
推荐采用分阶段迁移:
第一阶段:传统 Webhook 保持不变
第二阶段:低风险产品更新迁移
第三阶段:比较两套系统的结果
第四阶段:确认稳定后再迁移更多事件
不要为了使用新功能,直接关闭所有已有 Webhook。
开发者预览阶段的生产注意事项
不要直接用于关键生产流程
由于新事件订阅仍是 Developer Preview,生产使用前需要谨慎评估。
尤其是以下业务不建议直接依赖预览功能:
- 订单履约。
- 库存扣减。
- 支付状态。
- 客户数据删除。
- 财务系统同步。
- 关键商品价格更新。
这些流程应优先使用当前稳定的 Shopify Webhook 和应用 API。
记录原始事件
开发测试时,建议记录:
- Event ID。
- Shopify 店铺域名。
- 事件类型。
- 接收时间。
- HMAC 校验结果。
- Payload 摘要。
- Upsert 结果。
- 错误信息。
不要在日志中保存不必要的客户敏感数据或完整秘密信息。
让 Route 快速返回
事件接收 Route 不应该执行很长时间的任务。
推荐流程:
验证 HMAC
→ 解析必要字段
→ 保存事件或产品
→ 快速返回成功
→ 复杂任务放到后台队列
如果要执行复杂计算、调用外部服务或同步大量子记录,可以先保存事件,再使用 Gadget Background Job 异步处理。
处理重试和重复投递
必须设计:
- Upsert。
- 唯一索引。
- 事件去重。
- 失败重试。
- 错误日志。
- 死信或人工处理机制。
不能依赖“Shopify 只会发送一次”的假设。
总结
这份字幕实际演示的是 Shopify 开发者预览中的新事件订阅机制。
它与传统 Webhook 的最大区别是:
传统 Webhook
→ Shopify 按固定 Payload 发送数据
新事件订阅
→ 开发者用 GraphQL 查询指定需要的数据
→ Shopify 发送更精确的事件 Payload
项目使用 Gadget 完成:
- Shopify 产品创建事件订阅。
- Shopify 产品更新事件订阅。
- 传统产品删除 Webhook。
- Shopify Client Secret 配置。
- HMAC 签名校验。
- Gadget POST Route。
- GraphQL 字段选择。
- Product Upsert。
- Shopify Development Store 测试。
- Gadget 日志调试。
完整流程可以概括为:
配置 Shopify 事件订阅
→ 指定 POST Route
→ 编写 GraphQL 字段查询
→ 配置 Shopify Client Secret
→ 接收 Shopify 事件
→ 验证 HMAC
→ 解析 Payload
→ 根据店铺和产品 ID 执行 Upsert
→ 检查 Gadget 日志和数据库
这套机制最适合需要精确控制同步字段的应用。它可以减少无用数据传输、降低事件处理压力,并让大型 Shopify 应用更容易管理不同店铺产生的大量产品事件。
但在功能仍处于开发者预览阶段时,建议先在 Development Store 中测试,并继续使用稳定的传统 Webhook 处理关键生产数据。




