前言
這次的需求是把 Shopify 訂單與會員資料同步到 Ragic。商家希望新訂單建立、訂單狀態更新或會員資料異動後,Ragic 對應表單可以自動新增或更新,不必每天匯出 CSV 再人工整理。
看起來只是「收到 Webhook,呼叫另一支 API」。真正實作後,資料至少會經過 Webhook 驗證、工作落地、欄位 Mapping、格式轉換、唯一鍵查詢、Ragic 寫入與同步紀錄。任一層省略,重送、欄位異動或外部 API 暫時失敗時,都可能產生重複資料或無法追查的半成品。
上一篇談的是 Serverless 環境如何處理 Shopify 大量商品更新。這篇沿用「先持久化工作」的觀念,先把 Shopify 到 Ragic 的完整同步路徑攤開。
原本的做法與問題症狀
第一個直覺方案是在 Webhook route 裡完成所有事情:
Shopify Webhook
→ 驗證 request
→ 組 Ragic payload
→ 查詢是否已有資料
→ create / update
→ 回傳 200
這條路在測試資料少、Ragic 回應快時可以工作,但 Webhook delivery 有很短的回應時間要求。若欄位很多、外部 API 變慢,Shopify 看到的不是「Ragic 正在處理」,而是 endpoint 沒有及時成功回應。
重送又帶來第二個問題。同一筆訂單如果因 delivery retry 被處理兩次,直接 POST 可能建立兩筆 Ragic record。反過來,只用「Shopify 訂單已處理過」一律略過,也會讓 orders/updated 無法更新付款或退款狀態。
第三個問題是 payload 不能直接照抄。Shopify 的資料是巢狀 JSON,Ragic 寫入時則以 Field ID 當欄位 key。金額需要從 money object 取出數字,狀態可能需要 value map,缺少欄位也不該默默變成空字串覆寫既有資料。
問題是怎麼推敲出來的?
先從 route 邊界看起。來源專案後來把訂單與會員 Webhook route 縮到三件事:用 Shopify 提供的 helper 驗證 delivery、取得 shop/topic/payload、把內容寫入 SyncJob。job 成功建立後 route 就回傳 2xx;無法落地才回傳失敗,讓 Shopify retry。
再往下追 processor。SyncJob 保存 target、topic、source、Shopify resource ID、payload、attempts、下次執行時間與狀態。Worker 取出最早可執行工作,標記 running,再分派到 order 或 customer processor。失敗時不直接遺失,而是依 attempt 延後下一次執行;達上限才標記 error。
接著檢查重複判斷。案例把 orders/create 與 orders/updated 分開:
orders/create已有同一 Shopify resource ID 的成功紀錄時,可以略過相同建立事件。orders/updated仍要進入 upsert,因為遠端 record 可能真的需要更新。- Ragic 端再以 mapping 指定的 unique key 查詢,決定最後是 create 或 update。
這比只信 Webhook topic 更可靠,因為「是否存在」由遠端資料與明確唯一鍵共同決定。
最後採用的架構
最後的資料流拆成接收與執行兩段:
Shopify Webhook
→ HMAC 驗證
→ 建立 SyncJob
→ 快速回傳 2xx
SyncJob processor
→ 檢查同步開關與重複事件
→ 正規化 Order / Customer
→ 讀取 active mapping
→ 產生 Ragic payload 與 warnings
→ 以 unique key 查詢 Ragic
→ create 或 update
→ 寫入 SyncLog
Mapping 是兩個平台之間的合約。每個 mapping field 至少記錄 Shopify path、Ragic Field ID、transform 與來源;mapping 本身還要指定 target sheet 與 unique key。
同步紀錄則保存 source、operation、狀態、mapping、Shopify resource ID、Ragic record ID 與必要的錯誤摘要。這讓商家看到的是「哪一筆建立、更新、略過或失敗」,不是只有 server console 裡一行模糊訊息。
關鍵實作
Webhook route 不直接寫 Ragic,只落地工作:
await enqueueSyncJob({
shop,
target: 'order',
topic: 'orders/create',
payload,
shopifyResourceId: payload.admin_graphql_api_id,
});
return new Response();
Payload builder 只處理 mapping 允許的欄位;缺值留下 warning,不主動寫成空值:
for (const field of mapping.fields) {
const rawValue = getNestedValue(shopifyPayload, field.shopifyPath);
if (rawValue === undefined || rawValue === null || rawValue === '') {
warnings.push(`缺少值:${field.shopifyPath}`);
continue;
}
payload[field.ragicFieldId] = transformValue(rawValue, field.transform);
}
真正寫入前先用 unique key 查詢:
const existing = await findByField({
fieldId: dryRun.uniqueKey.ragicFieldId,
value: dryRun.uniqueKey.value,
});
return existing
? updateRecord(existing.recordId, dryRun.payload)
: createRecord(dryRun.payload);
範例刻意不放正式 API URL、Field ID、訂單內容或任何 credential。公開文章只需要說清楚責任邊界,不需要把 production payload 搬上網。
實際驗證
來源測試已覆蓋:
- 自動同步未啟用時不呼叫 Ragic,並留下 skipped log。
orders/create已有成功紀錄時略過重複事件。orders/updated即使有舊成功紀錄,仍執行 update。- Shopify REST webhook 欄位會正規化成 mapping 使用的巢狀結構。
- unique key 未命中時建立 record,命中時更新既有 record。
- 金額與狀態 transform 後才進入 Ragic payload。
- Ragic 回傳錯誤時保存 error log,而不是誤標成功。
- job 失敗會增加 attempt、延後 retry,達上限才進入 error。
這些是 repository 內的 unit/integration-level 證據。沒有 production 流量、Ragic rate limit 或長時間 outage 的公開測試,因此文章不把目前設計寫成已證明可承受任意規模。
仍然存在的限制
Shopify 官方目前明確指出 Webhook 不保證同一 topic 或不同 topic 之間的順序,也可能因應用程式停機或 handler 錯誤而漏掉事件。只有 Webhook 加 retry 不足以保證最終一致,仍需要依 updated_at 或其他游標做 reconciliation。
案例已把 job 寫進資料庫,卻不能因此直接稱為完整 durable queue。現行的「找第一筆 pending,再標成 running」不是單一 atomic claim;多 worker 同時執行時還需要 transaction、lease 或資料庫鎖。production scheduler 如何持續喚醒 processor,也必須在部署層另外完成。
訂單與會員 payload 屬 Shopify protected customer data。保存 job payload、log 或錯誤 response 時,應只留功能必要欄位,設定 retention,並確保傳輸、資料庫與備份的保護方式符合目前的資料保護要求。
最後,Ragic HTTP 回應成功不代表業務資料一定正確。Field ID、unique key、Link/Load、formula 與 workflow 是否符合商家表單,仍要靠 mapping 審核與必要的 read-back 驗證。
可以延伸到哪些情境?
這套架構也適用於 ERP、CRM、倉儲或會計系統:
- Webhook route 只負責驗證與可靠接收。
- 外部欄位差異交給版本化 mapping。
- create/update 由明確 unique key 決定。
- 每次執行留下可以對帳的同步紀錄。
- 另以 reconciliation 修補漏送與亂序。
下一篇會深入 Mapping 的產品決策:Shopify × Ragic 欄位 Mapping,為什麼從 AI 自動配對改回人工確認?
參考資料
- Shopify:About webhooks
- Shopify:Troubleshoot webhooks
- Shopify:Work with protected customer data
- Ragic:Field Naming
- Ragic:Creating a New Entry
- Ragic:Modifying an Entry
- Ragic:API Request Error Response
以上平台文件查核日期:2026-07-26。
