快递查询API

← 技术博客

Spring Boot 对接快递查询接口(Java):RestClient 与类型安全

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

Java 接快递查询接口,最大的优势是类型安全:把返回结构定义成 DTO, 编译期就能挡住字段拼错、类型用错。本文以 Spring Boot 3 的 RestClient 为例, 用 record 描述响应、把错误码收进枚举,让 IDE 和编译器替你把关。

一、依赖与配置

注册拿到 pk_sk_ 两段密钥(邮箱注册即可)。 Spring Boot 3.2+ 的 spring-boot-starter-web 已自带 RestClient 与 Jackson,无需额外依赖。 密钥写进 application.properties

kuaidi.base-url=https://api.kuaidichaxunapi.com
kuaidi.key=pk_xxx
kuaidi.secret=sk_xxx

二、定义响应 DTO(record)

把统一信封映射成不可变的 record。注意信封层用的是 isSuccess、 轨迹里用的是 isDelivered,两个布尔字段的 JSON 名带 is 前缀, 用 @JsonProperty 显式对齐最稳:

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

public record TraceResponse(
        @JsonProperty("isSuccess") boolean success,
        Data data) {

    public record Data(List<Result> results, Summary summary) {}

    public record Summary(int total, int successful,
                          int failed, int billable) {}

    public record Result(boolean success, Tracking data, ApiError error) {}

    public record Tracking(
            String trackingNumber,
            String courierCode,
            String courierName,
            String deliveryStatus,
            String deliveryStatusText,
            @JsonProperty("isDelivered") boolean delivered,
            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, boolean billable) {}
}

响应里还有 cachedemoUsage 等字段没映射也没关系——Spring Boot 默认 FAIL_ON_UNKNOWN_PROPERTIES=false,多出来的字段会被忽略。请求体也用一个 record:

public record TraceRequest(List<Item> items) {
    public record Item(String courierCode, String trackingNumber) {}
}

三、用 RestClient 发送请求

把 RestClient 声明成一个 Bean,默认头里带上认证——两段密钥用英文冒号拼成 Bearer pk_xxx:sk_xxx

@Configuration
public class KuaidiConfig {

    @Bean
    public RestClient kuaidiRestClient(
            @Value("${kuaidi.base-url}") String baseUrl,
            @Value("${kuaidi.key}") String key,
            @Value("${kuaidi.secret}") String secret) {
        return RestClient.builder()
            .baseUrl(baseUrl)
            .defaultHeader("Authorization", "Bearer " + key + ":" + secret)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }
}

服务类里发请求并直接反序列化成 TraceResponse

@Service
public class KuaidiService {

    private final RestClient client;

    public KuaidiService(RestClient kuaidiRestClient) {
        this.client = kuaidiRestClient;
    }

    /** 一次最多 10 个运单号。 */
    public List<Result> trace(List<Item> items) {
        TraceResponse resp = client.post()
            .uri("/v1/tracking/trace")
            .body(new TraceRequest(items))
            .retrieve()
            .body(TraceResponse.class);
        return resp.data().results();
    }
}

四、遍历 results 与判断签收

返回的 results 与请求顺序一一对应。逐条先看 success(), 用归一状态码 DELIVERED 判断是否签收——注意把常量放前面调 equals,天然防空指针:

List<Result> results = service.trace(List.of(
    new Item("yto", "YT7500000123456")
));

for (Result r : results) {
    if (r.success()) {
        Tracking t = r.data();
        System.out.printf("%s → %s%n", t.trackingNumber(), t.deliveryStatusText());
        boolean done = "DELIVERED".equals(t.deliveryStatus());  // 归一状态码判断
        // progresses 最新在前
        for (Progress p : t.progresses()) {
            System.out.printf("  %s %s %s%n", p.dateTime(), p.statusCode(), p.description());
        }
    } else {
        ApiError e = r.error();
        System.out.println("失败: " + e.code() + " " + e.message());
    }
}

deliveryStatusText 已是中文(如「已签收」「运输中」),可直接展示; 真正拿来做逻辑判断的是归一后的 deliveryStatus(12 种状态码之一)。

五、把错误码映射成枚举

错误码是固定的一组字符串,正好用枚举收住,既有类型安全又能覆盖未知值:

public enum TrackError {
    MISSING_PARAMS,
    INVALID_TRACKING_NUMBER,
    UNSUPPORTED_COURIER,
    NOT_FOUND,          // 有该快递公司但暂无轨迹,计费
    TRACKING_FAILED,
    COURIER_PREPARING,  // 该公司查询开发中,不计费
    UNKNOWN;

    public static TrackError from(String code) {
        try {
            return TrackError.valueOf(code);
        } catch (IllegalArgumentException | NullPointerException ex) {
            return UNKNOWN;
        }
    }
}

用 switch 表达式分支处理,编译器会提醒你有没有漏掉分支:

switch (TrackError.from(e.code())) {
    case COURIER_PREPARING -> log.info("开发中,不计费: {}", e.trackingNumber());
    case NOT_FOUND         -> log.warn("暂无轨迹,稍后重试: {}", e.trackingNumber());
    default                -> log.error("查询失败 {}: {}", e.code(), e.message());
}

六、批量分片查询

单次上限是 10 个运单号,要查更多就按 10 个一组切片,再合并结果:

public List<Result> traceAll(List<Item> all) {
    List<Result> out = new ArrayList<>();
    for (int i = 0; i < all.size(); i += 10) {
        List<Item> chunk = all.subList(i, Math.min(i + 10, all.size()));
        out.addAll(trace(chunk));
    }
    return out;
}

每片返回都带 summarytotal/successful/failed/billable), 把各片的 billable 累加,就是这次实际计费的条数——COURIER_PREPARING 那些不计入。

小结

  • record 定义 DTO;布尔字段 isSuccess/isDelivered@JsonProperty 对齐。
  • RestClient 声明为 Bean,默认头带 Bearer pk_xxx:sk_xxx,密钥入 application.properties
  • 逐条 success() 判断;签收认准 DELIVERED,常量在前调 equals 防空。
  • 错误码收进枚举并保留 UNKNOWN 兜底;超过 10 个按片查询。

完整字段与状态码表见接口文档,快递编码见快递公司页。 免费额度每月 10000 次,无需企业认证。