×

api开发 电商平台 数据挖掘

微服务架构下的电商数据采集:封装淘宝搜索 API 为独立数据服务

admin admin 发表于2025-12-11 09:38:04 浏览29 评论0

抢沙发发表评论

在电商平台的数字化运营中,数据采集是核心环节之一。淘宝作为国内头部电商平台,其搜索数据包含了商品价格、销量、评价等关键信息,是电商分析、竞品监控、智能推荐的重要数据源。在微服务架构体系下,将淘宝搜索 API 封装为独立的数据服务,不仅能实现数据采集能力的解耦复用,还能提升系统的可扩展性、容错性和维护性。本文将详细讲解如何基于微服务思想设计并实现这一数据服务,并提供完整的代码示例。

一、微服务设计思路

1. 核心需求分析

封装淘宝搜索 API 的独立数据服务需满足以下核心需求:

  • 提供统一的接口供上层服务调用,屏蔽淘宝 API 的底层细节;

  • 支持参数灵活配置(如搜索关键词、页码、排序方式等);

  • 实现请求限流、异常重试、数据缓存,保障服务稳定性;

  • 输出标准化的数据格式,便于后续处理和分析;

  • 具备独立部署、水平扩展的能力。

2. 技术选型

  • 开发框架:Spring Boot + Spring Cloud(微服务核心,快速构建独立服务);

  • HTTP 客户端:OkHttp(高效处理 HTTP 请求,适配淘宝 API 的 HTTPS 通信);

  • 数据缓存:Redis(缓存高频搜索关键词的结果,降低 API 调用频率);

  • 序列化:FastJSON2(处理淘宝 API 返回的 JSON 数据,转换为标准化 DTO);

  • 服务治理:Sentinel(实现接口限流、熔断,防止服务雪崩);

  • 构建工具:Maven(依赖管理与打包部署)。

3. 服务架构设计

该数据服务作为微服务集群中的 “数据采集层”,核心模块划分如下:

  • 接口层:对外暴露 RESTful API,接收上层服务的搜索请求;

  • 适配层:封装淘宝 API 的请求参数、签名规则、响应解析逻辑;

  • 缓存层:基于 Redis 实现结果缓存,设置合理的过期时间;

  • 容错层:实现请求重试、限流、熔断,保障服务稳定性;

  • 数据转换层:将淘宝 API 的原始响应转换为标准化的 DTO 对象。

二、淘宝 API 接入准备

1. 开发者资质与 API 申请

首先需注册淘宝开发者账号并获取 ApiKey、ApiSecret 等关键参数。

2. API 调用规则说明

淘宝搜索 API 的核心调用规则:

  • 请求方式:HTTP GET/POST,需按规则生成签名;

  • 核心参数:q(搜索关键词)、page_no(页码)、page_size(每页条数)、sort(排序方式)等;

  • 响应格式:JSON,包含商品 ID、标题、价格、销量、佣金等信息;

  • 调用限制:单应用日调用量、QPS 均有上限,需合理控制请求频率。

三、代码实现

1. 项目依赖配置(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.ecommerce</groupId>
    <artifactId>taobao-search-service</artifactId>
    <version>1.0.0</version>
    <name>taobao-search-service</name>

    <dependencies>
        <!-- Spring Boot核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- HTTP客户端 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.12.0</version>
        </dependency>

        <!-- JSON序列化 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.45</version>
        </dependency>

        <!-- 服务治理:限流熔断 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
            <version>1.8.7</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-webmvc-adapter</artifactId>
            <version>1.8.7</version>
        </dependency>

        <!-- 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.22</version>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 配置文件(application.yml)

server:
  port: 8081
  servlet:
    context-path: /taobao-search

spring:
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 2000ms
  # 日志配置
  logging:
    level:
      com.ecommerce.taobao: INFO
      org.springframework.web: WARN

# 淘宝API配置
taobao:
  api:
    url: https://eco.taobao.com/router/rest
    app-key: 你的AppKey
    app-secret: 你的AppSecret
    format: json
    v: 2.0
    sign-method: md5
    # 缓存过期时间(秒)
    cache-expire: 300
    # 最大重试次数
    max-retry: 3
    # 重试间隔(毫秒)
    retry-interval: 1000

# Sentinel限流配置
sentinel:
  rules:
    flow:
      # 限流阈值:QPS
      qps-threshold: 10

3. 核心代码实现

(1)DTO 对象:标准化响应格式

package com.ecommerce.taobao.dto;

import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;

/**
 * 淘宝搜索结果标准化DTO
 */
@Data
public class TaobaoSearchResponseDTO implements Serializable {
    /** 响应状态:success/fail */
    private String status;
    /** 错误信息 */
    private String message;
    /** 搜索关键词 */
    private String keyword;
    /** 页码 */
    private Integer pageNo;
    /** 每页条数 */
    private Integer pageSize;
    /** 总条数 */
    private Long totalCount;
    /** 商品列表 */
    private List<ItemDTO> items;

    /**
     * 商品DTO
     */
    @Data
    public static class ItemDTO implements Serializable {
        /** 商品ID */
        private String itemId;
        /** 商品标题 */
        private String title;
        /** 商品价格 */
        private BigDecimal price;
        /** 商品销量 */
        private Long salesCount;
        /** 商品图片URL */
        private String picUrl;
        /** 商品链接 */
        private String itemUrl;
        /** 店铺名称 */
        private String shopName;
    }
}

(2)配置类:API 参数与 Redis 配置

package com.ecommerce.taobao.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "taobao.api")
public class TaobaoApiConfig {
    private String url;
    private String appKey;
    private String appSecret;
    private String format;
    private String v;
    private String signMethod;
    private Integer cacheExpire;
    private Integer maxRetry;
    private Integer retryInterval;
}

(3)核心服务类:API 调用与数据处理

package com.ecommerce.taobao.service;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ecommerce.taobao.config.TaobaoApiConfig;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO.ItemDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class TaobaoSearchService {

    private final TaobaoApiConfig taobaoApiConfig;
    private final StringRedisTemplate redisTemplate;
    private final OkHttpClient okHttpClient = new OkHttpClient();

    /**
     * 淘宝搜索核心方法
     * @param keyword 搜索关键词
     * @param pageNo 页码
     * @param pageSize 每页条数
     * @return 标准化搜索结果
     */
    public TaobaoSearchResponseDTO search(String keyword, Integer pageNo, Integer pageSize) {
        TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
        response.setKeyword(keyword);
        response.setPageNo(pageNo);
        response.setPageSize(pageSize);

        // 1. 构建缓存Key
        String cacheKey = "taobao:search:" + SecureUtil.md5(keyword + "_" + pageNo + "_" + pageSize);
        // 2. 尝试从缓存获取数据
        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
        if (cacheValue != null) {
            log.info("从缓存获取淘宝搜索结果,关键词:{},页码:{}", keyword, pageNo);
            return JSON.parseObject(cacheValue, TaobaoSearchResponseDTO.class);
        }

        // 3. 缓存未命中,调用淘宝API
        try {
            String apiResponse = callTaobaoApi(keyword, pageNo, pageSize);
            // 4. 解析API响应
            TaobaoSearchResponseDTO result = parseApiResponse(apiResponse);
            result.setKeyword(keyword);
            result.setPageNo(pageNo);
            result.setPageSize(pageSize);
            result.setStatus("success");

            // 5. 将结果存入缓存
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result),
                    taobaoApiConfig.getCacheExpire(), TimeUnit.SECONDS);
            log.info("调用淘宝API并缓存结果,关键词:{},页码:{}", keyword, pageNo);
            return result;
        } catch (Exception e) {
            log.error("淘宝搜索API调用失败,关键词:{},页码:{}", keyword, pageNo, e);
            response.setStatus("fail");
            response.setMessage("数据采集失败:" + e.getMessage());
            return response;
        }
    }

    /**
     * 调用淘宝API
     */
    private String callTaobaoApi(String keyword, Integer pageNo, Integer pageSize) throws Exception {
        // 构建请求参数
        Map<String, String> params = new TreeMap<>();
        params.put("method", "taobao.tbk.item.search");
        params.put("app_key", taobaoApiConfig.getAppKey());
        params.put("format", taobaoApiConfig.getFormat());
        params.put("v", taobaoApiConfig.getV());
        params.put("sign_method", taobaoApiConfig.getSignMethod());
        params.put("timestamp", new Date().toString());
        params.put("q", keyword);
        params.put("page_no", pageNo.toString());
        params.put("page_size", pageSize.toString());

        // 生成签名
        String sign = generateSign(params);
        params.put("sign", sign);

        // 构建请求URL
        String url = taobaoApiConfig.getUrl() + "?" + HttpUtil.toParams(params);
        log.info("淘宝API请求URL:{}", url);

        // 发送请求(带重试机制)
        int retryCount = 0;
        while (retryCount < taobaoApiConfig.getMaxRetry()) {
            try (Response response = okHttpClient.newCall(new Request.Builder().url(url).get().build()).execute()) {
                if (response.isSuccessful() && response.body() != null) {
                    return response.body().string();
                }
            } catch (Exception e) {
                retryCount++;
                log.warn("淘宝API调用失败,重试次数:{},原因:{}", retryCount, e.getMessage());
                Thread.sleep(taobaoApiConfig.getRetryInterval());
            }
        }

        throw new RuntimeException("淘宝API调用重试" + taobaoApiConfig.getMaxRetry() + "次后仍失败");
    }

    /**
     * 生成淘宝API签名
     */
    private String generateSign(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        sb.append(taobaoApiConfig.getAppSecret());
        for (Map.Entry<String, String> entry : params.entrySet()) {
            sb.append(entry.getKey()).append(entry.getValue());
        }
        sb.append(taobaoApiConfig.getAppSecret());
        return SecureUtil.md5(sb.toString()).toUpperCase();
    }

    /**
     * 解析淘宝API响应,转换为标准化DTO
     */
    private TaobaoSearchResponseDTO parseApiResponse(String apiResponse) {
        TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
        JSONObject jsonObject = JSON.parseObject(apiResponse);
        JSONObject resultObject = jsonObject.getJSONObject("tbk_item_search_response")
                .getJSONObject("results")
                .getJSONObject("n_tbk_item");

        // 解析总条数
        Long totalCount = jsonObject.getJSONObject("tbk_item_search_response")
                .getJSONObject("results")
                .getLong("total_results");
        response.setTotalCount(totalCount);

        // 解析商品列表
        List<ItemDTO> itemList = new ArrayList<>();
        if (resultObject != null) {
            // 兼容单商品和多商品场景
            if (resultObject.containsKey("item_id")) {
                ItemDTO item = parseItem(resultObject);
                itemList.add(item);
            } else {
                List<JSONObject> items = resultObject.getList("n_tbk_item", JSONObject.class);
                if (!CollectionUtils.isEmpty(items)) {
                    items.forEach(itemJson -> itemList.add(parseItem(itemJson)));
                }
            }
        }
        response.setItems(itemList);
        return response;
    }

    /**
     * 解析单个商品数据
     */
    private ItemDTO parseItem(JSONObject itemJson) {
        ItemDTO item = new ItemDTO();
        item.setItemId(itemJson.getString("item_id"));
        item.setTitle(itemJson.getString("title"));
        item.setPrice(new BigDecimal(itemJson.getString("zk_final_price")));
        item.setSalesCount(itemJson.getLong("volume"));
        item.setPicUrl(itemJson.getString("pict_url"));
        item.setItemUrl(itemJson.getString("item_url"));
        item.setShopName(itemJson.getString("shop_title"));
        return item;
    }
}

(4)控制器:对外暴露 RESTful 接口

package com.ecommerce.taobao.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO;
import com.ecommerce.taobao.service.TaobaoSearchService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@Validated
@RequiredArgsConstructor
public class TaobaoSearchController {

    private final TaobaoSearchService taobaoSearchService;

    /**
     * 淘宝搜索接口
     * @param keyword 搜索关键词(必填)
     * @param pageNo 页码(默认1)
     * @param pageSize 每页条数(默认20)
     * @return 标准化搜索结果
     */
    @GetMapping("/search")
    @SentinelResource(value = "taobaoSearch", blockHandler = "searchBlockHandler")
    public ResponseEntity<TaobaoSearchResponseDTO> search(
            @RequestParam @NotBlank(message = "搜索关键词不能为空") String keyword,
            @RequestParam(defaultValue = "1") @Positive(message = "页码必须为正整数") Integer pageNo,
            @RequestParam(defaultValue = "20") @Positive(message = "每页条数必须为正整数") Integer pageSize) {
        TaobaoSearchResponseDTO result = taobaoSearchService.search(keyword, pageNo, pageSize);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * Sentinel限流降级处理
     */
    public ResponseEntity<TaobaoSearchResponseDTO> searchBlockHandler(
            String keyword, Integer pageNo, Integer pageSize, BlockException e) {
        log.warn("淘宝搜索接口触发限流,关键词:{},页码:{},原因:{}", keyword, pageNo, e.getRule());
        TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
        response.setStatus("fail");
        response.setMessage("请求过于频繁,请稍后重试");
        response.setKeyword(keyword);
        response.setPageNo(pageNo);
        response.setPageSize(pageSize);
        return new ResponseEntity<>(response, HttpStatus.TOO_MANY_REQUESTS);
    }
}

(5)启动类

package com.ecommerce.taobao;

import com.alibaba.csp.sentinel.init.InitExecutor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TaobaoSearchServiceApplication {

    public static void main(String[] args) {
        // 初始化Sentinel
        InitExecutor.doInit();
        SpringApplication.run(TaobaoSearchServiceApplication.class, args);
    }
}

4. Sentinel 限流规则配置

创建 Sentinel 配置类,实现接口 QPS 限流:

package com.ecommerce.taobao.config;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class SentinelConfig {

    @Value("${sentinel.rules.flow.qps-threshold:10}")
    private int qpsThreshold;

    @PostConstruct
    public void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        // 对应控制器中的@SentinelResource值
        rule.setResource("taobaoSearch");
        // 限流阈值类型:QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // QPS阈值
        rule.setCount(qpsThreshold);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

四、服务测试与部署

1. 本地测试

启动 Redis 服务,替换配置文件中的淘宝 ApiKey 和 ApiSecret,运行启动类,通过 Postman 或浏览器访问接口:

GET http://localhost:8081/taobao-search/search?keyword=手机&pageNo=1&pageSize=20

响应示例:

{
  "status": "success",
  "message": "",
  "keyword": "手机",
  "pageNo": 1,
  "pageSize": 20,
  "totalCount": 1000,
  "items": [
    {
      "itemId": "123456789",
      "title": "新款智能手机 全网通",
      "price": 1999.00,
      "salesCount": 10000,
      "picUrl": "https://img.alicdn.com/imgextra/i1/xxx.jpg",
      "itemUrl": "https://detail.tmall.com/item.htm?id=123456789",
      "shopName": "XX官方旗舰店"
    }
  ]
}

2. 微服务部署

该服务可通过 Docker 容器化部署,或接入 Spring Cloud 注册中心(如 Nacos)实现服务发现,配合 Gateway 网关实现统一入口,结合 Sentinel Dashboard 实现限流规则的动态配置。

五、扩展与优化

  1. 分布式缓存:若服务集群部署,可使用 Redis 集群或 Redisson 实现分布式锁,避免缓存击穿;

  2. 异步处理:对于大批量数据采集,可引入消息队列(如 RocketMQ)实现异步请求,提升吞吐量;

  3. 数据持久化:将采集的核心数据存入 MySQL/ClickHouse,支持离线分析;

  4. 监控告警:接入 Prometheus + Grafana 监控 API 调用成功率、响应时间,配置告警规则;

  5. 多平台适配:基于接口抽象,扩展京东、拼多多等平台的 API 封装,实现多平台数据采集统一接口。

六、总结

将淘宝搜索 API 封装为独立的微服务,不仅解决了传统单体架构中数据采集与业务逻辑耦合的问题,还通过缓存、限流、重试等机制保障了服务的高可用。该服务作为电商数据中台的基础组件,可灵活对接上层的数据分析、智能推荐、竞品监控等业务系统,为电商企业的数字化运营提供稳定、高效的数据支撑。在实际应用中,可根据业务需求进一步扩展功能,适配更多电商平台的 API,构建完整的电商数据采集体系。


少长咸集

群贤毕至

访客