Shopify 新事件订阅实战:使用 GraphQL 精确控制 Webhook 数据

文件目录

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
}

日志和数据库中就不应该期待完整的产品字段。

如果需要新的字段,应:

  1. 更新 GraphQL 查询。
  2. 更新产品数据模型。
  3. 更新 Route 处理代码。
  4. 重新测试事件 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 处理关键生产数据。

参考资料

citation:Shopify Webhook 创建与管理

citation:Shopify Winter ’25 开发者更新