快递查询API

← 技术博客

C# / .NET 对接快递查询接口:HttpClient 与 System.Text.Json

2026-07-26 · 阅读约 8 分钟

在 .NET 里接快递查询接口,最顺手的组合是 HttpClient + System.Text.Json:无需第三方 JSON 库,用 record 描述返回、 async/await 发请求,再把它包成 typed client 交给依赖注入。 本文用 .NET 8 走完这套路子,代码可直接放进 ASP.NET Core 项目。

一、准备工作

注册拿到 pk_sk_ 两段密钥(邮箱注册即可,无需企业认证)。 密钥放进 appsettings.json,别硬编码进代码:

{
  "Kuaidi": {
    "BaseUrl": "https://api.kuaidichaxunapi.com",
    "Key": "pk_xxx",
    "Secret": "sk_xxx"
  }
}

还没注册也行,首页的在线演示用公开测试密钥就能先跑一遍。

二、定义 record DTO

把统一信封映射成不可变的 recordSystem.Net.Http.Json 的扩展方法默认走 Web 规则(camelCase、大小写不敏感),所以 trackingNumberdeliveryStatusText 这些会自动对应到 PascalCase 属性;只有 isSuccessisDelivered 这种带 is 前缀的,用 [property: JsonPropertyName] 显式对齐更清楚。dataerror 用可空引用类型,缺省时为 null

using System.Text.Json.Serialization;

public record TraceResponse(
    [property: JsonPropertyName("isSuccess")] bool IsSuccess,
    TraceData Data);

public record TraceData(List<Result> Results, Summary Summary);

public record Summary(int Total, int Successful, int Failed, int Billable);

public record Result(bool Success, Tracking? Data, ApiError? Error);

public record Tracking(
    string TrackingNumber,
    string CourierCode,
    string CourierName,
    string DeliveryStatus,
    string DeliveryStatusText,
    [property: JsonPropertyName("isDelivered")] bool IsDelivered,
    string? DateDelivered,
    string? DateLastProgress,
    List<Progress> Progresses,
    string QueriedAt);

public record Progress(
    string DateTime, string Location, string Status,
    string StatusCode, string Description);

public record ApiError(
    string Code, string Message, string CourierCode,
    string TrackingNumber, bool Billable);

public record Item(string CourierCode, string TrackingNumber);
public record TraceRequest(List<Item> Items);

响应里的 cachedemoUsage 等字段没写进 record 也没关系—— System.Text.Json 默认会忽略多出来的属性。

三、封装成 typed client

把请求逻辑收进一个类,注入 HttpClient。认证放默认请求头:两段密钥用英文冒号拼成 Bearer pk_xxx:sk_xxx。用 PostAsJsonAsync / ReadFromJsonAsync 直接完成序列化与反序列化:

using System.Net.Http.Json;

public class KuaidiClient
{
    private readonly HttpClient _http;

    public KuaidiClient(HttpClient http) => _http = http;

    /// <summary>一次最多 10 个运单号。</summary>
    public async Task<List<Result>> TraceAsync(IEnumerable<Item> items)
    {
        var req = new TraceRequest(items.ToList());
        using var resp = await _http.PostAsJsonAsync("/v1/tracking/trace", req);
        resp.EnsureSuccessStatusCode();
        var body = await resp.Content.ReadFromJsonAsync<TraceResponse>();
        return body!.Data.Results;
    }
}

Program.cs 里把它注册为 typed client,认证头在这里统一注入:

var cfg = builder.Configuration;

builder.Services.AddHttpClient<KuaidiClient>(c =>
{
    c.BaseAddress = new Uri(cfg["Kuaidi:BaseUrl"]!);
    var key = cfg["Kuaidi:Key"];
    var secret = cfg["Kuaidi:Secret"];
    c.DefaultRequestHeaders.Add("Authorization", $"Bearer {key}:{secret}");
    c.Timeout = TimeSpan.FromSeconds(10);
});

之后在控制器或服务里,构造函数注入 KuaidiClient 即可,连接池由框架统一管理, 不必自己 new HttpClient()(那容易耗尽 socket)。

四、遍历 results 与判断签收

返回的 Results 与请求顺序一一对应。逐条先看 Success,成功再读 Data

var results = await client.TraceAsync(new[]
{
    new Item("yto", "YT7500000123456"),
});

foreach (var r in results)
{
    if (r.Success)
    {
        var t = r.Data!;
        Console.WriteLine($"{t.TrackingNumber} → {t.DeliveryStatusText}");
        bool done = t.DeliveryStatus == "DELIVERED"; // 归一状态码判断,跨快递通用
        // progresses 最新在前
        foreach (var p in t.Progresses)
            Console.WriteLine($"  {p.DateTime} {p.StatusCode} {p.Description}");
    }
    else
    {
        var e = r.Error!;
        Console.WriteLine($"失败: {e.Code} {e.Message}");
    }
}

DeliveryStatusText 已是中文(如「已签收」「运输中」),可直接展示; 真正拿来做逻辑判断的是归一后的 DeliveryStatus(12 种状态码之一)。也可直接用布尔字段 IsDelivered 判断是否到终态——为真就不必再查,能省额度。

五、错误处理:错误码 switch 与异常分层

错误分两层:一层是 HTTP 层(认证失败、超时、5xx),由 EnsureSuccessStatusCode 抛出异常;另一层是批量里逐条的业务错误码,走 switch。两层分开处理:

try
{
    var results = await client.TraceAsync(items);
    foreach (var r in results)
    {
        if (r.Success) continue;
        var e = r.Error!;
        switch (e.Code)
        {
            case "COURIER_PREPARING":
                logger.LogInformation("{No} 查询开发中,本次不计费", e.TrackingNumber);
                break;
            case "NOT_FOUND":
                logger.LogWarning("{No} 暂无轨迹,稍后重试", e.TrackingNumber);
                break;
            default:
                logger.LogError("查询失败 {Code}: {Msg}", e.Code, e.Message);
                break;
        }
    }
}
catch (HttpRequestException ex)
{
    // .NET 5+ 里 ex.StatusCode 可区分 4xx / 5xx
    if ((int?)ex.StatusCode >= 500)
        logger.LogError(ex, "服务端错误,可重试");
    else
        logger.LogError(ex, "请求被拒(检查密钥或请求体)");
}
catch (TaskCanceledException)
{
    logger.LogError("请求超时"); // 超时可退避后重试
}

记住两个业务码:NOT_FOUND 是单号有效但暂无轨迹(新单常见),会计费COURIER_PREPARING 是该公司查询功能仍在开发中,不消耗额度Summary.Billable 告诉你这批实际计费了几条。重试方面,生产环境推荐用 AddStandardResilienceHandler(Polly)给 typed client 挂上退避与超时策略, 只对超时和 5xx 重试,4xx 不重试。

六、批量分片查询

单次上限 10 个运单号,要查更多就用 .NET 6+ 自带的 Chunk 切片再合并:

public async Task<List<Result>> TraceAllAsync(IReadOnlyList<Item> all)
{
    var output = new List<Result>();
    foreach (var chunk in all.Chunk(10)) // 每 10 个一组
    {
        var part = await TraceAsync(chunk);
        output.AddRange(part);
    }
    return output;
}

各片都带 Summary,把每片的 Billable 累加就是本次实际计费的总条数。

小结

  • record 定义 DTO;System.Net.Http.Json 默认走 Web 规则自动对齐 camelCase,isSuccess/isDelivered[JsonPropertyName] 更清楚。
  • 包成 typed client 交给 DI,认证头 Bearer pk_xxx:sk_xxx 统一注入,别自己 new HttpClient()
  • 逐条判断 Success;业务码走 switch,其中 COURIER_PREPARING 不计费。
  • HTTP 异常用 ex.StatusCode 区分 4xx / 5xx;重试交给 Polly,只对超时和 5xx 退避。
  • 签收认准 DELIVERED / IsDelivered;超过 10 个用 Chunk(10) 分片。

完整字段与限制见接口文档,各家快递编码见快递公司页。 免费额度每月 10000 次,无需企业认证,接进 ASP.NET 项目足够起步。