将 onemall 老代码,统一到归档目录,后续不断迁移移除
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.mall.searchservice;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients(basePackages = {"cn.iocoder.mall.productservice.rpc"})
|
||||
public class SearchServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 解决 ES java.lang.IllegalStateException: availableProcessors is already
|
||||
System.setProperty("es.set.netty.runtime.available.processors", "false");
|
||||
SpringApplication.run(SearchServiceApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.mall.searchservice.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
|
||||
|
||||
@Configuration
|
||||
@EnableElasticsearchRepositories(basePackages = "cn.iocoder.mall.search.biz.dao")
|
||||
public class ElasticsearchConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.mall.searchservice.controller;
|
||||
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.common.framework.vo.PageResult;
|
||||
import cn.iocoder.mall.searchservice.manager.product.SearchProductManager;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductConditionReqDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductConditionRespDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductPageReqDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductRespDTO;
|
||||
import io.swagger.annotations.Api;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.common.framework.vo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* Title:
|
||||
* Description:
|
||||
*
|
||||
* @author zhuyang
|
||||
* @version 1.0 2021/10/9
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/search/product")
|
||||
@Api("商品搜索")
|
||||
public class SearchProductController {
|
||||
@Autowired
|
||||
private SearchProductManager searchProductManager;
|
||||
/**
|
||||
* 获得商品搜索分页
|
||||
*
|
||||
* @param pageReqDTO 分页请求 DTO
|
||||
* @return 商品搜索分页结果
|
||||
*/
|
||||
@PostMapping("/pageSearchProduct")
|
||||
CommonResult<PageResult<SearchProductRespDTO>> pageSearchProduct(@RequestBody SearchProductPageReqDTO pageReqDTO){
|
||||
return success(searchProductManager.pageSearchProduct(pageReqDTO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得商品搜索条件
|
||||
*
|
||||
* @param conditionReqDTO 搜索条件 DTO
|
||||
* @return 搜索条件
|
||||
*/
|
||||
@PostMapping("/getSearchProductCondition")
|
||||
CommonResult<SearchProductConditionRespDTO> getSearchProductCondition(@RequestBody SearchProductConditionReqDTO conditionReqDTO){
|
||||
return success(searchProductManager.getSearchProductCondition(conditionReqDTO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package cn.iocoder.mall.searchservice.convert;
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.iocoder.mall.searchservice.convert.product;
|
||||
|
||||
import cn.iocoder.common.framework.vo.PageResult;
|
||||
import cn.iocoder.mall.productservice.rpc.category.dto.ProductCategoryRespDTO;
|
||||
import cn.iocoder.mall.productservice.rpc.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.mall.searchservice.dal.es.dataobject.ESProductDO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductConditionRespDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductPageReqDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductRespDTO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductConditionBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductPageQueryBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductSaveBO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface SearchProductConvert {
|
||||
|
||||
SearchProductConvert INSTANCE = Mappers.getMapper(SearchProductConvert.class);
|
||||
|
||||
|
||||
@Mapping(source = "spu.id", target = "id")
|
||||
@Mapping(source = "spu.name", target = "name")
|
||||
@Mapping(source = "spu.sellPoint", target = "sellPoint")
|
||||
@Mapping(source = "spu.description", target = "description")
|
||||
@Mapping(source = "spu.cid", target = "cid")
|
||||
@Mapping(source = "category.name", target = "categoryName")
|
||||
@Mapping(source = "spu.picUrls", target = "picUrls")
|
||||
@Mapping(source = "spu.visible", target = "visible")
|
||||
@Mapping(source = "spu.sort", target = "sort")
|
||||
SearchProductSaveBO convert(ProductSpuRespDTO spu, ProductCategoryRespDTO category);
|
||||
|
||||
ESProductDO convert(SearchProductSaveBO bean);
|
||||
|
||||
List<SearchProductBO> convertList(List<ESProductDO> list);
|
||||
|
||||
default PageResult<SearchProductBO> convertPage(Page<ESProductDO> page) {
|
||||
return new PageResult<SearchProductBO>().setList(convertList(page.getContent()))
|
||||
.setTotal(page.getTotalElements());
|
||||
}
|
||||
|
||||
SearchProductPageQueryBO convert(SearchProductPageReqDTO bean);
|
||||
|
||||
PageResult<SearchProductRespDTO> convertPage(PageResult<SearchProductBO> page);
|
||||
|
||||
SearchProductConditionRespDTO convert(SearchProductConditionBO bean);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.iocoder.mall.searchservice.dal.es;
|
||||
|
||||
/**
|
||||
* ES 字段分析器的枚举类
|
||||
*
|
||||
* 关于 IK 分词,文章 https://blog.csdn.net/xsdxs/article/details/72853288 不错。
|
||||
* 目前项目使用的 ES 版本是 6.7.1 ,可以在 https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-7-1 下载。
|
||||
* 如果不知道怎么安装 ES ,可以看 https://blog.csdn.net/chengyuqiang/article/details/78837712 简单。
|
||||
*/
|
||||
public class FieldAnalyzer {
|
||||
|
||||
/**
|
||||
* IK 最大化分词
|
||||
*
|
||||
* 会将文本做最细粒度的拆分
|
||||
*/
|
||||
public static final String IK_MAX_WORD = "ik_max_word";
|
||||
|
||||
/**
|
||||
* IK 智能分词
|
||||
*
|
||||
* 会做最粗粒度的拆分
|
||||
*/
|
||||
public static final String IK_SMART = "ik_smart";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package cn.iocoder.mall.searchservice.dal.es.dataobject;
|
||||
|
||||
import cn.iocoder.mall.searchservice.dal.es.FieldAnalyzer;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品 ES DO
|
||||
*/
|
||||
@Document(indexName = "product", type = "product", shards = 1, replicas = 0)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ESProductDO {
|
||||
|
||||
@Id
|
||||
private Integer id;
|
||||
|
||||
// ========== 基本信息 =========
|
||||
/**
|
||||
* SPU 名字
|
||||
*/
|
||||
@Field(analyzer = FieldAnalyzer.IK_MAX_WORD, type = FieldType.Text)
|
||||
private String name;
|
||||
/**
|
||||
* 卖点
|
||||
*/
|
||||
@Field(analyzer = FieldAnalyzer.IK_MAX_WORD, type = FieldType.Text)
|
||||
private String sellPoint;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Field(analyzer = FieldAnalyzer.IK_MAX_WORD, type = FieldType.Text)
|
||||
private String description;
|
||||
/**
|
||||
* 分类编号
|
||||
*/
|
||||
private Integer cid;
|
||||
/**
|
||||
* 分类名
|
||||
*/
|
||||
@Field(analyzer = FieldAnalyzer.IK_MAX_WORD, type = FieldType.Text)
|
||||
private String categoryName;
|
||||
/**
|
||||
* 商品主图地数组
|
||||
*/
|
||||
private List<String> picUrls;
|
||||
|
||||
// ========== 其他信息 =========
|
||||
/**
|
||||
* 是否上架商品(是否可见)。
|
||||
*
|
||||
* true 为已上架
|
||||
* false 为已下架
|
||||
*/
|
||||
private Boolean visible;
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
// ========== Sku 相关字段 =========
|
||||
/**
|
||||
* 原价格,单位:分
|
||||
*/
|
||||
private Integer originalPrice;
|
||||
/**
|
||||
* 购买价格,单位:分。
|
||||
*/
|
||||
private Integer buyPrice;
|
||||
/**
|
||||
* 库存数量
|
||||
*/
|
||||
private Integer quantity;
|
||||
|
||||
// ========== 促销活动相关字段 =========
|
||||
// 目前只促销单体商品促销,目前仅限制折扣。
|
||||
/**
|
||||
* 促销活动编号
|
||||
*/
|
||||
private Integer promotionActivityId;
|
||||
/**
|
||||
* 促销活动标题
|
||||
*/
|
||||
private String promotionActivityTitle;
|
||||
/**
|
||||
* 促销活动类型
|
||||
*/
|
||||
private Integer promotionActivityType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.iocoder.mall.searchservice.dal.es.repository;
|
||||
|
||||
import cn.iocoder.common.framework.util.CollectionUtils;
|
||||
import cn.iocoder.common.framework.util.StringUtils;
|
||||
import cn.iocoder.common.framework.vo.SortingField;
|
||||
import cn.iocoder.mall.searchservice.dal.es.dataobject.ESProductDO;
|
||||
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
|
||||
import org.elasticsearch.search.sort.SortBuilders;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
|
||||
|
||||
@Repository
|
||||
public interface ESProductRepository extends ElasticsearchRepository<ESProductDO, Integer> {
|
||||
|
||||
@Deprecated
|
||||
ESProductDO findByName(String name);
|
||||
|
||||
default Page<ESProductDO> search(Integer cid, String keyword, Integer pageNo, Integer pageSize,
|
||||
List<SortingField> sortFields) {
|
||||
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
|
||||
.withPageable(PageRequest.of(pageNo - 1, pageSize));
|
||||
// 筛选条件 cid
|
||||
if (cid != null) {
|
||||
nativeSearchQueryBuilder.withFilter(QueryBuilders.termQuery("cid", cid));
|
||||
}
|
||||
// 筛选
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = { // TODO 芋艿,分值随便打的
|
||||
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("name", keyword),
|
||||
ScoreFunctionBuilders.weightFactorFunction(10)),
|
||||
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("sellPoint", keyword),
|
||||
ScoreFunctionBuilders.weightFactorFunction(2)),
|
||||
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("categoryName", keyword),
|
||||
ScoreFunctionBuilders.weightFactorFunction(3)),
|
||||
// new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("description", keyword),
|
||||
// ScoreFunctionBuilders.weightFactorFunction(2)), // TODO 芋艿,目前这么做,如果商品描述很长,在按照价格降序,会命中超级多的关键字。
|
||||
};
|
||||
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(functions)
|
||||
.scoreMode(FunctionScoreQuery.ScoreMode.SUM)
|
||||
.setMinScore(2F); // TODO 芋艿,需要考虑下 score
|
||||
nativeSearchQueryBuilder.withQuery(functionScoreQueryBuilder);
|
||||
} else {
|
||||
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
|
||||
}
|
||||
// 排序
|
||||
if (!CollectionUtils.isEmpty(sortFields)) {
|
||||
sortFields.forEach(sortField -> nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField.getField())
|
||||
.order(SortOrder.fromString(sortField.getOrder()))));
|
||||
} else if (StringUtils.hasText(keyword)) {
|
||||
nativeSearchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
|
||||
} else {
|
||||
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("sort").order(SortOrder.DESC));
|
||||
}
|
||||
// 执行查询
|
||||
return search(nativeSearchQueryBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.mall.searchservice.manager;
|
||||
@@ -0,0 +1,133 @@
|
||||
package cn.iocoder.mall.searchservice.manager.product;
|
||||
|
||||
import cn.iocoder.common.framework.util.CollectionUtils;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.common.framework.vo.PageResult;
|
||||
import cn.iocoder.mall.productservice.rpc.category.ProductCategoryFeign;
|
||||
import cn.iocoder.mall.productservice.rpc.category.dto.ProductCategoryRespDTO;
|
||||
import cn.iocoder.mall.productservice.rpc.sku.ProductSkuFeign;
|
||||
import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuListQueryReqDTO;
|
||||
import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuRespDTO;
|
||||
import cn.iocoder.mall.productservice.rpc.spu.ProductSpuFeign;
|
||||
import cn.iocoder.mall.productservice.rpc.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.mall.searchservice.convert.product.SearchProductConvert;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductConditionReqDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductConditionRespDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductPageReqDTO;
|
||||
import cn.iocoder.mall.searchservice.rpc.product.dto.SearchProductRespDTO;
|
||||
import cn.iocoder.mall.searchservice.service.product.SearchProductService;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductConditionBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductSaveBO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SearchProductManager {
|
||||
|
||||
private static final Integer REBUILD_FETCH_PER_SIZE = 100;
|
||||
|
||||
|
||||
@Autowired
|
||||
private ProductSkuFeign productSkuFeign;
|
||||
@Autowired
|
||||
private ProductCategoryFeign productCategoryFeign;
|
||||
@Autowired
|
||||
private ProductSpuFeign productSpuFeign;
|
||||
|
||||
// @DubboReference( version = "${dubbo.consumer.CartService.version}")
|
||||
// private CartService cartService;
|
||||
|
||||
@Autowired
|
||||
private SearchProductService searchProductService;
|
||||
|
||||
public PageResult<SearchProductRespDTO> pageSearchProduct(SearchProductPageReqDTO pageReqDTO) {
|
||||
PageResult<SearchProductBO> pageResult = searchProductService.pageSearchProduct(SearchProductConvert.INSTANCE.convert(pageReqDTO));
|
||||
return SearchProductConvert.INSTANCE.convertPage(pageResult);
|
||||
}
|
||||
|
||||
public SearchProductConditionRespDTO getSearchProductCondition(SearchProductConditionReqDTO conditionReqDTO) {
|
||||
SearchProductConditionBO conditionBO =
|
||||
searchProductService.getSearchProductCondition(conditionReqDTO.getKeyword(), conditionReqDTO.getFields());
|
||||
return SearchProductConvert.INSTANCE.convert(conditionBO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建所有商品的 ES 索引
|
||||
*
|
||||
* @return 重建数量
|
||||
*/
|
||||
public Integer rebuild() {
|
||||
// TODO 芋艿,因为目前商品比较少,所以写的很粗暴。等未来重构
|
||||
Integer lastId = null;
|
||||
int rebuildCounts = 0;
|
||||
while (true) {
|
||||
// 从商品服务,增量获取商品列表编号
|
||||
CommonResult<List<Integer>> listProductSpuIdsResult = productSpuFeign.listProductSpuIds(lastId, REBUILD_FETCH_PER_SIZE);
|
||||
listProductSpuIdsResult.checkError();
|
||||
List<Integer> spuIds = listProductSpuIdsResult.getData();
|
||||
// 逐个重建索引到 ES 中
|
||||
spuIds.forEach(this::saveProduct);
|
||||
// 设置新的 lastId ,或者结束
|
||||
rebuildCounts += listProductSpuIdsResult.getData().size();
|
||||
if (spuIds.size() < REBUILD_FETCH_PER_SIZE) {
|
||||
break;
|
||||
} else {
|
||||
lastId = spuIds.get(spuIds.size() - 1);
|
||||
}
|
||||
}
|
||||
// 返回成功
|
||||
return rebuildCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建指定商品的 ES 索引
|
||||
*
|
||||
* @param id 商品 SPU 编号
|
||||
* @return 是否重建成功
|
||||
*/
|
||||
public Boolean saveProduct(Integer id) {
|
||||
// 获得商品 SPU
|
||||
CommonResult<ProductSpuRespDTO> productSpuResult = productSpuFeign.getProductSpu(id);
|
||||
productSpuResult.checkError();
|
||||
if (productSpuResult.getData() == null) {
|
||||
log.error("[saveProduct][商品 SPU({}) 不存在]", id);
|
||||
return false;
|
||||
}
|
||||
// 获得商品 SKU
|
||||
CommonResult<List<ProductSkuRespDTO>> listProductSkusResult =
|
||||
productSkuFeign.listProductSkus(new ProductSkuListQueryReqDTO().setProductSpuId(id));
|
||||
listProductSkusResult.checkError();
|
||||
if (CollectionUtils.isEmpty(listProductSkusResult.getData())) {
|
||||
log.error("[saveProduct][商品 SPU({}) 的 SKU 不存在]", id);
|
||||
return false;
|
||||
}
|
||||
// 获得商品分类
|
||||
CommonResult<ProductCategoryRespDTO> getProductCategoryResult =
|
||||
productCategoryFeign.getProductCategory(productSpuResult.getData().getCid());
|
||||
getProductCategoryResult.checkError();
|
||||
if (getProductCategoryResult.getData() == null) {
|
||||
log.error("[saveProduct][商品 SPU({}) 的分类({}) 不存在]", id, productSpuResult.getData().getCid());
|
||||
return false;
|
||||
}
|
||||
// 保存商品到 ES 中
|
||||
SearchProductSaveBO searchProductCreateBO = SearchProductConvert.INSTANCE.convert(
|
||||
productSpuResult.getData(), getProductCategoryResult.getData());
|
||||
ProductSkuRespDTO productSku = listProductSkusResult.getData().stream()
|
||||
.min(Comparator.comparing(ProductSkuRespDTO::getPrice)).orElse(null);
|
||||
assert productSku != null;
|
||||
// // 价格计算 TODO 芋艿:需要补充,暂时使用这个逻辑
|
||||
// CalcSkuPriceBO calSkuPriceResult = cartService.calcSkuPrice(sku.getId());
|
||||
searchProductCreateBO.setOriginalPrice(productSku.getPrice());
|
||||
searchProductCreateBO.setBuyPrice(productSku.getPrice());
|
||||
searchProductCreateBO.setQuantity(productSku.getQuantity());
|
||||
searchProductService.saveSearchProduct(searchProductCreateBO);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.mall.searchservice.mq.consumer;
|
||||
|
||||
import cn.iocoder.mall.searchservice.manager.product.SearchProductManager;
|
||||
import cn.iocoder.mall.searchservice.mq.consumer.message.ProductUpdateMessage;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 商品更新 Topic 的消费者,重建对应的商品的 ES 索引
|
||||
*/
|
||||
@Service
|
||||
@RocketMQMessageListener(
|
||||
topic = ProductUpdateMessage.TOPIC,
|
||||
consumerGroup = "${spring.application.name}-consumer-group-" + ProductUpdateMessage.TOPIC
|
||||
)
|
||||
public class ProductUpdateConsumer implements RocketMQListener<ProductUpdateMessage> {
|
||||
|
||||
@Autowired
|
||||
private SearchProductManager productSearchManager;
|
||||
|
||||
@Override
|
||||
public void onMessage(ProductUpdateMessage message) {
|
||||
Boolean result = productSearchManager.saveProduct(message.getId());
|
||||
Assert.isTrue(result, String.format("重构商品(%d)的 ES 索引,必然成功。实际结果是 %s", message.getId(), result));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.mall.searchservice.mq.consumer.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 商品更新(包括创建)消息
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ProductUpdateMessage {
|
||||
|
||||
public static final String TOPIC = "ProductUpdate";
|
||||
|
||||
/**
|
||||
* 商品编号
|
||||
*/
|
||||
private Integer id;
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package cn.iocoder.mall.searchservice.rpc;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位,避免包折叠
|
||||
*/
|
||||
package cn.iocoder.mall.searchservice.service;
|
||||
@@ -0,0 +1,112 @@
|
||||
package cn.iocoder.mall.searchservice.service.product;
|
||||
|
||||
import cn.iocoder.common.framework.util.CollectionUtils;
|
||||
import cn.iocoder.common.framework.util.StringUtils;
|
||||
import cn.iocoder.common.framework.vo.PageResult;
|
||||
import cn.iocoder.common.framework.vo.SortingField;
|
||||
import cn.iocoder.mall.searchservice.convert.product.SearchProductConvert;
|
||||
import cn.iocoder.mall.searchservice.dal.es.dataobject.ESProductDO;
|
||||
import cn.iocoder.mall.searchservice.dal.es.repository.ESProductRepository;
|
||||
import cn.iocoder.mall.searchservice.enums.product.SearchProductConditionFieldEnum;
|
||||
import cn.iocoder.mall.searchservice.enums.product.SearchProductPageQuerySortFieldEnum;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductConditionBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductPageQueryBO;
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductSaveBO;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.search.aggregations.Aggregation;
|
||||
import org.elasticsearch.search.aggregations.AggregationBuilders;
|
||||
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
|
||||
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SearchProductService {
|
||||
|
||||
@Autowired
|
||||
private ESProductRepository productRepository;
|
||||
@Autowired
|
||||
private ElasticsearchTemplate elasticsearchTemplate; // 因为需要使用到聚合操作,只好引入 ElasticsearchTemplate 。
|
||||
|
||||
/**
|
||||
* 搜索商品分页结果
|
||||
*
|
||||
* @param pageQueryBO 分页查询条件
|
||||
* @return 商品信息
|
||||
*/
|
||||
public PageResult<SearchProductBO> pageSearchProduct(SearchProductPageQueryBO pageQueryBO) {
|
||||
checkSortFieldInvalid(pageQueryBO.getSorts());
|
||||
// 执行查询
|
||||
Page<ESProductDO> searchPage = productRepository.search(pageQueryBO.getCid(), pageQueryBO.getKeyword(),
|
||||
pageQueryBO.getPageNo(), pageQueryBO.getPageSize(), pageQueryBO.getSorts());
|
||||
// 转换结果
|
||||
return SearchProductConvert.INSTANCE.convertPage(searchPage);
|
||||
}
|
||||
|
||||
private void checkSortFieldInvalid(List<SortingField> sorts) {
|
||||
if (CollectionUtils.isEmpty(sorts)) {
|
||||
return;
|
||||
}
|
||||
sorts.forEach(sortingField -> Assert.isTrue(SearchProductPageQuerySortFieldEnum.contains(sortingField.getField()),
|
||||
String.format("排序字段(%s) 不在允许范围内", sortingField.getField())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存商品信息到 ES 中
|
||||
*
|
||||
* @param saveBO 商品信息
|
||||
*/
|
||||
public void saveSearchProduct(SearchProductSaveBO saveBO) {
|
||||
ESProductDO productDO = SearchProductConvert.INSTANCE.convert(saveBO);
|
||||
productRepository.save(productDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得指定关键字对应的搜索条件
|
||||
*
|
||||
* 在我们搜索商品时,需要获得关键字可选择的分类、品牌等等搜索条件,方便用户进一步检索
|
||||
*
|
||||
* @param keyword 关键字
|
||||
* @param fields 需要返回的搜索条件{@link SearchProductConditionFieldEnum}。目前可传入的参数为
|
||||
* 1. category :商品分类,会返回商品分类编号
|
||||
* @return 搜索条件
|
||||
*/
|
||||
public SearchProductConditionBO getSearchProductCondition(String keyword, Collection<String> fields) {
|
||||
// 创建 ES 搜索条件
|
||||
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
|
||||
// 筛选
|
||||
if (StringUtils.hasText(keyword)) { // 如果有 keyword ,就去匹配
|
||||
nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keyword,
|
||||
"name", "sellPoint", "categoryName"));
|
||||
} else {
|
||||
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
|
||||
}
|
||||
// 聚合
|
||||
if (fields.contains("category")) { // 商品分类
|
||||
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("cids").field("cid"));
|
||||
}
|
||||
// 执行查询,返回结果
|
||||
return elasticsearchTemplate.query(nativeSearchQueryBuilder.build(), response -> {
|
||||
SearchProductConditionBO result = new SearchProductConditionBO();
|
||||
// categoryIds 聚合
|
||||
Aggregation categoryIdsAggregation = response.getAggregations().get("cids");
|
||||
if (categoryIdsAggregation != null) {
|
||||
result.setCids(new ArrayList<>());
|
||||
for (LongTerms.Bucket bucket : (((LongTerms) categoryIdsAggregation).getBuckets())) {
|
||||
result.getCids().add(bucket.getKeyAsNumber().intValue());
|
||||
}
|
||||
}
|
||||
// 返回结果
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package cn.iocoder.mall.searchservice.service.product.bo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 搜索商品 BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class SearchProductBO {
|
||||
|
||||
private Integer id;
|
||||
|
||||
// ========== 基本信息 =========
|
||||
/**
|
||||
* SPU 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 卖点
|
||||
*/
|
||||
private String sellPoint;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 分类编号
|
||||
*/
|
||||
private Integer cid;
|
||||
/**
|
||||
* 分类名
|
||||
*/
|
||||
private String categoryName;
|
||||
/**
|
||||
* 商品主图地数组
|
||||
*/
|
||||
private List<String> picUrls;
|
||||
|
||||
// ========== 其他信息 =========
|
||||
/**
|
||||
* 是否上架商品(是否可见)。
|
||||
*
|
||||
* true 为已上架
|
||||
* false 为已下架
|
||||
*/
|
||||
private Boolean visible;
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
// ========== Sku 相关字段 =========
|
||||
/**
|
||||
* 原价格,单位:分
|
||||
*/
|
||||
private Integer originalPrice;
|
||||
/**
|
||||
* 购买价格,单位:分。
|
||||
*/
|
||||
private Integer buyPrice;
|
||||
/**
|
||||
* 库存数量
|
||||
*/
|
||||
private Integer quantity;
|
||||
|
||||
// ========== 促销活动相关字段 =========
|
||||
// 目前只促销单体商品促销,目前仅限制折扣。
|
||||
/**
|
||||
* 促销活动编号
|
||||
*/
|
||||
private Integer promotionActivityId;
|
||||
/**
|
||||
* 促销活动标题
|
||||
*/
|
||||
private String promotionActivityTitle;
|
||||
/**
|
||||
* 促销活动类型
|
||||
*/
|
||||
private Integer promotionActivityType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.mall.searchservice.service.product.bo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品搜索条件返回 BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class SearchProductConditionBO {
|
||||
|
||||
/**
|
||||
* 商品分类数组
|
||||
*/
|
||||
private List<Integer> cids;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.mall.searchservice.service.product.bo;
|
||||
|
||||
import cn.iocoder.common.framework.vo.PageParam;
|
||||
import cn.iocoder.common.framework.vo.SortingField;
|
||||
import cn.iocoder.mall.searchservice.enums.product.SearchProductPageQuerySortFieldEnum;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品检索分查询 BO
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class SearchProductPageQueryBO extends PageParam {
|
||||
|
||||
/**
|
||||
* 分类编号
|
||||
*/
|
||||
private Integer cid;
|
||||
/**
|
||||
* 关键字
|
||||
*/
|
||||
private String keyword;
|
||||
/**
|
||||
* 排序字段数组
|
||||
*
|
||||
* 可支持排序的字段,见 {@link SearchProductPageQuerySortFieldEnum}
|
||||
*/
|
||||
private List<SortingField> sorts;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package cn.iocoder.mall.searchservice.service.product.bo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 搜索商品保存 BO
|
||||
*/
|
||||
@Data
|
||||
@Accessors
|
||||
public class SearchProductSaveBO {
|
||||
|
||||
private Integer id;
|
||||
|
||||
// ========== 基本信息 =========
|
||||
/**
|
||||
* SPU 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 卖点
|
||||
*/
|
||||
private String sellPoint;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 分类编号
|
||||
*/
|
||||
private Integer cid;
|
||||
/**
|
||||
* 分类名
|
||||
*/
|
||||
private String categoryName;
|
||||
/**
|
||||
* 商品主图地数组
|
||||
*/
|
||||
private List<String> picUrls;
|
||||
|
||||
// ========== 其他信息 =========
|
||||
/**
|
||||
* 是否上架商品(是否可见)。
|
||||
*
|
||||
* true 为已上架
|
||||
* false 为已下架
|
||||
*/
|
||||
private Boolean visible;
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
// ========== Sku 相关字段 =========
|
||||
/**
|
||||
* 原价格,单位:分
|
||||
*/
|
||||
private Integer originalPrice;
|
||||
/**
|
||||
* 购买价格,单位:分。
|
||||
*/
|
||||
private Integer buyPrice;
|
||||
/**
|
||||
* 库存数量
|
||||
*/
|
||||
private Integer quantity;
|
||||
|
||||
// ========== 促销活动相关字段 =========
|
||||
// 目前只促销单体商品促销,目前仅限制折扣。
|
||||
/**
|
||||
* 促销活动编号
|
||||
*/
|
||||
private Integer promotionActivityId;
|
||||
/**
|
||||
* 促销活动标题
|
||||
*/
|
||||
private String promotionActivityTitle;
|
||||
/**
|
||||
* 促销活动类型
|
||||
*/
|
||||
private Integer promotionActivityType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
spring:
|
||||
# Spring Cloud 配置项
|
||||
cloud:
|
||||
nacos:
|
||||
# Spring Cloud Nacos Discovery 配置项
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务器地址
|
||||
namespace: dev # Nacos 命名空间
|
||||
|
||||
# Dubbo 配置项
|
||||
dubbo:
|
||||
# Dubbo 注册中心
|
||||
registry:
|
||||
# address: spring-cloud://localhost:8848 # 指定 Dubbo 服务注册中心的地址
|
||||
address: nacos://localhost:8848?namespace=dev # 指定 Dubbo 服务注册中心的地址
|
||||
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
# Spring Cloud 配置项
|
||||
cloud:
|
||||
nacos:
|
||||
# Spring Cloud Nacos Discovery 配置项
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务器地址
|
||||
namespace: dev # Nacos 命名空间
|
||||
|
||||
# Dubbo 配置项
|
||||
dubbo:
|
||||
# Dubbo 注册中心
|
||||
registry:
|
||||
# address: spring-cloud://localhost:8848 # 指定 Dubbo 服务注册中心的地址
|
||||
address: nacos://localhost:8848?namespace=dev # 指定 Dubbo 服务注册中心的地址
|
||||
# Dubbo 服务提供者的配置
|
||||
provider:
|
||||
tag: ${DUBBO_TAG} # Dubbo 路由分组
|
||||
# Dubbo 服务消费者的配置
|
||||
consumer:
|
||||
tag: ${DUBBO_TAG} # Dubbo 路由分组
|
||||
@@ -0,0 +1,62 @@
|
||||
spring:
|
||||
# Application 的配置项
|
||||
application:
|
||||
name: search-service
|
||||
# Profile 的配置项
|
||||
profiles:
|
||||
active: local
|
||||
# Elasticsearch 配置项
|
||||
data:
|
||||
elasticsearch:
|
||||
cluster-name: elasticsearch
|
||||
cluster-nodes: localhost:9300
|
||||
repositories:
|
||||
enable: true
|
||||
elasticsearch:
|
||||
rest:
|
||||
uris: localhost:9200
|
||||
|
||||
# Dubbo 配置项
|
||||
dubbo:
|
||||
# Spring Cloud Alibaba Dubbo 专属配置
|
||||
cloud:
|
||||
subscribed-services: '' # 设置订阅的应用列表,默认为 * 订阅所有应用
|
||||
# Dubbo 提供者的协议
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
# Dubbo 提供服务的扫描基础包
|
||||
scan:
|
||||
base-packages: cn.iocoder.mall.searchservice.rpc
|
||||
# Dubbo 服务提供者的配置
|
||||
provider:
|
||||
filter: -exception
|
||||
validation: true # 开启 Provider 参数校验
|
||||
version: 1.0.0 # 服务的版本号
|
||||
# Dubbo 服务消费者的配置
|
||||
consumer:
|
||||
ErrorCodeRpc:
|
||||
version: 1.0.0
|
||||
ProductCategoryRpc:
|
||||
version: 1.0.0
|
||||
ProductSpuRpc:
|
||||
version: 1.0.0
|
||||
ProductSkuRpc:
|
||||
version: 1.0.0
|
||||
|
||||
# RocketMQ 配置项
|
||||
rocketmq:
|
||||
name-server: localhost:9876
|
||||
|
||||
# Actuator 监控配置项
|
||||
management:
|
||||
server.port: 38083 # 独立端口,避免被暴露出去
|
||||
endpoints.web.exposure.include: '*' # 暴露所有监控端点
|
||||
server.port: ${management.server.port} # 设置使用 Actuator 的服务器端口,因为 RPC 服务不需要 Web 端口
|
||||
|
||||
# Mall 配置项
|
||||
mall:
|
||||
# 错误码配置项对应 ErrorCodeProperties 配置类
|
||||
error-code:
|
||||
group: ${spring.application.name}
|
||||
constants-class: cn.iocoder.mall.searchservice.enums.ProductErrorCodeConstants
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.mall.searchservice.manager;
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.mall.searchservice.manager.product;
|
||||
|
||||
import cn.iocoder.mall.searchservice.dal.es.repository.ESProductRepository;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* {@link SearchProductManager} 的测试类,目前是集成测试类
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public class SearchProductManagerTest {
|
||||
|
||||
static {
|
||||
System.setProperty("es.set.netty.runtime.available.processors", "false");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SearchProductManager searchProductManager;
|
||||
|
||||
@Autowired
|
||||
private ESProductRepository esProductRepository;
|
||||
|
||||
@Test
|
||||
public void testRebuild() {
|
||||
int counts = searchProductManager.rebuild();
|
||||
System.out.println("重建数量:" + counts);
|
||||
|
||||
System.out.println(esProductRepository.count());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.mall.searchservice.service;
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.mall.searchservice.service.product;
|
||||
|
||||
import cn.iocoder.mall.searchservice.service.product.bo.SearchProductConditionBO;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link SearchProductService} 的测试类,目前是集成测试类
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public class SearchProductServiceTest {
|
||||
|
||||
static {
|
||||
System.setProperty("es.set.netty.runtime.available.processors", "false");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SearchProductService searchProductService;
|
||||
|
||||
@Test
|
||||
public void testGetSearchCondition() {
|
||||
SearchProductConditionBO conditionBO = searchProductService.getSearchProductCondition("商品", Collections.singletonList("category"));
|
||||
System.out.println(conditionBO);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user