一、接口概述
微店开放平台提供的 micro.item_search 接口用于按关键词搜索店铺内的商品,支持分页、排序、筛选等功能,适用于商品采集、比价系统、供应链管理等场景。
接口基本信息
| 属性 | 说明 |
|---|---|
| 接口名称 | micro.item_search |
| 请求方式 | GET / POST |
| 数据格式 | JSON |
| 权限要求 | 需要申请商品搜索权限 |
| 频率限制 | 通常 100次/分钟(视账号等级) |
| 单次最大返回 | 50条/页 |
二、准备工作
1. 注册开发者账号
-
注册开发者账号并创建应用
-
获取 AppKey 和 AppSecret
-
申请
micro.item_search接口权限
2. Maven依赖配置
<dependencies>
<!-- HTTP请求 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- 分页工具 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
</dependencies>
三、核心代码实现
1. 配置类
package com.example.weidian.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "weidian")
public class WeidianConfig {
/** 应用Key */
private String appKey;
/** 应用密钥 */
private String appSecret;
/** API网关地址 */
private String gatewayUrl = "https://api.vdian.com/api";
/** 接口版本 */
private String version = "1.0";
/** 数据格式 */
private String format = "json";
/** 默认分页大小 */
private Integer defaultPageSize = 20;
/** 最大分页大小 */
private Integer maxPageSize = 50;
}
2. API请求工具类(复用+优化)
package com.example.weidian.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.springframework.stereotype.Component;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
@Slf4j
@Component
public class WeidianApiClient {
private final CloseableHttpClient httpClient;
public WeidianApiClient() {
this.httpClient = HttpClients.createDefault();
}
/**
* 发送GET请求
*/
public JSONObject doGet(String url, Map<String, String> params) throws Exception {
StringBuilder fullUrl = new StringBuilder(url);
if (!params.isEmpty()) {
fullUrl.append("?");
List<String> paramList = new ArrayList<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
paramList.add(entry.getKey() + "=" +
URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
}
fullUrl.append(String.join("&", paramList));
}
HttpGet httpGet = new HttpGet(fullUrl.toString());
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
log.debug("API响应: {}", result);
return JSON.parseObject(result);
}
}
/**
* 发送POST请求
*/
public JSONObject doPost(String url, Map<String, String> params) throws Exception {
HttpPost httpPost = new HttpPost(url);
List<NameValuePair> formParams = new ArrayList<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
httpPost.setEntity(new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8));
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
log.debug("API响应: {}", result);
return JSON.parseObject(result);
}
}
/**
* 生成签名(MD5加密)
*/
public String generateSign(Map<String, String> params, String appSecret) {
Map<String, String> filteredParams = new HashMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()
&& !"sign".equals(entry.getKey())) {
filteredParams.put(entry.getKey(), entry.getValue());
}
}
List<String> keys = new ArrayList<>(filteredParams.keySet());
Collections.sort(keys);
StringBuilder signStr = new StringBuilder();
for (String key : keys) {
signStr.append(key).append(filteredParams.get(key));
}
signStr.append(appSecret);
return md5(signStr.toString());
}
private String md5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(str.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append("0");
}
sb.append(hex);
}
return sb.toString().toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5加密失败", e);
}
}
}
3. 搜索参数构建器
package com.example.weidian.model;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Builder
public class ItemSearchParam {
/** 搜索关键词(商品标题) */
private String keyword;
/** 店铺ID(必填,指定搜索哪个店铺) */
private String shopId;
/** 页码,从1开始 */
@Builder.Default
private Integer page = 1;
/** 每页数量,默认20,最大50 */
@Builder.Default
private Integer pageSize = 20;
/** 排序方式:default-默认 time-上新 time_desc-上新倒序 price-价格升序 price_desc-价格降序 sale-销量 */
@Builder.Default
private String orderBy = "default";
/** 最低价格 */
private BigDecimal minPrice;
/** 最高价格 */
private BigDecimal maxPrice;
/** 商品状态:0-下架 1-上架 空-全部 */
private Integer status;
/** 商品分类ID */
private String cateId;
/**
* 转换为API请求参数
*/
public JSONObject toBizParam() {
JSONObject param = new JSONObject();
if (keyword != null && !keyword.isEmpty()) {
param.put("keyword", keyword);
}
param.put("shop_id", shopId);
param.put("page", page);
param.put("page_size", Math.min(pageSize, 50));
param.put("order_by", orderBy);
if (minPrice != null) {
param.put("min_price", minPrice.toString());
}
if (maxPrice != null) {
param.put("max_price", maxPrice.toString());
}
if (status != null) {
param.put("status", status);
}
if (cateId != null) {
param.put("cate_id", cateId);
}
return param;
}
}
4. 搜索结果模型
package com.example.weidian.model;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class ItemSearchResult {
/** 总记录数 */
@JSONField(name = "total_count")
private Integer totalCount;
/** 当前页 */
private Integer page;
/** 每页大小 */
@JSONField(name = "page_size")
private Integer pageSize;
/** 总页数 */
@JSONField(name = "total_page")
private Integer totalPage;
/** 商品列表 */
private List<Item> items;
/** 是否有下一页 */
public boolean hasNext() {
return page < totalPage;
}
@Data
public static class Item {
/** 商品ID */
@JSONField(name = "itemid")
private String itemId;
/** 商品标题 */
@JSONField(name = "item_name")
private String title;
/** 当前售价 */
private BigDecimal price;
/** 原价 */
@JSONField(name = "original_price")
private BigDecimal originalPrice;
/** 库存数量 */
private Integer stock;
/** 销量 */
@JSONField(name = "sold_num")
private Integer soldNum;
/** 主图 */
@JSONField(name = "img")
private String mainImage;
/** 商品状态 0-下架 1-上架 */
private Integer status;
/** 店铺ID */
@JSONField(name = "shop_id")
private String shopId;
/** 店铺名称 */
@JSONField(name = "shop_name")
private String shopName;
/** 分类ID */
@JSONField(name = "cate_id")
private String cateId;
/** 分类名称 */
@JSONField(name = "cate_name")
private String cateName;
/** 上架时间 */
@JSONField(name = "add_time")
private String addTime;
}
}
5. 搜索服务类(核心)
package com.example.weidian.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.example.weidian.config.WeidianConfig;
import com.example.weidian.model.ItemSearchParam;
import com.example.weidian.model.ItemSearchResult;
import com.example.weidian.util.WeidianApiClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class WeidianSearchService {
private final WeidianConfig config;
private final WeidianApiClient apiClient;
/**
* 搜索商品(单页)
*/
public ItemSearchResult searchItems(ItemSearchParam param) {
try {
// 1. 构建公共参数
Map<String, String> params = buildCommonParams();
// 2. 构建业务参数
JSONObject bizParam = param.toBizParam();
params.put("param", bizParam.toJSONString());
// 3. 生成签名
String sign = apiClient.generateSign(params, config.getAppSecret());
params.put("sign", sign);
// 4. 发送请求
log.info("搜索商品, 参数: {}", bizParam);
JSONObject response = apiClient.doPost(config.getGatewayUrl(), params);
// 5. 解析响应
return parseSearchResponse(response);
} catch (Exception e) {
log.error("搜索商品失败, 参数: {}", param, e);
throw new RuntimeException("搜索商品失败: " + e.getMessage());
}
}
/**
* 搜索所有商品(自动分页获取)
*/
public List<ItemSearchResult.Item> searchAllItems(ItemSearchParam param) {
List<ItemSearchResult.Item> allItems = new ArrayList<>();
int page = 1;
while (true) {
param.setPage(page);
ItemSearchResult result = searchItems(param);
if (result.getItems() != null) {
allItems.addAll(result.getItems());
}
log.info("获取第{}页, 本页{}条, 累计{}条",
page, result.getItems().size(), allItems.size());
if (!result.hasNext() || result.getItems().isEmpty()) {
break;
}
page++;
// 防止请求过快,添加延时
if (result.hasNext()) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
return allItems;
}
/**
* 根据关键词快速搜索
*/
public ItemSearchResult quickSearch(String keyword, String shopId) {
ItemSearchParam param = ItemSearchParam.builder()
.keyword(keyword)
.shopId(shopId)
.page(1)
.pageSize(config.getDefaultPageSize())
.build();
return searchItems(param);
}
/**
* 按价格区间搜索
*/
public ItemSearchResult searchByPriceRange(String shopId,
java.math.BigDecimal minPrice,
java.math.BigDecimal maxPrice) {
ItemSearchParam param = ItemSearchParam.builder()
.shopId(shopId)
.minPrice(minPrice)
.maxPrice(maxPrice)
.page(1)
.pageSize(config.getDefaultPageSize())
.orderBy("price")
.build();
return searchItems(param);
}
/**
* 获取店铺所有上架商品
*/
public List<ItemSearchResult.Item> getAllOnSaleItems(String shopId) {
ItemSearchParam param = ItemSearchParam.builder()
.shopId(shopId)
.status(1) // 只查上架
.pageSize(config.getMaxPageSize())
.build();
return searchAllItems(param);
}
/**
* 构建公共参数
*/
private Map<String, String> buildCommonParams() {
Map<String, String> params = new HashMap<>();
params.put("appkey", config.getAppKey());
params.put("method", "micro.item_search");
params.put("version", config.getVersion());
params.put("format", config.getFormat());
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
return params;
}
/**
* 解析搜索响应
*/
private ItemSearchResult parseSearchResponse(JSONObject response) {
// 检查状态
Integer status = response.getInteger("status");
if (status == null || status != 0) {
String msg = response.getString("msg");
throw new RuntimeException("API错误[" + status + "]: " + msg);
}
JSONObject result = response.getJSONObject("result");
if (result == null) {
throw new RuntimeException("响应结果为空");
}
ItemSearchResult searchResult = new ItemSearchResult();
searchResult.setTotalCount(result.getInteger("total_count"));
searchResult.setPage(result.getInteger("page"));
searchResult.setPageSize(result.getInteger("page_size"));
searchResult.setTotalPage(result.getInteger("total_page"));
// 解析商品列表
JSONArray itemsArray = result.getJSONArray("items");
List<ItemSearchResult.Item> items = new ArrayList<>();
if (itemsArray != null) {
for (int i = 0; i < itemsArray.size(); i++) {
JSONObject itemJson = itemsArray.getJSONObject(i);
ItemSearchResult.Item item = JSON.parseObject(
itemJson.toJSONString(),
ItemSearchResult.Item.class
);
items.add(item);
}
}
searchResult.setItems(items);
log.info("搜索完成, 共{}条, 当前第{}页, 本页{}条",
searchResult.getTotalCount(),
searchResult.getPage(),
items.size());
return searchResult;
}
}
四、API响应示例
成功响应示例
{
"status": 0,
"msg": "success",
"result": {
"total_count": 156,
"page": 1,
"page_size": 20,
"total_page": 8,
"items": [
{
"itemid": "1234567890",
"item_name": "2024夏季新款纯棉T恤男宽松休闲",
"price": "59.00",
"original_price": "99.00",
"stock": 200,
"sold_num": 1580,
"img": "https://img.vdian.com/xxx1.jpg",
"status": 1,
"shop_id": "987654321",
"shop_name": "潮流男装店",
"cate_id": "1001",
"cate_name": "男装T恤",
"add_time": "2024-03-15 10:30:00"
},
{
"itemid": "1234567891",
"item_name": "纯棉短袖T恤女韩版宽松显瘦",
"price": "49.00",
"original_price": "89.00",
"stock": 150,
"sold_num": 2300,
"img": "https://img.vdian.com/xxx2.jpg",
"status": 1,
"shop_id": "987654321",
"shop_name": "潮流男装店",
"cate_id": "1002",
"cate_name": "女装T恤",
"add_time": "2024-03-10 14:20:00"
}
]
}
}
错误响应示例
{
"status": 1002,
"msg": "店铺不存在或无权限访问",
"result": null
}
五、使用示例
1. Controller层
package com.example.weidian.controller;
import com.example.weidian.model.ItemSearchParam;
import com.example.weidian.model.ItemSearchResult;
import com.example.weidian.service.WeidianSearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/api/search")
@RequiredArgsConstructor
public class SearchController {
private final WeidianSearchService searchService;
/**
* 关键词搜索
* GET /api/search?keyword=T恤&shopId=987654321&page=1&pageSize=20
*/
@GetMapping
public ItemSearchResult search(
@RequestParam(required = false) String keyword,
@RequestParam String shopId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize,
@RequestParam(defaultValue = "default") String orderBy) {
ItemSearchParam param = ItemSearchParam.builder()
.keyword(keyword)
.shopId(shopId)
.page(page)
.pageSize(pageSize)
.orderBy(orderBy)
.build();
return searchService.searchItems(param);
}
/**
* 价格区间搜索
* GET /api/search/price?shopId=xxx&min=10&max=100
*/
@GetMapping("/price")
public ItemSearchResult searchByPrice(
@RequestParam String shopId,
@RequestParam BigDecimal min,
@RequestParam BigDecimal max) {
return searchService.searchByPriceRange(shopId, min, max);
}
/**
* 获取全部商品(自动分页)
* GET /api/search/all?shopId=xxx
*/
@GetMapping("/all")
public List<ItemSearchResult.Item> searchAll(@RequestParam String shopId) {
ItemSearchParam param = ItemSearchParam.builder()
.shopId(shopId)
.pageSize(50)
.build();
return searchService.searchAllItems(param);
}
/**
* 高级搜索(支持所有参数)
* POST /api/search/advanced
*/
@PostMapping("/advanced")
public ItemSearchResult advancedSearch(@RequestBody ItemSearchParam param) {
return searchService.searchItems(param);
}
}
2. 高级搜索请求示例
POST /api/search/advanced
Content-Type: application/json
{
"keyword": "纯棉T恤",
"shopId": "987654321",
"page": 1,
"pageSize": 30,
"orderBy": "sale",
"minPrice": 30.00,
"maxPrice": 100.00,
"status": 1,
"cateId": "1001"
}
六、关键注意事项
| 注意点 | 说明 |
|---|---|
| shop_id必填 | 必须指定搜索哪个店铺,不支持全平台搜索 |
| 分页限制 | page_size最大50,超过会被截断 |
| 排序字段 | 注意不同排序对性能的影响,sale排序可能较慢 |
| 关键词匹配 | 仅匹配商品标题,不支持详情内容搜索 |
| 延时控制 | 批量获取时建议200ms间隔,避免触发限流 |
| 缓存建议 | 搜索结果建议缓存5-10分钟,减少API调用 |
七、进阶功能扩展
1. 搜索结果缓存(Redis)
@Service
@RequiredArgsConstructor
public class CachedSearchService {
private final WeidianSearchService searchService;
private final StringRedisTemplate redisTemplate;
private static final String CACHE_KEY_PREFIX = "weidian:search:";
private static final long CACHE_TTL = 5; // 5分钟
public ItemSearchResult searchWithCache(ItemSearchParam param) {
String cacheKey = generateCacheKey(param);
// 尝试从缓存获取
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
log.info("命中缓存: {}", cacheKey);
return JSON.parseObject(cached, ItemSearchResult.class);
}
// 调用API
ItemSearchResult result = searchService.searchItems(param);
// 写入缓存
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(result),
CACHE_TTL,
TimeUnit.MINUTES
);
return result;
}
private String generateCacheKey(ItemSearchParam param) {
return CACHE_KEY_PREFIX +
DigestUtils.md5DigestAsHex(JSON.toJSONString(param).getBytes());
}
}
2. 异步批量采集
@Service
@RequiredArgsConstructor
public class AsyncCollectionService {
private final WeidianSearchService searchService;
private final ExecutorService executor = Executors.newFixedThreadPool(5);
/**
* 并发采集多个店铺
*/
public Map<String, List<ItemSearchResult.Item>> collectMultiShops(List<String> shopIds) {
Map<String, Future<List<ItemSearchResult.Item>>> futures = new HashMap<>();
for (String shopId : shopIds) {
Future<List<ItemSearchResult.Item>> future = executor.submit(() ->
searchService.getAllOnSaleItems(shopId)
);
futures.put(shopId, future);
}
// 收集结果
Map<String, List<ItemSearchResult.Item>> results = new HashMap<>();
for (Map.Entry<String, Future<List<ItemSearchResult.Item>>> entry : futures.entrySet()) {
try {
results.put(entry.getKey(), entry.getValue().get(30, TimeUnit.SECONDS));
} catch (Exception e) {
log.error("采集失败, shopId: {}", entry.getKey(), e);
}
}
return results;
}
}
八、与item_get接口配合使用
@Service
@RequiredArgsConstructor
public class ItemFullService {
private final WeidianSearchService searchService;
private final WeidianItemService itemService; // 上一篇文章的详情服务
/**
* 搜索并获取完整商品详情
*/
public List<ItemDetail> searchAndGetDetails(String keyword, String shopId) {
// 1. 搜索获取商品ID列表
ItemSearchResult searchResult = searchService.quickSearch(keyword, shopId);
// 2. 并发获取详情
return searchResult.getItems().parallelStream()
.map(item -> {
try {
return itemService.getItemDetail(item.getItemId());
} catch (Exception e) {
log.error("获取详情失败: {}", item.getItemId());
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
456



被折叠的 条评论
为什么被折叠?



