本文面向维护商品读取接口的开发者和发布验收人员。目标是判断一次 GraphQL 响应能否进入下游商品快照:版本是否一致、结果是否完整、字段类型是否符合约定,以及新增枚举会不会被旧代码误解释。
资料核验于 2026年10月4日,查询固定使用 GraphQL Admin API 2026-10。需要已安装并获授权的应用、read_products 商品读取权限,以及开发店中的商品 ID。以下商品 ID、响应和测试都是假设样例;Python 代码已做离线测试,未调用商家 API。这些规则按 API 与授权条件适用,不能凭 Shopify Plus 套餐名推断权限。API 版本规则、Product 权限与字段。
1. 先固定查询,避免验收对象随代码漂移
保存查询文件、变量、请求版本和消费者字段清单。这个例子只读取一个商品,返回别名固定为 catalogItem:
query ProductContract($id: ID!) {
catalogItem: product(id: $id) {
id
title
status
updatedAt
}
}
变量示例是 {"id":"gid://shopify/Product/1001"},其中 ID 必须换成获授权开发店中的真实值。product(id: ID!) 的返回类型是可为空的 Product;输入 ID 必填,不代表结果一定存在。2026-10 product 查询。
需要验证查询时,可在自己的 Shopify CLI 应用项目运行 shopify app dev,按 g 打开该应用的 GraphiQL;它使用当前应用的权限。先在 Docs Explorer 对照字段,再在开发店执行获授权的只读查询。独立安装的 GraphiQL 应用具有自己的权限与数据边界,不能替代当前应用的授权验收。官方 GraphiQL 使用入口。
原始 HTTP 请求入口是 POST https://{store}.myshopify.com/admin/api/2026-10/graphql.json,认证请求使用 X-Shopify-Access-Token。使用官方应用模板或客户端时按其认证流程处理,不把 token 放进浏览器页面、样例响应或验收日志。Admin API 认证与端点。
2. 把 schema 类型翻译成消费者约定
schema 是 API 的类型契约。表中的 ! 表示非空;它约束是否为 null,不代表字符串必然有内容。消费者若要求非空白标题,应另列业务规则,不把业务规则冒充 API 保证。GraphQL 类型与 Non-Null。
| 对象或字段 | 2026-10 类型 | 本例的验收动作 |
|---|---|---|
catalogItem | 查询 product 的返回值 Product | 区分键缺失和明确的 null;为空时暂停更新 |
id | ID! | 保留字符串原值,并与请求 ID 比较;不转成整数 |
title | String! | 检查字符串类型;空字符串通过类型检查 |
status | ProductStatus! | 识别当前四个值;未知值暂停并检查版本与映射 |
updatedAt | DateTime! | 保留原值;本例另要求可解析且带时区的时间字符串 |
字段类型见 Product 字段定义。ID 按字符串序列化;DateTime 是 ISO 8601 日期时间字符串。本例的带时区解析要求属于消费者策略,并非完整的 ISO 8601 校验器。GraphQL ID、Shopify DateTime。
updatedAt 也可能因订单导致的库存调整而变化,不能把它直接当成“标题被修改”的证据。Product.updatedAt。
3. HTTP 200 后继续检查 errors、data 和实际版本
GraphQL 的成功 HTTP 状态不能替代业务验收:Shopify 可以返回 HTTP 200 和 errors。GraphQL 执行中的字段错误还可能与部分 data 同时出现;执行前的请求错误则可能没有 data。Shopify 错误响应、GraphQL 响应结构。
本例采用适合完整商品快照的保守策略:只要出现非空 errors,这次响应就不能覆盖上一次有效快照。允许部分数据显示的产品可以另订策略,但必须说明哪些字段仍可使用。
| 观察到的结果 | 本例处理 | 下一步 |
|---|---|---|
| 非 200 或 JSON 无法解析 | 不进入字段验收 | 保留状态码与请求标识,排查传输或服务错误 |
| 实际版本与请求版本不同 | 暂停提升为有效快照 | 检查请求 URL 和版本支持状态 |
存在非空 errors,即使也有商品 | 保留旧快照 | 根据错误码处理授权、节流或服务故障 |
data 没有 catalogItem 键 | 视为结果形状不符 | 比较查询别名、缓存和解析代码 |
catalogItem 明确为 null | 标为暂不可用 | 检查 ID、授权与资源状态,不据此直接删除本地商品 |
| ID、类型、枚举与时间检查均通过 | 返回允许字段组成的快照 | 由业务流程决定何时提交 |
记录响应头 X-Shopify-API-Version 的实际值;HTTP 头名应由调用层按大小写不敏感方式读取。Shopify 会在请求版本失去支持时回退到最早仍受支持的版本,所以“URL 写了版本”与“实际按该版本执行”需要分别取证。API 版本与响应头。
4. 用一个小函数落实快照入口
下面的 Python 3.11+ 函数只处理已解析的响应,不发请求。调用层负责获取状态码、实际版本头和 JSON;解析失败时停止调用。返回值由状态与快照组成,失败时快照为 None。它只输出允许的四个字段,额外 JSON 字段不会自动流向下游。
from datetime import datetime
VERSION = "2026-10"
STATUSES = {"ACTIVE", "ARCHIVED", "DRAFT", "UNLISTED"}
FIELDS = ("id", "title", "status", "updatedAt")
def accept_product(http_status, actual_version, payload, requested_id):
if http_status != 200:
return "HTTP_FAILURE", None
if actual_version != VERSION:
return "VERSION_MISMATCH", None
if not isinstance(payload, dict):
return "BAD_ENVELOPE", None
if "errors" in payload:
errors = payload["errors"]
if not isinstance(errors, list):
return "BAD_ENVELOPE", None
if errors:
return "GRAPHQL_ERRORS", None
data = payload.get("data")
if not isinstance(data, dict) or "catalogItem" not in data:
return "MISSING_RESULT", None
item = data["catalogItem"]
if item is None:
return "UNAVAILABLE", None
if not isinstance(item, dict) or any(k not in item for k in FIELDS):
return "BAD_FIELDS", None
if any(not isinstance(item[k], str) for k in FIELDS):
return "BAD_TYPES", None
if item["id"] != requested_id:
return "ID_MISMATCH", None
if item["status"] not in STATUSES:
return "UNKNOWN_STATUS", None
value = item["updatedAt"]
try:
stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
if "T" not in value or stamp.tzinfo is None:
return "BAD_TIMESTAMP", None
except ValueError:
return "BAD_TIMESTAMP", None
return "ACCEPT", {k: item[k] for k in FIELDS}
例如,假设 body 是一个完整的合成响应:
body = {"data": {"catalogItem": {
"id": "gid://shopify/Product/1001",
"title": "Example shirt",
"status": "UNLISTED",
"updatedAt": "2026-10-04T00:00:00Z"
}}}
state, snapshot = accept_product(
200, "2026-10", body, "gid://shopify/Product/1001"
)
assert state == "ACCEPT"
body["errors"] = [{"message": "synthetic field error"}]
assert accept_product(200, "2026-10", body,
"gid://shopify/Product/1001")[0] == "GRAPHQL_ERRORS"
离线测试覆盖了正常值、空标题、额外字段、结果为空、键缺失、部分数据带错误、错误类型、ID 不一致、未知枚举、非法或无时区时间,以及版本和 HTTP 状态不符。这些测试证明示例函数按约定分流,不证明任何商家授权或真实查询已经成功。
5. 用 UNLISTED 检验枚举升级,保留失败材料
2026-10 的 ProductStatus 包含 ACTIVE、ARCHIVED、DRAFT、UNLISTED。UNLISTED 自 2025-10 起可见,在更老版本中会被转换为 ACTIVE。这是一个可用于兼容验收的真实版本差异:旧消费者若只认识前三个值,升级后会拒绝新值;把未知值默认映射成 ACTIVE 则可能掩盖商品可见性变化。ProductStatus 具体定义。
为四个已知值分别准备 fixture,也保留一个合成未知值来检查暂停路径。不要从 ACTIVE 推断商品已发布到某个销售渠道;官方定义明确区分状态与渠道发布。
升级验收交付物应包括:旧与新版本的查询、所选字段类型和枚举差异、脱敏 fixture、函数与 fixture 的版本、每个失败状态的负责人,以及恢复上一个消费者版本的办法。仅当新 schema 对照完成、离线反例通过、获授权开发店的实际响应也通过同一函数时,才完成该环境的兼容验收。未执行的开发店步骤继续标为待执行。
大批量读取还需处理分页、成本和中断恢复,可继续阅读 GraphQL 性能与数据查询验收。这里的单对象响应门禁可放在每次读取后的消费者入口,不能替代整批任务的完成核对。