00:00 / 00:00

Subspace Shop

LAPLACE Fulfiller

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

  1. 安装 Turso CLI

    curl -sSfL https://get.tur.so/install.sh | bash
  2. 创建数据库

    turso auth signup  # 或 turso auth login
    turso db create laplace-fulfiller
  3. 获取数据库凭据

    # 获取数据库 URL
    turso db show laplace-fulfiller --url
    
    # 创建鉴权 token
    turso db tokens create laplace-fulfiller
  4. 将 schema 推送到数据库

    bun run db:push

注意:从本地 SQLite 迁移时,需要先从旧数据库导出数据再导入 Turso,两者的 schema 保持兼容

获取柔造 Token

  1. 登录柔造 (https://www.rouzao.com)
  2. 打开浏览器开发者工具 (F12)
  3. 切换到 Network 标签页
  4. 执行任意会调用 API 的操作
  5. 在请求中查找 Rouzao-Token 请求头

获取 HiCustom 凭据

  1. 登录 HiCustom (https://www.hicustom.com)
  2. 进入 API 设置或开发者专区
  3. 创建新的应用或 API 客户端
  4. 复制 API Key 与 API Secret
  5. 记录 HiCustom 负责发货的 Shopify 位置 ID

HiCustom 集成使用其 OAuth API,并会自动刷新 token。参见其 API 文档:

配置 Shopify API 访问权限

  1. 在 Shopify 后台创建私有应用
  2. 授予以下权限:
    • 读取订单 (read_orders)
    • 写入订单 (write_orders)
    • 读取位置 (read_locations),可选但推荐
    • 读取商家管理的履约订单 (read_merchant_managed_fulfillment_orders)
    • 写入商家管理的履约订单 (write_merchant_managed_fulfillment_orders)
  3. 复制 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:latest

Docker 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

每次发布都会自动构建并推送镜像,确保你始终能获取最新的稳定版本

工作原理

  1. 服务商注册:启动时注册并初始化所有已启用的服务商
  2. 订单监控:服务每 30 分钟从所有已启用的服务商拉取订单
  3. 已发货订单识别:各服务商按发货状态筛选自己的订单
  4. 订单详情:为每笔已发货订单拉取详细信息,包括物流跟踪信息
  5. Shopify 订单查找:各服务商用自己的逻辑提取 Shopify 订单号
  6. 智能履约:仅履约分配给该服务商指定仓库位置的商品
  7. 防重复处理:将已履约订单连同服务商信息记录到 Turso 数据库

数据库

服务使用 Turso(分布式 SQLite)配合 Drizzle ORM,记录所有服务商已履约的订单:

表结构

  • provider:服务商标识,例如 'rouzao'
  • provider_order_id:服务商侧的订单 ID
  • shopify_order_number:Shopify 订单号
  • shopify_order_id:Shopify 订单 ID
  • fulfilled_at:履约时间戳
  • created_at:记录创建时间戳

特性

  • (provider, provider_order_id) 唯一约束可防止重复
  • 按服务商与 Shopify 订单号建立索引,加快查询
  • 每天凌晨自动清理超过 365 天的旧记录
  • 借助 Drizzle ORM 实现类型安全的查询
  • Turso 的全球边缘部署带来低延迟访问
  • 自动备份与时间点恢复
  • 与本地 SQLite 不同,没有文件大小限制

日志

服务使用 Pino 输出结构化日志,时间戳为 ISO 格式。默认以 JSON 输出,每行一条记录,适合生产环境的日志聚合系统。开发时,devoncediagnose 脚本会把输出通过 pino-pretty 管道处理,转成人类可读的格式

每个模块都有自己的子 logger,例如 mainrouzaohicustom,因此可以按组件过滤日志。日志级别通过 LOG_LEVEL 环境变量调整,默认为 info

需要关注的输出内容:

  • 订单拉取结果
  • 已发货订单的处理过程
  • 履约成功或失败
  • 错误信息

故障排查

常见问题

  1. 「Rouzao location not found」:确保 Shopify 中存在名为「Rouzao」或「柔造」的位置
  2. 「Order already fulfilled」:该订单已在 Shopify 中处理或履约完成
  3. 「Invalid third party order SN format」:该订单没有有效的 Shopify 关联信息
  4. 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;
}

添加新服务商

  1. 复制示例模板

    cp src/lib/providers/example.ts src/lib/providers/myprovider.ts
  2. 实现服务商逻辑

    • 更新 API 端点与鉴权方式
    • 映射该服务商的数据结构
    • 配置承运商映射与物流跟踪链接
    • 填入对应的 Shopify 位置 ID
  3. 注册服务商,在 src/lib/providers/registry.ts 中:

    import { myProvider } from "./myprovider";
    
    // 在构造函数中
    this.register(myProvider);
  4. 添加环境变量.env

    MYPROVIDER_API_KEY=your-api-key
    MYPROVIDER_API_SECRET=your-secret
  5. 运行应用,你的服务商会自动生效!

服务商特性

  • 自动订单跟踪:各服务商的订单分开跟踪
  • 自定义业务逻辑:服务商可以实现自己的订单号提取规则
  • 集中式承运商体系:所有服务商共用一套承运商配置
  • 灵活的承运商别名:不同服务商可以用不同代码指向同一家承运商
  • 自动生成物流链接:根据承运商与运单号生成物流跟踪链接
  • 错误隔离:单个服务商出错不影响其他服务商

集中式承运商配置

应用使用集中式承运商体系 (src/lib/carriers.ts):

  1. 承运商只定义一次:每家承运商都有名称和物流跟踪链接模板
  2. 支持多个别名:不同服务商可以用不同的代码或名称指向同一家承运商
  3. 提供统一查询:所有服务商都用同一组函数获取承运商信息

示例:

// 柔造用 'sf' 代表顺丰速运
// HiCustom 用 '顺丰速运' 代表顺丰速运
// 两者都会解析到同一家承运商,得到相同的物流跟踪链接

const trackingDetails = getTrackingDetails("sf", "123456");
// 或
const trackingDetails = getTrackingDetails("顺丰速运", "123456");

// 两者的返回值相同:
// {
//   carrierName: 'SF Express',
//   trackingUrl: 'https://www.sf-express.com/.../123456'
// }

要新增承运商或别名支持:

  1. 编辑 src/lib/carriers.ts
  2. CARRIERS 数组中找到该承运商
  3. 把服务商使用的代码或名称加入 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日

Tech otakus destroy the world