CarrierAPI — 自托管的多承运商发货平台

一个 REST API 打通多家承运商的面单、运费、轨迹追踪与批量发货——自托管,作为 SendCloud 这类 SaaS 工具的替代方案,让自有承运商合同完全掌握在自己手中。

多承运商发货
FastAPI / Python
NetSuite 与 Shopify 同步
自托管

角色与范围

一家拥有自建 ERP(NetSuite)的电商公司此前通过 SaaS 发货工具出货,随之而来的是相应成本,以及对承运商合同有限的掌控力。CarrierAPI 是它的自托管替代品:一个 API 即可创建面单、比较运费、追踪包裹并在多家承运商间处理批量任务;由于保留了与旧工具相同的对接点,从 NetSuite 侧看这次切换是直接替换而非重建。

  • 按承运商划分的适配器架构(PostNL、DHL Express 与 UPS 为核心组;DHL Parcel、FedEx、GLS 以及 SendCloud 本身作为额外适配器),因此新增一家承运商无需改动系统其余部分。
  • 在 NetSuite 内对旧 SaaS 工具的直接替换:自定义字段名完全一致,对既有 SuiteScript 集成的改动降到最低。
  • 直接挂接 NetSuite 的 Item Fulfillment 记录,并在承运商之外提供 Shopify 同步与 Amazon 集成。
  • 管理承运商凭据的两种方式:通过界面(存于数据库,推荐)或通过环境变量(面向既有部署的遗留路径)。

成果

  • 运费与承运商合同重新掌握在自己手中,不再受制于 SaaS 中间层的费率与条款。
  • 修复了一类此前被错误解析的地址的识别逻辑(门牌号提取误取了最后一个数字,而非街道名之后的第一个数字——例如同时带门牌号与小屋编号的度假村地址就会出错)。
  • 在 UPS 两次修改账单导出格式后,对账依然可用:列识别改为基于内容而非固定位置,并与账单自身声明的合计金额相互校验。
  • 堵上了一处会话漏洞:被删除的用户最长可在 24 小时内继续访问,原因是系统只校验会话令牌的签名与有效期——而不检查账号是否仍然存在。

架构

每家承运商都是共享接口背后的一个适配器(创建与取消运单、查询运费、轨迹追踪、可用服务)——接入新承运商不会触及系统其余部分。后端是 FastAPI(Python,异步),以 PostgreSQL 为主数据库,并使用基于 Redis 的后台工作进程(arq)批量处理面单,因此一次生成数百张面单也不会阻塞 API。面单本身存放在 MinIO(S3 兼容),使存储不被单一云厂商绑定。前端是 Next.js/React 仪表盘,用于管理运单、承运商设置与凭据。整体通过 Docker Compose 自托管运行在 Traefik 之后,由 Coolify 部署。

模型选择

不含 AI/LLM——这是刻意为之的确定性系统:面单、运费与轨迹必须可预测、可核查,而不是被生成出来。这里的关键抉择是自托管还是 SaaS:SaaS 发货工具上线更快,但自托管平台带来对承运商合同的掌控、免除向中间层支付的按面单或按运单费用,并且能把功能(例如直连 NetSuite)完全按需构建,而不必等待 SaaS 厂商是否支持。

成本与延迟

  • 自托管基础设施(Docker Compose、PostgreSQL、Redis、MinIO),取代按运单持续支付的 SaaS 费用。
  • 批量面单生成通过 Redis 队列(arq)异步执行,因此一次生成数百张面单仍能保持 API 响应,而不是让请求方干等。
  • 对账逻辑已在真实 UPS 账单上验证:其中包括一份 1,275 行、金额 4,855.72 欧元的账单,以及一份 2,275 行的导出文件,在列识别修复后均被正确处理。

经验总结

荷兰地址解析在一类特定地址上出错(同时带门牌号与独立小屋编号的度假村地址,例如“Hessenweg 83 41”),因为解析器把街道行中的最后一个数字当作门牌号,而不是街道名之后的第一个数字。改用统一的解析器后解决,并以真实案例验证。UPS 账单导入两次中断,原因是 UPS 修改了账单导出格式(列数与列序发生位移);固定列位置的做法被替换为基于内容的列识别,其结果会与账单自身声明的合计金额交叉核对。地址校验告警起初会完全阻断运单创建;后来收敛为仅阻断自动购买面单,使被标记的运单保持可见、可重试,而不会卡死。还有一处会话漏洞:被删除的用户最长可保留 24 小时访问权限,因为令牌只按签名与有效期校验,而不校验账号是否存在——通过在每次请求时验证账号仍然存在得以修复。