feat(warehouse): 优化库存搜索功能支持关键词分词匹配
- 将产品信息搜索字段改为关键词搜索,支持输入多个条件 - 实现分词搜索算法,支持按名称/规格/品牌/类别/单位/库区等多维度匹配 - 添加搜索结果相关性评分排序机制 - 优化移动端和PC端搜索界面提示文案 - 实现关键词高亮显示和精确/前缀/包含匹配策略 - 添加最大分词数量限制防止过度匹配 - 重构后端分页处理逻辑以适应新的搜索模式
This commit is contained in:
parent
e76c07fb5e
commit
46ec60d6f5
@ -1,5 +1,6 @@
|
|||||||
package com.ruoyi.web.controller.warehouse;
|
package com.ruoyi.web.controller.warehouse;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -11,10 +12,14 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.common.annotation.Log;
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.constant.HttpStatus;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.PageDomain;
|
||||||
import com.ruoyi.common.core.page.TableDataInfo;
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.core.page.TableSupport;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||||
import com.ruoyi.warehouse.domain.OutRecord;
|
import com.ruoyi.warehouse.domain.OutRecord;
|
||||||
import com.ruoyi.warehouse.domain.StockInfo;
|
import com.ruoyi.warehouse.domain.StockInfo;
|
||||||
@ -31,10 +36,31 @@ public class StockInfoController extends BaseController
|
|||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(StockInfo stockInfo)
|
public TableDataInfo list(StockInfo stockInfo)
|
||||||
{
|
{
|
||||||
|
if (StringUtils.isNotBlank(stockInfo.getProductName()))
|
||||||
|
{
|
||||||
|
return getKeywordDataTable(stockInfoService.selectStockInfoList(stockInfo));
|
||||||
|
}
|
||||||
startPage();
|
startPage();
|
||||||
return getDataTable(stockInfoService.selectStockInfoList(stockInfo));
|
return getDataTable(stockInfoService.selectStockInfoList(stockInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TableDataInfo getKeywordDataTable(List<StockInfo> list)
|
||||||
|
{
|
||||||
|
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||||
|
int pageNum = pageDomain.getPageNum() == null || pageDomain.getPageNum() < 1 ? 1 : pageDomain.getPageNum();
|
||||||
|
int pageSize = pageDomain.getPageSize() == null || pageDomain.getPageSize() < 1 ? 10 : pageDomain.getPageSize();
|
||||||
|
int total = list == null ? 0 : list.size();
|
||||||
|
int fromIndex = Math.min((pageNum - 1) * pageSize, total);
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||||
|
|
||||||
|
TableDataInfo rspData = new TableDataInfo();
|
||||||
|
rspData.setCode(HttpStatus.SUCCESS);
|
||||||
|
rspData.setMsg("查询成功");
|
||||||
|
rspData.setTotal(total);
|
||||||
|
rspData.setRows(total == 0 ? Collections.emptyList() : list.subList(fromIndex, toIndex));
|
||||||
|
return rspData;
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('warehouse:stock:export')")
|
@PreAuthorize("@ss.hasPermi('warehouse:stock:export')")
|
||||||
@Log(title = "库存信息", businessType = BusinessType.EXPORT)
|
@Log(title = "库存信息", businessType = BusinessType.EXPORT)
|
||||||
@PostMapping("/export")
|
@PostMapping("/export")
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
package com.ruoyi.warehouse.service.impl;
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.warehouse.domain.OutRecord;
|
import com.ruoyi.warehouse.domain.OutRecord;
|
||||||
import com.ruoyi.warehouse.domain.StockInfo;
|
import com.ruoyi.warehouse.domain.StockInfo;
|
||||||
import com.ruoyi.warehouse.mapper.StockInfoMapper;
|
import com.ruoyi.warehouse.mapper.StockInfoMapper;
|
||||||
@ -13,6 +16,8 @@ import com.ruoyi.warehouse.service.IStockInfoService;
|
|||||||
@Service
|
@Service
|
||||||
public class StockInfoServiceImpl implements IStockInfoService
|
public class StockInfoServiceImpl implements IStockInfoService
|
||||||
{
|
{
|
||||||
|
private static final int MAX_KEYWORD_TOKEN_COUNT = 5;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StockInfoMapper stockInfoMapper;
|
private StockInfoMapper stockInfoMapper;
|
||||||
|
|
||||||
@ -22,7 +27,121 @@ public class StockInfoServiceImpl implements IStockInfoService
|
|||||||
@Override
|
@Override
|
||||||
public List<StockInfo> selectStockInfoList(StockInfo stockInfo)
|
public List<StockInfo> selectStockInfoList(StockInfo stockInfo)
|
||||||
{
|
{
|
||||||
return stockInfoMapper.selectStockInfoList(stockInfo);
|
List<String> tokens = parseKeywordTokens(stockInfo);
|
||||||
|
List<StockInfo> list = stockInfoMapper.selectStockInfoList(stockInfo);
|
||||||
|
if (tokens.isEmpty())
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
List<ScoredStock> scoredList = new ArrayList<>();
|
||||||
|
for (StockInfo item : list)
|
||||||
|
{
|
||||||
|
int score = scoreStock(item, tokens);
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
scoredList.add(new ScoredStock(item, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scoredList.sort((left, right) -> {
|
||||||
|
int scoreCompare = Integer.compare(right.score, left.score);
|
||||||
|
if (scoreCompare != 0)
|
||||||
|
{
|
||||||
|
return scoreCompare;
|
||||||
|
}
|
||||||
|
long leftId = left.stock.getId() == null ? 0L : left.stock.getId();
|
||||||
|
long rightId = right.stock.getId() == null ? 0L : right.stock.getId();
|
||||||
|
return Long.compare(rightId, leftId);
|
||||||
|
});
|
||||||
|
List<StockInfo> result = new ArrayList<>(scoredList.size());
|
||||||
|
for (ScoredStock item : scoredList)
|
||||||
|
{
|
||||||
|
result.add(item.stock);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseKeywordTokens(StockInfo stockInfo)
|
||||||
|
{
|
||||||
|
if (stockInfo == null || StringUtils.isBlank(stockInfo.getProductName()))
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
LinkedHashSet<String> tokens = new LinkedHashSet<>();
|
||||||
|
String keyword = stockInfo.getProductName().trim().toLowerCase();
|
||||||
|
for (String item : keyword.split("[\\s,,;;、]+"))
|
||||||
|
{
|
||||||
|
if (StringUtils.isNotBlank(item))
|
||||||
|
{
|
||||||
|
tokens.add(item.trim());
|
||||||
|
}
|
||||||
|
if (tokens.size() >= MAX_KEYWORD_TOKEN_COUNT)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tokens.isEmpty())
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
stockInfo.getParams().put("keywordTokens", new ArrayList<>(tokens));
|
||||||
|
return new ArrayList<>(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int scoreStock(StockInfo stock, List<String> tokens)
|
||||||
|
{
|
||||||
|
int totalScore = 0;
|
||||||
|
for (String token : tokens)
|
||||||
|
{
|
||||||
|
int tokenScore = 0;
|
||||||
|
tokenScore += scoreField(stock.getProductName(), token, 120, 90, 60);
|
||||||
|
tokenScore += scoreField(stock.getSpec(), token, 90, 70, 45);
|
||||||
|
tokenScore += scoreField(stock.getBrand(), token, 80, 60, 40);
|
||||||
|
tokenScore += scoreField(stock.getCategory(), token, 70, 50, 35);
|
||||||
|
tokenScore += scoreField(stock.getAreaName(), token, 65, 45, 30);
|
||||||
|
tokenScore += scoreField(stock.getUnit(), token, 35, 0, 20);
|
||||||
|
tokenScore += scoreField(stock.getProductCode(), token, 0, 0, 10);
|
||||||
|
tokenScore += scoreField(stock.getAreaCode(), token, 0, 0, 10);
|
||||||
|
if (tokenScore <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
totalScore += tokenScore;
|
||||||
|
}
|
||||||
|
return totalScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int scoreField(String value, String token, int exactScore, int prefixScore, int containsScore)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(value) || StringUtils.isBlank(token))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String lowerValue = value.toLowerCase();
|
||||||
|
if (exactScore > 0 && lowerValue.equals(token))
|
||||||
|
{
|
||||||
|
return exactScore;
|
||||||
|
}
|
||||||
|
if (prefixScore > 0 && lowerValue.startsWith(token))
|
||||||
|
{
|
||||||
|
return prefixScore;
|
||||||
|
}
|
||||||
|
if (containsScore > 0 && lowerValue.contains(token))
|
||||||
|
{
|
||||||
|
return containsScore;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ScoredStock
|
||||||
|
{
|
||||||
|
private final StockInfo stock;
|
||||||
|
private final int score;
|
||||||
|
|
||||||
|
private ScoredStock(StockInfo stock, int score)
|
||||||
|
{
|
||||||
|
this.stock = stock;
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -33,8 +33,8 @@
|
|||||||
<if test="areaId != null">and si.area_id = #{areaId}</if>
|
<if test="areaId != null">and si.area_id = #{areaId}</if>
|
||||||
<if test="productId != null">and si.product_id = #{productId}</if>
|
<if test="productId != null">and si.product_id = #{productId}</if>
|
||||||
<if test="areaName != null and areaName != ''">and wa.area_name like concat('%', #{areaName}, '%')</if>
|
<if test="areaName != null and areaName != ''">and wa.area_name like concat('%', #{areaName}, '%')</if>
|
||||||
<if test="productName != null and productName != ''">
|
<if test="productName != null and productName != '' and (params.keywordTokens == null or params.keywordTokens.size() == 0)">
|
||||||
and lower(concat_ws(' ', pi.product_name, pi.brand, pi.category, pi.spec, pi.unit))
|
and lower(concat_ws(' ', pi.product_name, pi.brand, pi.category, pi.spec, pi.unit, wa.area_name))
|
||||||
like concat('%', lower(#{productName}), '%')
|
like concat('%', lower(#{productName}), '%')
|
||||||
</if>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
|
|||||||
@ -118,7 +118,7 @@
|
|||||||
<el-select v-model="stockQuery.areaId" placeholder="库区" clearable filterable>
|
<el-select v-model="stockQuery.areaId" placeholder="库区" clearable filterable>
|
||||||
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="stockQuery.productName" placeholder="产品信息" clearable />
|
<el-input v-model="stockQuery.productName" placeholder="关键词,如:电机 20" clearable />
|
||||||
<el-button type="primary" icon="el-icon-search" @click="loadStock">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" @click="loadStock">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="产品信息" prop="productName">
|
<el-form-item label="关键词" prop="productName">
|
||||||
<el-input v-model="queryParams.productName" placeholder="名称/品牌/类别/规格/单位" clearable @keyup.enter.native="handleQuery" />
|
<el-input v-model="queryParams.productName" placeholder="名称/规格/品牌/类别/单位/库区,如:电机 20" clearable @keyup.enter.native="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user