快递100 API 工具类封装实践:签名、请求与缓存防锁单

在订单物流查询场景中,系统通常需要根据快递公司编码和快递单号查询物流轨迹。本文结合项目中的 Kuaidi100Util 工具类,分享快递100 API 的封装方式,并重点说明缓存机制的作用。

快递100对同一个快递单号的查询频率有要求:

每一单查询频率至少间隔半小时以上,否则可能会造成锁单。

因此,在调用快递100接口前,需要先查询缓存,避免短时间内重复请求同一个快递单号。


一、工具类完整代码

java 复制代码
package com.study.server;

import com.alibaba.fastjson.JSONObject;
import com.study.server.cache.Express100Cache;
import com.study.server.web.exception.BaseException;
import com.study.server.web.utils.MD5Util;
import com.study.server.web.utils.RequestSendUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 快递100接口工具类
 *
 * 主要功能:
 * 1. 根据快递公司编码、快递单号、手机号查询物流信息
 * 2. 生成快递100接口签名
 * 3. 调用快递100实时查询接口
 * 4. 使用缓存控制同一单号的查询频率,避免锁单
 */
@Slf4j
@Component
public class Kuaidi100Util {

    /**
     * 快递100接口地址
     */
    @Value("${express.kuaidi100.url:xxx}")
    private String url;

    /**
     * 快递100分配的授权 key
     */
    @Value("${express.kuaidi100.key:xxx}")
    private String key;

    /**
     * 快递100分配的客户编号 customer
     */
    @Value("${express.kuaidi100.customer:xxx}")
    private String customer;

    /**
     * 获取快递物流信息
     *
     * 注意:
     * 快递100要求同一快递单号查询频率至少间隔半小时以上,
     * 否则可能会造成锁单。
     * 因此这里先从缓存中读取,如果缓存存在,则直接返回缓存数据,
     * 不再请求快递100接口。
     *
     * @param expressCompanyNumber 快递公司编码,例如:yuantong、shunfeng、zhongtong
     * @param expressOrderNo       快递单号
     * @param phone                收件人或寄件人手机号,部分快递公司必填
     * @return 快递100返回的物流信息 JSON 字符串
     */
    public String getExpressInfo(String expressCompanyNumber, String expressOrderNo, String phone) {

        // 1. 先根据快递单号从缓存中获取物流信息
        // 作用:避免同一个快递单号在半小时内重复请求快递100,防止锁单
        String expressInfo = Express100Cache.get(expressOrderNo);

        // 2. 如果缓存中已经存在物流信息,则直接返回缓存结果
        if (!StringUtils.isEmpty(expressInfo)) {
            return expressInfo;
        }

        // 3. 构造快递100接口外层请求参数
        JSONObject paramObject = new JSONObject();

        // customer 是快递100分配给客户的唯一编号
        paramObject.put("customer", customer);

        // 4. 构造快递100接口业务参数 param
        JSONObject bodyObject = new JSONObject();

        // 快递公司编码
        bodyObject.put("com", expressCompanyNumber);

        // 快递单号
        bodyObject.put("num", expressOrderNo);

        // 收件人或寄件人手机号
        bodyObject.put("phone", phone);

        try {
            // 5. 生成接口签名
            // 快递100签名规则:
            // sign = MD5(param + key + customer).toUpperCase()
            // 其中 param 为业务参数 JSON 字符串
            String sign = MD5Util.upperCaseMD5Encode(bodyObject.toJSONString() + key + customer);

            // 将签名放入外层请求参数
            paramObject.put("sign", sign);
        } catch (Exception e) {
            // 6. 签名生成异常时,记录日志并抛出业务异常
            log.error("快递100接口签名异常:{},异常信息:{}", paramObject.toJSONString(), e.getMessage());
            throw new BaseException("快递100接口签名异常!");
        }

        // 7. 将业务参数 param 放入请求参数
        paramObject.put("param", bodyObject);

        // 8. 设置请求头
        JSONObject headerObject = new JSONObject();

        // 快递100接口使用 application/x-www-form-urlencoded 方式提交
        headerObject.put("Content-type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType());

        // 9. 发送 HTTP POST 请求
        // 连接超时时间:3000ms
        // 读取超时时间:3000ms
        String response = RequestSendUtil.httpPost(
                url + "/poll/query.do",
                paramObject,
                headerObject,
                3000,
                3000
        );

        // 10. 将快递100返回结果解析为 JSON
        JSONObject responseJson = JSONObject.parseObject(response);

        // 11. 判断接口是否返回异常编码
        // 如果 returnCode 不为空,说明快递100接口返回了错误信息
        String returnCode = responseJson.getString("returnCode");

        if (!StringUtils.isEmpty(returnCode)) {
            throw new BaseException("快递信息异常,代码:" + returnCode);
        }

        // 12. 将查询结果写入缓存
        // 建议缓存时间设置为 30 分钟以上,例如 31 分钟或 35 分钟
        // 作用:满足快递100"同一单号至少半小时查询一次"的限制
        Express100Cache.set(expressOrderNo, response);

        // 13. 返回快递100接口响应结果
        return response;
    }
}

二、缓存的核心作用

这里的缓存不是为了简单提升性能,而是为了满足快递100的接口限制:

同一快递单号查询频率至少间隔半小时以上,否则可能造成锁单。

所以代码中先执行:

java 复制代码
String expressInfo = Express100Cache.get(expressOrderNo);

如果缓存中有数据:

java 复制代码
if (!StringUtils.isEmpty(expressInfo)) {
    return expressInfo;
}

就直接返回,不再调用快递100。

只有缓存不存在时,才真正请求接口:

java 复制代码
String response = RequestSendUtil.httpPost(...);

请求成功后再写入缓存:

java 复制代码
Express100Cache.set(expressOrderNo, response);

三、调用流程总结

text 复制代码
开始查询物流
    ↓
根据快递单号查询缓存
    ↓
缓存存在?
    ↓ 是
直接返回缓存结果
    ↓ 否
组装请求参数
    ↓
生成 sign 签名
    ↓
调用快递100接口
    ↓
解析响应结果
    ↓
判断是否异常
    ↓
写入缓存
    ↓
返回物流信息

四、技术要点总结

这个工具类主要解决了几个问题:

技术点 说明
配置化 url、key、customer 从配置文件读取
签名 按快递100规则生成 MD5 大写签名
HTTP请求 使用 POST 请求调用 /poll/query.do
异常处理 签名异常、接口异常统一抛出业务异常
缓存控制 防止同一单号半小时内重复查询
防锁单 满足快递100接口频率要求

五、总结

快递100 API 工具类的封装重点不只是"能调通接口",更重要的是:

通过缓存控制同一快递单号的查询频率,避免因频繁查询导致锁单。

最终实现效果:

  • 减少第三方接口调用次数
  • 提高物流查询响应速度
  • 降低接口异常影响
  • 避免同一单号短时间重复查询
  • 满足快递100半小时查询频率要求
相关推荐
码农阿豪2 小时前
群晖部署Moodist配内网穿透穿透,把白噪音服务搬到公网上
数据库·spring boot·后端
敲敲千反田2 小时前
redis常见问题
数据库·redis·缓存
人道领域2 小时前
【Redis实战篇】秒杀系统:一人一单高并发实战(synchronized锁实战与事务失效问题)
java·开发语言·数据库·redis·spring
大大杰哥2 小时前
Spring AI 开发笔记:ChatClient 的创建、配置与工具函数注册
人工智能·笔记·spring
one_love_zfl2 小时前
java面试-spring篇
java·spring·面试
MuzySuntree2 小时前
Ubuntu 下 Maven 构建 Spring Boot 项目报错 release version 17 not supported 解决方案
spring boot·ubuntu·maven
想不明白的过度思考者3 小时前
一个叫Swagger的工具,让写接口文档变成享受
java·spring boot·接口·swagger
回忆2012初秋3 小时前
.NET 实战:Redis 缓存穿透、击穿与雪崩的原理剖析与解决方案
redis·缓存·.net
贾斯汀玛尔斯3 小时前
每天学一个算法--缓存淘汰策略(LRU / LFU · 结构与复杂度)
算法·缓存