当供应商成本发生变化时,自动更新价格和对比价格

September 3, 2026

您的供应商将某款产品的价格从 4.00 提高到 5.20。Shopify 中没有任何反应。零售价保持不变,而您计划的利润率却悄然消失,直到几周后有人在盘点时才发现这一情况。

Shopify Flow 在此情况下无法自动处理,因为 Shopify 没有针对单件成本变化的内置触发器。此外,它也没有能够根据单件成本重新计算价格的操作。

本指南填补了这两方面的空白。当任何变体的单件成本发生变化时,就会触发一个流程,一个简短的函数会将新价格和对比价格写回Shopify。该流程的设置大约需要十分钟,之后会在每次成本变化时自动运行,并持续运行下去。

你将构建的内容

  1. Workflow Trigger Extensions 检测到成本变化后,启动一个流程,其中包含该变体、其SKU以及新旧两套成本数据。
  2. Shopify Flow 将这些值作为 JSON 传递给一个函数。
  3. 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时该函数将失败。

update-variant-price.jsjavascript
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 中获取一个,或者从触发的事件历史记录中获取。

设定一个你能认出的价格,以便确认消息已成功送达。

test-input.jsonjson
{
  "variantId": "gid://shopify/ProductVariant/54589751394579",
  "price": "24.99",
  "compareAtPrice": "39.99"
}

第 4 步:连接 Flow 的线缆

在Shopify Flow中创建一个工作流:

  1. 触发器产品变体价格发生变化
  2. 操作运行_Workflow Functions中的_函数,并选择您刚刚创建的函数

在 JSON 输入框中粘贴此内容。Flow 每次运行时都会将 Liquid 替换为实际值。

flow-input.jsonjson
{
  "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 会在显示时自动处理货币转换,因此您无需手动操作。

确认端到端功能正常

  1. 在Shopify管理员后台打开任意产品变体,并修改**“单件成本”**
  2. 保存
  3. 几秒钟内,价格和对比价就会自动更新

如果没有任何反应,请按以下顺序逐一排查,因为每个步骤都会排除前一个步骤的可能性:

检查 在哪里
扳机扣动了吗? Workflow Trigger Extensions 中的事件历史记录
Flow 运行了吗? Shopify Flow 活动日志
该函数运行了吗?日志中记录了什么内容? Workflow Functions 中的运行历史记录

运行报告失败的最常见原因是缺少范围。ctx.shopify 仅在**“需要 Shopify 数据**”选项开启时可用,且只有在授予**“写入产品**”权限后,写入操作才会成功。

接下来去哪里

对于任何受成本变动影响的项目,都可以采用同样的模式。只需交换函数体中的代码,你就可以在成本上涨超过10%时将产品标记为待审核、将新旧利润率发布到Slack,或者将变动写入元字段以供报表使用。

触发器是 Shopify 没有提供的部分。接下来的内容就是普通的代码了。

Related article权限与数据访问“库存数据访问”权限能带来什么,以及如何授予或撤销该权限。Related article触发器完整参考指南Workflow Trigger Extensions 中的每个触发器及其各自包含的字段。

Workflow Functions 文档