自动化履约框架,通过 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日