自動化履約框架,透過 GraphQL Admin API 把第三方倉庫(不具備 Shopify 原生整合)的訂單出貨資訊同步至 Shopify
功能特性
- 多服務商支援:可擴充架構,支援接入多個履約服務商
- 自動監控:每 30 分鐘檢查一次服務商訂單
- 智慧履約:僅履約指定服務商倉庫地點的訂單
- 防重複處理:Turso 資料庫記錄所有服務商已履約的訂單
- 服務商專屬邏輯:每個服務商都可以自訂訂單擷取與物流追蹤邏輯
- 型別安全的 GraphQL:使用 Shopify GraphQL Admin API (2025-07),並自動產生型別
- 內建服務商:
- 柔造:中國履約服務商
- HiCustom:中國 POD 履約服務商
前置需求
- Bun 執行環境
- 履約服務商的 API 憑證
- 已開通 API 存取權限的 Shopify 商店
- 與服務商倉庫相對應的 Shopify 地點
安裝
bun install設定
在專案根目錄建立 .env 檔案,寫入以下變數:
# Turso 資料庫設定(必填)
TURSO_DATABASE_URL=libsql://your-database-turso.io
TURSO_AUTH_TOKEN=your-turso-auth-token
# Shopify API 設定(必填)
SHOPIFY_API_KEY=your_shopify_api_key_here
SHOPIFY_API_SECRET=your_shopify_api_secret_here
SHOPIFY_ACCESS_TOKEN=your_shopify_access_token_here
SHOPIFY_SHOP_DOMAIN=yourshop.myshopify.com
SHOPIFY_APP_URL=https://your-app-url.com
# 服務商 API 設定
# 柔造(設定 ROUZAO_TOKEN 後自動啟用)
ROUZAO_TOKEN=your_rouzao_token_here
ROUZAO_LOCATION_IDS=location_id_1,location_id_2 # 選填:以半形逗號分隔的 Shopify 地點 ID
# 未設定時預設使用柔造的倉庫地點 ID
# HiCustom(設定 API_KEY 與 API_SECRET 後自動啟用)
HICUSTOM_API_KEY=your_hicustom_api_key
HICUSTOM_API_SECRET=your_hicustom_api_secret
HICUSTOM_LOCATION_IDS=location_id_1,location_id_2 # 選填:以半形逗號分隔的 Shopify 地點 ID
# HICUSTOM_API_URL=https://api.hicustom.com # 選填:覆寫 API 基礎網址
# 視需要新增更多服務商
# PROVIDER3_API_KEY=your_api_key
# PROVIDER3_API_SECRET=your_secret設定 Turso 資料庫
-
安裝 Turso CLI:
curl -sSfL https://get.tur.so/install.sh | bash -
建立資料庫:
turso auth signup # 或 turso auth login turso db create laplace-fulfiller -
取得資料庫憑證:
# 取得資料庫 URL turso db show laplace-fulfiller --url # 建立驗證 token turso db tokens create laplace-fulfiller -
將 schema 推送至資料庫:
bun run db:push
注意:從本機 SQLite 遷移時,需要先從舊資料庫匯出資料再匯入 Turso,兩者的 schema 保持相容
取得柔造 Token
- 登入柔造 (https://www.rouzao.com)
- 開啟瀏覽器開發者工具 (F12)
- 切換至 Network 標籤頁
- 執行任意會呼叫 API 的操作
- 在請求中尋找
Rouzao-Token標頭
取得 HiCustom 憑證
- 登入 HiCustom (https://www.hicustom.com)
- 前往 API 設定或開發者專區
- 建立新的應用程式或 API 用戶端
- 複製 API Key 與 API Secret
- 記錄 HiCustom 負責出貨的 Shopify 地點 ID
HiCustom 整合使用其 OAuth API,並會自動更新 token。參見其 API 文件:
設定 Shopify API 存取權限
- 在 Shopify 後台建立私有應用程式
- 授予以下權限:
- 讀取訂單 (
read_orders) - 寫入訂單 (
write_orders) - 讀取地點 (
read_locations),可選但建議 - 讀取商家管理的履約訂單 (
read_merchant_managed_fulfillment_orders) - 寫入商家管理的履約訂單 (
write_merchant_managed_fulfillment_orders)
- 讀取訂單 (
- 複製 API 憑證
重要:應用程式正常運作必須具備履約訂單相關權限。缺少這些權限時,處理訂單會回傳 403 Forbidden 錯誤
Shopify 地點設定
每個服務商都必須在 Shopify 中有對應的地點。服務商只會履約其已註冊地點的訂單
執行 bun run diagnose 可以取得 Shopify 商店中的地點 ID
柔造:
- 在
ROUZAO_LOCATION_IDS環境變數中指定地點 ID - 或建立名稱中包含「Rouzao」的地點
HiCustom:
- 在
HICUSTOM_LOCATION_IDS環境變數中指定地點 ID - 或建立名稱中包含「HiCustom」的地點
其他服務商:查看該服務商的 locationIds 或地點名稱比對規則
執行
開發
# 以 cron 方式持續執行,日誌經 pino-pretty 美化輸出
bun run dev
# 只執行一次後結束(同樣美化輸出)
bun run once
# 或直接呼叫,不做美化輸出
bun run src/index.ts --once正式環境
正式環境使用 bun run start,它會先執行資料庫遷移,再以原始 JSON 日誌輸出啟動服務(適合日誌彙整系統):
bun run start使用 GitHub Container Registry 上官方維護的容器映像檔部署本應用程式。為確保一致性、安全性與相容性,這是唯一支援的部署方式
使用官方映像檔
# 拉取最新映像檔
docker pull ghcr.io/laplace-live/fulfiller:latest
# 執行容器
docker run -d \
--name laplace-fulfiller \
--restart unless-stopped \
--env-file .env \
ghcr.io/laplace-live/fulfiller:latestDocker Compose 範例
建立 docker-compose.yml 檔案可以簡化部署:
services:
fulfiller:
image: ghcr.io/laplace-live/fulfiller:latest
restart: unless-stopped
env_file: .env接著執行:
docker-compose up -d容器映像檔倉庫
官方映像檔已公開發布於 GitHub Container Registry:
# 拉取官方映像檔
docker pull ghcr.io/laplace-live/fulfiller:latest
# 檢視可用的標籤與版本
# 造訪 https://github.com/laplace-live/fulfiller/pkgs/container/fulfiller每次發布都會自動建置並推送映像檔,確保你始終能取得最新的穩定版本
運作方式
- 服務商註冊:啟動時註冊並初始化所有已啟用的服務商
- 訂單監控:服務每 30 分鐘從所有已啟用的服務商拉取訂單
- 已出貨訂單辨識:各服務商依出貨狀態篩選自己的訂單
- 訂單詳情:為每筆已出貨訂單拉取詳細資訊,包括物流追蹤資訊
- Shopify 訂單查找:各服務商用自己的邏輯擷取 Shopify 訂單編號
- 智慧履約:僅履約分配給該服務商指定倉庫地點的商品
- 防重複處理:將已履約訂單連同服務商資訊記錄至 Turso 資料庫
資料庫
服務使用 Turso(分散式 SQLite)搭配 Drizzle ORM,記錄所有服務商已履約的訂單:
資料表結構:
provider:服務商識別碼,例如 'rouzao'provider_order_id:服務商端的訂單 IDshopify_order_number:Shopify 訂單編號shopify_order_id:Shopify 訂單 IDfulfilled_at:履約時間戳記created_at:記錄建立時間戳記
特性:
(provider, provider_order_id)唯一約束可防止重複- 依服務商與 Shopify 訂單編號建立索引,加快查詢
- 每天凌晨自動清理超過 365 天的舊記錄
- 藉由 Drizzle ORM 實作型別安全的查詢
- Turso 的全球邊緣部署帶來低延遲存取
- 自動備份與時間點還原
- 與本機 SQLite 不同,沒有檔案大小限制
日誌
服務使用 Pino 輸出結構化日誌,時間戳記為 ISO 格式。預設以 JSON 輸出,每行一筆記錄,適合正式環境的日誌彙整系統。開發時,dev、once 與 diagnose 腳本會把輸出透過 pino-pretty 管線處理,轉成人類可讀的格式
每個模組都有自己的子 logger,例如 main、rouzao、hicustom,因此可以依元件篩選日誌。日誌等級透過 LOG_LEVEL 環境變數調整,預設為 info
需要關注的輸出內容:
- 訂單拉取結果
- 已出貨訂單的處理過程
- 履約成功或失敗
- 錯誤訊息
疑難排解
常見問題
- 「Rouzao location not found」:確保 Shopify 中存在名為「Rouzao」或「柔造」的地點
- 「Order already fulfilled」:該訂單已在 Shopify 中處理或履約完成
- 「Invalid third party order SN format」:該訂單沒有有效的 Shopify 關聯資訊
- API 錯誤:檢查 API token 與網路連線狀況
除錯模式
需要更詳細的日誌時,可以修改程式碼中的 console.log 陳述式或補上額外的日誌輸出
診斷工具
執行診斷腳本,檢查 Shopify API 權限與連線狀態:
bun run diagnose它會檢測:
- 環境變數設定
- Shopify API 驗證
- 授予應用程式的存取權限範圍
- Locations API 存取權限(列出所有倉庫地點)
- Orders 與 Fulfillment Orders API 存取權限
- 柔造地點是否可用
開發
技術堆疊
- 執行環境:Bun(快速的一體化 JavaScript 執行環境)
- 語言:TypeScript,啟用 strict 模式
- 資料庫:Turso(分散式 SQLite),搭配 Drizzle ORM 與 @libsql/client
- API:Shopify GraphQL Admin API (2025-07)
- 型別產生:GraphQL Code Generator,使用 Shopify preset
- 排程:使用 Croner 執行 cron 工作
- 日誌:Pino(結構化 JSON),開發環境搭配
pino-pretty - 程式碼品質:Biome(linter + formatter),並啟用 import 排序
- 容器化:GitHub Container Registry 提供可用於正式環境的 Docker 映像檔
GraphQL 型別產生
專案會依據 GraphQL 查詢自動產生 TypeScript 型別:
# 產生一次型別
bun run graphql-codegen
# 開發用的 watch 模式(尚未設定)
# bun run graphql-codegen:watch產生的型別儲存在 src/types/admin.generated.d.ts,不要手動編輯
資料庫管理
使用 Drizzle ORM 進行型別安全的資料庫操作:
# 產生遷移檔案
bun run db:generate
# 套用遷移
bun run db:migrate
# 直接推送 schema 變更(開發環境)
bun run db:push
# 開啟 Drizzle Studio(視覺化資料庫瀏覽器)
bun run db:studio多服務商架構
應用程式採用以服務商為基礎的架構,便於接入新的履約服務商
服務商介面
每個服務商都必須實作 Provider 介面:
interface Provider {
id: string; // 服務商唯一識別碼
name: string; // 可讀名稱
locationIds: string[]; // 該服務商管理的 Shopify 地點 ID
// 檢查服務商是否已完成必要設定
isConfigured(): boolean;
// 檢查某個地點是否屬於該服務商
isProviderLocation(locationName: string, locationId: string): boolean;
// 從服務商拉取已出貨訂單
fetchShippedOrders(): Promise<ProviderOrder[]>;
// 拉取訂單詳情
fetchOrderDetail(orderId: string): Promise<ProviderOrderDetail | null>;
// 從服務商資料中擷取 Shopify 訂單編號
extractShopifyOrderNumber(orderDetail: ProviderOrderDetail): string | null;
// 取得物流追蹤資訊
getTrackingInfo(orderDetail: ProviderOrderDetail): TrackingInfo;
}新增服務商
-
複製範例範本:
cp src/lib/providers/example.ts src/lib/providers/myprovider.ts -
實作服務商邏輯:
- 更新 API 端點與驗證方式
- 對映該服務商的資料結構
- 設定物流商對應與物流追蹤連結
- 填入對應的 Shopify 地點 ID
-
註冊服務商,在
src/lib/providers/registry.ts中:import { myProvider } from "./myprovider"; // 在建構函式中 this.register(myProvider); -
新增環境變數至
.env:MYPROVIDER_API_KEY=your-api-key MYPROVIDER_API_SECRET=your-secret -
執行應用程式,你的服務商會自動生效!
服務商特性
- 自動訂單追蹤:各服務商的訂單分開追蹤
- 自訂商業邏輯:服務商可以實作自己的訂單編號擷取規則
- 集中式物流商體系:所有服務商共用一套物流商設定
- 彈性的物流商別名:不同服務商可以用不同代碼指向同一家物流商
- 自動產生物流連結:依物流商與運單號產生物流追蹤連結
- 錯誤隔離:單一服務商出錯不影響其他服務商
集中式物流商設定
應用程式使用集中式物流商體系 (src/lib/carriers.ts):
- 物流商只定義一次:每家物流商都有名稱和物流追蹤連結範本
- 支援多個別名:不同服務商可以用不同的代碼或名稱指向同一家物流商
- 提供統一查詢:所有服務商都用同一組函式取得物流商資訊
範例:
// 柔造用 'sf' 代表順豐速運
// HiCustom 用 '顺丰速运' 代表順豐速運
// 兩者都會解析到同一家物流商,得到相同的物流追蹤連結
const trackingDetails = getTrackingDetails("sf", "123456");
// 或
const trackingDetails = getTrackingDetails("顺丰速运", "123456");
// 兩者的回傳值相同:
// {
// carrierName: 'SF Express',
// trackingUrl: 'https://www.sf-express.com/.../123456'
// }要新增物流商或別名支援:
- 編輯
src/lib/carriers.ts - 在
CARRIERS陣列中找到該物流商 - 將服務商使用的代碼或名稱加入
aliases陣列
服務商設定
服務商會依自身設定自動啟用或停用:
自動偵測:
- 必要的環境變數已設定 → 啟用該服務商
- 必要的環境變數缺失 → 停用該服務商
例如:
# 柔造會啟用(已提供必要的 token)
ROUZAO_TOKEN=abc123
ROUZAO_LOCATION_IDS=gid://shopify/Location/123,gid://shopify/Location/456 # 選填
# HiCustom 會啟用(已提供必要的憑證)
HICUSTOM_API_KEY=client123
HICUSTOM_API_SECRET=secret456
HICUSTOM_LOCATION_IDS=gid://shopify/Location/789,gid://shopify/Location/101 # 選填
# example 服務商會停用(缺少必要的金鑰)
# EXAMPLE_API_KEY=運作方式:
每個服務商都實作 isConfigured() 方法,檢查必要的環境變數是否存在:
class MyProvider implements Provider {
isConfigured(): boolean {
return !!process.env["MYPROVIDER_API_KEY"];
}
}新增功能
- 修改輪詢間隔:調整
src/index.ts中的 cron 運算式 - 新增 GraphQL 查詢:編輯
src/lib/queries.graphql.ts並執行bun run graphql-codegen - 支援更多物流商:在服務商實作中更新對應表
- 所有匯入都使用
@/路徑別名,例如import { Provider } from '@/types'
授權條款
AGPL-3.0
最後更新於 2026年9月19日