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
把统一信封映射成不可变的 record。System.Net.Http.Json 的扩展方法默认走
Web 规则(camelCase、大小写不敏感),所以 trackingNumber、deliveryStatusText
这些会自动对应到 PascalCase 属性;只有 isSuccess、isDelivered 这种带 is
前缀的,用 [property: JsonPropertyName] 显式对齐更清楚。data 与 error
用可空引用类型,缺省时为 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);
响应里的 cache、demoUsage 等字段没写进 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 项目足够起步。