From aff24c83d25f9d3bfc426d5ae3a6a874d63c7df7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=AC=A7=E6=B6=9B?= <731554297@qq.com>
Date: Wed, 15 Apr 2026 13:54:04 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E5=85=A8=E5=B1=80=E6=90=9C=E7=B4=A2):=20?=
=?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=B6=85=E7=AE=97=E4=B8=93=E5=8C=BA=E5=85=A8?=
=?UTF-8?q?=E5=B1=80=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现MySQL全文检索:基于MATCH...AGAINST NATURAL LANGUAGE MODE
搜索范围覆盖文档标题(name)和摘要(summary)双字段
结果按全文相关度降序+更新时间降序双重排序
仅检索已审核通过且非目录类型的文档
- 实现项目全文检索:基于MATCH...AGAINST搜索zone_project.name字段
仅返回已审核项目(audit_status='1'),按相关度+更新时间排序
- 实现资源全文检索:搜索资源名称(name)+关键词(keywords)复合字段
创新性支持附件文件名全文匹配(通过EXISTS子查询关联sys_file_info表)
资源结果附带匹配附件信息(文件ID、文件名、文件大小),存储于extendData
通过RemoteFileService远程调用获取附件详情,支持大小写不敏感匹配
- 引入Redis缓存策略:第一页搜索结果缓存5分钟
---
.../microservices-modules-cms/pom.xml | 6 +
.../cms/doc/controller/OpenController.java | 42 +++
.../cms/doc/mapper/CmsDocMapper.java | 21 ++
.../cms/project/mapper/CmsProjectMapper.java | 22 ++
.../cms/search/dto/DocSearchRequest.java | 43 +++
.../cms/search/dto/DocSearchResponse.java | 63 ++++
.../cms/search/dto/DocSearchResult.java | 70 +++++
.../search/service/IGlobalSearchService.java | 20 ++
.../service/impl/GlobalSearchServiceImpl.java | 189 ++++++++++++
.../resources/mapper/cms/CmsDocMapper.xml | 28 ++
.../resources/mapper/cms/CmsProjectMapper.xml | 34 +++
.../microservices-modules-zone/pom.xml | 6 +
.../common/controller/OpenController.java | 44 +++
.../project/mapper/ZoneProjectMapper.java | 24 ++
.../resource/mapper/ZoneResourceMapper.java | 23 ++
.../zone/search/dto/GlobalSearchRequest.java | 51 ++++
.../zone/search/dto/GlobalSearchResponse.java | 63 ++++
.../zone/search/dto/GlobalSearchResult.java | 82 +++++
.../search/service/IGlobalSearchService.java | 20 ++
.../service/impl/GlobalSearchServiceImpl.java | 280 ++++++++++++++++++
.../mapper/zone/ZoneProjectMapper.xml | 44 +++
.../mapper/zone/ZoneResourceMapper.xml | 66 +++++
22 files changed, 1241 insertions(+)
create mode 100644 microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchRequest.java
create mode 100644 microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResponse.java
create mode 100644 microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/dto/DocSearchResult.java
create mode 100644 microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/IGlobalSearchService.java
create mode 100644 microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/search/service/impl/GlobalSearchServiceImpl.java
create mode 100644 microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchRequest.java
create mode 100644 microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResponse.java
create mode 100644 microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/dto/GlobalSearchResult.java
create mode 100644 microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/IGlobalSearchService.java
create mode 100644 microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/search/service/impl/GlobalSearchServiceImpl.java
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