diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java index c81be713f..c947894dc 100644 --- a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java @@ -9,6 +9,10 @@ import java.util.List; public class StringUtil { + public static boolean hasText(String str) { + return StringUtils.hasText(str); + } + public static String join(Collection coll, String delim) { return StringUtils.collectionToDelimitedString(coll, delim); } @@ -31,4 +35,4 @@ public class StringUtil { return org.apache.commons.lang3.StringUtils.substring(str, start); } -} \ No newline at end of file +} diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/vo/SortingField.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/vo/SortingField.java new file mode 100644 index 000000000..6c6af5336 --- /dev/null +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/vo/SortingField.java @@ -0,0 +1,41 @@ +package cn.iocoder.common.framework.vo; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +public class SortingField { + + /** + * 字段 + */ + private String field; + /** + * 排序 + */ + private String order; + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/mobile-web/src/api/search.js b/mobile-web/src/api/search.js new file mode 100644 index 000000000..34633bb75 --- /dev/null +++ b/mobile-web/src/api/search.js @@ -0,0 +1,16 @@ +import request from "../config/request"; + +export function getProductPage({cid, keyword, pageNo, pageSize, sortField, sortOrder}) { + return request({ + url: '/search-api/users/product/page', + method: 'get', + params: { + cid, + keyword, + pageNo: pageNo || 1, + pageSize: pageSize || 10, + sortField: sortField, + sortOrder: sortOrder, + } + }); +} diff --git a/mobile-web/src/components/common/productcard.vue b/mobile-web/src/components/common/productcard.vue index 0d0ec9092..e5e9444f1 100644 --- a/mobile-web/src/components/common/productcard.vue +++ b/mobile-web/src/components/common/productcard.vue @@ -8,17 +8,20 @@ style="background:#fff" > @@ -27,14 +27,19 @@ export default { components:{ [Search.name]:Search, }, + props: { + // keyword: String, + // onSearch: Function, + }, data(){ return{ value:'', } }, methods:{ - onSearch() { - console.log(this.value); + onSearchClick() { + // this.props.onSearch(this.keyword); + this.$emit('onSearch', this.value); }, onBack() { history.back(); diff --git a/mobile-web/src/config/request.js b/mobile-web/src/config/request.js index e4e5bcbf5..90183546f 100644 --- a/mobile-web/src/config/request.js +++ b/mobile-web/src/config/request.js @@ -28,6 +28,10 @@ const serviceRouter = function(requestUrl) { prefix: '/pay-api', target: 'http://127.0.0.1:18084/pay-api', }, + '/search-api': { + prefix: '/search-api', + target: 'http://127.0.0.1:18086/search-api', + }, }; const configProd = { @@ -51,6 +55,10 @@ const serviceRouter = function(requestUrl) { prefix: '/pay-api', target: 'http://api.shop.iocoder.cn:18099/pay-api', }, + '/search-api': { + prefix: '/search-api', + target: 'http://api.shop.iocoder.cn:18099/search-api', + }, }; if (process.env.NODE_ENV == 'development') { diff --git a/mobile-web/src/config/router.js b/mobile-web/src/config/router.js index 4e492d8c8..7fd1ac559 100644 --- a/mobile-web/src/config/router.js +++ b/mobile-web/src/config/router.js @@ -153,13 +153,6 @@ const routes = [ title: '进度详情' } }, - { - path: '/product/:id', - component: () => import('../page/product/detail'), - meta: { - title: '商品详情' - } - }, { path: '/product/search', component: () => import('../page/product/search'), @@ -167,6 +160,13 @@ const routes = [ title: '商品搜索' } }, + { + path: '/product/:id', + component: () => import('../page/product/detail'), + meta: { + title: '商品详情' + } + }, { path: '/products/list', component: () => import('../page/product/list'), diff --git a/mobile-web/src/page/product/search.vue b/mobile-web/src/page/product/search.vue index 53757f628..fd06a6eb0 100644 --- a/mobile-web/src/page/product/search.vue +++ b/mobile-web/src/page/product/search.vue @@ -1,21 +1,21 @@ diff --git a/order/order-service-api/src/main/java/cn/iocoder/mall/order/api/bo/CalcSkuPriceBO.java b/order/order-service-api/src/main/java/cn/iocoder/mall/order/api/bo/CalcSkuPriceBO.java index 7d3613044..38281274d 100644 --- a/order/order-service-api/src/main/java/cn/iocoder/mall/order/api/bo/CalcSkuPriceBO.java +++ b/order/order-service-api/src/main/java/cn/iocoder/mall/order/api/bo/CalcSkuPriceBO.java @@ -4,12 +4,14 @@ import cn.iocoder.mall.promotion.api.bo.PromotionActivityBO; import lombok.Data; import lombok.experimental.Accessors; +import java.io.Serializable; + /** * 计算商品 SKU 价格结果 BO */ @Data @Accessors(chain = true) -public class CalcSkuPriceBO { +public class CalcSkuPriceBO implements Serializable { /** * 满减送促销活动 diff --git a/product/product-service-impl/src/main/resources/mapper/ProductSpuMapper.xml b/product/product-service-impl/src/main/resources/mapper/ProductSpuMapper.xml index 8697c130f..96d9b1311 100644 --- a/product/product-service-impl/src/main/resources/mapper/ProductSpuMapper.xml +++ b/product/product-service-impl/src/main/resources/mapper/ProductSpuMapper.xml @@ -33,10 +33,10 @@ FROM product_spu - id >= #{id} + id > #{id} + AND deleted = 0 - AND deleted = 0 ORDER BY id ASC LIMIT #{limit} diff --git a/search/search-application/pom.xml b/search/search-application/pom.xml index 519c43bef..5fdb09375 100644 --- a/search/search-application/pom.xml +++ b/search/search-application/pom.xml @@ -11,5 +11,79 @@ search-application + + + cn.iocoder.mall + common-framework + 1.0-SNAPSHOT + + + cn.iocoder.mall + user-sdk + 1.0-SNAPSHOT + + + cn.iocoder.mall + search-service-api + 1.0-SNAPSHOT + + + cn.iocoder.mall + search-service-impl + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba + dubbo + + + com.alibaba.boot + dubbo-spring-boot-starter + + + + org.apache.curator + curator-framework + + + + io.springfox + springfox-swagger2 + + + io.springfox + springfox-swagger-ui + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/search/search-application/src/main/java/cn/iocoder/mall/search/application/SearchApplication.java b/search/search-application/src/main/java/cn/iocoder/mall/search/application/SearchApplication.java new file mode 100644 index 000000000..f4c7e243f --- /dev/null +++ b/search/search-application/src/main/java/cn/iocoder/mall/search/application/SearchApplication.java @@ -0,0 +1,13 @@ +package cn.iocoder.mall.search.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"cn.iocoder.mall.search"}) +public class SearchApplication { + + public static void main(String[] args) { + SpringApplication.run(SearchApplication.class, args); + } + +} diff --git a/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/MVCConfiguration.java b/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/MVCConfiguration.java new file mode 100644 index 000000000..a6e4b7d13 --- /dev/null +++ b/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/MVCConfiguration.java @@ -0,0 +1,59 @@ +package cn.iocoder.mall.search.application.config; + +import cn.iocoder.common.framework.config.GlobalExceptionHandler; +import cn.iocoder.common.framework.servlet.CorsFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@EnableWebMvc +@Configuration +@Import(value = {GlobalExceptionHandler.class, // 统一全局返回 +// AdminSecurityInterceptor.class, UserAccessLogInterceptor.class, +// UserSecurityInterceptor.class, AdminAccessLogInterceptor.class, +}) +public class MVCConfiguration implements WebMvcConfigurer { + +// @Autowired +// private UserSecurityInterceptor securityInterceptor; + +// @Autowired +// private UserSecurityInterceptor userSecurityInterceptor; +// @Autowired +// private UserAccessLogInterceptor userAccessLogInterceptor; +// @Autowired +// private AdminSecurityInterceptor adminSecurityInterceptor; +// @Autowired +// private AdminAccessLogInterceptor adminAccessLogInterceptor; +// + @Override + public void addInterceptors(InterceptorRegistry registry) { +// // 用户 +// registry.addInterceptor(userAccessLogInterceptor).addPathPatterns("/users/**"); +// registry.addInterceptor(userSecurityInterceptor).addPathPatterns("/users/**"); // 只拦截我们定义的接口 +// // 管理员 +// registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**"); +// registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决 + registry.addResourceHandler("swagger-ui.html**").addResourceLocations("classpath:/META-INF/resources/swagger-ui.html"); + registry.addResourceHandler("webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); + } + + @Bean + public FilterRegistrationBean corsFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new CorsFilter()); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } + +} diff --git a/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/SwaggerConfiguration.java b/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/SwaggerConfiguration.java new file mode 100644 index 000000000..260c1f4c2 --- /dev/null +++ b/search/search-application/src/main/java/cn/iocoder/mall/search/application/config/SwaggerConfiguration.java @@ -0,0 +1,36 @@ +package cn.iocoder.mall.search.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 // TODO 生产环境时,禁用掉。 +public class SwaggerConfiguration { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("cn.iocoder.mall.search.application.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("搜索子系统") + .description("搜索子系统") + .termsOfServiceUrl("http://www.iocoder.cn") + .version("1.0.0") + .build(); + } + +} diff --git a/search/search-application/src/main/java/cn/iocoder/mall/search/application/controller/users/UsersProductSearchController.java b/search/search-application/src/main/java/cn/iocoder/mall/search/application/controller/users/UsersProductSearchController.java new file mode 100644 index 000000000..5d53fb4a6 --- /dev/null +++ b/search/search-application/src/main/java/cn/iocoder/mall/search/application/controller/users/UsersProductSearchController.java @@ -0,0 +1,43 @@ +package cn.iocoder.mall.search.application.controller.users; + +import cn.iocoder.common.framework.util.StringUtil; +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.common.framework.vo.SortingField; +import cn.iocoder.mall.search.api.ProductSearchService; +import cn.iocoder.mall.search.api.bo.ESProductPageBO; +import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO; +import com.alibaba.dubbo.config.annotation.Reference; +import io.swagger.annotations.Api; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; + +@RestController +@RequestMapping("users/product") +@Api("商品搜索") +public class UsersProductSearchController { + + @Reference(validation = "true") + private ProductSearchService productSearchService; + + @GetMapping("/page") // TODO 芋艿,后面把 BO 改成 VO + public CommonResult page(@RequestParam(value = "cid", required = false) Integer cid, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "pageNo", required = false) Integer pageNo, + @RequestParam(value = "pageSize", required = false) Integer pageSize, + @RequestParam(value = "sortField", required = false) String sortField, + @RequestParam(value = "sortOrder", required = false) String sortOrder) { + // 创建 ProductSearchPageDTO 对象 + ProductSearchPageDTO productSearchPageDTO = new ProductSearchPageDTO().setCid(cid).setKeyword(keyword) + .setPageNo(pageNo).setPageSize(pageSize); + if (StringUtil.hasText(sortField) && StringUtil.hasText(sortOrder)) { + productSearchPageDTO.setSorts(Collections.singletonList(new SortingField(sortField, sortOrder))); + } + // 执行搜索 + return productSearchService.searchPage(productSearchPageDTO); + } + +} diff --git a/search/search-application/src/main/resources/application.yaml b/search/search-application/src/main/resources/application.yaml new file mode 100644 index 000000000..3ace875fe --- /dev/null +++ b/search/search-application/src/main/resources/application.yaml @@ -0,0 +1,9 @@ +spring: + application: + name: search-application + +# server +server: + port: 18086 + servlet: + context-path: /search-api/ diff --git a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/ProductSearchService.java b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/ProductSearchService.java index 3d709801c..ea24fa3ec 100644 --- a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/ProductSearchService.java +++ b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/ProductSearchService.java @@ -1,12 +1,13 @@ package cn.iocoder.mall.search.api; import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.search.api.bo.ESProductPageBO; import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO; public interface ProductSearchService { CommonResult rebuild(); - CommonResult searchPage(ProductSearchPageDTO searchPageDTO); + CommonResult searchPage(ProductSearchPageDTO searchPageDTO); } diff --git a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductBO.java b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductBO.java new file mode 100644 index 000000000..585af5042 --- /dev/null +++ b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductBO.java @@ -0,0 +1,86 @@ +package cn.iocoder.mall.search.api.bo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * 商品 ES BO + */ +@Data +@Accessors(chain = true) +public class ESProductBO implements Serializable { + + private Integer id; + + // ========== 基本信息 ========= + /** + * SPU 名字 + */ + private String name; + /** + * 卖点 + */ + private String sellPoint; + /** + * 描述 + */ + private String description; + /** + * 分类编号 + */ + private Integer cid; + /** + * 分类名 + */ + private String categoryName; + /** + * 商品主图地数组 + */ + private List 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; + +} diff --git a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductPageBO.java b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductPageBO.java new file mode 100644 index 000000000..a4e0a155d --- /dev/null +++ b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/bo/ESProductPageBO.java @@ -0,0 +1,22 @@ +package cn.iocoder.mall.search.api.bo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +@Data +@Accessors(chain = true) +public class ESProductPageBO implements Serializable { + + /** + * 管理员数组 + */ + private List list; + /** + * 总量 + */ + private Integer total; + +} diff --git a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/ProductSearchPageDTO.java b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/ProductSearchPageDTO.java index 09d95c0af..bfc465bd1 100644 --- a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/ProductSearchPageDTO.java +++ b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/ProductSearchPageDTO.java @@ -1,9 +1,12 @@ package cn.iocoder.mall.search.api.dto; +import cn.iocoder.common.framework.util.CollectionUtil; +import cn.iocoder.common.framework.vo.SortingField; import lombok.Data; import lombok.experimental.Accessors; import java.util.List; +import java.util.Set; /** * 商品检索分页 DTO @@ -12,6 +15,8 @@ import java.util.List; @Accessors(chain = true) public class ProductSearchPageDTO { + public static final Set SORT_FIELDS = CollectionUtil.asSet("buyPrice"); + /** * 分类编号 */ @@ -33,6 +38,6 @@ public class ProductSearchPageDTO { /** * 排序字段数组 */ - private List sorts; + private List sorts; } diff --git a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/SortFieldDTO.java b/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/SortFieldDTO.java deleted file mode 100644 index d093be9f9..000000000 --- a/search/search-service-api/src/main/java/cn/iocoder/mall/search/api/dto/SortFieldDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.mall.search.api.dto; - -/** - * 排序字段 DTO - */ -public class SortFieldDTO { - - /** - * 字段 - */ - private String field; - /** - * 排序 - */ - private String order; - -} diff --git a/search/search-service-impl/pom.xml b/search/search-service-impl/pom.xml index 6cc7e612a..00c33ea0d 100644 --- a/search/search-service-impl/pom.xml +++ b/search/search-service-impl/pom.xml @@ -49,6 +49,18 @@ spring-boot-starter-test test + + com.alibaba.boot + dubbo-spring-boot-starter + + + org.apache.curator + curator-framework + + + org.springframework.boot + spring-boot-starter-web + diff --git a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/config/JPAConfiguration.java b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/config/JPAConfiguration.java new file mode 100644 index 000000000..acb35fd21 --- /dev/null +++ b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/config/JPAConfiguration.java @@ -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 { +} diff --git a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/convert/ProductSearchConvert.java b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/convert/ProductSearchConvert.java new file mode 100644 index 000000000..660851d66 --- /dev/null +++ b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/convert/ProductSearchConvert.java @@ -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 convert(List list); + +} diff --git a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/dao/ProductRepository.java b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/dao/ProductRepository.java index 931c7b55d..dc03498a3 100644 --- a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/dao/ProductRepository.java +++ b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/dao/ProductRepository.java @@ -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 { @Deprecated ESProductDO findByName(String name); + default Page search(Integer cid, String keyword, Integer pageNo, Integer pageSize, + List 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()); + } + } diff --git a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImpl.java b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImpl.java index 3935e1bef..b6a1d66b8 100644 --- a/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImpl.java +++ b/search/search-service-impl/src/main/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImpl.java @@ -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> result = productSpuService.getProductSpuDetailListForSync(lastId, REBUILD_FETCH_PER_SIZE); - Assert.isTrue(result.isError(), "获得商品列表必然成功"); + Assert.isTrue(result.isSuccess(), "获得商品列表必然成功"); List spus = result.getData(); rebuildCounts += spus.size(); // 存储到 ES 中 - List products = spus.stream().map(new Function() { - @Override - public ESProductDO apply(ProductSpuDetailBO spu) { - return convert(spu); - } - }).collect(Collectors.toList()); - + List 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 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 searchPage(ProductSearchPageDTO searchPageDTO) { + checkSortFieldInvalid(searchPageDTO.getSorts()); + // 执行查询 + Page 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 sorts) { + if (CollectionUtil.isEmpty(sorts)) { + return; + } + sorts.forEach(sortingField -> Assert.isTrue(ProductSearchPageDTO.SORT_FIELDS.contains(sortingField.getField()), + String.format("排序字段(%s) 不在允许范围内", sortingField.getField()))); } } diff --git a/search/search-service-impl/src/main/resources/application.yaml b/search/search-service-impl/src/main/resources/config/application.yaml similarity index 83% rename from search/search-service-impl/src/main/resources/application.yaml rename to search/search-service-impl/src/main/resources/config/application.yaml index 56c6fc1d8..3a6a8daa1 100644 --- a/search/search-service-impl/src/main/resources/application.yaml +++ b/search/search-service-impl/src/main/resources/config/application.yaml @@ -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 diff --git a/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/dao/ProductRepositoryTest.java b/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/dao/ProductRepositoryTest.java index 73ce7be65..2d7f669d0 100644 --- a/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/dao/ProductRepositoryTest.java +++ b/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/dao/ProductRepositoryTest.java @@ -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 page = productRepository.search(639, null, 1, 10); +// console(page.getContent()); + +// Page page = productRepository.search(null, "数据库Oracle", 1, 10); +// console(page.getContent()); + } + + private void console(List list) { + list.forEach(System.out::println); + } + } diff --git a/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImplTest.java b/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImplTest.java new file mode 100644 index 000000000..bc6626dc1 --- /dev/null +++ b/search/search-service-impl/src/test/java/cn/iocoder/mall/search/biz/service/ProductSearchServiceImplTest.java @@ -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()); + } + +}