最终效果如下
可以扫描小程序码体验,切换到搜索Tabbar。
小程序端界面实现
网页端实现界面
后端核心源码
对外提供的api
/**
* 搜索网盘资源
*
* @param condition 条件
* @return {@link Result< NetDiskSearchDTO >} 网盘资源列表
*/
@ApiOperation(value = "搜索网盘资源")
@GetMapping("/netdisks/search")
public Result<?> listNetDisksBySearch(ConditionVO condition) {
PageInfo pageInfo = netDiskService.listNetDisksBySearch(condition);
return Result.ok(pageInfo);
}
接口声明
/**
* 搜索网盘资源
*
* @param condition 条件
* @return 网盘资源列表
*/
PageInfo listNetDisksBySearch(ConditionVO condition);
接口实现
@Override
public PageInfo listNetDisksBySearch(ConditionVO condition) {
return searchStrategyContext.executeNetDiskSearchStrategy(condition);
}
执行搜索策略。
提供2种搜索策略,分别是MySQL和ElasticSearch搜索策略。在配置文件进行配置搜索策略。
package top.liuleinet.blog.strategy.context;
import com.github.pagehelper.PageInfo;
import top.liuleinet.blog.dto.NetDiskSearchDTO;
import top.liuleinet.blog.strategy.SearchStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import top.liuleinet.blog.vo.ConditionVO;
import java.util.List;
import java.util.Map;
import static top.liuleinet.blog.enums.SearchModeEnum.getStrategy;
/**
* 搜索策略上下文
*
* @author Liu'Lei
* @date 2021/07/27
*/
@Service
public class SearchStrategyContext {
/**
* 搜索模式
*/
@Value("${search.mode}")
private String searchMode;
@Autowired
private Map<String, SearchStrategy> searchStrategyMap;
/**
* 执行网盘资源搜索策略
*
* @param condition 关键字
* @return {@link List<NetDiskSearchDTO>} 搜索网盘资源
*/
public PageInfo executeNetDiskSearchStrategy(ConditionVO condition) {
return searchStrategyMap.get(getStrategy(searchMode)).searchNetDisk(condition);
}
}
搜索类型枚举
package top.liuleinet.blog.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 搜索类型枚举
*
* @author Liu'Lei
* @date 2021/07/27
*/
@Getter
@AllArgsConstructor
public enum SearchModeEnum {
/**
* mysql
*/
MYSQL("mysql", "mySqlSearchStrategyImpl"),
/**
* elasticsearch
*/
ELASTICSEARCH("elasticsearch", "esSearchStrategyImpl");
/**
* 模式
*/
private final String mode;
/**
* 策略
*/
private final String strategy;
/**
* 获取策略
*
* @param mode 模式
* @return {@link String} 搜索策略
*/
public static String getStrategy(String mode) {
for (SearchModeEnum value : SearchModeEnum.values()) {
if (value.getMode().equals(mode)) {
return value.getStrategy();
}
}
return null;
}
}
配置文件中的搜索策略相关配置
# 搜索模式 可选 elasticsearch或mysql
search:
mode: elasticsearch
es搜索策略实现
package top.liuleinet.blog.strategy.impl;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageInfo;
import top.liuleinet.blog.dto.NetDiskSearchDTO;
import top.liuleinet.blog.strategy.SearchStrategy;
import lombok.extern.log4j.Log4j2;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import top.liuleinet.blog.vo.ConditionVO;
import java.util.List;
import java.util.stream.Collectors;
import static top.liuleinet.blog.constant.CommonConst.*;
import static top.liuleinet.blog.enums.NetDiskStatusEnum.OPEN;
/**
* es搜索策略实现
*
* @author Liu'Lei
* @date 2021/07/27
*/
@Log4j2
@Service("esSearchStrategyImpl")
public class EsSearchStrategyImpl implements SearchStrategy {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public PageInfo searchNetDisk(ConditionVO condition) {
if (StringUtils.isBlank(condition.getKeywords())) {
return new PageInfo();
}
return searchNetdisk(buildNetdiskQuery(condition.getKeywords()), condition);
}
/**
* 搜索网盘资源构造
*
* @param keywords 关键字
* @return es条件构造器
*/
private NativeSearchQueryBuilder buildNetdiskQuery(String keywords) {
// 条件构造器
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 根据关键词搜索网盘资源标题
boolQueryBuilder.must(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("netdiskTitle", keywords)))
.must(QueryBuilders.termQuery("isDelete", FALSE))
.must(QueryBuilders.termQuery("isVip",FALSE))
.must(QueryBuilders.termQuery("status", OPEN.getStatus()));
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
return nativeSearchQueryBuilder;
}
/**
* 网盘资源搜索结果高亮
*
* @param nativeSearchQueryBuilder es条件构造器
* @return 搜索结果
*/
private PageInfo searchNetdisk(NativeSearchQueryBuilder nativeSearchQueryBuilder, ConditionVO condition) {
// 添加网盘资源标题高亮
HighlightBuilder.Field titleField = new HighlightBuilder.Field("netdiskTitle");
titleField.preTags(PRE_TAG);
titleField.postTags(POST_TAG);
nativeSearchQueryBuilder.withHighlightFields(titleField);
// 搜索
try {
SearchHits<NetDiskSearchDTO> search = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), NetDiskSearchDTO.class);
List<NetDiskSearchDTO> netDiskSearchDTOS = search.getSearchHits().stream().map(hit -> {
NetDiskSearchDTO netDisk = hit.getContent();
// 获取标题高亮数据
List<String> titleHighLightList = hit.getHighlightFields().get("netdiskTitle");
if (CollectionUtils.isNotEmpty(titleHighLightList)) {
// 替换标题数据
netDisk.setNetdiskTitle(titleHighLightList.get(0));
}
return netDisk;
}).collect(Collectors.toList());
Page page = new Page(condition.getCurrent().intValue(), condition.getSize().intValue());
page.setTotal(netDiskSearchDTOS.size());
int startIndex = (condition.getCurrent().intValue() - 1) * condition.getSize().intValue();
int endIndex = Math.min(startIndex + condition.getSize().intValue(),netDiskSearchDTOS.size());
page.addAll(netDiskSearchDTOS.subList(startIndex,endIndex));
PageInfo pageInfo = new PageInfo<>(page);
return pageInfo;
} catch (Exception e) {
log.error(e.getMessage());
}
return new PageInfo();
}
}
ConditionVO类如下
package top.liuleinet.blog.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 查询条件
*
* @author Liu'Lei
* @date 2021/07/29
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "查询条件")
public class ConditionVO {
/**
* 页码
*/
@ApiModelProperty(name = "current", value = "页码", dataType = "Long")
private Long current;
/**
* 条数
*/
@ApiModelProperty(name = "size", value = "条数", dataType = "Long")
private Long size;
/**
* 搜索内容
*/
@ApiModelProperty(name = "keywords", value = "搜索内容", dataType = "String")
private String keywords;
/**
* 网盘分类
*/
@ApiModelProperty(name = "category", value = "分类", dataType = "String")
private String category;
/**
* 类型
*/
@ApiModelProperty(name = "type", value = "类型", dataType = "Integer")
private Integer type;
/**
* 状态
*/
@ApiModelProperty(name = "status", value = "状态", dataType = "Integer")
private Integer status;
/**
* 是否删除
*/
@ApiModelProperty(name = "isDelete", value = "是否删除", dataType = "Integer")
private Integer isDelete;
/**
* 是否vip网盘资源
*/
@ApiModelProperty(name = "isVip", value = "是否vip网盘资源", dataType = "Integer")
private Integer isVip;
}
小程序端JavaScript代码逻辑
import {
searchNetdisk
} from '../../services/api'
const app = getApp();
// 在页面中定义插屏广告
let interstitialAd = null;
Page({
data: {
StatusBar: app.globalData.StatusBar,
CustomBar: app.globalData.CustomBar,
noContentImage: '/images/default/no-data.png',
historyKeyword: [],
preRecommendKeyword: [
'PPT模板', '动漫', 'Java', '狮子王', '万里归途', '阿甘正传', '星际穿越', '满江红', '流浪地球2', '长津湖'
],
recommendKeyword: [],
recommendKeywordView: true,
title: '资源搜索',
keywords: '',
current: 1,
size: 10,
loading: false,
isSearch: false,
searchResult: {},
content: [],
windowHeight: app.globalData.windowHeight
},
async onLoad() {
var that = this;
var historyKeyword = wx.getStorageSync('netdiskHistory');
if (historyKeyword) {
that.setData({
historyKeyword: historyKeyword
})
}
var preRecommendKeyword = that.data.preRecommendKeyword;
var recommendKeyword = preRecommendKeyword.slice(0, 5);
that.setData({
recommendKeyword: recommendKeyword
})
// 在页面onLoad回调事件中创建插屏广告实例
if (wx.createInterstitialAd) {
interstitialAd = wx.createInterstitialAd({
adUnitId: 'adunit-9b1d1f3ffba7cf75'
})
interstitialAd.onLoad(() => {})
interstitialAd.onError((err) => {})
interstitialAd.onClose(() => {})
}
},
async clickSearch(e) {
var that = this;
that.setData({
keywords: e.currentTarget.dataset.keyword
})
await that.search();
},
/**
* 搜索
* @param {*} event
*/
async search() {
var that = this;
var keywords = that.data.keywords;
var historyKeywordArray = that.data.historyKeyword;
if (historyKeywordArray.indexOf(keywords) === -1) {
historyKeywordArray.unshift(keywords);
if (historyKeywordArray.length > 10) {
historyKeywordArray.pop();
}
wx.setStorageSync('netdiskHistory', historyKeywordArray);
}
that.setData({
historyKeyword: historyKeywordArray
})
await that.getNetdiskList(true, keywords);
that.setData({
isSearch: true,
})
},
/**
* 搜索输入事件
* @param {s} event
*/
searchInput(event) {
var that = this;
//console.log(event)
that.setData({
keywords: event.detail.value,
isSearch: true
})
},
/**
* 清空历史搜索关键字
*/
clearHistoryKeyword() {
var that = this;
wx.removeStorageSync('netdiskHistory');
that.setData({
historyKeyword: []
})
},
changeRecommend() {
var that = this;
var arr = that.data.preRecommendKeyword;
var randomCommendArr = that.getRandomArrayElements(arr, 5);
that.setData({
recommendKeyword: randomCommendArr
})
},
changeRecommendView() {
var that = this;
that.setData({
recommendKeywordView: !that.data.recommendKeywordView
})
},
/**
* 顶部刷新
*/
// async onPullDownRefresh() {
// // 显示顶部刷新图标
// wx.showNavigationBarLoading()
// const that = this
// const content = await this.getNetdiskList(true, this.data.keywords)
// that.setData({
// content: content
// })
// setTimeout(function () {
// // 隐藏导航栏加载框
// wx.hideNavigationBarLoading()
// // 停止当前页面下拉刷新。
// wx.stopPullDownRefresh()
// }, 1500)
// },
/**
* 向下滑动拉去下一页
*/
async onReachBottom() {
var that = this;
var current = ++that.data.current;
that.setData({
current: current,
content: []
})
// 在适合的场景显示插屏广告
if (interstitialAd && current == 2) {
interstitialAd.show().catch((err) => {
console.error(err);
})
}
//console.log(current)
if (current > that.data.searchResult.pages) {
wx.showToast({
title: '已经是最后一页',
})
}
var keyword = that.data.keywords;
await that.getNetdiskList(false,keyword);
},
/**
* 向上一页
*/
async toPreviousPage() {
var that = this;
var current = --that.data.current;
that.setData({
current: current
})
var keyword = that.data.keywords;
await that.getNetdiskList(false, keyword);
},
/**
* 获取网盘资源列表
*/
async getNetdiskList(init, keywords) {
var that = this;
if (keywords.length == 0) {
wx.showToast({
title: '关键词不能为空!',
})
return;
}
if (init) {
that.initParams();
}
var current = that.data.current;
var size = that.data.size;
var param = {
keywords: keywords,
current: current,
size: size
}
var result = await searchNetdisk(param);
var resData = result.data;
var resDataList = resData.list;
that.setData({
searchResult: resData,
content: resDataList
})
return;
},
/**
* 初始化页数和内容
*/
initParams() {
var that = this;
that.setData({
searchResult: {},
content: [],
current: 1
})
},
toHome() {
var that = this;
that.setData({
isSearch:false
})
},
// backPre() {
// this.setData({
// isSearch: !this.data.isSearch
// })
// },
/**
* 数组中随机取几个元素
* @param {*} arr 来源数组
* @param {*} count 取数量
*/
getRandomArrayElements(arr, count) {
var shuffled = arr.slice(0),
i = arr.length,
min = i - count,
temp, index;
while (i-- > min) {
index = Math.floor((i + 1) * Math.random());
temp = shuffled[index];
shuffled[index] = shuffled[i];
shuffled[i] = temp;
}
return shuffled.slice(min);
},
/**
* 分享
* @param {*} res
*/
onShareAppMessage: function (res) {
var that = this;
return {
title: that.data.keywords + '!,点击打开',
path: '/pages/netdisk/index'
}
},
onShareTimeline: function (res) {
var that = this;
return {
title: that.data.keywords + '!,点击打开'
}
}
})