当供应商成本发生变化时,自动更新价格和对比价格
September 3, 2026
您的供应商将某款产品的价格从 4.00 提高到 5.20。Shopify 中没有任何反应。零售价保持不变,而您计划的利润率却悄然消失,直到几周后有人在盘点时才发现这一情况。
Shopify Flow 在此情况下无法自动处理,因为 Shopify 没有针对单件成本变化的内置触发器。此外,它也没有能够根据单件成本重新计算价格的操作。
本指南填补了这两方面的空白。当任何变体的单件成本发生变化时,就会触发一个流程,一个简短的函数会将新价格和对比价格写回Shopify。该流程的设置大约需要十分钟,之后会在每次成本变化时自动运行,并持续运行下去。
你将构建的内容
- Workflow Trigger Extensions 检测到成本变化后,启动一个流程,其中包含该变体、其SKU以及新旧两套成本数据。
- Shopify Flow 将这些值作为 JSON 传递给一个函数。
- Workflow Functions 运行几行 JavaScript 代码,计算出新价格,并通过 Admin API 将其写回系统。
定价规则只存在于一个地方,即由您控制的代码中,因此日后若需修改,只需编辑一行代码,而无需重建工作流。
步骤 1:开启成本触发器
在 Workflow Trigger Extensions 中,打开**“触发器**”页面,找到**“库存**”组,然后开启**“产品变体成本发生变化**”触发器。
此触发器需要**“库存数据访问**”权限,因为单件成本存储在库存项目中,而非变体本身。如果尚未授予该权限,启用该触发器时系统会通过一个对话框请求该权限。在获得该权限之前,触发器将保持启用状态,但会显示_“需要权限_”标记,且不会触发。
扳机能为你带来什么
一旦触发,您的 Flow 即可访问:
| 字段 | 示例 |
|---|---|
variantCostChange.variantId |
gid://shopify/ProductVariant/54589751394579 |
variantCostChange.variantTitle |
Medium / Black |
variantCostChange.sku |
TSHIRT-M-BLK |
variantCostChange.oldCost.amount |
4.00 |
variantCostChange.newCost.amount |
5.20 |
variantCostChange.newCost.currencyCode |
EUR |
该产品本身也被列为产品参考,因此您可以在触及任何价格之前,根据供应商、类型、标签或系列添加流程条件。
步骤 2:创建函数
在Workflow Functions中,创建一个新函数,并将以下代码粘贴进去。
保存前,请开启“需要 Shopify 数据”选项。这样才能让您的代码访问 ctx.shopify.graphql(...),从而读取和修改商店数据。然后在 Workflow Functions 中打开**“权限**”页面,授予**“写入产品**”权限 - - 这是该函数修改价格所需的权限范围。
如果没有该切换开关,则无法使用ctx.shopify对象,且在首次调用Admin API时该函数将失败。
export default async function (input, ctx) {
const toGid = (value, type) => {
if (value === undefined || value === null) return null;
const str = String(value).trim();
if (!str) return null;
if (str.startsWith('gid://')) return str;
if (/^\d+$/.test(str)) return `gid://shopify/${type}/${str}`;
return str;
};
try {
const variantId = toGid(input.variantId || input.variant_id || input.id, 'ProductVariant');
if (!variantId) throw new Error('Missing "variantId" in input.');
const hasPrice = input.price !== undefined && input.price !== null && String(input.price).trim() !== '';
const hasCompareAt = Object.prototype.hasOwnProperty.call(input, 'compareAtPrice') || Object.prototype.hasOwnProperty.call(input, 'compare_at_price');
const rawCompareAt = input.compareAtPrice !== undefined ? input.compareAtPrice : input.compare_at_price;
if (!hasPrice && !hasCompareAt) throw new Error('Nothing to update: provide "price" and/or "compareAtPrice".');
const asMoney = (value) => {
const num = Number(value);
if (!Number.isFinite(num) || num < 0) throw new Error(`Invalid money value: ${JSON.stringify(value)}`);
return num.toFixed(2);
};
let productId = toGid(input.productId || input.product_id, 'Product');
if (!productId) {
const readQuery = `
query VariantForPriceUpdate($id: ID!) {
productVariant(id: $id) { id product { id } }
}`;
const readRes = await ctx.shopify.graphql(readQuery, { id: variantId });
if (readRes.errors) throw new Error('Failed to read variant: ' + JSON.stringify(readRes.errors));
const variant = readRes.data && readRes.data.productVariant;
if (!variant) throw new Error(`Variant not found: ${variantId}`);
productId = variant.product && variant.product.id;
if (!productId) throw new Error('Could not resolve the parent product id for the variant.');
}
const variantInput = { id: variantId };
if (hasPrice) variantInput.price = asMoney(input.price);
if (hasCompareAt) {
variantInput.compareAtPrice = rawCompareAt === null || String(rawCompareAt).trim() === '' ? null : asMoney(rawCompareAt);
}
ctx.log('Updating variant', variantId, 'on product', productId, variantInput);
const mutation = `
mutation UpdateVariantPrice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id }
userErrors { field message }
}
}`;
const res = await ctx.shopify.graphql(mutation, { productId, variants: [variantInput] });
if (res.errors) throw new Error('GraphQL error updating variant: ' + JSON.stringify(res.errors));
const payload = res.data && res.data.productVariantsBulkUpdate;
if (!payload) throw new Error('Unexpected empty response from productVariantsBulkUpdate.');
if (payload.userErrors && payload.userErrors.length) {
throw new Error('Variant update failed: ' + payload.userErrors.map(e => `${(e.field || []).join('.')}: ${e.message}`).join('; '));
}
return { success: true };
} catch (err) {
ctx.log('Error updating variant price:', err.message);
return { success: false };
}
}关于这段代码,有几点值得注意:
- 它接受纯数字 ID 或完整的 GID。
toGid会将二者进行规范化处理,因此您无需担心 Flow 返回的是哪种形式。 - 它仅在必要时才会查询父产品。
productVariantsBulkUpdate需要一个产品 ID;如果未传入该参数,该函数会自动获取。在输入中传入productId即可省去这一轮往返调用。 - **它会检查
userErrors状态码,而不仅仅是传输错误。**一个GraphQL调用可能会返回HTTP 200状态码,但仍然拒绝写入操作。这是价格更新看似成功但实际没有任何变化的最常见原因。 - **清除“compare-at”是刻意为之。**传递
compareAtPrice: null或空字符串会将其移除,而非被视为错误。
步骤 3:在进行接线之前先测试该功能
在**“测试**”选项卡中使用您商店中的真实变体 ID。您可以从后台的任意商品 URL 中获取一个,或者从触发的事件历史记录中获取。
设定一个你能认出的价格,以便确认消息已成功送达。
{
"variantId": "gid://shopify/ProductVariant/54589751394579",
"price": "24.99",
"compareAtPrice": "39.99"
}第 4 步:连接 Flow 的线缆
在Shopify Flow中创建一个工作流:
- 触发器:产品变体价格发生变化
- 操作:运行_Workflow Functions中的_函数,并选择您刚刚创建的函数
在 JSON 输入框中粘贴此内容。Flow 每次运行时都会将 Liquid 替换为实际值。
{
"variantId": "{{variantCostChange.variantId}}",
"price": "{{variantCostChange.newCost.amount | times: 5}}",
"compareAtPrice": "{{variantCostChange.newCost.amount | times: 7}}"
}这就是定价规则的全部内容:售价为成本的五倍,对比价为成本的七倍,这样产品就能始终显示一致的折扣。
这些倍数仅供示例,并非建议。请将 times: 5 替换为您的分类实际支持的数值。Liquid 会自动处理运算,因此若使用固定加价而非倍数,应为 plus: 12;若使用百分比加价,则应为 times: 1.6。
如果您支持多种货币销售,请注意,newCost.amount 采用的是店铺的货币单位。Markets 会在显示时自动处理货币转换,因此您无需手动操作。
确认端到端功能正常
- 在Shopify管理员后台打开任意产品变体,并修改**“单件成本”**
- 保存
- 几秒钟内,价格和对比价就会自动更新
如果没有任何反应,请按以下顺序逐一排查,因为每个步骤都会排除前一个步骤的可能性:
| 检查 | 在哪里 |
|---|---|
| 扳机扣动了吗? | Workflow Trigger Extensions 中的事件历史记录 |
| Flow 运行了吗? | Shopify Flow 活动日志 |
| 该函数运行了吗?日志中记录了什么内容? | Workflow Functions 中的运行历史记录 |
运行报告失败的最常见原因是缺少范围。ctx.shopify 仅在**“需要 Shopify 数据**”选项开启时可用,且只有在授予**“写入产品**”权限后,写入操作才会成功。
接下来去哪里
对于任何受成本变动影响的项目,都可以采用同样的模式。只需交换函数体中的代码,你就可以在成本上涨超过10%时将产品标记为待审核、将新旧利润率发布到Slack,或者将变动写入元字段以供报表使用。
触发器是 Shopify 没有提供的部分。接下来的内容就是普通的代码了。
Workflow Functions 文档
- 权限和商店数据访问 - 启用“需要 Shopify 数据”并授予权限范围
- 将变量传递给函数 - - JSON 输入与 Liquid 的工作原理
- 在Shopify Flow中设置“运行函数”操作
- 运行历史记录和故障排除

