chinphy / express-tracking
Unified express/courier tracking SDK for JD Logistics, SF Express, ZTO and China Post EMS with normalized output and optional raw trace retention.
Requires
- php: ^8.0
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.5
- guzzlehttp/psr7: ^2.4
- phpunit/phpunit: ^9.6 || ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
统一快递轨迹查询 SDK(PHP 8.0+,Composer 包)。一个接口查询 京东(JD/JOS 双通道)、顺丰(SF)、中通(ZTO)、中国邮政(EMS)、申通(STO)、菜鸟橙运(CNCY)、快递鸟(KDN) 等开放平台/聚合平台的物流轨迹,输出统一格式结果,并支持按需保留各家原始轨迹数据(全局开关 + 单次传参覆盖)。
中国邮政/EMS 无公开自助 API,本包对接的是邮政新一代寄递平台签约大客户接口(
mailTrackGjcx),需与邮政商务签约获取companyId(客户标识)+queryKey(轨迹查询密钥)。快递鸟(KDN)为聚合通道,可查快递鸟支持的多家快递。
特性
- ✅ 标准 Composer 包:PSR-4 自动加载,
composer require即用 - ✅ 八条通道完整实现:京东 LOP 新版(
queryCommonTracking)、京东 JOS 老平台(Waybill2CTraceApi)、顺丰(EXP_RECE_SEARCH_ROUTES)、中通(zto.merchant.waybill.track.query)、EMS(新一代寄递平台mailTrackGjcx)、申通(STO_TRACE_QUERY_COMMON)、菜鸟橙运(cn.ap.ld.query)、快递鸟(KDNRequestType=2002聚合) - ✅ 统一输出:
TrackingResult(统一状态、轨迹列表、查询时间),各家原始状态码透传 - ✅ 状态归一化:已揽收 / 运输中 / 派送中 / 已签收 / 异常 / 退回 / 未知
- ✅ 原始数据开关:构造时全局配置 + 单次查询传参覆盖
- ✅ 可区分异常:认证失败 / 运单不存在 / 网络失败 / 响应解析失败
- ✅ PSR-18 HTTP 客户端注入(Guzzle、Symfony HttpClient 等任意实现)
- ✅ 沙箱/测试环境切换(顺丰 sfapi-sbox、中通 japi-test、京东 uat-api.jdl.com、EMS 测试地址)
- ✅ Fixture 单元测试(无凭据可跑)+ 可选集成测试
安装
composer require chinphy/express-tracking
composer require guzzlehttp/guzzle # 任选一个 PSR-18 实现
快速开始
<?php use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; use Chinphy\ExpressTracking\Carrier; use Chinphy\ExpressTracking\ExpressTrackingClient; use Chinphy\ExpressTracking\Provider\CNCYProvider; use Chinphy\ExpressTracking\Provider\EMSProvider; use Chinphy\ExpressTracking\Provider\JDProvider; use Chinphy\ExpressTracking\Provider\KDNProvider; use Chinphy\ExpressTracking\Provider\SFProvider; use Chinphy\ExpressTracking\Provider\STOProvider; use Chinphy\ExpressTracking\Provider\ZTOProvider; $http = new HttpFactory(); $guzzle = new Client(['timeout' => 10]); $client = new ExpressTrackingClient([ new SFProvider('你的customerCode', '你的checkword', $guzzle, $http, $http), new ZTOProvider('你的AppKey', '你的AppSecret', $guzzle, $http, $http), new JDProvider( '你的AppKey', '你的AppSecret', '你的accessToken', '你的客户编码(010K...)', $guzzle, $http, $http, ), new EMSProvider('你的companyId(客户标识)', '你的queryKey(查询密钥)', $guzzle, $http, $http), new STOProvider('你的AppKey', '你的Secret', $guzzle, $http, $http), new CNCYProvider('你的AppKey', '你的AppSecret', '你的customerId', '你的ownerCode', $guzzle, $http, $http), new KDNProvider('你的EBusinessID', '你的ApiKey', 'CNCY', '客户名', $guzzle, $http, $http), // JOS 老平台通道(已在 JOS 开通的账号使用,与 LOP 新版二选一即可): // new JDOSProvider('你的AppKey', '你的AppSecret', '你的accessToken', '你的tradeCode(事业部编码)', $guzzle, $http, $http), ]); // 单次查询,保留原始数据 $result = $client->track(Carrier::SF, 'SF1234567890', keepRaw: true); echo $result->status; // delivered / in_transit / ... echo $result->statusText; // 最新一条轨迹描述 foreach ($result->traces as $point) { printf( "%s | %s | %s | %s\n", $point->time?->format('Y-m-d H:i:s'), $point->location ?? '-', $point->status, $point->description ); } // 原始轨迹数据(开启 keepRaw 时可用) var_dump($result->raw);
keepRaw 开关
| 位置 | 方式 | 说明 |
|---|---|---|
| 全局 | new ExpressTrackingClient($providers, keepRaw: true) |
所有查询默认附带 raw |
| 单次 | $client->track(Carrier::SF, 'xxx', keepRaw: true) |
覆盖全局默认 |
| 跟随全局 | $client->track(Carrier::SF, 'xxx') 或传 null |
用全局配置 |
开启后 TrackingResult::$raw 为各家完整原始响应数组(含网关包装与业务数据),仅供排查/透传,不应依赖其内部结构。
统一状态集合
| 常量 | 值 | 含义 |
|---|---|---|
TrackingStatus::PICKED_UP |
picked_up |
已揽收 |
TrackingStatus::IN_TRANSIT |
in_transit |
运输中 |
TrackingStatus::OUT_FOR_DELIVERY |
out_for_delivery |
派送中 |
TrackingStatus::DELIVERED |
delivered |
已签收 |
TrackingStatus::EXCEPTION |
exception |
异常 |
TrackingStatus::RETURNED |
returned |
退回 |
TrackingStatus::UNKNOWN |
unknown |
未知 |
每家原始状态码/类型保留在 TracePoint::$rawStatus(如顺丰 opCode、中通 scanType、京东 operationCode、EMS opCode)。
各通道凭据准备
| 通道 | 凭据 | 申请入口 |
|---|---|---|
| 顺丰 | customerCode + checkword(需企业资质,通常为月结客户) |
https://open.sf-express.com |
| 中通 | AppKey + AppSecret(需企业实名认证 + 电子面单账号或电话后4位鉴权) |
https://open.zto.com |
| 京东 | AppKey + AppSecret + accessToken(OAuth,365天有效)+ 客户编码(需签约,B2C 需软件著作权) |
https://open.jdl.com |
| EMS | companyId(客户标识)+ queryKey(查询密钥),邮政新一代寄递平台签约大客户接口 |
与邮政商务签约 |
| 申通 | AppKey + Secret(申通开放平台/云通信网关签约) |
与申通商务签约(cloudinter 网关) |
| 菜鸟橙运 | AppKey + AppSecret + customerId + ownerCode(菜鸟物流开放平台) |
菜鸟开放平台(link.cainiao.com) |
| 快递鸟 | EBusinessID + ApiKey + ShipperCode(聚合通道,ShipperCode 选快递公司) |
https://www.kdniao.com |
| 京东 JOS | AppKey + AppSecret + accessToken + tradeCode(JOS 老平台,参考实现 JDDrive 同款) |
https://jos.jd.com |
京东 access_token 刷新
access_token 一年有效。SDK 提供刷新回调,token 过期(错误码 2001)时自动调用一次:
$provider = new JDProvider( // ... 其他参数, tokenRefresher: function (string $trackingNo): string { return refreshJdToken(); // 自行实现 OAuth 刷新,返回新 token }, );
多站点 / 多仓库兼容(多 tradeCode / customerCode)
不同仓库与京东不同站点合作时,会分配不同的 tradeCode(JOS)或 customerCode(LOP),但系统账号与代码是同一套。两个京东 Provider 的站点参数均支持数组:查询时按顺序依次尝试,任一命中即返回(与参考实现 tradeCodes 循环行为一致)。
// JOS:多个事业部编码 new JDOSProvider( 'AppKey', 'AppSecret', 'accessToken', ['010K916912', '028K7801302'], // 多个 tradeCode,按序尝试 $guzzle, $http, $http, ); // LOP:多个客户编码 new JDProvider( 'AppKey', 'AppSecret', 'accessToken', ['010K1233455', '010K916912'], // 多个 customerCode,按序尝试 $guzzle, $http, $http, );
- 传单个字符串(如
'010K916912')与旧用法完全兼容 - 全部站点都查不到时,抛出第一个异常(通常是
NotFoundException) - 每次查询会对每个站点发一次请求,站点数较多时注意调用量
按指定站点直查(已知运单归属时,跳过多站点尝试,只发一次请求):
// 门面统一入口(carrier + 站点参数) $result = $client->trackWithSite(Carrier::JDOS, 'JDV002516977833', '010K916912', keepRaw: true); // 或直接调用 Provider $jdos->trackWithSite('JDV002516977833', '010K916912'); $jd->trackWithSite('JDVA1234567890', '010K1233455');
- JDOS 的
site参数为 tradeCode;JD(LOP)的site参数为 customerCode - 支持该能力的 Provider 实现
SiteQueryProviderInterface;不支持的通道调用trackWithSite()会抛InvalidArgumentException - 直查未命中直接抛异常(不做其他站点回退)
异常处理
所有异常继承 Chinphy\ExpressTracking\Exception\TrackingException:
| 异常 | 触发场景 |
|---|---|
AuthenticationException |
签名错误、无权限(顺丰非 S0000、中通 S211/S210、京东 2001 等) |
NotFoundException |
运单不存在/无轨迹(顺丰 S0001、中通 E416、京东 2010/2100、EMS 空 responseItems) |
NetworkException |
HTTP 传输层失败 |
BadResponseException |
响应无法解析或网关级错误(EMS 返回错误 message) |
NotSupportedException |
未实现通道(当前无) |
异常携带 $carrier、$trackingNo、$providerCode 上下文。
沙箱 / 测试环境
各 Provider 构造参数 sandbox: true 时自动切换:
- 顺丰:
https://sfapi-sbox.sf-express.com/std/service - 中通:
https://japi-test.zto.com - 京东:
https://uat-api.jdl.com(京东快递 B2C 无独立沙箱,用预发环境与预发凭据) - EMS:
http://211.156.197.242:8080/querypush-gjcx/mailTrackGjcx/mailTrackGjcxTwswn/plus(生产为http://211.156.195.11/...;可用$url构造参数覆盖) - 申通:
http://cloudinter-linkgatewaytest.sto.cn/gateway/link.do(生产为https://cloudinter-linkgateway.sto.cn/gateway/link.do) - 菜鸟橙运:
https://link.cainiao.com/gateway/custom/CNAP_RECEIVE_RESTFUL/qimen($url可覆盖) - 快递鸟:
https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx($url可覆盖)
测试
composer test # 单元测试(fixture 驱动,无网络) composer test:integration # 集成测试(需环境变量凭据,默认跳过) composer test:all # 全部
集成测试凭据(环境变量):SF_CUSTOMER_CODE/SF_CHECKWORD/SF_TRACKING_NO、ZTO_APP_KEY/ZTO_APP_SECRET/ZTO_TRACKING_NO、JD_APP_KEY/JD_APP_SECRET/JD_ACCESS_TOKEN/JD_CUSTOMER_CODE/JD_TRACKING_NO、JDOS_APP_KEY/JDOS_APP_SECRET/JDOS_ACCESS_TOKEN/JDOS_TRADE_CODE/JDOS_TRACKING_NO、EMS_COMPANY_ID/EMS_QUERY_KEY/EMS_TRACKING_NO、STO_APP_KEY/STO_SECRET/STO_TRACKING_NO、CNCY_APP_KEY/CNCY_APP_SECRET/CNCY_CUSTOMER_ID/CNCY_OWNER_CODE/CNCY_TRACKING_NO、KDN_BUSINESS_ID/KDN_API_KEY/KDN_SHIPPER_CODE/KDN_TRACKING_NO。
开发说明
- 最低 PHP 8.0(不使用 enum/readonly 等 8.1+ 语法)
- 各家签名器独立成类(
src/Support/Signature/),官方签名规则如有变化只改对应类 - 各家真实响应 fixture 存放于
tests/Fixtures/,新字段/新状态码直接补充 fixture 与归一化映射 - 快递公司仅手动指定(
Carrier::SF等),不做单号自动识别;如需批量查询,由调用方循环调用
License
MIT