×

Java对接京东商品详情API:从授权调用到数据解析实战

admin admin 发表于2026-04-13 17:24:56 浏览17 评论0

抢沙发发表评论

一、京东API接入方案概览

京东提供两类商品数据获取途径:
表格
方案适用场景特点门槛
京东宙斯开放平台(JOS)京东商家、ISV服务商官方接口,数据全面,需入驻需企业资质,审核周期
第三方数据服务API开发者快速接入简化封装,即开即用付费,需注意合规性
京东联盟API推广者获取商品信息侧重佣金、推广链接需开通京东联盟账号
⚠️ 合规提示:非京东商家爬取商品数据需遵守《京东开放平台服务协议》,个人开发者建议使用京东联盟或正规第三方数据服务。

二、方案一:京东宙斯开放平台(JOS)官方接入

2.1 入驻与权限申请流程

plain
复制
1. 注册京东开放平台账号(open.jd.com)
      ↓
2. 企业实名认证(营业执照、对公账户)
      ↓
3. 创建应用,申请"商品API"权限(jd.item.get)
      ↓
4. 获取 AppKey / AppSecret
      ↓
5. 沙箱测试 → 上线审核

2.2 核心API说明

jd.item.get - 获取单个商品详情
表格
参数类型必填说明
skuIdLong京东商品SKU编号
fieldsString指定返回字段,逗号分隔
常用返回字段:
  • skuId, name(商品名称)
  • price, marketPrice(售价/市场价)
  • imageUrl(主图)
  • stock(库存状态)
  • category(类目信息)
  • detailUrl(PC详情页)
  • mobileDetailUrl(无线详情页)

2.3 Java SDK集成实战

Maven依赖引入:
xml
复制
<!-- 京东宙斯SDK --><dependency>
    <groupId>com.jd</groupId>
    <artifactId>jos-sdk</artifactId>
    <version>2.0.20231215</version></dependency><!-- JSON处理 --><dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.43</version></dependency><!-- HTTP客户端 --><dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-http</artifactId>
    <version>5.8.22</version></dependency>
核心实现类:
java
复制
import com.jd.open.api.sdk.DefaultJdClient;import com.jd.open.api.sdk.JdClient;import com.jd.open.api.sdk.JdException;import com.jd.open.api.sdk.request.item.ItemGetRequest;import com.jd.open.api.sdk.response.item.ItemGetResponse;import com.alibaba.fastjson2.JSON;import com.alibaba.fastjson2.JSONObject;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;@Slf4j@Servicepublic class JdItemService {

    @Value("${jd.app.key}")
    private String appKey;
    
    @Value("${jd.app.secret}")
    private String appSecret;
    
    @Value("${jd.server.url:https://api.jd.com/routerjson}")
    private String serverUrl;
    
    @Value("${jd.access.token}")
    private String accessToken;  // 需通过OAuth2授权获取
    
    private JdClient jdClient;

    @PostConstruct
    public void init() {
        // 初始化京东客户端
        this.jdClient = new DefaultJdClient(serverUrl, accessToken, appKey, appSecret);
        log.info("京东JOS客户端初始化完成");
    }

    /**
     * 获取商品详情(官方API)
     */
    public JdItemDTO getItemDetail(Long skuId) {
        try {
            ItemGetRequest request = new ItemGetRequest();
            request.setSkuId(skuId);
            // 指定返回字段,减少数据传输
            request.setFields("skuId,name,price,marketPrice,imageUrl," +
                            "stock,category,detailUrl,mobileDetailUrl," +
                            "brandName,upc,weight");
            
            ItemGetResponse response = jdClient.execute(request);
            
            if (!"0".equals(response.getCode())) {
                log.error("京东API调用失败: code={}, msg={}", 
                         response.getCode(), response.getMsg());
                throw new BizException("获取商品信息失败: " + response.getMsg());
            }
            
            return convertToDTO(response.getItem());
            
        } catch (JdException e) {
            log.error("京东SDK异常: skuId={}", skuId, e);
            throw new BizException("京东服务异常", e);
        }
    }
    
    /**
     * 批量获取商品(使用批量API)
     */
    public List<JdItemDTO> batchGetItems(List<Long> skuIds) {
        // 京东限制单次最多20个SKU
        List<JdItemDTO> result = new ArrayList<>();
        List<List<Long>> partitions = Lists.partition(skuIds, 20);
        
        for (List<Long> batch : partitions) {
            // 使用 jd.item.sku.get 批量接口
            // 注意:批量接口需单独申请权限
            result.addAll(batchGetByIds(batch));
        }
        return result;
    }
    
    private JdItemDTO convertToDTO(ItemGetResponse.Item item) {
        return JdItemDTO.builder()
                .skuId(item.getSkuId())
                .name(item.getName())
                .price(new BigDecimal(item.getPrice()))
                .marketPrice(new BigDecimal(item.getMarketPrice()))
                .mainImage(item.getImageUrl())
                .stockStatus("1".equals(item.getStock()) ? "有货" : "无货")
                .categoryId(item.getCategory())
                .detailUrl(item.getDetailUrl())
                .brandName(item.getBrandName())
                .weight(item.getWeight())
                .build();
    }}

2.4 签名算法详解(自研接入时用)

京东API使用MD5签名,流程如下:
java
复制
import cn.hutool.crypto.digest.MD5;import cn.hutool.http.HttpUtil;import java.util.Map;import java.util.TreeMap;public class JdSignUtil {

    /**
     * 生成京东API请求签名
     */
    public static String generateSign(Map<String, String> params, String appSecret) {
        // 1. 参数排序(TreeMap自动排序)
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        
        // 2. 拼接字符串: key1value1key2value2...
        StringBuilder paramStr = new StringBuilder();
        sortedParams.forEach((k, v) -> {
            if (v != null && !v.isEmpty()) {
                paramStr.append(k).append(v);
            }
        });
        
        // 3. 首尾拼接AppSecret
        String signStr = appSecret + paramStr.toString() + appSecret;
        
        // 4. MD5加密,转大写
        return MD5.create().digestHex(signStr).toUpperCase();
    }
    
    /**
     * 构建完整请求URL
     */
    public static String buildRequestUrl(String apiMethod, Map<String, String> params, 
                                        String appKey, String appSecret) {
        // 系统级参数
        params.put("app_key", appKey);
        params.put("method", apiMethod);
        params.put("timestamp", DateUtil.now());
        params.put("v", "2.0");
        params.put("sign_method", "md5");
        params.put("format", "json");
        
        // 生成签名
        String sign = generateSign(params, appSecret);
        params.put("sign", sign);
        
        // 构建URL
        return "https://api.jd.com/routerjson?" + HttpUtil.toParams(params);
    }}

三、方案二:第三方商品数据API接入

3.1 技术架构设计

对于快速验证或非商家场景,可对接聚合数据服务:
plain
复制
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   你的应用    │────▶│  代理服务层   │────▶│  第三方API   │
│  (SpringBoot)│◀────│ (缓存+限流)  │◀────│ (按次计费)   │
└─────────────┘     └─────────────┘     └─────────────┘
                           │
                    ┌──────┴──────┐
                    ▼             ▼
              ┌─────────┐   ┌─────────┐
              │  Redis  │   │  本地缓存 │
              │ (热点数据)│   │ (Guava) │
              └─────────┘   └─────────┘

3.2 完整Java实现

配置类:
yaml
复制
# application.ymljd:
  api:
    provider: third-party  # 或 official
    third-party:
      base-url: https://api.example-data.com/jd      api-key: ${JD_API_KEY:your_api_key_here}
      secret: ${JD_API_SECRET:your_secret_here}
      timeout: 10000
      retry-times: 3
DTO设计:
java
复制
@Data@Builderpublic class JdItemDetailDTO {
    // 基础信息
    private Long skuId;
    private String name;
    private String subTitle;
    
    // 价格信息
    private BigDecimal jdPrice;      // 京东价
    private BigDecimal marketPrice;  // 市场价
    private BigDecimal commission;   // 佣金(联盟)
    
    // 媒体信息
    private String mainImage;
    private List<String> imageList;
    private String detailHtml;       // 详情页HTML
    
    // 库存物流
    private Boolean hasStock;
    private String delivery;
    
    // 类目规格
    private Long categoryId;
    private String categoryName;
    private List<SkuSpec> specs;
    
    // 店铺信息
    private Long shopId;
    private String shopName;
    
    @Data
    public static class SkuSpec {
        private String specName;
        private String specValue;
    }}
服务实现(含缓存与降级):
java
复制
import cn.hutool.http.HttpRequest;import cn.hutool.http.HttpResponse;import com.github.benmanes.caffeine.cache.Caffeine;import com.github.benmanes.caffeine.cache.LoadingCache;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;import javax.annotation.PostConstruct;import java.math.BigDecimal;import java.util.concurrent.TimeUnit;@Slf4j@Servicepublic class ThirdPartyJdService {

    @Autowired
    private JdApiProperties properties;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 本地缓存(Caffeine)- 防热点穿透
    private LoadingCache<Long, JdItemDetailDTO> localCache;

    @PostConstruct
    public void init() {
        localCache = Caffeine.newBuilder()
                .maximumSize(10_000)              // 最大条目
                .expireAfterWrite(5, TimeUnit.MINUTES)  // 写入后5分钟过期
                .recordStats()                       // 开启统计
                .build(this::fetchFromApi);          // 未命中时调用API
    }

    /**
     * 获取商品详情(多级缓存)
     */
    public JdItemDetailDTO getItemDetail(Long skuId) {
        // 1. 本地缓存
        JdItemDetailDTO cached = localCache.getIfPresent(skuId);
        if (cached != null) {
            log.debug("本地缓存命中: skuId={}", skuId);
            return cached;
        }
        
        // 2. Redis缓存
        String redisKey = "jd:item:" + skuId;
        String json = redisTemplate.opsForValue().get(redisKey);
        if (StringUtils.hasText(json)) {
            JdItemDetailDTO dto = JSON.parseObject(json, JdItemDetailDTO.class);
            localCache.put(skuId, dto);  // 回填本地缓存
            return dto;
        }
        
        // 3. 调用API(带熔断降级)
        return fetchWithCircuitBreaker(skuId);
    }

    /**
     * API调用(带重试与签名)
     */
    private JdItemDetailDTO fetchFromApi(Long skuId) {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String sign = generateHmacSign(skuId, timestamp);
        
        String url = properties.getBaseUrl() + "/item/detail";
        
        // 重试机制
        int retry = 0;
        while (retry < properties.getRetryTimes()) {
            try (HttpResponse response = HttpRequest.get(url)
                    .header("X-Api-Key", properties.getApiKey())
                    .header("X-Timestamp", timestamp)
                    .header("X-Sign", sign)
                    .form("sku_id", skuId)
                    .timeout(properties.getTimeout())
                    .execute()) {
                
                if (response.getStatus() == 200) {
                    String body = response.body();
                    ApiResult<JdItemDetailDTO> result = JSON.parseObject(body, 
                        new TypeReference<ApiResult<JdItemDetailDTO>>() {});
                    
                    if (result.isSuccess()) {
                        JdItemDetailDTO dto = result.getData();
                        // 异步写入Redis
                        asyncSaveToRedis(skuId, dto);
                        return dto;
                    }
                    throw new BizException("API返回错误: " + result.getMsg());
                }
                
            } catch (Exception e) {
                log.warn("API调用失败,准备重试: skuId={}, retry={}", skuId, retry, e);
                retry++;
                if (retry >= properties.getRetryTimes()) {
                    throw new BizException("获取商品信息失败,请稍后重试");
                }
                try {
                    Thread.sleep(100 * retry);  // 指数退避
                } catch (InterruptedException ignored) {}
            }
        }
        return null;
    }

    /**
     * HMAC-SHA256签名
     */
    private String generateHmacSign(Long skuId, String timestamp) {
        String content = "sku_id=" + skuId + "&timestamp=" + timestamp;
        return HmacUtil.hmacSha256(content, properties.getSecret());
    }

    /**
     * 异步保存到Redis(延长过期时间)
     */
    private void asyncSaveToRedis(Long skuId, JdItemDetailDTO dto) {
        CompletableFuture.runAsync(() -> {
            String key = "jd:item:" + skuId;
            // 价格信息过期时间短(5分钟),基础信息过期时间长(1小时)
            redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), 5, TimeUnit.MINUTES);
        });
    }

    /**
     * 熔断降级处理(使用Resilience4j)
     */
    @CircuitBreaker(name = "jdApi", fallbackMethod = "fallbackMethod")
    private JdItemDetailDTO fetchWithCircuitBreaker(Long skuId) {
        return localCache.get(skuId);
    }
    
    // 降级方法:返回基础信息或从数据库获取
    private JdItemDetailDTO fallbackMethod(Long skuId, Throwable ex) {
        log.error("京东API熔断降级: skuId={}", skuId, ex);
        // 返回简化版数据或抛出友好提示
        return JdItemDetailDTO.builder()
                .skuId(skuId)
                .name("商品信息暂不可用")
                .build();
    }}

四、高级场景:详情页HTML解析

京东详情页为富文本HTML,需特殊处理:
java
复制
import org.jsoup.Jsoup;import org.jsoup.nodes.Document;import org.jsoup.nodes.Element;import org.jsoup.select.Elements;@Servicepublic class JdDetailParser {

    /**
     * 解析京东详情页HTML,提取图片与文本
     */
    public ItemDescription parseDetailHtml(String html) {
        Document doc = Jsoup.parse(html);
        ItemDescription desc = new ItemDescription();
        
        // 提取主图列表
        Elements mainImages = doc.select("#spec-list img");
        List<String> imageUrls = mainImages.stream()
                .map(img -> img.attr("data-url"))
                .map(this::completeImageUrl)
                .collect(Collectors.toList());
        desc.setImageList(imageUrls);
        
        // 提取详情内容(过滤广告与无效标签)
        Elements detailContent = doc.select(".detail-content img, .detail-content p");
        List<ContentNode> nodes = new ArrayList<>();
        
        for (Element el : detailContent) {
            if ("img".equals(el.tagName())) {
                String src = el.attr("data-lazyload") // 懒加载图片
                              .defaultIfEmpty(el.attr("src"));
                if (!src.isEmpty()) {
                    nodes.add(ContentNode.image(completeImageUrl(src)));
                }
            } else if ("p".equals(el.tagName())) {
                String text = el.text().trim();
                if (!text.isEmpty()) {
                    nodes.add(ContentNode.text(text));
                }
            }
        }
        desc.setContentNodes(nodes);
        
        return desc;
    }
    
    /**
     * 补全图片URL(京东图片有多种尺寸:n0/n1/n5等)
     */
    private String completeImageUrl(String url) {
        if (url.startsWith("//")) {
            url = "https:" + url;
        }
        // 替换为高清图:n5是800*800,n1是350*350
        return url.replace("/n1/", "/n5/").replace("/n0/", "/n5/");
    }}

五、性能优化与监控

5.1 缓存策略矩阵

表格
数据类型本地缓存Redis缓存过期时间更新策略
商品基础信息Caffeine1小时被动过期
价格信息5分钟主动推送
库存状态Caffeine30秒实时查询
详情页HTML24小时版本号校验

5.2 监控指标

java
复制
@Componentpublic class JdApiMetrics {
    
    private final MeterRegistry registry;
    
    public void recordApiCall(String apiName, boolean success, long durationMs) {
        registry.counter("jd.api.calls", 
                "api", apiName,
                "status", success ? "success" : "failure")
                .increment();
        
        registry.timer("jd.api.duration", "api", apiName)
                .record(durationMs, TimeUnit.MILLISECONDS);
    }
    
    public void recordCacheHit(String cacheType) {
        registry.counter("jd.cache.hit", "type", cacheType).increment();
    }}

六、完整代码仓库结构

plain
复制
src/main/java/com/example/jdapi/
├── config/
│   ├── JdApiProperties.java      # 配置类
│   └── CacheConfig.java          # 缓存配置
├── controller/
│   └── ItemController.java       # REST接口
├── service/
│   ├── JdItemService.java        # 业务层
│   ├── ThirdPartyJdService.java  # 第三方实现
│   └── JdDetailParser.java       # HTML解析
├── client/
│   ├── JdSdkClient.java          # 官方SDK封装
│   └── HttpApiClient.java        # HTTP客户端
├── dto/
│   ├── JdItemDetailDTO.java      # 数据传输对象
│   └── ApiResult.java             # 通用响应
├── util/
│   ├── JdSignUtil.java           # 签名工具
│   └── HmacUtil.java             # HMAC加密
└── fallback/
    └── JdApiFallback.java        # 降级处理

七、合规与风控建议

  1. 频率控制:官方API通常限制 1000次/分钟,第三方API按套餐计费
  2. 数据存储:商品图片URL可缓存,但详情内容需定期更新
  3. 隐私保护:不存储用户京东账号相关信息
  4. 版权合规:商品图片使用需遵守京东素材使用协议


群贤毕至

访客