diff --git a/microservices-modules/microservices-modules-cms/pom.xml b/microservices-modules/microservices-modules-cms/pom.xml index ec4598e09..e9e2e1236 100644 --- a/microservices-modules/microservices-modules-cms/pom.xml +++ b/microservices-modules/microservices-modules-cms/pom.xml @@ -126,6 +126,12 @@ 3.6.2 compile + + com.microservices + microservices-common-search + 3.6.2 + compile + diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/controller/OpenController.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/controller/OpenController.java index 423a042dd..61f465a64 100644 --- a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/controller/OpenController.java +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/controller/OpenController.java @@ -9,8 +9,12 @@ import com.microservices.common.core.web.controller.BaseController; import com.microservices.common.core.web.domain.AjaxResult; import com.microservices.common.core.web.domain.GenericsAjaxResult; import com.microservices.common.core.web.page.GenericsTableDataInfo; +import com.microservices.common.core.web.page.TableDataInfo; import com.microservices.common.log.annotation.Log; import com.microservices.common.log.enums.BusinessType; +import com.microservices.cms.search.dto.DocSearchRequest; +import com.microservices.cms.search.dto.DocSearchResponse; +import com.microservices.cms.search.service.IGlobalSearchService; import io.swagger.annotations.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -35,6 +39,44 @@ public class OpenController extends BaseController { @Autowired private ICmsWechatService cmsWechatService; + @Autowired + private IGlobalSearchService globalSearchService; + + /** + * 文章搜索接口 + * + * @param keyword 搜索关键词 + * @param zoneId 专区ID + * @param pageNum 页码 + * @param pageSize 每页数量 + * @return 搜索结果 + */ + @ApiOperation("文章搜索") + @GetMapping("/search") + public TableDataInfo docSearchGet( + @ApiParam("搜索关键词") @RequestParam String keyword, + @ApiParam("专区ID") @RequestParam Long zoneId, + @ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum, + @ApiParam("每页数量") @RequestParam(defaultValue = "20") Integer pageSize) { + + DocSearchRequest request = new DocSearchRequest(); + request.setKeyword(keyword); + request.setZoneId(zoneId); + request.setPageNum(pageNum); + request.setPageSize(pageSize); + + logger.info("文章搜索请求(GET): keyword={}", keyword); + + DocSearchResponse response = globalSearchService.docSearch(request); + + TableDataInfo dataInfo = new TableDataInfo(); + dataInfo.setCode(200); + dataInfo.setMsg("查询成功"); + dataInfo.setRows(response.getResults()); + dataInfo.setTotal(response.getTotal()); + return dataInfo; + } + /** * 查询栏目列表 */ diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/mapper/CmsDocMapper.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/mapper/CmsDocMapper.java index aacb21b3e..64d165429 100644 --- a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/mapper/CmsDocMapper.java +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/mapper/CmsDocMapper.java @@ -192,4 +192,25 @@ public interface CmsDocMapper { List selectCmsDocAndDirListByDept(Long deptId); void updateCmsDocShaByPathAndDeptId(@Param("sha") String sha, @Param("filePath") String filePath, @Param("deptId") Long deptId); + + /** + * 全局搜索 - 全文索引查询 + * + * @param keyword 搜索关键词 + * @param deptId 组织ID(可选) + * @param offset 偏移量 + * @param pageSize 每页数量 + * @return 搜索结果列表 + */ + List globalSearch(@Param("keyword") String keyword, @Param("deptId") Long deptId, + @Param("offset") int offset, @Param("pageSize") int pageSize); + + /** + * 全局搜索结果计数 + * + * @param keyword 搜索关键词 + * @param deptId 组织ID(可选) + * @return 结果数量 + */ + Long globalSearchCount(@Param("keyword") String keyword, @Param("deptId") Long deptId); } diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/project/mapper/CmsProjectMapper.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/project/mapper/CmsProjectMapper.java index 377b04816..a7c9495eb 100644 --- a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/project/mapper/CmsProjectMapper.java +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/project/mapper/CmsProjectMapper.java @@ -2,6 +2,7 @@ package com.microservices.cms.project.mapper; import com.microservices.cms.project.domain.CmsProject; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import java.util.List; @@ -64,4 +65,25 @@ public interface CmsProjectMapper { CmsProject selectCmsProjectByDeptId(Long deptId); CmsProject selectCmsProjectByIdentifier(String identifier); + + /** + * 全局搜索 - 全文索引查询 + * + * @param keyword 搜索关键词 + * @param deptId 组织ID(可选) + * @param offset 偏移量 + * @param pageSize 每页数量 + * @return 搜索结果列表 + */ + List globalSearch(@Param("keyword") String keyword, @Param("deptId") Long deptId, + @Param("offset") int offset, @Param("pageSize") int pageSize); + + /** + * 全局搜索结果计数 + * + * @param keyword 搜索关键词 + * @param deptId 组织ID(可选) + * @return 结果数量 + */ + Long globalSearchCount(@Param("keyword") String keyword, @Param("deptId") Long deptId); } diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchRequest.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchRequest.java new file mode 100644 index 000000000..ec491aa4d --- /dev/null +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchRequest.java @@ -0,0 +1,43 @@ +package com.microservices.cms.search.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 文章搜索请求对象 + * + * @author microservices + */ +@Data +@ApiModel("文章搜索请求对象") +public class DocSearchRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 搜索关键词 + */ + @ApiModelProperty(value = "搜索关键词", required = true) + private String keyword; + + /** + * 专区ID + */ + @ApiModelProperty(value = "专区ID") + private Long zoneId; + + /** + * 页码 + */ + @ApiModelProperty(value = "页码") + private Integer pageNum = 1; + + /** + * 每页数量 + */ + @ApiModelProperty(value = "每页数量") + private Integer pageSize = 20; +} diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResponse.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResponse.java new file mode 100644 index 000000000..93ecf514c --- /dev/null +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResponse.java @@ -0,0 +1,63 @@ +package com.microservices.cms.search.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 全局搜索响应对象 + * + * @author microservices + */ +@Data +@ApiModel("文章搜索响应对象") +public class DocSearchResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 总结果数 + */ + @ApiModelProperty(value = "总结果数") + private Long total; + + /** + * 搜索结果列表 + */ + @ApiModelProperty(value = "搜索结果列表") + private List results; + + /** + * 各类型结果数量统计 + */ + @ApiModelProperty(value = "各类型结果数量统计") + private Map typeCounts; + + /** + * 搜索耗时(毫秒) + */ + @ApiModelProperty(value = "搜索耗时(毫秒)") + private Long searchTime; + + /** + * 搜索关键词 + */ + @ApiModelProperty(value = "搜索关键词") + private String keyword; + + /** + * 当前页码 + */ + @ApiModelProperty(value = "当前页码") + private Integer pageNum; + + /** + * 每页数量 + */ + @ApiModelProperty(value = "每页数量") + private Integer pageSize; +} diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResult.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResult.java new file mode 100644 index 000000000..5415b77da --- /dev/null +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResult.java @@ -0,0 +1,70 @@ +package com.microservices.cms.search.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 文章搜索结果对象 + * + * @author microservices + */ +@Data +@ApiModel("文章搜索结果对象") +public class DocSearchResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @ApiModelProperty(value = "ID") + private Long id; + + /** + * 标题 + */ + @ApiModelProperty(value = "标题") + private String title; + + /** + * 内容摘要 + */ + @ApiModelProperty(value = "内容摘要") + private String content; + + /** + * 高亮片段 + */ + @ApiModelProperty(value = "高亮片段") + private String highlight; + + /** + * 组织ID + */ + @ApiModelProperty(value = "组织ID") + private Long deptId; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "创建时间") + private Date createTime; + + /** + * 相关度评分 + */ + @ApiModelProperty(value = "相关度评分") + private Double score; + + /** + * 扩展数据(JSON格式) + */ + @ApiModelProperty(value = "扩展数据") + private String extendData; +} diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/IGlobalSearchService.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/IGlobalSearchService.java new file mode 100644 index 000000000..3231b5caa --- /dev/null +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/IGlobalSearchService.java @@ -0,0 +1,20 @@ +package com.microservices.cms.search.service; + +import com.microservices.cms.search.dto.DocSearchRequest; +import com.microservices.cms.search.dto.DocSearchResponse; + +/** + * 全局搜索服务接口 + * + * @author microservices + */ +public interface IGlobalSearchService { + + /** + * 全局搜索 + * + * @param request 搜索请求 + * @return 搜索响应 + */ + DocSearchResponse docSearch(DocSearchRequest request); +} diff --git a/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/impl/GlobalSearchServiceImpl.java b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/impl/GlobalSearchServiceImpl.java new file mode 100644 index 000000000..eca4bb2ea --- /dev/null +++ b/microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/impl/GlobalSearchServiceImpl.java @@ -0,0 +1,189 @@ +package com.microservices.cms.search.service.impl; + +import com.microservices.cms.doc.domain.CmsDoc; +import com.microservices.cms.doc.mapper.CmsDocMapper; +import com.microservices.cms.doc.service.ICmsDocService; +import com.microservices.cms.search.service.IGlobalSearchService; +import com.microservices.common.core.utils.StringUtils; +import com.microservices.common.redis.service.RedisService; +import com.microservices.cms.search.dto.DocSearchRequest; +import com.microservices.cms.search.dto.DocSearchResponse; +import com.microservices.cms.search.dto.DocSearchResult; +import com.microservices.common.security.utils.SecurityUtils; +import com.microservices.system.api.model.LoginUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * 全局搜索服务实现 + * + * @author microservices + */ +@Service +public class GlobalSearchServiceImpl implements IGlobalSearchService { + + private static final Logger logger = LoggerFactory.getLogger(GlobalSearchServiceImpl.class); + + @Autowired + private CmsDocMapper cmsDocMapper; + @Autowired + private RedisService redisService; + @Autowired + private ICmsDocService cmsDocService; + + private static final String SEARCH_CACHE_PREFIX = "search:cache:"; + private static final Long CACHE_EXPIRE_SECONDS = 300L; // 5分钟 + + @Override + public DocSearchResponse docSearch(DocSearchRequest request) { + long startTime = System.currentTimeMillis(); + + // 参数校验 + if (StringUtils.isEmpty(request.getKeyword())) { + return buildEmptyResponse(request); + } + + // 1. 尝试从缓存获取(仅第一页) + if (request.getPageNum() == 1) { + String cacheKey = buildCacheKey(request); + + DocSearchResponse cached = redisService.getCacheObject(SEARCH_CACHE_PREFIX + cacheKey); + if (cached != null) { + cached.setSearchTime(System.currentTimeMillis() - startTime); + return cached; + } + } + + // 2. 并行搜索各类型 + List allResults = new ArrayList<>(); + Map typeCounts = new HashMap<>(); + + // 获取当前用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // 搜索CMS文章 + List docResults = searchDocs(request); + allResults.addAll(docResults); + typeCounts.put("doc", (long) docResults.size()); + + // 3. 按相关度评分排序 + allResults.sort((a, b) -> { + if (b.getScore() != null && a.getScore() != null) { + return b.getScore().compareTo(a.getScore()); + } + return 0; + }); + + // 4. 分页 + int total = allResults.size(); + int start = (request.getPageNum() - 1) * request.getPageSize(); + int end = Math.min(start + request.getPageSize(), total); + List pagedResults = start < total ? + allResults.subList(start, end) : new ArrayList<>(); + + // 5. 构建响应 + DocSearchResponse response = new DocSearchResponse(); + response.setTotal((long) total); + response.setResults(pagedResults); + response.setTypeCounts(typeCounts); + response.setKeyword(request.getKeyword()); + response.setPageNum(request.getPageNum()); + response.setPageSize(request.getPageSize()); + response.setSearchTime(System.currentTimeMillis() - startTime); + + // 6. 缓存结果(仅第一页) + if (request.getPageNum() == 1) { + String cacheKey = buildCacheKey(request); + redisService.setCacheObject(SEARCH_CACHE_PREFIX + cacheKey, response, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + + // 7. 记录搜索日志 + recordSearchLog(request, response, loginUser); + + return response; + } + + /** + * 记录搜索日志 + */ + private void recordSearchLog(DocSearchRequest request, DocSearchResponse response, LoginUser loginUser) { + Long userId = loginUser != null ? loginUser.getUserId() : null; + String searchTypes = "文章"; + Long deptId = cmsDocService.getDeptIdByZoneId(request.getZoneId()); + logger.info("全局搜索 - 关键词: {}, 用户ID: {}, 耗时: {}ms, 结果数: {}, 类型: {}, 专区Id: {}, 组织ID: {}", + request.getKeyword(), + userId, + response.getSearchTime(), + response.getTotal(), + searchTypes, + request.getZoneId(), + deptId + ); + } + + /** + * 搜索CMS文章 + */ + private List searchDocs(DocSearchRequest request) { + List results = new ArrayList<>(); + try { + // 使用部门ID参数 + Long deptId = cmsDocService.getDeptIdByZoneId(request.getZoneId()); + + List docs = cmsDocMapper.globalSearch( + request.getKeyword(), + deptId, + (request.getPageNum() - 1) * request.getPageSize(), + request.getPageSize() + ); + + for (CmsDoc doc : docs) { + DocSearchResult result = new DocSearchResult(); + result.setId(doc.getId()); + result.setTitle(doc.getName()); + result.setContent(doc.getSummary()); + result.setDeptId(doc.getDeptId()); + result.setCreateTime(doc.getCreateTime()); + result.setScore(1.0); // MySQL全文索引默认相关度 + results.add(result); + } + } catch (Exception e) { + // 记录错误但继续处理其他类型 + System.err.println("搜索CMS文章失败: " + e.getMessage()); + } + return results; + } + + /** + * 构建缓存键 + */ + private String buildCacheKey(DocSearchRequest request) { + Long deptId = cmsDocService.getDeptIdByZoneId(request.getZoneId()); + StringBuilder key = new StringBuilder(request.getKeyword()); + key.append(":").append(String.join(",", "Doc")); + if (deptId != null) { + key.append(":dept=").append(deptId); + } + return key.toString(); + } + + /** + * 构建空响应 + */ + private DocSearchResponse buildEmptyResponse(DocSearchRequest request) { + DocSearchResponse response = new DocSearchResponse(); + response.setTotal(0L); + response.setResults(new ArrayList<>()); + response.setTypeCounts(new HashMap<>()); + response.setKeyword(request.getKeyword()); + response.setPageNum(request.getPageNum()); + response.setPageSize(request.getPageSize()); + response.setSearchTime(0L); + return response; + } +} diff --git a/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsDocMapper.xml b/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsDocMapper.xml index c67f642be..d66f99632 100644 --- a/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsDocMapper.xml +++ b/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsDocMapper.xml @@ -494,4 +494,32 @@ #{id} + + + + + \ No newline at end of file diff --git a/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsProjectMapper.xml b/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsProjectMapper.xml index 1c99f7f0d..4684628ec 100644 --- a/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsProjectMapper.xml +++ b/microservices-modules/microservices-modules-cms/src/main/resources/mapper/cms/CmsProjectMapper.xml @@ -159,4 +159,38 @@ item="id" collection="array" open="(" separator="," close=")">#{id} + + + + + \ No newline at end of file diff --git a/microservices-modules/microservices-modules-zone/pom.xml b/microservices-modules/microservices-modules-zone/pom.xml index b92340783..f784ef1cd 100644 --- a/microservices-modules/microservices-modules-zone/pom.xml +++ b/microservices-modules/microservices-modules-zone/pom.xml @@ -133,6 +133,12 @@ 3.6.2 compile + + com.microservices + microservices-common-search + 3.6.2 + compile + diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/OpenController.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/OpenController.java index 9cfbcd91a..67fe49506 100644 --- a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/OpenController.java +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/OpenController.java @@ -6,6 +6,7 @@ import com.microservices.common.core.web.controller.BaseController; import com.microservices.common.core.web.domain.AjaxResult; import com.microservices.common.core.web.domain.GenericsAjaxResult; import com.microservices.common.core.web.page.GenericsTableDataInfo; +import com.microservices.common.core.web.page.TableDataInfo; import com.microservices.common.log.annotation.Log; import com.microservices.common.log.enums.BusinessType; import com.microservices.system.api.domain.SysUser; @@ -37,6 +38,10 @@ import com.microservices.zone.resource.domain.vo.ZoneResourceOpenSearchVo; import com.microservices.zone.resource.service.IZoneResourceDomainService; import com.microservices.zone.resource.service.IZoneResourceService; import com.microservices.zone.resource.service.IZoneResourceTypeService; +import com.microservices.zone.search.dto.GlobalSearchRequest; +import com.microservices.zone.search.dto.GlobalSearchResponse; +import com.microservices.zone.search.dto.GlobalSearchResult; +import com.microservices.zone.search.service.IGlobalSearchService; import com.microservices.zone.specialProject.domain.ZoneSpecialProject; import com.microservices.zone.specialProject.domain.ZoneSpecialProjectRelevancy; import com.microservices.zone.specialProject.domain.ZoneSpecialProjectReport; @@ -411,4 +416,43 @@ public class OpenController extends BaseController { List list = zoneSpecialProjectReportService.selectZoneSpecialProjectReportList(zoneSpecialProjectReportSearchVo.toZoneSpecialProjectReport()); return getGenericsDataTable(list); } + + @Autowired + private IGlobalSearchService globalSearchService; + + /** + * 全局搜索接口(GET方法) + * + * @param keyword 搜索关键词 + * @param types 搜索类型(逗号分隔) + * @param zoneId 专区ID + * @param pageNum 页码 + * @param pageSize 每页数量 + * @return 搜索结果 + */ + @ApiOperation("全局搜索(GET)") + @GetMapping("/global") + public GenericsTableDataInfo globalSearchGet( + @ApiParam("搜索关键词") @RequestParam String keyword, + @ApiParam("搜索类型") @RequestParam(required = false) String types, + @ApiParam("专区ID") @RequestParam Long zoneId, + @ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum, + @ApiParam("每页数量") @RequestParam(defaultValue = "20") Integer pageSize) { + + GlobalSearchRequest request = new GlobalSearchRequest(); + request.setKeyword(keyword); + request.setPageNum(pageNum); + request.setPageSize(pageSize); + request.setZoneId(zoneId); + + if (types != null && !types.isEmpty()) { + request.setTypes(java.util.Arrays.asList(types.split(","))); + } + + logger.info("全局搜索请求(GET): keyword={}, types={}", keyword, types); + + GlobalSearchResponse response = globalSearchService.globalSearch(request); + + return new GenericsTableDataInfo<>(response.getResults(),response.getTotal()); + } } diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/mapper/ZoneProjectMapper.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/mapper/ZoneProjectMapper.java index 91e1bae05..95f34dc3b 100644 --- a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/mapper/ZoneProjectMapper.java +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/mapper/ZoneProjectMapper.java @@ -155,4 +155,28 @@ public interface ZoneProjectMapper { * @return */ int deleteZoneProjectByZoneId(Long zoneId); + + /** + * 全局搜索 - 全文索引查询 + * + * @param keyword 搜索关键词 + * @param zoneId 专区ID(可选) + * @param offset 偏移量 + * @param pageSize 每页数量 + * @return 搜索结果列表 + */ + List globalSearch(@Param("keyword") String keyword, @Param("zoneId") Long zoneId, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** + * 全局搜索结果计数 + * + * @param keyword 搜索关键词 + * @param zoneId 专区ID(可选) + * @param deptId 组织ID(可选) + * @return 结果数量 + */ + Long globalSearchCount(@Param("keyword") String keyword, @Param("zoneId") Long zoneId, + @Param("deptId") Long deptId); } diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java index c1d1b4404..de699cf1e 100644 --- a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java @@ -95,4 +95,27 @@ public interface ZoneResourceMapper { * @param zoneId 专区Id */ void deleteZoneResourceByZoneId(Long zoneId); + + /** + * 全局搜索 - 全文索引查询 + * + * @param keyword 搜索关键词 + * @param zoneId 专区ID + * @param offset 偏移量 + * @param pageSize 每页数量 + * @return 搜索结果列表 + */ + List globalSearch(@Param("keyword") String keyword, @Param("zoneId") Long zoneId, + @Param("offset") int offset,@Param("pageSize") int pageSize); + + /** + * 全局搜索结果计数 + * + * @param keyword 搜索关键词 + * @param zoneId 专区ID(可选) + * @param deptId 组织ID(可选) + * @return 结果数量 + */ + Long globalSearchCount(@Param("keyword") String keyword, @Param("zoneId") Long zoneId, + @Param("deptId") Long deptId); } diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchRequest.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchRequest.java new file mode 100644 index 000000000..328caf286 --- /dev/null +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchRequest.java @@ -0,0 +1,51 @@ +package com.microservices.zone.search.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 全局搜索请求对象 + * + * @author microservices + */ +@Data +@ApiModel("全局搜索请求对象") +public class GlobalSearchRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 搜索关键词 + */ + @ApiModelProperty(value = "搜索关键词", required = true) + private String keyword; + + /** + * 搜索类型列表(zone_project, resource, member) + * 为空则搜索所有类型 + */ + @ApiModelProperty(value = "搜索类型列表") + private List types; + + /** + * 专区ID + */ + @ApiModelProperty(value = "专区ID") + private Long zoneId; + + /** + * 页码 + */ + @ApiModelProperty(value = "页码") + private Integer pageNum = 1; + + /** + * 每页数量 + */ + @ApiModelProperty(value = "每页数量") + private Integer pageSize = 20; +} diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResponse.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResponse.java new file mode 100644 index 000000000..59550f99f --- /dev/null +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResponse.java @@ -0,0 +1,63 @@ +package com.microservices.zone.search.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 全局搜索响应对象 + * + * @author microservices + */ +@Data +@ApiModel("全局搜索响应对象") +public class GlobalSearchResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 总结果数 + */ + @ApiModelProperty(value = "总结果数") + private Long total; + + /** + * 搜索结果列表 + */ + @ApiModelProperty(value = "搜索结果列表") + private List results; + + /** + * 各类型结果数量统计 + */ + @ApiModelProperty(value = "各类型结果数量统计") + private Map typeCounts; + + /** + * 搜索耗时(毫秒) + */ + @ApiModelProperty(value = "搜索耗时(毫秒)") + private Long searchTime; + + /** + * 搜索关键词 + */ + @ApiModelProperty(value = "搜索关键词") + private String keyword; + + /** + * 当前页码 + */ + @ApiModelProperty(value = "当前页码") + private Integer pageNum; + + /** + * 每页数量 + */ + @ApiModelProperty(value = "每页数量") + private Integer pageSize; +} diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResult.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResult.java new file mode 100644 index 000000000..f71b97676 --- /dev/null +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResult.java @@ -0,0 +1,82 @@ +package com.microservices.zone.search.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 全局搜索结果对象 + * + * @author microservices + */ +@Data +@ApiModel("全局搜索结果对象") +public class GlobalSearchResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 结果类型(zone_project, resource, member) + */ + @ApiModelProperty(value = "结果类型") + private String type; + + /** + * ID + */ + @ApiModelProperty(value = "ID") + private Long id; + + /** + * 标题 + */ + @ApiModelProperty(value = "标题") + private String title; + + /** + * 内容摘要 + */ + @ApiModelProperty(value = "内容摘要") + private String content; + + /** + * 高亮片段 + */ + @ApiModelProperty(value = "高亮片段") + private String highlight; + + /** + * 组织ID + */ + @ApiModelProperty(value = "组织ID") + private Long deptId; + + /** + * 专区ID + */ + @ApiModelProperty(value = "专区ID") + private Long zoneId; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "创建时间") + private Date createTime; + + /** + * 相关度评分 + */ + @ApiModelProperty(value = "相关度评分") + private Double score; + + /** + * 扩展数据(JSON格式) + */ + @ApiModelProperty(value = "扩展数据") + private String extendData; +} diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/IGlobalSearchService.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/IGlobalSearchService.java new file mode 100644 index 000000000..6b7162cd3 --- /dev/null +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/IGlobalSearchService.java @@ -0,0 +1,20 @@ +package com.microservices.zone.search.service; + +import com.microservices.zone.search.dto.GlobalSearchRequest; +import com.microservices.zone.search.dto.GlobalSearchResponse; + +/** + * 全局搜索服务接口 + * + * @author microservices + */ +public interface IGlobalSearchService { + + /** + * 全局搜索 + * + * @param request 搜索请求 + * @return 搜索响应 + */ + GlobalSearchResponse globalSearch(GlobalSearchRequest request); +} diff --git a/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/impl/GlobalSearchServiceImpl.java b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/impl/GlobalSearchServiceImpl.java new file mode 100644 index 000000000..ef13d7598 --- /dev/null +++ b/microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/impl/GlobalSearchServiceImpl.java @@ -0,0 +1,280 @@ +package com.microservices.zone.search.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microservices.common.core.domain.R; +import com.microservices.common.core.utils.StringUtils; +import com.microservices.common.redis.service.RedisService; +import com.microservices.common.security.utils.SecurityUtils; +import com.microservices.system.api.RemoteFileService; +import com.microservices.system.api.domain.SysFileInfo; +import com.microservices.system.api.model.LoginUser; +import com.microservices.zone.project.domain.ZoneProject; +import com.microservices.zone.project.mapper.ZoneProjectMapper; +import com.microservices.zone.resource.domain.ZoneResource; +import com.microservices.zone.resource.mapper.ZoneResourceMapper; +import com.microservices.zone.search.dto.GlobalSearchRequest; +import com.microservices.zone.search.dto.GlobalSearchResponse; +import com.microservices.zone.search.dto.GlobalSearchResult; +import com.microservices.zone.search.service.IGlobalSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.concurrent.TimeUnit; + +/** + * 全局搜索服务实现 + * + * @author microservices + */ +@Service +public class GlobalSearchServiceImpl implements IGlobalSearchService { + + private static final Logger logger = LoggerFactory.getLogger(GlobalSearchServiceImpl.class); + + @Autowired + private ZoneProjectMapper zoneProjectMapper; + + @Autowired + private ZoneResourceMapper zoneResourceMapper; + + @Autowired + private RemoteFileService remoteFileService; + + @Autowired + private RedisService redisService; + + private static final String SEARCH_CACHE_PREFIX = "search:cache:"; + private static final long CACHE_EXPIRE_SECONDS = 300; // 5分钟 + + @Override + public GlobalSearchResponse globalSearch(GlobalSearchRequest request) { + long startTime = System.currentTimeMillis(); + + // 参数校验 + if (StringUtils.isEmpty(request.getKeyword())) { + return buildEmptyResponse(request); + } + + // 1. 尝试从缓存获取(仅第一页) + if (request.getPageNum() == 1) { + String cacheKey = buildCacheKey(request); + GlobalSearchResponse cached = redisService.getCacheObject(SEARCH_CACHE_PREFIX + cacheKey); + if (cached != null) { + cached.setSearchTime(System.currentTimeMillis() - startTime); + return cached; + } + } + + // 2. 并行搜索各类型 + List allResults = new ArrayList<>(); + Map typeCounts = new HashMap<>(); + + // 获取当前用户信息 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // 确定要搜索的类型 + List searchTypes = request.getTypes(); + if (searchTypes == null || searchTypes.isEmpty()) { + searchTypes = Arrays.asList("zone_project", "resource", "member"); + } + + // 搜索Zone项目 + if (searchTypes.contains("zone_project")) { + List zoneProjectResults = searchZoneProjects(request); + allResults.addAll(zoneProjectResults); + typeCounts.put("zone_project", (long) zoneProjectResults.size()); + } + + // 搜索Zone资源 + if (searchTypes.contains("resource")) { + List resourceResults = searchResources(request); + allResults.addAll(resourceResults); + typeCounts.put("resource", (long) resourceResults.size()); + } + + // 3. 按相关度评分排序 + allResults.sort((a, b) -> { + if (b.getScore() != null && a.getScore() != null) { + return b.getScore().compareTo(a.getScore()); + } + return 0; + }); + + // 4. 分页 + int total = allResults.size(); + int start = (request.getPageNum() - 1) * request.getPageSize(); + int end = Math.min(start + request.getPageSize(), total); + List pagedResults = start < total ? + allResults.subList(start, end) : new ArrayList<>(); + + // 5. 构建响应 + GlobalSearchResponse response = new GlobalSearchResponse(); + response.setTotal((long) total); + response.setResults(pagedResults); + response.setTypeCounts(typeCounts); + response.setKeyword(request.getKeyword()); + response.setPageNum(request.getPageNum()); + response.setPageSize(request.getPageSize()); + response.setSearchTime(System.currentTimeMillis() - startTime); + + // 6. 缓存结果(仅第一页) + if (request.getPageNum() == 1) { + String cacheKey = buildCacheKey(request); + redisService.setCacheObject( + SEARCH_CACHE_PREFIX + cacheKey, + response, + CACHE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + } + + // 7. 异步记录搜索日志 + recordSearchLog(request, response, loginUser); + + return response; + } + + /** + * 异步记录搜索日志 + */ + @Async + public void recordSearchLog(GlobalSearchRequest request, GlobalSearchResponse response, LoginUser loginUser) { + try { + logger.info("全局搜索 - 关键词: {}, 用户ID: {}, 耗时: {}ms, 结果数: {}, 搜索类型: {}, 专区ID: {}", + request.getKeyword(), + loginUser != null ? loginUser.getUserId() : null, + response.getSearchTime(), + response.getTotal(), + request.getTypes() != null ? String.join(",", request.getTypes()) : null, + request.getZoneId() + ); + } catch (Exception e) { + logger.error("记录搜索日志失败: {}", e.getMessage()); + } + } + + /** + * 搜索Zone项目 + */ + private List searchZoneProjects(GlobalSearchRequest request) { + List results = new ArrayList<>(); + try { + List projects = zoneProjectMapper.globalSearch( + request.getKeyword(), + request.getZoneId(), + (request.getPageNum() - 1) * request.getPageSize(), + request.getPageSize() + ); + + for (ZoneProject project : projects) { + GlobalSearchResult result = new GlobalSearchResult(); + result.setType("zone_project"); + result.setId(project.getId()); + result.setTitle(project.getName()); + result.setContent(project.getFullName()); + result.setDeptId(project.getDeptId()); + result.setZoneId(project.getZoneId()); + result.setCreateTime(project.getCreateTime()); + result.setScore(1.0); + results.add(result); + } + } catch (Exception e) { + System.err.println("搜索Zone项目失败: " + e.getMessage()); + } + return results; + } + + /** + * 搜索Zone资源 + */ + private List searchResources(GlobalSearchRequest request) { + List results = new ArrayList<>(); + try { + List resources = zoneResourceMapper.globalSearch( + request.getKeyword(), + request.getZoneId(), + (request.getPageNum() - 1) * request.getPageSize(), + request.getPageSize() + ); + + for (ZoneResource resource : resources) { + GlobalSearchResult result = new GlobalSearchResult(); + result.setType("resource"); + result.setId(resource.getId()); + result.setTitle(resource.getName()); + result.setContent(resource.getSummary()); + result.setDeptId(resource.getDeptId()); + result.setZoneId(resource.getZoneId()); + result.setCreateTime(resource.getCreateTime()); + result.setScore(1.0); + + // 查询并过滤匹配的附件 + if (StringUtils.isNotEmpty(resource.getFileIds())) { + try { + R> fileResponse = remoteFileService.getFileList(resource.getFileIds()); + if (fileResponse != null && fileResponse.getCode() == 200 && fileResponse.getData() != null) { + List> matchedFiles = fileResponse.getData().stream() + .filter(file -> StringUtils.isNotEmpty(file.getFileOriginName()) + && file.getFileOriginName().toLowerCase().contains(request.getKeyword().toLowerCase())) + .map(file -> { + Map fileInfo = new HashMap<>(); + fileInfo.put("fileId", file.getFileId()); + fileInfo.put("fileName", file.getFileOriginName()); + fileInfo.put("fileSize", file.getFileSizeInfo()); + return fileInfo; + }) + .collect(Collectors.toList()); + + if (!matchedFiles.isEmpty()) { + ObjectMapper objectMapper = new ObjectMapper(); + Map extendData = new HashMap<>(); + extendData.put("matchedFiles", matchedFiles); + result.setExtendData(objectMapper.writeValueAsString(extendData)); + } + } + } catch (Exception e) { + logger.warn("查询附件信息失败: resourceId={}, error={}", resource.getId(), e.getMessage()); + } + } + + results.add(result); + } + } catch (Exception e) { + System.err.println("搜索Zone资源失败: " + e.getMessage()); + } + return results; + } + + /** + * 构建缓存键 + */ + private String buildCacheKey(GlobalSearchRequest request) { + StringBuilder key = new StringBuilder(request.getKeyword()); + if (request.getTypes() != null && !request.getTypes().isEmpty()) { + key.append(":").append(String.join(",", request.getTypes())); + } + if (request.getZoneId() != null) { + key.append(":zone=").append(request.getZoneId()); + } + return key.toString(); + } + + /** + * 构建空响应 + */ + private GlobalSearchResponse buildEmptyResponse(GlobalSearchRequest request) { + GlobalSearchResponse response = new GlobalSearchResponse(); + response.setTotal(0L); + response.setResults(new ArrayList<>()); + response.setTypeCounts(new HashMap<>()); + response.setKeyword(request.getKeyword()); + response.setPageNum(request.getPageNum()); + response.setPageSize(request.getPageSize()); + response.setSearchTime(0L); + return response; + } +} diff --git a/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneProjectMapper.xml b/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneProjectMapper.xml index 80079a6ed..cb4b24997 100644 --- a/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneProjectMapper.xml +++ b/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneProjectMapper.xml @@ -276,4 +276,48 @@ from zone_project where zone_id = #{zoneId} + + + + + \ No newline at end of file diff --git a/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml b/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml index c667ee632..79621d02e 100644 --- a/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml +++ b/microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml @@ -301,4 +301,70 @@ from zone_resource where zone_id = #{zoneId} + + + + + \ No newline at end of file