前端 + 后端:商品搜索
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.mall.search.biz.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 JPAConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.mall.search.biz.convert;
|
||||
|
||||
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
|
||||
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
|
||||
import cn.iocoder.mall.promotion.api.bo.PromotionActivityBO;
|
||||
import cn.iocoder.mall.search.api.bo.ESProductBO;
|
||||
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ProductSearchConvert {
|
||||
|
||||
ProductSearchConvert INSTANCE = Mappers.getMapper(ProductSearchConvert.class);
|
||||
|
||||
@Mappings({})
|
||||
ESProductDO convert(ProductSpuDetailBO spu);
|
||||
|
||||
@Mappings({})
|
||||
default ESProductDO convert(ProductSpuDetailBO spu, CalcSkuPriceBO calcSkuPrice) {
|
||||
// Spu 的基础数据
|
||||
ESProductDO product = this.convert(spu);
|
||||
product.setOriginalPrice(calcSkuPrice.getOriginalPrice()).setBuyPrice(calcSkuPrice.getBuyPrice());
|
||||
// 设置促销活动相关字段
|
||||
if (calcSkuPrice.getTimeLimitedDiscount() != null) {
|
||||
PromotionActivityBO activity = calcSkuPrice.getTimeLimitedDiscount();
|
||||
product.setPromotionActivityId(activity.getId()).setPromotionActivityTitle(activity.getTitle())
|
||||
.setPromotionActivityType(activity.getActivityType());
|
||||
}
|
||||
// 返回
|
||||
return product;
|
||||
}
|
||||
|
||||
List<ESProductBO> convert(List<ESProductDO> list);
|
||||
|
||||
}
|
||||
@@ -1,13 +1,67 @@
|
||||
package cn.iocoder.mall.search.biz.dao;
|
||||
|
||||
import cn.iocoder.common.framework.util.CollectionUtil;
|
||||
import cn.iocoder.common.framework.util.StringUtil;
|
||||
import cn.iocoder.common.framework.vo.SortingField;
|
||||
import cn.iocoder.mall.search.biz.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 ProductRepository 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 (StringUtil.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 (CollectionUtil.isEmpty(sortFields)) {
|
||||
nativeSearchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
|
||||
} else {
|
||||
sortFields.forEach(sortField -> nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField.getField())
|
||||
.order(SortOrder.fromString(sortField.getOrder()))));
|
||||
}
|
||||
// 执行查询
|
||||
return search(nativeSearchQueryBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
package cn.iocoder.mall.search.biz.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.CollectionUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.common.framework.vo.SortingField;
|
||||
import cn.iocoder.mall.order.api.CartService;
|
||||
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
|
||||
import cn.iocoder.mall.product.api.ProductSpuService;
|
||||
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
|
||||
import cn.iocoder.mall.search.api.ProductSearchService;
|
||||
import cn.iocoder.mall.search.api.bo.ESProductPageBO;
|
||||
import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO;
|
||||
import cn.iocoder.mall.search.biz.convert.ProductSearchConvert;
|
||||
import cn.iocoder.mall.search.biz.dao.ProductRepository;
|
||||
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
||||
import com.alibaba.dubbo.config.annotation.Reference;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
|
||||
public class ProductSearchServiceImpl implements ProductSearchService {
|
||||
|
||||
private static final Integer REBUILD_FETCH_PER_SIZE = 2;
|
||||
private static final Integer REBUILD_FETCH_PER_SIZE = 100;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
@Reference(validation = "true")
|
||||
private ProductSpuService productSpuService;
|
||||
@Autowired
|
||||
@Reference(validation = "true")
|
||||
private CartService cartService;
|
||||
|
||||
@Override
|
||||
@@ -39,17 +44,12 @@ public class ProductSearchServiceImpl implements ProductSearchService {
|
||||
int rebuildCounts = 0;
|
||||
while (true) {
|
||||
CommonResult<List<ProductSpuDetailBO>> result = productSpuService.getProductSpuDetailListForSync(lastId, REBUILD_FETCH_PER_SIZE);
|
||||
Assert.isTrue(result.isError(), "获得商品列表必然成功");
|
||||
Assert.isTrue(result.isSuccess(), "获得商品列表必然成功");
|
||||
List<ProductSpuDetailBO> spus = result.getData();
|
||||
rebuildCounts += spus.size();
|
||||
// 存储到 ES 中
|
||||
List<ESProductDO> products = spus.stream().map(new Function<ProductSpuDetailBO, ESProductDO>() {
|
||||
@Override
|
||||
public ESProductDO apply(ProductSpuDetailBO spu) {
|
||||
return convert(spu);
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
List<ESProductDO> products = spus.stream().map(this::convert).collect(Collectors.toList());
|
||||
productRepository.saveAll(products);
|
||||
// 设置新的 lastId ,或者结束
|
||||
if (spus.size() < REBUILD_FETCH_PER_SIZE) {
|
||||
break;
|
||||
@@ -66,14 +66,30 @@ public class ProductSearchServiceImpl implements ProductSearchService {
|
||||
ProductSpuDetailBO.Sku sku = spu.getSkus().stream().min(Comparator.comparing(ProductSpuDetailBO.Sku::getPrice)).get();
|
||||
// 价格计算
|
||||
CommonResult<CalcSkuPriceBO> calSkuPriceResult = cartService.calcSkuPrice(sku.getId());
|
||||
Assert.isTrue(calSkuPriceResult.isError(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
|
||||
|
||||
return new ESProductDO();
|
||||
Assert.isTrue(calSkuPriceResult.isSuccess(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
|
||||
// 拼装结果
|
||||
return ProductSearchConvert.INSTANCE.convert(spu, calSkuPriceResult.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult searchPage(ProductSearchPageDTO searchPageDTO) {
|
||||
return null;
|
||||
public CommonResult<ESProductPageBO> searchPage(ProductSearchPageDTO searchPageDTO) {
|
||||
checkSortFieldInvalid(searchPageDTO.getSorts());
|
||||
// 执行查询
|
||||
Page<ESProductDO> searchPage = productRepository.search(searchPageDTO.getCid(), searchPageDTO.getKeyword(),
|
||||
searchPageDTO.getPageNo(), searchPageDTO.getPageSize(), searchPageDTO.getSorts());
|
||||
// 转换结果
|
||||
ESProductPageBO resultPage = new ESProductPageBO()
|
||||
.setList(ProductSearchConvert.INSTANCE.convert(searchPage.getContent()))
|
||||
.setTotal((int) searchPage.getTotalElements());
|
||||
return CommonResult.success(resultPage);
|
||||
}
|
||||
|
||||
private void checkSortFieldInvalid(List<SortingField> sorts) {
|
||||
if (CollectionUtil.isEmpty(sorts)) {
|
||||
return;
|
||||
}
|
||||
sorts.forEach(sortingField -> Assert.isTrue(ProductSearchPageDTO.SORT_FIELDS.contains(sortingField.getField()),
|
||||
String.format("排序字段(%s) 不在允许范围内", sortingField.getField())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#
|
||||
# es
|
||||
spring:
|
||||
data:
|
||||
elasticsearch:
|
||||
@@ -6,6 +6,7 @@ spring:
|
||||
cluster-nodes: 192.168.88.10:9300
|
||||
repositories:
|
||||
enable: true
|
||||
|
||||
# dubbo
|
||||
dubbo:
|
||||
application:
|
||||
@@ -16,4 +17,4 @@ dubbo:
|
||||
port: -1
|
||||
name: dubbo
|
||||
scan:
|
||||
base-packages: cn.iocoder.mall.search.service.biz
|
||||
base-packages: cn.iocoder.mall.search.biz.service
|
||||
@@ -1,12 +1,15 @@
|
||||
package cn.iocoder.mall.search.biz.dao;
|
||||
|
||||
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
||||
import org.junit.Ignore;
|
||||
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.List;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public class ProductRepositoryTest {
|
||||
@@ -15,6 +18,7 @@ public class ProductRepositoryTest {
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testSave() {
|
||||
// productRepository.deleteById(1);
|
||||
ESProductDO product = new ESProductDO()
|
||||
@@ -24,9 +28,23 @@ public class ProductRepositoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testFindByName() {
|
||||
ESProductDO product = productRepository.findByName("锤子");
|
||||
System.out.println(product);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch() {
|
||||
// Page<ESProductDO> page = productRepository.search(639, null, 1, 10);
|
||||
// console(page.getContent());
|
||||
|
||||
// Page<ESProductDO> page = productRepository.search(null, "数据库Oracle", 1, 10);
|
||||
// console(page.getContent());
|
||||
}
|
||||
|
||||
private void console(List<ESProductDO> list) {
|
||||
list.forEach(System.out::println);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.mall.search.biz.service;
|
||||
|
||||
import cn.iocoder.mall.search.biz.dao.ProductRepository;
|
||||
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;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
public class ProductSearchServiceImplTest {
|
||||
|
||||
@Autowired
|
||||
private ProductSearchServiceImpl productSearchService;
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Test
|
||||
public void testRebuild() {
|
||||
int counts = productSearchService.rebuild().getData();
|
||||
System.out.println("重建数量:" + counts);
|
||||
|
||||
System.out.println(productRepository.count());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user