feat(content): 增强内容搜索功能支持关键词匹配和标签关联推荐

- 在 CmsContentDto 中新增 tagIdList 和 keyword 字段用于批量查询和全文搜索
- 修改数据库查询映射文件支持基于关键词的标题、描述、标签多字段模糊匹配
- 实现标签 ID 列表批量查询功能,支持多标签条件筛选
- 添加基于关键词搜索的智能关联推荐机制,根据首个结果的标签自动推荐相关内容
- 优化分页查询逻辑,在关键词搜索结果较少时自动补充相关技能内容
- 增强搜索结果排序算法,综合考虑排序权重和创建时间因素
This commit is contained in:
wangzhiwei 2026-03-06 18:58:04 +08:00
parent af0ae4bac1
commit 1b0d102ef9
3 changed files with 126 additions and 12 deletions

View File

@ -46,4 +46,14 @@ public class CmsContentDto extends BaseQueryDto {
private Long tagId; private Long tagId;
/**
* 标签 ID 列表用于批量查询
*/
private List<Long> tagIdList;
/**
* 搜索关键字同时搜索 titledescriptiontags
*/
private String keyword;
} }

View File

@ -16,8 +16,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Date; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
@ -58,10 +58,83 @@ public class CmsContentServiceImpl implements CmsContentService {
queryDto.setPageSize(BaseQueryDto.DEFAULT_PAGE_SIZE); queryDto.setPageSize(BaseQueryDto.DEFAULT_PAGE_SIZE);
} }
// 使用PageHelper进行分页 // 使用 PageHelper 进行分页
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize()); PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<CmsContent> list = this.cmsContentMapper.getPageList(queryDto); List<CmsContent> list = this.cmsContentMapper.getPageList(queryDto);
PageInfo<CmsContent> pageInfo = new PageInfo<>(list); PageInfo<CmsContent> pageInfo = new PageInfo<>(list);
// 如果使用了 keyword 搜索且返回结果小于等于 3 则根据第一个 skill 的标签查询相关 skill
if (queryDto.getKeyword() != null && !queryDto.getKeyword().trim().isEmpty()
&& list.size() <= 3 && !list.isEmpty()) {
// 获取第一个 skill 的标签
CmsContent firstSkill = list.get(0);
String tagsStr = firstSkill.getTags();
if (tagsStr != null && !tagsStr.trim().isEmpty()) {
// 解析标签逗号分隔
String[] tags = tagsStr.split(",");
if (tags.length > 0) {
// 构建标签 ID 列表
Set<Long> tagIdSet = new HashSet<>();
for (String tag : tags) {
if (tag != null && !tag.trim().isEmpty()) {
try {
Long tagId = Long.parseLong(tag.trim());
tagIdSet.add(tagId);
} catch (NumberFormatException e) {
// 忽略格式不正确的标签
}
}
}
// 如果有有效的标签 ID查询相关内容
if (!tagIdSet.isEmpty()) {
// 排除已返回的 contentId
Set<Long> existingIds = list.stream()
.map(CmsContent::getContentId)
.collect(Collectors.toSet());
// 构建查询条件使用 tagIdList 一次性查询
CmsContentDto tagQueryDto = new CmsContentDto();
tagQueryDto.setDeleteFlag(0);
tagQueryDto.setPublishStatus(2); // 已发布
tagQueryDto.setTagIdList(new ArrayList<>(tagIdSet));
tagQueryDto.setPageNum(1);
tagQueryDto.setPageSize(1000);
// 查询包含这些标签的所有 skill
List<CmsContent> taggedContents = this.cmsContentMapper.getPageList(tagQueryDto);
// 过滤掉已返回的内容
List<CmsContent> relatedContents = taggedContents.stream()
.filter(content -> !existingIds.contains(content.getContentId()))
.collect(Collectors.toList());
// 如果有相关 skill添加到结果中
if (!relatedContents.isEmpty()) {
// sort create_time 排序
relatedContents.sort((a, b) -> {
int sortCompare = Integer.compare(
a.getSort() != null ? a.getSort() : 0,
b.getSort() != null ? b.getSort() : 0);
if (sortCompare != 0) {
return sortCompare;
}
if (a.getCreateTime() == null || b.getCreateTime() == null) {
return 0;
}
return b.getCreateTime().compareTo(a.getCreateTime());
});
list.addAll(relatedContents);
// 重新构建 PageInfo
pageInfo = new PageInfo<>(list);
pageInfo.setTotal(list.size());
}
}
}
}
}
return pageInfo; return pageInfo;
} }

View File

@ -65,7 +65,7 @@
and content_id = #{contentId} and content_id = #{contentId}
</if> </if>
<if test="title != null and title != ''"> <if test="title != null and title != ''">
and (title like concat('%', #{title}, '%') or summary like concat('%', #{title}, '%')) and (title like concat('%', #{title}, '%') or description like concat('%', #{title}, '%'))
</if> </if>
<if test="contentType != null"> <if test="contentType != null">
and content_type = #{contentType} and content_type = #{contentType}
@ -94,6 +94,13 @@
<if test="tagId != null"> <if test="tagId != null">
and find_in_set(#{tagId}, tags) and find_in_set(#{tagId}, tags)
</if> </if>
<if test="tagIdList != null and tagIdList.size() > 0">
and (
<foreach collection="tagIdList" item="tagId" separator=" or ">
find_in_set(#{tagId}, tags)
</foreach>
)
</if>
</where> </where>
<if test="sortBy != null and sortBy != ''"> <if test="sortBy != null and sortBy != ''">
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'} order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
@ -115,7 +122,12 @@
and content_id = #{queryDto.contentId} and content_id = #{queryDto.contentId}
</if> </if>
<if test="queryDto.title != null and queryDto.title != ''"> <if test="queryDto.title != null and queryDto.title != ''">
and (title like concat('%', #{queryDto.title}, '%') or summary like concat('%', #{queryDto.title}, '%')) and (title like concat('%', #{queryDto.title}, '%') or description like concat('%', #{queryDto.title}, '%'))
</if>
<if test="queryDto.keyword != null and queryDto.keyword != ''">
and (title like concat('%', #{queryDto.keyword}, '%')
or description like concat('%', #{queryDto.keyword}, '%')
or tags like concat('%', #{queryDto.keyword}, '%'))
</if> </if>
<if test="queryDto.contentType != null"> <if test="queryDto.contentType != null">
and content_type = #{queryDto.contentType} and content_type = #{queryDto.contentType}
@ -144,6 +156,13 @@
<if test="queryDto.tagId != null"> <if test="queryDto.tagId != null">
and find_in_set(#{queryDto.tagId}, tags) and find_in_set(#{queryDto.tagId}, tags)
</if> </if>
<if test="queryDto.tagIdList != null and queryDto.tagIdList.size() > 0">
and (
<foreach collection="queryDto.tagIdList" item="tagId" separator=" or ">
find_in_set(#{tagId}, tags)
</foreach>
)
</if>
</where> </where>
<if test="queryDto.sortBy != null and queryDto.sortBy != ''"> <if test="queryDto.sortBy != null and queryDto.sortBy != ''">
order by ${queryDto.sortBy} ${queryDto.sortDesc ? 'desc' : 'asc'} order by ${queryDto.sortBy} ${queryDto.sortDesc ? 'desc' : 'asc'}
@ -209,6 +228,11 @@
<if test="title != null and title != ''"> <if test="title != null and title != ''">
and (title like concat('%', #{title}, '%') or summary like concat('%', #{title}, '%')) and (title like concat('%', #{title}, '%') or summary like concat('%', #{title}, '%'))
</if> </if>
<if test="keyword != null and keyword != ''">
and (title like concat('%', #{keyword}, '%')
or description like concat('%', #{keyword}, '%')
or tags like concat('%', #{keyword}, '%'))
</if>
<if test="contentType != null"> <if test="contentType != null">
and content_type = #{contentType} and content_type = #{contentType}
</if> </if>
@ -230,6 +254,13 @@
<if test="tagId != null"> <if test="tagId != null">
and find_in_set(#{tagId}, tags) and find_in_set(#{tagId}, tags)
</if> </if>
<if test="tagIdList != null and tagIdList.size() > 0">
and (
<foreach collection="tagIdList" item="tagId" separator=" or ">
find_in_set(#{tagId}, tags)
</foreach>
)
</if>
</where> </where>
order by sort asc, create_time desc order by sort asc, create_time desc
</select> </select>