前言

Shopify 與 Ragic 的欄位名稱常常很像。Shopify 的訂單編號、付款狀態、總金額,看起來應該可以自動對到 Ragic 的「訂單編號」、「付款狀態」、「訂單金額」。如果規則判斷不夠,再交給 AI 補上,似乎能省掉大量設定時間。

這個專案確實依序做過規則式建議、信心分數、AI 草稿與人工確認。但商家設定流程最後反而移除了建議與 AI 開關,改成逐欄選擇。原因不是 AI 毫無價值,而是欄位 Mapping 同時決定資料會寫到哪裡、什麼值可以覆寫,以及哪個欄位是 upsert 的唯一鍵。這些責任不能只靠名稱相似度代表商家做決定。

上一篇先攤開了 Shopify Webhook 到 Ragic Upsert 的完整同步架構。這篇聚焦在中間那份最關鍵的 Mapping 合約。

原本的做法與問題症狀

第一版規則式 matcher 會遍歷 Shopify field catalog,對每個 Ragic 欄位計分。分數包含:

  • 正規化後的欄位名稱是否相同或包含彼此。
  • 資料型別是否相容。
  • 目前是訂單還是會員 mapping。
  • 欄位是否可寫入、是否已被其他 Shopify path 使用。

低於門檻就保持 unmatched,中間分數標成 needs review,高分則標成 auto。若規則結果仍有非 auto 欄位,系統可以請 AI 產生草稿;AI 回傳後還會經過 allow-list,只接受 catalog 中存在的 Shopify path 與 parser 證明可寫入的 Ragic Field ID。

這套方案在工程上並不草率,卻仍遇到產品層問題:

  • 「訂單編號」可能是 Ragic 自動編號,也可能是外部 Shopify GID。
  • 「總金額」可能指原始訂單金額、折扣後金額、已退款後金額或報表用金額。
  • 「付款狀態」的文字相同,選項集合與後續 workflow 可能完全不同。
  • 名稱像 unique key 的欄位,不代表允許 App 用它更新既有 record。
  • 主表與子表可能出現同名欄位,寫入語意卻不同。

建議越像「已幫你完成」,商家越容易略過真正需要確認的資料責任。

問題是怎麼推敲出來的?

Git 演進提供了清楚的決策路徑。專案先加入規則式建議與「儲存建議」,接著加入人工 editor;之後又加入 AI draft、商店層級 AI 開關,以及只有規則信心不足才呼叫 AI 的 gate。

這說明問題不是缺少更聰明的 matcher。即使 AI 輸出會經過 schema allow-list,系統仍只能確認「這個 Field ID 存在且可寫」,無法證明「商家的業務定義允許這樣寫」。

最後的 manual-only commit 移除了商家頁面的:

  • 規則建議統計與預覽。
  • 直接儲存建議的 action。
  • AI mapping 開關。
  • 產生 AI 草稿的 action 與 UI。

取而代之的是明確的 target catalog、Ragic 可寫欄位選項,以及逐欄選擇的表單。這不是把驗證責任丟給使用者;server 儲存時仍會重新驗證每個 Shopify path 與 Ragic Field ID,並拒絕缺少 unique key 的 mapping。

最後採用的架構

最終流程把「系統知道的事」與「商家要決定的事」分開:

系統:
  解析 Ragic 表單與 Field ID
  → 排除 readonly / loaded 欄位
  → 提供固定 Shopify field catalog
  → 驗證型別、target 與 unique key

商家:
  選擇要同步訂單或會員
  → 逐欄決定 Ragic destination
  → 可以明確選擇不送某欄
  → 儲存後再用預覽確認 payload

Mapping spec 只保存經確認的欄位。沒有選 destination 的 Shopify path 不會被補猜,也不會在 payload 中出現。

Unique key 則是 hard gate。訂單 mapping 必須包含可識別訂單的穩定 path,會員 mapping 也要有對應識別;否則系統無法安全決定 create 或 update,就直接拒絕啟用。

關鍵實作

Browser 送回的是 Shopify path 到 Ragic Field ID 的 selection,但 server 不直接相信:

const catalogByPath = new Map(
  getShopifyFieldCatalog(target).map((field) => [field.shopifyPath, field]),
);

const writableById = new Map(
  parsedSheet.fields
    .filter(isWritableRagicField)
    .map((field) => [field.id, field]),
);

每一個已選欄位都要同時通過兩邊:

const shopifyField = catalogByPath.get(shopifyPath);
const ragicField = writableById.get(ragicFieldId);

if (!shopifyField) throw new Error('未知的 Shopify 欄位');
if (!ragicField) throw new Error('Ragic Field ID 不可用');

沒有選擇的欄位會被過濾;選了不存在、唯讀或 loaded 的欄位則拒絕儲存。最後再從通過的 fields 找 unique key:

const uniqueKey = getUniqueKey(target, fields);
if (!uniqueKey) {
  throw new Error(`缺少可用的 unique key:${target}`);
}

這裡沒有把 AI 草稿程式刪除等同於「永遠禁止 AI」。它可以留在開發者診斷、離線建議或未來的 opt-in 輔助流程;只是不能在商家主流程裡偽裝成已經替人完成業務決策。

實際驗證

來源測試與 commit diff 可證明:

  • 規則式 matcher 會排除不可寫欄位與已使用的 Field ID。
  • 低分結果保持 unmatched,中等結果要求 review,高分才標成 auto。
  • AI 草稿輸出只允許已知 Shopify path 與可寫 Ragic Field ID。
  • Manual mapping 會忽略商家明確選擇「不送出」的欄位。
  • Browser 傳回未知 Shopify path 時儲存失敗。
  • Browser 傳回唯讀或不存在的 Ragic Field ID 時儲存失敗。
  • 缺少必要 unique key 時 mapping 不會啟用。
  • 最終商家頁已移除 AI 開關、AI 草稿與直接儲存建議,改為 manual-only。

驗證只能證明流程與 guard 存在,不能證明商家每次都會選對。因此 Mapping 儲存後仍需要 dry-run、sample record 與同步 log 形成第二層確認。

仍然存在的限制

人工選擇不等於自然正確。若兩個可寫文字欄位名稱相似,商家仍可能選錯;UI 應同時顯示 Field ID、欄位型別、write format 與必要 memo,而不是只有 display name。

人工 Mapping 也會增加初次設定成本。較合理的演進不是偷偷恢復 auto-save,而是提供可解釋的建議:顯示候選、原因與風險,預設不勾選,最後仍由有權限的使用者確認。

Ragic schema 之後可能改名、改型別或刪除欄位。已儲存 Mapping 不能永久視為有效;需要保存 schema snapshot、比對 diff,將受影響的 Mapping 標成 needs review。這部分會在下一批的 schema repair 文章再深入。

最後,訂單與會員欄位可能包含 protected customer data。Mapping editor 不應因「方便預覽」就把所有可取得欄位都列入預設同步;Shopify 目前要求只處理 App 功能真正需要的最小資料。

可以延伸到哪些情境?

任何跨系統 Mapping 都可以採相同責任模型:

  • 系統負責 schema 解析、型別檢查與不可寫欄位排除。
  • 建議引擎可以排序候選,但不替使用者承擔覆寫責任。
  • unique key、資料用途與 retention 必須明確確認。
  • 儲存後用真實但受控的 sample 做 dry-run。
  • schema 變更後重新要求 review。

下一篇會處理 editor 的資料來源:如何把 Ragic API 文件轉成 Shopify 欄位 Mapping 編輯器?

參考資料

以上平台文件查核日期:2026-07-26。