1688 API接口测试实战指南

6 阅读10分钟

一、1688开放平台概述

1.1 平台特点

表格

特性说明
平台定位B2B批发采购平台
API体系阿里开放平台统一架构(TOP协议)
认证方式OAuth 2.0 + AppKey签名
数据范围商品、订单、物流、会员、店铺等
调用限制应用级别流量控制,需申请权限包

1.2 核心API分类

┌─────────────────────────────────────────────────────────┐
│                    1688 API体系                          │
├─────────────────────────────────────────────────────────┤
│  商品API        │  1688.product.get / 1688.product.search │
│  订单API        │  1688.trade.get / 1688.trade.list       │
│  物流API        │  1688.logistics.trace.get                │
│  会员API        │  1688.member.get / 1688.member.basicInfo │
│  店铺API        │  1688.shop.get / 1688.shop.category.get  │
│  分销API        │  1688.distribution.product.get           │
└─────────────────────────────────────────────────────────┘

二、环境准备与SDK集成

2.1 Maven依赖配置

<dependencies>
    <!-- 阿里开放平台SDK(与淘宝共用) -->
    <dependency>
        <groupId>com.taobao.api</groupId>
        <artifactId>taobao-sdk-java-auto</artifactId>
        <version>20230824</version>
    </dependency>
    
    <!-- 或手动集成OKHTTP -->
    <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.43</version>
    </dependency>
    
    <!-- 测试框架 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- 断言库 -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.24.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2 配置文件

# application-test.yml
alibaba:
  openplatform:
    app-key: ${ALIBABA_APP_KEY}
    app-secret: ${ALIBABA_APP_SECRET}
    server-url: https://gw.open.1688.com/openapi
    # 沙箱环境(测试用)
    sandbox-url: https://gw.api.tbsandbox.com/router/rest
    # 超时配置
    connect-timeout: 10000
    read-timeout: 30000
    # 重试配置
    max-retries: 3
    retry-interval: 1000

三、核心客户端实现

3.1 1688 API客户端封装

package com.alibaba1688.api;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 1688开放平台API客户端
 * 基于TOP协议实现
 */
public class Alibaba1688Client {
    
    private static final Logger log = LoggerFactory.getLogger(Alibaba1688Client.class);
    
    private static final String DEFAULT_SERVER_URL = "https://gw.open.1688.com/openapi";
    private static final String SIGN_METHOD_HMAC = "hmac-sha256";
    private static final String API_VERSION = "1";
    
    private final String appKey;
    private final String appSecret;
    private final String serverUrl;
    private final OkHttpClient httpClient;
    
    public Alibaba1688Client(String appKey, String appSecret) {
        this(appKey, appSecret, DEFAULT_SERVER_URL);
    }
    
    public Alibaba1688Client(String appKey, String appSecret, String serverUrl) {
        this.appKey = appKey;
        this.appSecret = appSecret;
        this.serverUrl = serverUrl;
        this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES))
            .addInterceptor(new LoggingInterceptor())
            .addInterceptor(new RetryInterceptor(3, 1000))
            .build();
    }
    
    /**
     * 执行API调用(通用方法)
     */
    public ApiResponse execute(String apiName, Map<String, String> params, String accessToken) 
            throws ApiException {
        
        // 1. 构建公共参数
        Map<String, String> allParams = new TreeMap<>();
        allParams.put("method", apiName);
        allParams.put("app_key", appKey);
        allParams.put("timestamp", LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        allParams.put("v", API_VERSION);
        allParams.put("sign_method", SIGN_METHOD_HMAC);
        allParams.put("format", "json");
        
        // 2. 业务参数
        if (params != null) {
            allParams.putAll(params);
        }
        
        // 3. 授权参数
        if (accessToken != null) {
            allParams.put("access_token", accessToken);
        }
        
        // 4. 生成签名
        String sign = generateHmacSign(allParams, appSecret);
        allParams.put("sign", sign);
        
        // 5. 构建请求
        FormBody.Builder formBuilder = new FormBody.Builder();
        allParams.forEach((k, v) -> {
            if (v != null) {
                formBuilder.add(k, v);
            }
        });
        
        Request request = new Request.Builder()
            .url(serverUrl + "/param2/1/" + apiName + "/" + appKey)
            .post(formBuilder.build())
            .header("User-Agent", "Alibaba1688-Java-SDK/1.0")
            .build();
        
        // 6. 执行请求
        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new ApiException("HTTP_ERROR", "HTTP " + response.code());
            }
            
            String body = response.body().string();
            log.debug("API响应: {}", body);
            
            JSONObject json = JSON.parseObject(body);
            
            // 7. 错误处理
            if (json.containsKey("error_response")) {
                JSONObject error = json.getJSONObject("error_response");
                throw new ApiException(
                    error.getString("code"),
                    error.getString("msg"),
                    error.getString("sub_msg")
                );
            }
            
            return new ApiResponse(json, apiName);
            
        } catch (IOException e) {
            throw new ApiException("NETWORK_ERROR", "请求失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * HMAC-SHA256签名生成
     */
    private String generateHmacSign(Map<String, String> params, String secret) {
        try {
            StringBuilder sb = new StringBuilder();
            
            for (Map.Entry<String, String> entry : params.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (value != null && !value.isEmpty()) {
                    sb.append(key).append(value);
                }
            }
            
            String stringToSign = sb.toString();
            log.debug("待签名字符串: {}", stringToSign);
            
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(
                (secret + "&").getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
            );
            mac.init(keySpec);
            
            byte[] bytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(bytes).toUpperCase();
            
        } catch (Exception e) {
            throw new RuntimeException("签名生成失败", e);
        }
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    /**
     * 获取商品详情(1688.product.get)
     */
    public ProductDetail getProduct(Long productId, String accessToken) throws ApiException {
        Map<String, String> params = new HashMap<>();
        params.put("productID", String.valueOf(productId));
        params.put("webSite", "1688");
        
        ApiResponse response = execute("1688.product.get", params, accessToken);
        return response.parse(ProductDetail.class, "productInfo");
    }
    
    /**
     * 搜索商品(1688.product.search)
     */
    public ProductSearchResult searchProducts(String keyword, int page, int pageSize, 
            String accessToken) throws ApiException {
        Map<String, String> params = new HashMap<>();
        params.put("q", keyword);
        params.put("page", String.valueOf(page));
        params.put("pageSize", String.valueOf(pageSize));
        params.put("webSite", "1688");
        
        ApiResponse response = execute("1688.product.search", params, accessToken);
        return response.parse(ProductSearchResult.class, "result");
    }
    
    /**
     * 获取订单详情(1688.trade.get)
     */
    public TradeDetail getTrade(Long orderId, String accessToken) throws ApiException {
        Map<String, String> params = new HashMap<>();
        params.put("orderId", String.valueOf(orderId));
        params.put("webSite", "1688");
        params.put("includeFields", "orderBaseInfo,orderEntryInfo");
        
        ApiResponse response = execute("1688.trade.get", params, accessToken);
        return response.parse(TradeDetail.class, "orderInfo");
    }
    
    public void close() {
        httpClient.dispatcher().executorService().shutdown();
        httpClient.connectionPool().evictAll();
    }
}

3.2 数据模型定义

package com.alibaba1688.api.model;

import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 商品详情
 */
@Data
public class ProductDetail {
    
    @JSONField(name = "productID")
    private Long productId;
    
    @JSONField(name = "productTitle")
    private String title;
    
    @JSONField(name = "productPrice")
    private PriceInfo price;
    
    @JSONField(name = "productImage")
    private ImageInfo image;
    
    @JSONField(name = "productSKU")
    private List<SkuInfo> skus;
    
    @JSONField(name = "saleInfo")
    private SaleInfo saleInfo;
    
    @JSONField(name = "deliveryInfo")
    private DeliveryInfo deliveryInfo;
    
    @Data
    public static class PriceInfo {
        @JSONField(name = "price")
        private BigDecimal price;
        
        @JSONField(name = "priceRange")
        private List<PriceRange> ranges;
    }
    
    @Data
    public static class PriceRange {
        @JSONField(name = "startQuantity")
        private Integer startQuantity;
        
        @JSONField(name = "price")
        private BigDecimal price;
    }
    
    @Data
    public static class SkuInfo {
        @JSONField(name = "skuId")
        private Long skuId;
        
        @JSONField(name = "specId")
        private String specId;
        
        @JSONField(name = "price")
        private BigDecimal price;
        
        @JSONField(name = "amountOnSale")
        private Integer stock;
        
        @JSONField(name = "attributes")
        private List<Attribute> attributes;
    }
    
    @Data
    public static class Attribute {
        @JSONField(name = "attributeName")
        private String name;
        
        @JSONField(name = "attributeValue")
        private String value;
    }
}

/**
 * 订单详情
 */
@Data
public class TradeDetail {
    
    @JSONField(name = "orderId")
    private Long orderId;
    
    @JSONField(name = "buyerID")
    private String buyerId;
    
    @JSONField(name = "sellerID")
    private String sellerId;
    
    @JSONField(name = "orderStatus")
    private String status;
    
    @JSONField(name = "totalAmount")
    private BigDecimal totalAmount;
    
    @JSONField(name = "createTime")
    private LocalDateTime createTime;
    
    @JSONField(name = "payTime")
    private LocalDateTime payTime;
    
    @JSONField(name = "orderEntryList")
    private List<OrderEntry> entries;
    
    @Data
    public static class OrderEntry {
        @JSONField(name = "productID")
        private Long productId;
        
        @JSONField(name = "skuID")
        private Long skuId;
        
        @JSONField(name = "quantity")
        private Integer quantity;
        
        @JSONField(name = "price")
        private BigDecimal price;
    }
}

四、完整测试套件

4.1 基础功能测试

package com.alibaba1688.api.test;

import com.alibaba1688.api.Alibaba1688Client;
import com.alibaba1688.api.ApiException;
import com.alibaba1688.api.model.ProductDetail;
import com.alibaba1688.api.model.TradeDetail;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.*;

/**
 * 1688 API基础功能测试
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class Alibaba1688ApiTest {
    
    private static final String APP_KEY = System.getenv("ALIBABA_APP_KEY");
    private static final String APP_SECRET = System.getenv("ALIBABA_APP_SECRET");
    private static final String ACCESS_TOKEN = System.getenv("ALIBABA_ACCESS_TOKEN");
    
    private Alibaba1688Client client;
    
    @BeforeAll
    void setUp() {
        Assumptions.assumeTrue(APP_KEY != null, "未配置ALIBABA_APP_KEY环境变量");
        client = new Alibaba1688Client(APP_KEY, APP_SECRET);
    }
    
    @AfterAll
    void tearDown() {
        if (client != null) client.close();
    }
    
    // ==================== 商品API测试 ====================
    
    @Test
    @DisplayName("测试获取商品详情 - 基础信息")
    void testGetProductBasicInfo() {
        Long testProductId = 123456789L;  // 替换为实际测试商品ID
        
        assertThatNoException().isThrownBy(() -> {
            ProductDetail product = client.getProduct(testProductId, ACCESS_TOKEN);
            
            assertThat(product).isNotNull();
            assertThat(product.getProductId()).isEqualTo(testProductId);
            assertThat(product.getTitle()).isNotEmpty();
            assertThat(product.getPrice()).isNotNull();
            
            System.out.println("商品标题: " + product.getTitle());
            System.out.println("商品价格: " + product.getPrice().getPrice());
        });
    }
    
    @Test
    @DisplayName("测试获取商品详情 - SKU信息完整性")
    void testGetProductSkuInfo() {
        Long skuProductId = 987654321L;  // 有SKU的商品
        
        ProductDetail product = client.getProduct(skuProductId, ACCESS_TOKEN);
        
        assertThat(product.getSkus()).isNotEmpty();
        
        for (ProductDetail.SkuInfo sku : product.getSkus()) {
            assertThat(sku.getSkuId()).isNotNull();
            assertThat(sku.getPrice()).isGreaterThan(BigDecimal.ZERO);
            assertThat(sku.getStock()).isGreaterThanOrEqualTo(0);
        }
    }
    
    @Test
    @DisplayName("测试获取商品详情 - 阶梯价格")
    void testGetProductPriceRange() {
        ProductDetail product = client.getProduct(111111111L, ACCESS_TOKEN);
        
        assertThat(product.getPrice().getRanges()).isNotEmpty();
        
        // 验证阶梯价格按起订量排序
        List<ProductDetail.PriceRange> ranges = product.getPrice().getRanges();
        for (int i = 1; i < ranges.size(); i++) {
            assertThat(ranges.get(i).getStartQuantity())
                .isGreaterThan(ranges.get(i-1).getStartQuantity());
        }
    }
    
    @ParameterizedTest
    @CsvSource({
        "手机配件, 1, 20",
        "工业设备, 1, 10",
        "服装批发, 2, 50"
    })
    @DisplayName("测试商品搜索 - 多关键词")
    void testSearchProducts(String keyword, int page, int pageSize) {
        var result = client.searchProducts(keyword, page, pageSize, ACCESS_TOKEN);
        
        assertThat(result.getTotal()).isGreaterThanOrEqualTo(0);
        assertThat(result.getProducts()).hasSizeLessThanOrEqualTo(pageSize);
        
        System.out.printf("关键词[%s]找到%d个商品%n", keyword, result.getTotal());
    }
    
    // ==================== 订单API测试 ====================
    
    @Test
    @DisplayName("测试获取订单详情")
    void testGetTradeDetail() {
        Assumptions.assumeTrue(ACCESS_TOKEN != null, "需要ACCESS_TOKEN");
        
        Long testOrderId = 555666777L;  // 测试订单ID
        
        TradeDetail trade = client.getTrade(testOrderId, ACCESS_TOKEN);
        
        assertThat(trade.getOrderId()).isEqualTo(testOrderId);
        assertThat(trade.getStatus()).isIn(
            "waitbuyerpay", "waitsellersend", "waitbuyerreceive", 
            "success", "cancel", "terminated"
        );
        assertThat(trade.getTotalAmount()).isGreaterThan(BigDecimal.ZERO);
    }
    
    // ==================== 异常场景测试 ====================
    
    @Test
    @DisplayName("测试无效商品ID - 应抛出异常")
    void testInvalidProductId() {
        Long invalidId = 999999999999L;
        
        ApiException exception = catchThrowableOfType(
            () -> client.getProduct(invalidId, ACCESS_TOKEN),
            ApiException.class
        );
        
        assertThat(exception.getErrorCode())
            .containsAnyOf("isv.product-not-exist", "PRODUCT_NOT_FOUND");
    }
    
    @Test
    @DisplayName("测试无权限访问 - 应抛出异常")
    void testNoPermission() {
        // 使用无效token
        String invalidToken = "invalid_token_123";
        
        assertThatThrownBy(() -> 
            client.getProduct(123456L, invalidToken)
        ).isInstanceOf(ApiException.class)
         .satisfies(e -> {
             ApiException ae = (ApiException) e;
             assertThat(ae.getErrorCode()).contains("TOKEN");
         });
    }
    
    @Test
    @DisplayName("测试参数缺失 - 应抛出异常")
    void testMissingRequiredParam() {
        // 不传入productID
        assertThatThrownBy(() -> 
            client.execute("1688.product.get", new HashMap<>(), ACCESS_TOKEN)
        ).isInstanceOf(ApiException.class);
    }
}

4.2 性能与并发测试

package com.alibaba1688.api.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 1688 API性能测试
 * 注意:严格遵守平台限流规则
 */
public class Alibaba1688PerformanceTest {
    
    private static final int WARM_UP_ITERATIONS = 10;
    private static final int TEST_ITERATIONS = 100;
    
    @Test
    @DisplayName("单接口基准测试 - P99延迟")
    void testSingleApiLatency() {
        Alibaba1688Client client = createClient();
        Long testId = 123456789L;
        
        // 预热
        for (int i = 0; i < WARM_UP_ITERATIONS; i++) {
            client.getProduct(testId, ACCESS_TOKEN);
        }
        
        // 正式测试
        long[] latencies = new long[TEST_ITERATIONS];
        
        for (int i = 0; i < TEST_ITERATIONS; i++) {
            long start = System.nanoTime();
            client.getProduct(testId, ACCESS_TOKEN);
            long end = System.nanoTime();
            latencies[i] = (end - start) / 1_000_000; // ms
        }
        
        // 统计分析
        java.util.Arrays.sort(latencies);
        long p50 = latencies[TEST_ITERATIONS / 2];
        long p95 = latencies[(int)(TEST_ITERATIONS * 0.95)];
        long p99 = latencies[(int)(TEST_ITERATIONS * 0.99)];
        
        double avg = java.util.Arrays.stream(latencies).average().orElse(0);
        
        System.out.println("=== 延迟统计 (ms) ===");
        System.out.printf("平均: %.2f%n", avg);
        System.out.printf("P50: %d%n", p50);
        System.out.printf("P95: %d%n", p95);
        System.out.printf("P99: %d%n", p99);
        
        // 1688 API通常要求P99 < 1000ms
        assertThat(p99).isLessThan(1000);
    }
    
    @Test
    @DisplayName("并发测试 - 限流行为验证")
    void testConcurrentRateLimiting() throws InterruptedException {
        Alibaba1688Client client = createClient();
        
        int threadCount = 5;
        int requestsPerThread = 20;
        Long testId = 123456789L;
        
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger rateLimitCount = new AtomicInteger(0);
        AtomicInteger errorCount = new AtomicInteger(0);
        AtomicLong totalLatency = new AtomicLong(0);
        
        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                try {
                    for (int j = 0; j < requestsPerThread; j++) {
                        long start = System.currentTimeMillis();
                        try {
                            client.getProduct(testId, ACCESS_TOKEN);
                            successCount.incrementAndGet();
                        } catch (ApiException e) {
                            if (e.getErrorCode().contains("LIMIT") || 
                                e.getMessage().contains("限流")) {
                                rateLimitCount.incrementAndGet();
                            } else {
                                errorCount.incrementAndGet();
                            }
                        } finally {
                            totalLatency.addAndGet(System.currentTimeMillis() - start);
                        }
                        
                        // 主动限速:避免触发平台封禁
                        Thread.sleep(100); 
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
        
        latch.await(2, TimeUnit.MINUTES);
        executor.shutdown();
        
        int total = threadCount * requestsPerThread;
        System.out.println("=== 并发测试结果 ===");
        System.out.printf("总请求: %d%n", total);
        System.out.printf("成功: %d (%.1f%%)%n", successCount.get(), 
            100.0 * successCount.get() / total);
        System.out.printf("限流: %d (%.1f%%)%n", rateLimitCount.get(),
            100.0 * rateLimitCount.get() / total);
        System.out.printf("错误: %d%n", errorCount.get());
        System.out.printf("平均延迟: %d ms%n", totalLatency.get() / total);
        
        // 断言:成功率应 > 90%
        assertThat(successCount.get()).isGreaterThan((int)(total * 0.9));
    }
    
    @Test
    @DisplayName("压力测试 - 阶梯加压")
    void testStepLoad() throws InterruptedException {
        // 模拟真实场景:逐步增加负载,观察系统表现
        int[] vusLevels = {1, 5, 10, 20};
        int durationPerLevel = 30; // 秒
        
        for (int vus : vusLevels) {
            System.out.printf("%n=== 加压至 %d 并发 ===%n", vus);
            
            ExecutorService executor = Executors.newFixedThreadPool(vus);
            CountDownLatch levelLatch = new CountDownLatch(vus);
            AtomicInteger levelRequests = new AtomicInteger(0);
            AtomicInteger levelErrors = new AtomicInteger(0);
            
            long levelStart = System.currentTimeMillis();
            
            for (int i = 0; i < vus; i++) {
                executor.submit(() -> {
                    while (System.currentTimeMillis() - levelStart < durationPerLevel * 1000) {
                        try {
                            client.getProduct(123456L, ACCESS_TOKEN);
                            levelRequests.incrementAndGet();
                            Thread.sleep(200); // 每个VU 5 RPS
                        } catch (Exception e) {
                            levelErrors.incrementAndGet();
                        }
                    }
                    levelLatch.countDown();
                });
            }
            
            levelLatch.await();
            executor.shutdown();
            
            long actualDuration = (System.currentTimeMillis() - levelStart) / 1000;
            double rps = (double) levelRequests.get() / actualDuration;
            
            System.out.printf("持续时间: %d秒, 总请求: %d, 错误: %d, RPS: %.1f%n",
                actualDuration, levelRequests.get(), levelErrors.get(), rps);
        }
    }
}

4.3 数据一致性测试

package com.alibaba1688.api.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 数据一致性测试
 * 验证并发场景下数据读写一致性
 */
public class Alibaba1688ConsistencyTest {
    
    @Test
    @DisplayName("测试库存数据一致性 - 并发读取")
    void testStockConsistency() throws InterruptedException {
        // 测试商品:高并发读取时库存数据应一致
        Long testProductId = 555666777L;
        int concurrentReaders = 10;
        int readsPerThread = 10;
        
        ExecutorService executor = Executors.newFixedThreadPool(concurrentReaders);
        List<ProductDetail> results = new CopyOnWriteArrayList<>();
        
        for (int i = 0; i < concurrentReaders; i++) {
            executor.submit(() -> {
                for (int j = 0; j < readsPerThread; j++) {
                    ProductDetail product = client.getProduct(testProductId, ACCESS_TOKEN);
                    results.add(product);
                    
                    // 验证关键字段非空
                    assertThat(product.getProductId()).isEqualTo(testProductId);
                    assertThat(product.getSkus()).isNotNull();
                }
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        // 一致性验证:所有SKU的ID应相同
        for (ProductDetail product : results) {
            for (ProductDetail.SkuInfo sku : product.getSkus()) {
                assertThat(sku.getSkuId()).isNotNull();
            }
        }
        
        System.out.println("总读取次数: " + results.size());
        System.out.println("数据一致性验证通过");
    }
    
    @Test
    @DisplayName("测试分页数据一致性")
    void testPaginationConsistency() {
        String keyword = "电子元器件";
        int totalPages = 3;
        
        // 连续读取多页,验证无重复、无遗漏
        List<Long> allProductIds = new ArrayList<>();
        
        for (int page = 1; page <= totalPages; page++) {
            var result = client.searchProducts(keyword, page, 20, ACCESS_TOKEN);
            
            for (ProductSummary summary : result.getProducts()) {
                // 验证无重复
                assertThat(allProductIds)
                    .withFailMessage("第%d页发现重复商品ID: %d", page, summary.getProductId())
                    .doesNotContain(summary.getProductId());
                
                allProductIds.add(summary.getProductId());
            }
        }
        
        System.out.printf("共读取%d页,获取%d个唯一商品%n", totalPages, allProductIds.size());
    }
}

五、Mock测试方案

package com.alibaba1688.api.test;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.*;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.*;

/**
 * 使用WireMock进行Mock测试
 * 适用于CI环境或无网络权限场景
 */
public class Alibaba1688MockTest {
    
    private static WireMockServer wireMockServer;
    private Alibaba1688Client client;
    
    @BeforeAll
    static void startServer() {
        wireMockServer = new WireMockServer(9999);
        wireMockServer.start();
        WireMock.configureFor("localhost", 9999);
    }
    
    @BeforeEach
    void setUp() {
        // 配置Mock响应
        stubFor(post(urlPathMatching("/openapi/param2/1/1688\.product\.get/.*"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                      "productInfo": {
                        "productID": 123456,
                        "productTitle": "Mock工业设备",
                        "productPrice": {
                          "price": 999.99,
                          "priceRange": [
                            {"startQuantity": 1, "price": 999.99},
                            {"startQuantity": 100, "price": 899.99}
                          ]
                        },
                        "productSKU": [
                          {
                            "skuId": 111,
                            "specId": "color:red",
                            "price": 999.99,
                            "amountOnSale": 1000
                          }
                        ]
                      }
                    }
                    """)));
        
        // 限流场景Mock
        stubFor(post(urlPathMatching("/openapi/param2/1/1688\.trade\.get/.*"))
            .willReturn(aResponse()
                .withStatus(200)
                .withBody("""
                    {
                      "error_response": {
                        "code": "LIMIT_REQUEST",
                        "msg": "请求过于频繁",
                        "sub_msg": "请稍后再试"
                      }
                    }
                    """)));
        
        client = new Alibaba1688Client("mock_key", "mock_secret", 
            "http://localhost:9999/openapi");
    }
    
    @Test
    @DisplayName("Mock测试 - 正常商品详情")
    void testMockProductDetail() {
        ProductDetail product = client.getProduct(123456L, "mock_token");
        
        assertThat(product.getTitle()).isEqualTo("Mock工业设备");
        assertThat(product.getPrice().getPrice()).isEqualByComparingTo(new BigDecimal("999.99"));
        assertThat(product.getSkus()).hasSize(1);
    }
    
    @Test
    @DisplayName("Mock测试 - 限流异常处理")
    void testMockRateLimit() {
        ApiException exception = catchThrowableOfType(
            () -> client.getTrade(123L, "mock_token"),
            ApiException.class
        );
        
        assertThat(exception.getErrorCode()).isEqualTo("LIMIT_REQUEST");
    }
    
    @AfterAll
    static void stopServer() {
        wireMockServer.stop();
    }
}

六、CI/CD集成

# .github/workflows/1688-api-test.yml
name: 1688 API Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 3 * * *'  # 每日凌晨3点运行

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'maven'
      
      - name: Run Unit Tests (Mock)
        run: mvn test -Dtest=Alibaba1688MockTest
      
      - name: Run Integration Tests
        env:
          ALIBABA_APP_KEY: ${{ secrets.ALIBABA_APP_KEY }}
          ALIBABA_APP_SECRET: ${{ secrets.ALIBABA_APP_SECRET }}
          ALIBABA_ACCESS_TOKEN: ${{ secrets.ALIBABA_ACCESS_TOKEN }}
        run: mvn test -Dtest=Alibaba1688ApiTest
      
      - name: Generate Test Report
        uses: dorny/test-reporter@v1
        if: success() || failure()
        with:
          name: 1688 API Test Report
          path: target/surefire-reports/*.xml
          reporter: java-junit

七、最佳实践总结

实践项建议原因
环境隔离使用沙箱环境进行测试避免影响生产数据
限流保护单App 5000次/分钟,单IP 1000次/分钟防止触发平台风控
Token管理使用短期Token,定期刷新降低安全风险
数据脱敏日志中隐藏敏感字段保护商业数据
重试策略指数退避,最多3次提高成功率,避免雪崩
监控告警监控错误率、延迟、限流次数及时发现问题