Search by

chinphy / express-tracking

chinphy

Unified express/courier tracking SDK for JD Logistics, SF Express, ZTO and China Post EMS with normalized output and optional raw trace retention.

Package info

github.com/chinphy/express-tracking

pkg:composer/chinphy/express-tracking

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-16 07:18 UTC

This package is auto-updated.

Last update: 2026-09-16 07:57:24 UTC


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)、快递鸟(KDN RequestType=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