當供應商成本變動時,自動更新價格及對照價格

September 3, 2026

您的供應商將某款變體產品的成本從 4.00 調高至 5.20。Shopify 系統中並未做出任何反應。零售價維持不變,而您原本規劃的利潤則悄然消失,直到數週後有人在盤點時才察覺此事。

Shopify Flow 在此情況下,系統無法自動處理,因為「Shopify沒有內建的觸發機制來應對單件成本的變更。此外,它也沒有任何動作能根據單件成本重新計算價格。

本指南同時彌補了這兩項缺口。當任何變體的單件成本發生變化時,便會觸發一個 Flow,並由一個簡短的函式將新價格與參考價格寫回 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

該產品本身也會被列為產品參考,因此您可以在觸及任何價格之前,依據供應商、類型、標籤或系列新增「Flow」條件。

步驟 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。您可以從管理後台的任何產品網址中取得一個,或從觸發器的事件歷史紀錄中取得。

請設定一個您能認出的價格,以便確認訊息已成功送達。

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 文件