被 209 家物流协议轮番折腾之后,我往项目里塞了一个统一门面
如果你做过电商后台、仓储系统,或者任何要对接快递轨迹查询的业务,大概都经历过这种状况:顺丰一套签名、DHL 要 OAuth2、UPS 报文是 XML、FedEx 又是 JSON,最离谱的是各家「已签收」「投递失败」的语义完全对不上。我当时的项目要同时接国内外十几家承运商,光是状态翻译那层代码就写了一千多行,还全是 if-else。
后来我把这部分全部砍掉,换成了 erikwang2013/global-logistics。这篇笔记讲讲我为什么这么选、它怎么工作、以及哪些场景下你别用它。
我遇到的具体问题
业务方只给我一个运单号,要求我返回轨迹列表和当前状态。听起来很简单对吧?实际上每接一家快递,就要写一套适配:有的接口要求签名串拼接方式特殊,有的要先拿 token 再查轨迹,有的返回 XML 有的返回 JSON,还有的「运输中」可能叫 IN_TRANSIT,也可能叫 TRANSPORTING。每加一家新承运商,排期至少两三天。
而且代码里不能写死密钥。我们以前就出过一次事故:某家快递的 API key 被提交进 Git 仓库,当天晚上就被爬虫扫到,账号立刻被封。从那以后密钥必须全部走配置注入。
为什么选 global-logistics
先看它解决了什么。这个包在 GitHub: https://github.com/erikwang2013/global-logistics,定位是一个不绑定框架的 PHP Composer 包,PHP 8.2+,PSR-4 自动加载,HTTP 层遵循 PSR-18。它做了三件核心的事:
- 单号自动识别:内置 187 条正则规则,顺序敏感,优先命中国内通道。你传一个
SF1234567890,它自己知道是顺丰;传1Z...它知道是 UPS。 - 统一数据模型:不管是 DHL 还是中通,最终都返回
Tracking/TrackingEvent对象,状态统一映射成 7 种枚举:待揽收 / 运输中 / 派送中 / 已签收 / 异常 / 退回 / 未知。 - 密钥零硬编码:所有密钥通过配置注入,代码里没有任何一家承运商的明文凭据。
我比较在意的另一点是它的认证层:src/Http/ 下有一个 OAuthTokenClient,懒加载 token 并自动缓存;RetryingClient 做失败重试。也就是说 DHL、FedEx、UPS 这类要 OAuth2 的,token 过期、网络抖动这些脏活它内部处理掉了,我不用在业务代码里写重试逻辑。
目录结构怎么读
装完之后我第一件事是扒源码,目录长这样(省略了一些非关键文件):
global-logistics/
├── src/
│ ├── Carriers/
│ │ ├── Domestic/ # 国内 45 家适配器(顺丰、中通、圆通...)
│ │ └── International/ # 国际 164 家(DHL、FedEx、UPS、各国邮政 S10...)
│ ├── Exceptions/ # 异常体系
│ ├── Framework/ # Laravel / ThinkPHP / Hyperf / Webman / Yii 2
│ ├── Http/ # PSR-18:OAuthTokenClient、RetryingClient
│ ├── Models/ # Tracking / TrackingEvent / Order / OrderRequest / Label
│ ├── Resources/
│ │ ├── carrier-registry.php # 209 家承运商注册表
│ │ └── detector-rules.php # 187 条单号识别规则
│ ├── Support/ # TrackStatus 等
│ ├── CarrierFactory.php # 注册表 → 适配器实例化
│ ├── CarrierInterface.php # 适配器统一契约
│ ├── Channel.php # 国内 / 国际通道枚举
│ ├── Detector.php # 单号规则检测
│ └── Logistics.php # 静态门面
├── config/
│ └── logistics.php # 配置模板(209 家密钥占位)
├── tests/
│ ├── Carriers/ # 每承运商 7 个用例
│ └── fixtures/ # 每家 track / empty / error 夹具
└── composer.json
核心设计是「门面 + 注册表 + 适配器」三层:
Logistics静态门面持有全局配置和检测器;- 收到单号后
Detector按顺序匹配 187 条规则,先判断通道(国内/国际),再锁定具体承运商,返回「通道 + 代码」; CarrierFactory根据注册表实例化对应适配器,注入配置和 HTTP 客户端;- 每个适配器实现
CarrierInterface,各自处理签名、OAuth2、XML/JSON 解析、状态映射。
所以新增一家承运商,只需要照模板写一个适配器类,然后往注册表里加两条记录。不需要动业务层。
代码层面长什么样
安装:
composer require erikwang2013/global-logistics
查轨迹:
use GlobalLogistics\Logistics;
Logistics::configure([
// 各家密钥经配置注入,如 sf / dhl 等
'sf' => getenv('SF_API_KEY'),
'dhl' => getenv('DHL_CLIENT_SECRET'),
]);
$tracking = Logistics::track('SF1234567890');
echo $tracking->status->name; // DELIVERED
echo $tracking->latestDescription; // 快件已签收
注意 Logistics::configure() 如果不调用,门面会以空配置初始化,这时候只有那些不需要密钥的承运商能用。密钥建议走环境变量或 .env,不要硬编码进仓库——这是我踩过坑之后定的铁律。
如果单号规则识别不了,可以显式指定通道和承运商:
Logistics::domestic()->track($trackingNo, 'zhongtong');
// 或
Logistics::international()->track($trackingNo, 'dhl');
还有回调签名验证:verifyCallbackSignature(),物流商订阅推送时拿它校验来源。我现在在用的场景是顺丰的回调推送,没出过签名校验失败的问题。
异常体系和失败表现
这包没有把异常吞掉,而是统一收敛到自己的异常体系里:LogisticsException 下面分了 5 个细分场景——认证失败、单号不存在、网络错误、承运商未注册、接口错误。我业务层只 catch 一个 LogisticsException 就能覆盖全部外部接口问题。
实际跑下来,几个典型的失败表现:
- 密钥配错:抛认证失败异常,不会出现「HTTP 200 但返回一段看不懂的错误码」这种情况;
- 单号不属于任何已注册承运商:抛承运商未注册异常,
Detector识别不出时返回unknown,不会硬猜; - 单号识别错误:这个要特别注意——187 条规则顺序敏感,优先命中国内通道。如果你从没配置某家承运商的密钥,但单号命中了它的规则,会直接抛认证异常而不是「未注册」。我当时排查过一次,最后发现是规则命中了但密钥没填。
适用场景和不适用场景
适用:
- 电商订单轨迹展示、仓储履约看板、ERP 物流模块、客服查询后台;
- 需要同时覆盖国内外承运商的跨境团队——164 家国际(DHL / FedEx / UPS / USPS + 各国邮政 S10)加上 45 家国内,做跨境一体化查询时不需要拼两套方案;
- 不想在业务代码里维护一堆各家协议适配逻辑、想统一异常处理入口的团队。
不适用:
- 你只需要对接一家快递、且对方协议已经写死在系统里——引入这个包反而多一层抽象,不值得;
- 你的 PHP 版本还停在 8.0 以下——它要求 8.2+;
- 你要的承运商恰好不在 209 家里,并且你不想自己写适配器——
src/Carriers/Domestic和International两个目录都可以扩展,但需要动手。
另外有一点原文未提供:这个包对每家的接口频率限制怎么处理,文档里没有细说。如果你们的单量很大,建议在接入前先确认目标承运商各自的 QPS 限制,RetryingClient 能处理瞬时错误,但处理不了持续限流。
测试情况
它声称有 1663 个测试用例、6662 条断言,全量跑绿,而且不依赖真实密钥。我实际跑过 vendor/bin/phpunit,测试夹具在 tests/fixtures/ 下,每家承运商都有 track / empty / error 三套 mock 数据。这一点比较实在——意味着我改完适配器可以直接跑测试验证,不用拿真实运单号去试。