增加:综合浏览增加搜索推荐功能

master
guoxin 1 year ago
parent f5ed96dca8
commit dae3c5b24a
  1. 6
      shandan-browser/src/main/java/com/keyware/shandan/BrowserApplication.java
  2. 139
      shandan-browser/src/main/java/com/keyware/shandan/browser/SearchLogProcessor.java
  3. 1
      shandan-browser/src/main/java/com/keyware/shandan/browser/controller/ReportController.java
  4. 1
      shandan-browser/src/main/java/com/keyware/shandan/browser/controller/SearchController.java
  5. 34
      shandan-browser/src/main/java/com/keyware/shandan/browser/controller/SearchSuggestController.java
  6. 104
      shandan-browser/src/main/resources/static/css/browser.css
  7. 84
      shandan-browser/src/main/resources/static/js/browser.js
  8. 49
      shandan-browser/src/main/resources/view/browser.html
  9. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/config/BianmuDataCache.java
  10. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/config/BianmuDataCacheConfig.java
  11. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/config/BrowserWebMvcConfig.java
  12. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/entity/FullSearchParam.java
  13. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/entity/PageVo.java
  14. 6
      shandan-system/src/main/java/com/keyware/shandan/browser/entity/SearchConditionVo.java
  15. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/entity/SearchResultRow.java
  16. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/entity/SearchResultSort.java
  17. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/enums/ConditionLogic.java
  18. 0
      shandan-system/src/main/java/com/keyware/shandan/browser/enums/ViewType.java
  19. 29
      shandan-system/src/main/java/com/keyware/shandan/frame/aspect/AppLogAspect.java
  20. 2
      shandan-system/src/main/java/com/keyware/shandan/system/entity/SysOperateLog.java
  21. 4
      shandan-system/src/main/resources/static/js/sys/dict/dict.js

@ -1,7 +1,9 @@
package com.keyware.shandan;
import com.keyware.shandan.browser.SearchLogProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
@ -15,7 +17,9 @@ import org.springframework.security.core.session.SessionRegistryImpl;
@SpringBootApplication
public class BrowserApplication {
public static void main(String[] args) {
SpringApplication.run(BrowserApplication.class, args);
ApplicationContext context = SpringApplication.run(BrowserApplication.class, args);
SearchLogProcessor processor = context.getBean(SearchLogProcessor.class);
processor.start();
}
/**

@ -0,0 +1,139 @@
package com.keyware.shandan.browser;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.keyware.shandan.common.util.StringUtils;
import com.keyware.shandan.system.entity.SysOperateLog;
import com.keyware.shandan.system.service.SysOperateLogService;
import com.keyware.shandan.system.utils.DictUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用于处理分析系统热词和用户搜索历史
*/
@Slf4j
@Component
public class SearchLogProcessor extends Thread {
private final SysOperateLogService logService;
private int cycle = 7;
private DateField unit = DateField.DAY_OF_MONTH;
private final Map<String, Integer> systemWords = new HashMap<>();
public SearchLogProcessor(SysOperateLogService logService) {
this.logService = logService;
}
@Override
public void run() {
log.info("搜索日志处理程序启动成功");
while (true) {
parseConfig();
systemWords.clear();
LambdaQueryWrapper<SysOperateLog> query = new LambdaQueryWrapper<>();
DateTime date = DateUtil.offset(new Date(), unit, cycle);
query.gt(SysOperateLog::getOperateTime, date.toJdkDate());
logService.list(query).forEach(logs -> {
String word = parseLogs(logs);
if (StringUtils.hasText(word)) {
addSystemWords(word);
}
});
try {
Thread.sleep(1000 * 60 * 30);
} catch (InterruptedException ignore) {
}
}
}
/**
* 获取用户的搜索历史
*
* @param loginName
* @return
*/
public List<String> getUserSearchHistory(String loginName) {
List<String> searchHistory = new ArrayList<>();
LambdaQueryWrapper<SysOperateLog> query = new LambdaQueryWrapper<>();
query.eq(SysOperateLog::getLoginName, loginName).orderByDesc(SysOperateLog::getOperateTime).last("limit 30");
for (SysOperateLog logs : logService.list(query)) {
String word = parseLogs(logs);
if (StringUtils.hasText(word) && !searchHistory.contains(word)) {
searchHistory.add(word);
}
}
return searchHistory;
}
/**
* 获取系统级热词
*
* @return
*/
public List<String> getSystemWords() {
return systemWords
.entrySet()
.stream()
//.sorted(Comparator.comparingInt(Map.Entry::getValue))
.sorted((a, b) -> b.getValue() - a.getValue())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
private String parseLogs(SysOperateLog logs) {
String value = null;
String url = logs.getUrl();
if (url.startsWith("/browser/search/")) {
String paramsText = logs.getParams();
if (JSONUtil.isJsonArray(logs.getParams())) {
for (Object object : JSONArray.parseArray(paramsText)) {
JSONObject json = (JSONObject) object;
String fieldName = json.getString("fieldName");
if ("metadataName".equals(fieldName)) {
value = json.getString("fieldValue");
break;
}
}
} else {
JSONObject json = JSONObject.parseObject(paramsText);
if (json.containsKey("search")) {
value = json.getString("search");
}
}
}
return value;
}
private void addSystemWords(String word) {
Integer count = systemWords.get(word);
count = count == null ? 0 : count;
systemWords.put(word, ++count);
}
private void parseConfig() {
String systemWordsConfig = DictUtil.getPublicDict("browser_system_words_config");
if (StringUtils.hasText(systemWordsConfig)) {
systemWordsConfig = systemWordsConfig.toLowerCase();
if (systemWordsConfig.endsWith("d")) {
cycle = -Math.abs(Integer.parseInt(systemWordsConfig.replace("d", "")));
unit = DateField.DAY_OF_YEAR;
} else if (systemWordsConfig.endsWith("m")) {
cycle = -Math.abs(Integer.parseInt(systemWordsConfig.replace("m", "")));
unit = DateField.MONTH;
} else if (systemWordsConfig.endsWith("y")) {
cycle = -Math.abs(Integer.parseInt(systemWordsConfig.replace("y", "")));
unit = DateField.YEAR;
}
}
}
}

@ -11,7 +11,6 @@ import com.keyware.shandan.browser.config.BianmuDataCache;
import com.keyware.shandan.browser.entity.SearchConditionVo;
import com.keyware.shandan.browser.entity.ExportParam;
import com.keyware.shandan.browser.entity.ReportVo;
import com.keyware.shandan.browser.service.ReportService;
import com.keyware.shandan.browser.service.SearchService;
import com.keyware.shandan.browser.service.impl.ReportDateServiceImpl;
import com.keyware.shandan.browser.service.impl.ReportNumberServiceImpl;

@ -187,6 +187,7 @@ public class SearchController {
return Result.of(new PageVo());
}
@AppLog(operate = "检索数据资源")
@PostMapping("/metadata/all/page/{directoryId}")
public Result<Object> searchMetadataPage(@PathVariable String directoryId, SearchConditionVo condition) {
return Result.of(metadataSearchService.searchByCondition(directoryId, condition));

@ -0,0 +1,34 @@
package com.keyware.shandan.browser.controller;
import com.keyware.shandan.browser.SearchLogProcessor;
import com.keyware.shandan.common.entity.Result;
import com.keyware.shandan.frame.config.security.SecurityUtil;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 搜索建议前端控制器
*/
@RestController
@RequestMapping("/search/suggest")
public class SearchSuggestController {
private final SearchLogProcessor logProcessor;
public SearchSuggestController(SearchLogProcessor logProcessor) {
this.logProcessor = logProcessor;
}
@GetMapping("/user/history")
public Result<Object> getUserHistory() {
String loginName = SecurityUtil.getLoginUsername();
return Result.of(logProcessor.getUserSearchHistory(loginName));
}
@GetMapping("/system/words")
public Result<Object> getSystemWords() {
return Result.of(logProcessor.getSystemWords());
}
}

@ -1,24 +1,57 @@
.layui-card .layui-form-item {
margin-bottom: 10
}
.layui-table-tool-self{top:auto; bottom: 10px;}
.layui-table-tool-self {
top: auto;
bottom: 10px;
}
.layui-card .layui-form-item .layui-inline button {
margin-top: 10px
}
.layui-table-tool-temp {
padding-right: 0;
}
.dict-component .layui-form-select, .dict-component .layui-select-title{width: fit-content !important;}
.b-search-slide-div .layui-form-item .layui-input-inline{display: flex;flex-direction: row;}
.select-box .layui-form-select{position: unset}
.select-box .layui-form-select input, .dict-component .layui-select-title input{min-width: 260px;}
.select-box .layui-form-select{width: auto}
.select-box i.layui-edge, .dict-component i.layui-edge{right: 20px !important;}
.input-date.c-input{min-width: 340px}
.mark-selector, .mark-selector .layui-form-select, .mark-selector input{width: 340px !important;}
.dict-component .layui-form-select, .dict-component .layui-select-title {
width: fit-content !important;
}
.b-search-slide-div .layui-form-item .layui-input-inline {
display: flex;
flex-direction: row;
}
.select-box .layui-form-select {
position: unset
}
.select-box .layui-form-select input, .dict-component .layui-select-title input {
min-width: 260px;
}
.select-box .layui-form-select {
width: auto
}
.select-box i.layui-edge, .dict-component i.layui-edge {
right: 20px !important;
}
.input-date.c-input {
min-width: 340px
}
.mark-selector, .mark-selector .layui-form-select, .mark-selector input {
width: 340px !important;
}
.layui-form-checkbox[lay-skin="primary"] {
padding-left: 20px;
}
.layui-form-checked[lay-skin="primary"] i {
border-color: #5FB878 !important;
background-color: #FFFFFF;
@ -30,6 +63,7 @@
padding-left: 1px;
font-size: 10px;
}
.layui-table-view .layui-form-checkbox[lay-skin="primary"] i {
width: 14px !important;
height: 14px !important;
@ -38,10 +72,60 @@
padding-left: 1px;
font-size: 10px;
}
.layui-tab-title{width: 100%}
.layui-tab-title {
width: 100%
}
.layui-tab-title .layui-form-checkbox.layui-form-checked span {
color: #009688;
}
li[data-key="markTag"] {
text-align: left;
}
.hot-words-box {
background: #FFFFFF;
max-height: 500px;
position: absolute;
left: 124px;
border: 1px solid #dedede;
border-radius: 0 0 5px 5px;
z-index: 1000;
display: none;
padding: 5px 10px;
}
.hot-words-box h6 {
font-weight: bold;
}
.words-history, .system-words {
}
.hot-words-box ul > li {
border-radius: 4px;
padding: 0 5px;
}
.words-history ul > li {
color: #639;
}
.hot-words-box ul > li:hover {
background: #f7f7f7;
overflow: hidden;
cursor: pointer;
}
.mengban {
width: 100%;
display: none;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: #0C0C0C47;
z-index: 800
}

@ -160,6 +160,11 @@ layui.use(['layer', 'listPage', 'globalTree', 'gtable', 'form', 'element', 'drop
}
renderConditionTabByForm(true);
}
const suggest = new SearchSuggest(layui, 'searchKeyInput')
suggest.onClick((text) => {
})
}
},
});
@ -199,7 +204,7 @@ layui.use(['layer', 'listPage', 'globalTree', 'gtable', 'form', 'element', 'drop
$('#condition-btn').click(function () {
const display = $('#condition-div').css('display');
if (display == 'none') {
$('#mengban').show().on('click', function () {
$('div[lay-id="dirMetadataTable"] .mengban').show().on('click', function () {
$(this).hide();
$('#condition-btn').click();
});
@ -222,7 +227,7 @@ layui.use(['layer', 'listPage', 'globalTree', 'gtable', 'form', 'element', 'drop
$('#condition-cancel-btn').on('click', function () {
$('#condition-div').slideToggle(100, 'linear')
$('#mengban').hide();
$('div[lay-id="dirMetadataTable"] .mengban').hide();
});
// 监听条件标签删除事件
@ -482,6 +487,7 @@ layui.use(['layer', 'listPage', 'globalTree', 'gtable', 'form', 'element', 'drop
cols: THeadSetLayer.convertColumns(theadConfig, 'dirFileTable'),
done: () => {
form.val('full-search-form', {queryType});
new SearchSuggest(layui, 'fileSearchKeyInput');
}
},
});
@ -512,6 +518,80 @@ layui.use(['layer', 'listPage', 'globalTree', 'gtable', 'form', 'element', 'drop
})
class SearchSuggest {
isHide = true;
// 系统级热词
systemHotWords = [];
// 用户级历史搜索
userHistoryWords = [];
constructor(lay, elemId) {
this.layer = lay.layer;
this.$bindElem = $(`#${elemId}`);
this.$searchBtn = this.$bindElem.parent().find('button[lay-event="query"]');
this.$shadeBox = $('.mengban');
this.$elem = this.$bindElem.parent().find('.hot-words-box')
this.bindElem();
// 获取系统级别的热词推荐
Util.get(`/search/suggest/system/words`, {}, false).then(res => {
if (res.flag) {
this.systemHotWords = res.data;
}
})
}
bindElem() {
this.$bindElem.on('focus', () => this.show())
// 获取系统级别的热词推荐
Util.get(`/search/suggest/user/history`, {}, false).then(res => {
if (res.flag) {
this.userHistoryWords = res.data;
}
})
}
show() {
if (this.isHide) {
const e_width = this.$bindElem.outerWidth(),
e_height = this.$bindElem.outerHeight(),
e_border_width = this.$bindElem.css('border-left-width').replace('px', ''),
width = e_width - parseFloat(e_border_width);
this.$bindElem.css({'z-index': '999', position: 'relative'})
this.$searchBtn.css({'z-index': '999', position: 'relative'});
this.$elem.css({'top': `${e_height + 9}px`, 'width': width + 'px'}).show();
this.$shadeBox.show().on('click', () => this.hide());
this.$elem.find('.words-history ul').html('');
for (let i = 0; i < Math.min(this.userHistoryWords.length, 5); i++) {
const word = this.userHistoryWords[i];
this.$elem.find('.words-history ul').append(`<li>${word}</li>`);
}
this.$elem.find('.system-words ul').html('');
for (let i = 0; i < Math.min(this.systemHotWords.length, 5); i++) {
const word = this.systemHotWords[i];
this.$elem.find('.system-words ul').append(`<li>${word}</li>`);
}
this.$elem.find('ul li').on('click', (event) => {
const $li = $(event.target);
this.$bindElem.val($li.text());
this.$searchBtn.click();
});
this.isHide = false;
}
}
hide() {
this.$elem.hide();
this.$shadeBox.hide();
this.$bindElem.css('display', 'unset');
this.isHide = true;
this.bindElem();
}
onClick(onclick) {
this.onclick = onclick;
}
}
/**
* 表格表头配置组件
*/

@ -27,7 +27,7 @@
<div class="layui-card-body">
<div class="layui-tab layui-tab-brief" id="directoryTreeBody" lay-filter="directoryTreeBody">
<ul class="layui-tab-title">
<li class="layui-this metadata-table">基础数据目录</li>
<li class="layui-this metadata-table">数据资源目录</li>
<!--<li>视图</li>--> <!-- TODO 暂时隐藏 -->
</ul>
<div class="layui-tab-content">
@ -90,13 +90,24 @@
<div class="layui-btn-container"
style="display: flex; justify-content: flex-start; margin-bottom: 0">
<div style="position: unset;">
<input type="checkbox" name="preciseQuery" title="全字符匹配" lay-skin="primary" value="eq" lay-filter="precise-query">
<input type="checkbox" name="preciseQuery" title="全字符匹配"
lay-skin="primary" value="eq" lay-filter="precise-query">
<input type="text" id="searchKeyInput" name="searchKeyInput"
autocomplete="off"
placeholder="请输入关键字查询" class="layui-input layui-btn-sm">
<button class="layui-btn layui-btn-sm" id="begin-search-btn"
lay-event="query">查询
</button>
<div class="layui-anim layui-anim-fadein hot-words-box">
<div class="words-history">
<h6>搜索历史</h6>
<ul></ul>
</div>
<div class="system-words">
<h6>热词推荐</h6>
<ul></ul>
</div>
</div>
</div>
<div style="position: unset;">
<button class="layui-btn layui-btn-primary layui-border-green"
@ -191,17 +202,9 @@
lay-filter="condition-tab" style="margin-bottom: -10px; display:flex;">
<label class="condition-title-label">查询条件:</label>
<ul class="layui-tab-title"></ul>
<!--<button type="button" class="layui-btn layui-border-green layui-btn-primary"
style="margin-left:auto; border:0;padding-right:0;" id="mark-filter" title="筛选已选择的标签">
<i class="layui-icon layui-icon-note"></i>标签
</button>-->
<!--<div style="width: 500px; height: 300px;border: 1px solid">
</div>-->
</div>
</div>
<div id="mengban"
style="width: 100%; display: none; height: 100%; position: fixed; top:0px; left: 0px; background: #0C0C0C47; z-index: 1"></div>
<div class="mengban"></div>
</script>
<script type="text/html" id="rowToolBar">
<div class="layui-btn-container">
@ -209,20 +212,36 @@
</div>
</script>
</div>
<div class="layui-tab-item">
<div class="current-position">当前位置:<label></label></div>
<table class="layui-hide" id="dirFileTable" lay-filter="dirFileTable"></table>
<script type="text/html" id="fileTableToolBar">
<div class="layui-btn-container layui-form" lay-filter="full-search-form">
<div class="layui-layout-left" style="top:10px; left: 20px">
<input type="checkbox" name="queryType" title="全字符匹配" lay-skin="primary" value="phrase" lay-filter="query-type">
<div class="layui-form" lay-filter="full-search-form"
style="display: flex;flex-direction: column;">
<div class="layui-btn-container"
style="display: flex; justify-content: flex-start; margin-bottom: 0">
<div style="position: unset;">
<input type="checkbox" name="queryType" title="全字符匹配"
lay-skin="primary"
value="phrase" lay-filter="query-type">
<input type="text" id="fileSearchKeyInput" name="searchKeyInput"
autocomplete="off" class="layui-input layui-btn-sm"
placeholder="请输入关键字查询" style="width: 330px">
<button class="layui-btn layui-btn-sm" lay-event="query">查询</button>
<div class="layui-anim layui-anim-fadein hot-words-box">
<div class="words-history">
<h6>搜索历史</h6>
<ul></ul>
</div>
<div class="system-words">
<h6>热词推荐</h6>
<ul></ul>
</div>
</div>
</div>
</div>
</div>
<div class="mengban"></div>
</script>
<script type="text/html" id="fileRowToolBar">
<div class="layui-btn-container">

@ -1,5 +1,6 @@
package com.keyware.shandan.browser.entity;
import com.alibaba.fastjson.JSONObject;
import com.keyware.shandan.browser.enums.ConditionLogic;
import com.keyware.shandan.common.util.StringUtils;
import joptsimple.internal.Strings;
@ -62,6 +63,11 @@ public class SearchConditionVo extends PageVo implements Serializable {
return sort.getOrderBy();
}
@Override
public String toString() {
return JSONObject.toJSONString(this.conditions);
}
/**
* 搜索条件项
*/

@ -1,10 +1,14 @@
package com.keyware.shandan.frame.aspect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.keyware.shandan.system.entity.SysOperateLog;
import com.keyware.shandan.browser.entity.FullSearchParam;
import com.keyware.shandan.browser.entity.SearchConditionVo;
import com.keyware.shandan.common.util.AspectUtil;
import com.keyware.shandan.common.util.StringUtils;
import com.keyware.shandan.frame.annotation.AppLog;
import com.keyware.shandan.frame.config.security.SecurityUtil;
import com.keyware.shandan.system.entity.SysOperateLog;
import com.keyware.shandan.system.service.SysOperateLogService;
import com.keyware.shandan.system.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
@ -24,6 +28,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;
/**
* 应用操作日志切面
@ -64,10 +69,28 @@ public class AppLogAspect {
argsArray.add(arg);
}
}
sysOperateLog.setParams(argsArray.toJSONString());
if (argsArray.size() > 1) {
Object obj = argsArray.get(1);
if (obj instanceof SearchConditionVo) {
SearchConditionVo conditions = (SearchConditionVo) obj;
List<SearchConditionVo.Item> items = conditions.getConditions();
if (!items.isEmpty()) {
sysOperateLog.setParams(conditions.toString());
operateLogService.save(sysOperateLog);
log.info(sysOperateLog.toString());
}
}
}else if(argsArray.size() == 1){
Object obj = argsArray.get(0);
if (obj instanceof FullSearchParam) {
FullSearchParam param = (FullSearchParam) obj;
if(StringUtils.hasText(param.getSearch())){
sysOperateLog.setParams(JSON.toJSONString(param));
operateLogService.save(sysOperateLog);
log.info(sysOperateLog.toString());
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("请求参数解析异常", e);

@ -1,5 +1,6 @@
package com.keyware.shandan.system.entity;
import com.baomidou.mybatisplus.annotation.OrderBy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -49,6 +50,7 @@ public class SysOperateLog implements Serializable {
* 操作时间
*/
@TableField("OPERATE_TIME")
//@OrderBy
private Date operateTime;
/**

@ -163,7 +163,7 @@ layui.use(['form', 'layer', 'gtable'], function () {
page: false,
cols: [[
{field: 'dictName', title: '字典项名称', minWidth: 120},
{field: 'typeName', title: '所属字典', minWidth: 120},
{field: 'typeName', title: '所属字典', minWidth: 100},
{
field: 'dictState',
title: '字典项状态',
@ -171,7 +171,7 @@ layui.use(['form', 'layer', 'gtable'], function () {
align: 'center',
templet: d => d.dictState ? '启用' : '禁用'
},
{field: 'dictDesc', title: '字典项描述', minWidth: 160},
{field: 'dictValue', title: '字典', minWidth: 160},
{fixed: 'right', title: '操作', toolbar: '#rowToolBar', align: 'center', width: 120}
]]
});