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) {}
}
响应里还有 cache、demoUsage 等字段没映射也没关系——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;
}
每片返回都带 summary(total/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 个按片查询。