前端 + 后端:商品搜索
This commit is contained in:
parent
68dadab873
commit
b98e21e157
@ -9,6 +9,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public class StringUtil {
|
public class StringUtil {
|
||||||
|
|
||||||
|
public static boolean hasText(String str) {
|
||||||
|
return StringUtils.hasText(str);
|
||||||
|
}
|
||||||
|
|
||||||
public static String join(Collection<?> coll, String delim) {
|
public static String join(Collection<?> coll, String delim) {
|
||||||
return StringUtils.collectionToDelimitedString(coll, delim);
|
return StringUtils.collectionToDelimitedString(coll, delim);
|
||||||
}
|
}
|
||||||
@ -31,4 +35,4 @@ public class StringUtil {
|
|||||||
return org.apache.commons.lang3.StringUtils.substring(str, start);
|
return org.apache.commons.lang3.StringUtils.substring(str, start);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
16
mobile-web/src/api/search.js
Normal file
16
mobile-web/src/api/search.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -8,17 +8,20 @@
|
|||||||
style="background:#fff"
|
style="background:#fff"
|
||||||
>
|
>
|
||||||
<template slot="thumb">
|
<template slot="thumb">
|
||||||
<img :src="product.picUrls[0]"/>
|
<img :src="product.picUrls && product.picUrls ? product.picUrls[0] : ''"/>
|
||||||
<!-- TODO 芋艿 暂时去掉 -->
|
<!-- TODO 芋艿 暂时去掉 -->
|
||||||
<!-- <p v-if="product.imageTag!=null&&product.imageTag!=''" class="image_tag">{{product.imageTag}}</p>-->
|
<!-- <p v-if="product.imageTag!=null&&product.imageTag!=''" class="image_tag">{{product.imageTag}}</p>-->
|
||||||
</template>
|
</template>
|
||||||
<template slot="tags">
|
<template slot="tags">
|
||||||
<p class="price" v-if="product.price!=null && product.price !== ''">
|
<p class="price" v-if="product.buyPrice || product.price">
|
||||||
¥<span>{{product.price / 100.00}}</span>
|
¥<span>{{product.buyPrice ? product.buyPrice / 100.00 : product.price / 100.00}}</span>
|
||||||
<!-- TODO 芋艿 暂时去掉 -->
|
<!-- TODO 芋艿 暂时去掉 -->
|
||||||
<!-- <van-tag v-if="product.tags!=null" v-for="tag in product.tags" :key="tag" plain type="danger">-->
|
<!-- <van-tag v-if="product.tags!=null" v-for="tag in product.tags" :key="tag" plain type="danger">-->
|
||||||
<!-- {{tag}}-->
|
<!-- {{tag}}-->
|
||||||
<!-- </van-tag>-->
|
<!-- </van-tag>-->
|
||||||
|
<van-tag v-if="product.promotionActivityTitle" plain type="danger">
|
||||||
|
{{ product.promotionActivityTitle }}
|
||||||
|
</van-tag>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<!-- TODO 芋艿 暂时去掉 -->
|
<!-- TODO 芋艿 暂时去掉 -->
|
||||||
|
@ -8,13 +8,13 @@
|
|||||||
placeholder="请输入搜索关键词"
|
placeholder="请输入搜索关键词"
|
||||||
background="#fff"
|
background="#fff"
|
||||||
show-action
|
show-action
|
||||||
@search="onSearch"
|
@search="onSearchClick"
|
||||||
slot="title"
|
slot="title"
|
||||||
>
|
>
|
||||||
<div slot="action" @click="onSearch">搜索</div>
|
<div slot="action" @click="onSearchClick">搜索</div>
|
||||||
</van-search>
|
</van-search>
|
||||||
</van-nav-bar>
|
</van-nav-bar>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -27,14 +27,19 @@ export default {
|
|||||||
components:{
|
components:{
|
||||||
[Search.name]:Search,
|
[Search.name]:Search,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
// keyword: String,
|
||||||
|
// onSearch: Function,
|
||||||
|
},
|
||||||
data(){
|
data(){
|
||||||
return{
|
return{
|
||||||
value:'',
|
value:'',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
onSearch() {
|
onSearchClick() {
|
||||||
console.log(this.value);
|
// this.props.onSearch(this.keyword);
|
||||||
|
this.$emit('onSearch', this.value);
|
||||||
},
|
},
|
||||||
onBack() {
|
onBack() {
|
||||||
history.back();
|
history.back();
|
||||||
|
@ -28,6 +28,10 @@ const serviceRouter = function(requestUrl) {
|
|||||||
prefix: '/pay-api',
|
prefix: '/pay-api',
|
||||||
target: 'http://127.0.0.1:18084/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 = {
|
const configProd = {
|
||||||
@ -51,6 +55,10 @@ const serviceRouter = function(requestUrl) {
|
|||||||
prefix: '/pay-api',
|
prefix: '/pay-api',
|
||||||
target: 'http://api.shop.iocoder.cn:18099/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') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
@ -153,13 +153,6 @@ const routes = [
|
|||||||
title: '进度详情'
|
title: '进度详情'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/product/:id',
|
|
||||||
component: () => import('../page/product/detail'),
|
|
||||||
meta: {
|
|
||||||
title: '商品详情'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/product/search',
|
path: '/product/search',
|
||||||
component: () => import('../page/product/search'),
|
component: () => import('../page/product/search'),
|
||||||
@ -167,6 +160,13 @@ const routes = [
|
|||||||
title: '商品搜索'
|
title: '商品搜索'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/product/:id',
|
||||||
|
component: () => import('../page/product/detail'),
|
||||||
|
meta: {
|
||||||
|
title: '商品详情'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/products/list',
|
path: '/products/list',
|
||||||
component: () => import('../page/product/list'),
|
component: () => import('../page/product/list'),
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-list">
|
<div class="product-list">
|
||||||
<searchtop/>
|
<searchtop @onSearch="onSearch" />
|
||||||
<div class="filterbar">
|
<div class="filterbar">
|
||||||
<ul :class="filtersort?'show':''">
|
<ul :class="filterSort?'show':''">
|
||||||
<li :class="filterindex==0?'selected':''" v-on:click="onFilterBar(0)"><span>{{filterindex==11?'价格最低':(filterindex==12?'价格最高':'综合')}}</span><van-icon name="arrow" class="down" /></li>
|
<li :class="filterIndex===0?'selected':''" v-on:click="onFilterBar(0)"><span>{{filterIndex==11?'价格最低':(filterIndex==12?'价格最高':'综合')}}</span><van-icon name="arrow" class="down" /></li>
|
||||||
<li :class="filterindex==1?'selected':''" v-on:click="onFilterBar(1)"><span>销量</span></li>
|
<li :class="filterIndex===1?'selected':''" v-on:click="onFilterBar(1)"><span>销量</span></li>
|
||||||
<li :class="filterindex==2?'selected':''" v-on:click="onFilterBar(2)"><span>上新</span></li>
|
<li :class="filterIndex===2?'selected':''" v-on:click="onFilterBar(2)"><span>上新</span></li>
|
||||||
<li :class="filterindex==3?'selected':''" v-on:click="onFilterBar(3)"><span>筛选</span></li>
|
<li :class="filterIndex===3?'selected':''" v-on:click="onFilterBar(3)"><span>筛选</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div :class="'item_options '+(filtersort?'show':'')">
|
<div :class="'item_options '+(filterSort?'show':'')">
|
||||||
<ul>
|
<ul>
|
||||||
<li :class="filterindex==10?'selected':''" v-on:click="onFilterBar(10)">综合</li>
|
<li :class="filterIndex==10?'selected':''" v-on:click="onFilterBar(10)">综合</li>
|
||||||
<li :class="filterindex==11?'selected':''" v-on:click="onFilterBar(11)">价格最低</li>
|
<li :class="filterIndex==11?'selected':''" v-on:click="onFilterBar(11)">价格降序</li>
|
||||||
<li :class="filterindex==12?'selected':''" v-on:click="onFilterBar(12)">价格最高</li>
|
<li :class="filterIndex==12?'selected':''" v-on:click="onFilterBar(12)">价格最高</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<van-popup v-model="filtershow" position="right" class="filterlayer" >
|
<van-popup v-model="filterShow" position="right" class="filterlayer" >
|
||||||
<div class="filterInner" style="overflow-y: scroll;max-height: 100%;">
|
<div class="filterInner" style="overflow-y: scroll;max-height: 100%;">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -163,14 +163,23 @@
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(product,i) in products" :key="i">
|
<van-list
|
||||||
<product-card :product='product' @click="showProduct(product)" />
|
v-model="loading"
|
||||||
</div>
|
:finished="finished"
|
||||||
|
finished-text="没有更多了"
|
||||||
|
@load="onLoad"
|
||||||
|
>
|
||||||
|
<div v-for="(product,i) in products" :key="i">
|
||||||
|
<product-card :product='product' @click="showProduct(product)" />
|
||||||
|
</div>
|
||||||
|
</van-list>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import searchtop from "../../components/search/searchtop";
|
import searchtop from "../../components/search/searchtop";
|
||||||
|
import {getProductPage} from "../../api/search";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -178,70 +187,120 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: "",
|
page: 0,
|
||||||
filterindex: 0,
|
pageSize: 10,
|
||||||
filtersort: false,
|
loading: false,
|
||||||
filtershow: false,
|
finished: false,
|
||||||
|
|
||||||
products:[
|
keyword: "",
|
||||||
{
|
|
||||||
id:1,
|
filterIndex: 0,
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
filterSort: false, // 是否展示几个【排序】
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
filterShow: false, // 是否展示【筛选】
|
||||||
price:'13.00',
|
|
||||||
},
|
sortField: undefined,
|
||||||
{
|
sortOrder: undefined,
|
||||||
id:1,
|
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
products:[]
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
|
||||||
price:'499.00',
|
|
||||||
tags:['满199减100','2件起购'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id:1,
|
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
|
||||||
price:'499.00',
|
|
||||||
tags:['新品'],
|
|
||||||
imageTag:'仅剩1件',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id:1,
|
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
|
||||||
price:'499.00',
|
|
||||||
tags:['赠'],
|
|
||||||
imageTag:'预约',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id:1,
|
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
|
||||||
price:'15.00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id:1,
|
|
||||||
imageURL:'https://pop.nosdn.127.net/19e33c9b-6c22-4a4b-96da-1cb7afb32712',
|
|
||||||
title:'BEYOND博洋家纺 床上套件 秋冬保暖纯棉床单被套 双人被罩 磨毛全棉印花床品四件套',
|
|
||||||
price:'125.50',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onFilterBar(value) {
|
onFilterBar(value) {
|
||||||
if (value == 0) {
|
if (value === 0) {
|
||||||
this.filtersort = !this.filtersort;
|
this.filterSort = !this.filterSort;
|
||||||
} else if (value == 3) {
|
} else if (value === 3) {
|
||||||
this.filtershow = !this.filtershow;
|
this.filterShow = !this.filterShow;
|
||||||
} else {
|
} else {
|
||||||
this.filtersort = false;
|
// 如果非合法 10、11、12
|
||||||
this.filterindex = value;
|
if (value !== 10
|
||||||
|
&& value !== 11
|
||||||
|
&& value !== 12) {
|
||||||
|
alert('暂不支持');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置 filterSort 和 filterIndex 属性
|
||||||
|
this.filterSort = false;
|
||||||
|
this.filterIndex = value;
|
||||||
|
// 标记加载中
|
||||||
|
this.loading = true;
|
||||||
|
// 根据 value 的值,设置 sortField、sortOrder
|
||||||
|
switch (value) {
|
||||||
|
case 10:
|
||||||
|
this.sortField = undefined;
|
||||||
|
this.sortOrder = undefined;
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
this.sortField = 'buyPrice';
|
||||||
|
this.sortOrder = 'desc';
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
this.sortField = 'buyPrice';
|
||||||
|
this.sortOrder = 'asc';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 根据排序,重新搜索
|
||||||
|
let page = 1;
|
||||||
|
getProductPage({
|
||||||
|
pageNo: page,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
keyword: this.keyword,
|
||||||
|
sortField: this.sortField,
|
||||||
|
sortOrder: this.sortOrder,
|
||||||
|
}).then(data => {
|
||||||
|
this.products = [];
|
||||||
|
this.handleData(page, data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showProduct(product){
|
showProduct(product){
|
||||||
this.$router.push('/product/'+product.id);
|
this.$router.push('/product/'+product.id);
|
||||||
|
},
|
||||||
|
onSearch(keyword) {
|
||||||
|
this.loading = true;
|
||||||
|
// 设置 keyword
|
||||||
|
this.keyword = keyword;
|
||||||
|
// 重置其它字段
|
||||||
|
this.filterIndex = 0;
|
||||||
|
this.filterSort = false;
|
||||||
|
this.filterShow = false;
|
||||||
|
this.sortField = undefined;
|
||||||
|
this.sortOrder = undefined;
|
||||||
|
// 查询
|
||||||
|
let page = 1;
|
||||||
|
getProductPage({
|
||||||
|
pageNo: page,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
keyword: keyword,
|
||||||
|
}).then(data => {
|
||||||
|
this.products = [];
|
||||||
|
this.handleData(page, data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
// 进入下一页
|
||||||
|
let page = this.page + 1;
|
||||||
|
getProductPage({
|
||||||
|
pageNo: page,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
}).then(data => {
|
||||||
|
this.handleData(page, data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleData(page, data) {
|
||||||
|
this.loading = true;
|
||||||
|
// 设置下页面
|
||||||
|
this.page = page;
|
||||||
|
// 数据保存到 list 中
|
||||||
|
this.products.push(...data.list);
|
||||||
|
// 判断页数
|
||||||
|
if (this.products.length >= data.total) {
|
||||||
|
this.finished = true;
|
||||||
|
}
|
||||||
|
// 标记不在加载中
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,12 +4,14 @@ import cn.iocoder.mall.promotion.api.bo.PromotionActivityBO;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算商品 SKU 价格结果 BO
|
* 计算商品 SKU 价格结果 BO
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class CalcSkuPriceBO {
|
public class CalcSkuPriceBO implements Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 满减送促销活动
|
* 满减送促销活动
|
||||||
|
@ -33,10 +33,10 @@
|
|||||||
FROM product_spu
|
FROM product_spu
|
||||||
<where>
|
<where>
|
||||||
<if test="id != null">
|
<if test="id != null">
|
||||||
id >= #{id}
|
id > #{id}
|
||||||
</if>
|
</if>
|
||||||
|
AND deleted = 0
|
||||||
</where>
|
</where>
|
||||||
AND deleted = 0
|
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT #{limit}
|
LIMIT #{limit}
|
||||||
</select>
|
</select>
|
||||||
|
@ -11,5 +11,79 @@
|
|||||||
|
|
||||||
<artifactId>search-application</artifactId>
|
<artifactId>search-application</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
<artifactId>common-framework</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
<artifactId>user-sdk</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
<artifactId>search-service-api</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
<artifactId>search-service-impl</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>dubbo</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.boot</groupId>
|
||||||
|
<artifactId>dubbo-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.curator</groupId>
|
||||||
|
<artifactId>curator-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- 提供给 mapstruct 使用 -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- 打包 -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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> corsFilter() {
|
||||||
|
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
|
||||||
|
registrationBean.setFilter(new CorsFilter());
|
||||||
|
registrationBean.addUrlPatterns("/*");
|
||||||
|
return registrationBean;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<ESProductPageBO> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: search-application
|
||||||
|
|
||||||
|
# server
|
||||||
|
server:
|
||||||
|
port: 18086
|
||||||
|
servlet:
|
||||||
|
context-path: /search-api/
|
@ -1,12 +1,13 @@
|
|||||||
package cn.iocoder.mall.search.api;
|
package cn.iocoder.mall.search.api;
|
||||||
|
|
||||||
import cn.iocoder.common.framework.vo.CommonResult;
|
import cn.iocoder.common.framework.vo.CommonResult;
|
||||||
|
import cn.iocoder.mall.search.api.bo.ESProductPageBO;
|
||||||
import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO;
|
import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO;
|
||||||
|
|
||||||
public interface ProductSearchService {
|
public interface ProductSearchService {
|
||||||
|
|
||||||
CommonResult<Integer> rebuild();
|
CommonResult<Integer> rebuild();
|
||||||
|
|
||||||
CommonResult searchPage(ProductSearchPageDTO searchPageDTO);
|
CommonResult<ESProductPageBO> searchPage(ProductSearchPageDTO searchPageDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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<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,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<ESProductBO> list;
|
||||||
|
/**
|
||||||
|
* 总量
|
||||||
|
*/
|
||||||
|
private Integer total;
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
package cn.iocoder.mall.search.api.dto;
|
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.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商品检索分页 DTO
|
* 商品检索分页 DTO
|
||||||
@ -12,6 +15,8 @@ import java.util.List;
|
|||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class ProductSearchPageDTO {
|
public class ProductSearchPageDTO {
|
||||||
|
|
||||||
|
public static final Set<String> SORT_FIELDS = CollectionUtil.asSet("buyPrice");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分类编号
|
* 分类编号
|
||||||
*/
|
*/
|
||||||
@ -33,6 +38,6 @@ public class ProductSearchPageDTO {
|
|||||||
/**
|
/**
|
||||||
* 排序字段数组
|
* 排序字段数组
|
||||||
*/
|
*/
|
||||||
private List<SortFieldDTO> sorts;
|
private List<SortingField> sorts;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package cn.iocoder.mall.search.api.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 排序字段 DTO
|
|
||||||
*/
|
|
||||||
public class SortFieldDTO {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字段
|
|
||||||
*/
|
|
||||||
private String field;
|
|
||||||
/**
|
|
||||||
* 排序
|
|
||||||
*/
|
|
||||||
private String order;
|
|
||||||
|
|
||||||
}
|
|
@ -49,6 +49,18 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.boot</groupId> <!-- 引入该包,为了写单元测试用 -->
|
||||||
|
<artifactId>dubbo-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.curator</groupId> <!-- 引入该包,为了写单元测试用 -->
|
||||||
|
<artifactId>curator-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId> <!-- 引入该包,为了写单元测试用 -->
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -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;
|
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 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.data.elasticsearch.repository.ElasticsearchRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ProductRepository extends ElasticsearchRepository<ESProductDO, Integer> {
|
public interface ProductRepository extends ElasticsearchRepository<ESProductDO, Integer> {
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
ESProductDO findByName(String name);
|
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;
|
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.CommonResult;
|
||||||
|
import cn.iocoder.common.framework.vo.SortingField;
|
||||||
import cn.iocoder.mall.order.api.CartService;
|
import cn.iocoder.mall.order.api.CartService;
|
||||||
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
|
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
|
||||||
import cn.iocoder.mall.product.api.ProductSpuService;
|
import cn.iocoder.mall.product.api.ProductSpuService;
|
||||||
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
|
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
|
||||||
import cn.iocoder.mall.search.api.ProductSearchService;
|
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.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.dao.ProductRepository;
|
||||||
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
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.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
|
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
|
||||||
public class ProductSearchServiceImpl implements ProductSearchService {
|
public class ProductSearchServiceImpl implements ProductSearchService {
|
||||||
|
|
||||||
private static final Integer REBUILD_FETCH_PER_SIZE = 2;
|
private static final Integer REBUILD_FETCH_PER_SIZE = 100;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ProductRepository productRepository;
|
private ProductRepository productRepository;
|
||||||
|
|
||||||
@Autowired
|
@Reference(validation = "true")
|
||||||
private ProductSpuService productSpuService;
|
private ProductSpuService productSpuService;
|
||||||
@Autowired
|
@Reference(validation = "true")
|
||||||
private CartService cartService;
|
private CartService cartService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -39,17 +44,12 @@ public class ProductSearchServiceImpl implements ProductSearchService {
|
|||||||
int rebuildCounts = 0;
|
int rebuildCounts = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
CommonResult<List<ProductSpuDetailBO>> result = productSpuService.getProductSpuDetailListForSync(lastId, REBUILD_FETCH_PER_SIZE);
|
CommonResult<List<ProductSpuDetailBO>> result = productSpuService.getProductSpuDetailListForSync(lastId, REBUILD_FETCH_PER_SIZE);
|
||||||
Assert.isTrue(result.isError(), "获得商品列表必然成功");
|
Assert.isTrue(result.isSuccess(), "获得商品列表必然成功");
|
||||||
List<ProductSpuDetailBO> spus = result.getData();
|
List<ProductSpuDetailBO> spus = result.getData();
|
||||||
rebuildCounts += spus.size();
|
rebuildCounts += spus.size();
|
||||||
// 存储到 ES 中
|
// 存储到 ES 中
|
||||||
List<ESProductDO> products = spus.stream().map(new Function<ProductSpuDetailBO, ESProductDO>() {
|
List<ESProductDO> products = spus.stream().map(this::convert).collect(Collectors.toList());
|
||||||
@Override
|
productRepository.saveAll(products);
|
||||||
public ESProductDO apply(ProductSpuDetailBO spu) {
|
|
||||||
return convert(spu);
|
|
||||||
}
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 设置新的 lastId ,或者结束
|
// 设置新的 lastId ,或者结束
|
||||||
if (spus.size() < REBUILD_FETCH_PER_SIZE) {
|
if (spus.size() < REBUILD_FETCH_PER_SIZE) {
|
||||||
break;
|
break;
|
||||||
@ -66,14 +66,30 @@ public class ProductSearchServiceImpl implements ProductSearchService {
|
|||||||
ProductSpuDetailBO.Sku sku = spu.getSkus().stream().min(Comparator.comparing(ProductSpuDetailBO.Sku::getPrice)).get();
|
ProductSpuDetailBO.Sku sku = spu.getSkus().stream().min(Comparator.comparing(ProductSpuDetailBO.Sku::getPrice)).get();
|
||||||
// 价格计算
|
// 价格计算
|
||||||
CommonResult<CalcSkuPriceBO> calSkuPriceResult = cartService.calcSkuPrice(sku.getId());
|
CommonResult<CalcSkuPriceBO> calSkuPriceResult = cartService.calcSkuPrice(sku.getId());
|
||||||
Assert.isTrue(calSkuPriceResult.isError(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
|
Assert.isTrue(calSkuPriceResult.isSuccess(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
|
||||||
|
// 拼装结果
|
||||||
return new ESProductDO();
|
return ProductSearchConvert.INSTANCE.convert(spu, calSkuPriceResult.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult searchPage(ProductSearchPageDTO searchPageDTO) {
|
public CommonResult<ESProductPageBO> searchPage(ProductSearchPageDTO searchPageDTO) {
|
||||||
return null;
|
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:
|
spring:
|
||||||
data:
|
data:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
@ -6,6 +6,7 @@ spring:
|
|||||||
cluster-nodes: 192.168.88.10:9300
|
cluster-nodes: 192.168.88.10:9300
|
||||||
repositories:
|
repositories:
|
||||||
enable: true
|
enable: true
|
||||||
|
|
||||||
# dubbo
|
# dubbo
|
||||||
dubbo:
|
dubbo:
|
||||||
application:
|
application:
|
||||||
@ -16,4 +17,4 @@ dubbo:
|
|||||||
port: -1
|
port: -1
|
||||||
name: dubbo
|
name: dubbo
|
||||||
scan:
|
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;
|
package cn.iocoder.mall.search.biz.dao;
|
||||||
|
|
||||||
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RunWith(SpringJUnit4ClassRunner.class)
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||||
public class ProductRepositoryTest {
|
public class ProductRepositoryTest {
|
||||||
@ -15,6 +18,7 @@ public class ProductRepositoryTest {
|
|||||||
private ProductRepository productRepository;
|
private ProductRepository productRepository;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore
|
||||||
public void testSave() {
|
public void testSave() {
|
||||||
// productRepository.deleteById(1);
|
// productRepository.deleteById(1);
|
||||||
ESProductDO product = new ESProductDO()
|
ESProductDO product = new ESProductDO()
|
||||||
@ -24,9 +28,23 @@ public class ProductRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore
|
||||||
public void testFindByName() {
|
public void testFindByName() {
|
||||||
ESProductDO product = productRepository.findByName("锤子");
|
ESProductDO product = productRepository.findByName("锤子");
|
||||||
System.out.println(product);
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user