# 全站搜索功能实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为碳信网实现全站搜索功能,包括搜索页面、API接口、搜索建议、热门搜索、搜索历史 **Architecture:** 前端聚合在 txw-mhzc-web(搜索结果页),后端聚合在 txw-mhzc(SearchController + SearchService)。搜索聚合服务第一阶段只搜索资讯中心数据。 **实现策略:** - 搜索历史:Phase 1 使用 localStorage(无用户认证),后续接入后端 API - 热门搜索:Phase 1 返回硬编码数据,后续从数据库读取 **Tech Stack:** Vue 2 + TDesign + Vue Router (前端), Spring Boot + MyBatis Plus (后端) --- ## 文件结构 ### 前端 (txw-mhzc-web) ``` txw-mhzc-web/src/pages/index/ ├── views/ │ └── search/ │ └── index.vue # 搜索结果页 [新建] ├── components/ │ └── search/ │ ├── SearchArea.vue # 搜索区域 [新建] │ ├── SearchSuggestion.vue # 搜索建议下拉 [新建] │ ├── SearchHistoryBar.vue # 搜索历史横条 [新建] │ ├── SearchCategoryTabs.vue # 分类Tab [新建] │ ├── SearchResultList.vue # 结果列表 [新建] │ └── SearchResultItem.vue # 结果项 [新建] └── api/ └── search.js # 搜索接口 [新建] ``` ### 后端 (txw-mhzc) ``` txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/ ├── controller/ │ └── SearchController.java # 搜索接口 [新建] ├── service/ │ ├── SearchService.java # 搜索服务接口 [新建] │ └── impl/ │ └── SearchServiceImpl.java # 搜索服务实现 [新建] ├── mapper/ │ └── SearchMapper.java # 搜索Mapper [新建] └── pojo/ ├── req/ │ └── SearchReqVO.java # 搜索请求 [新建] └── vo/ ├── SearchResultVO.java # 搜索结果 [新建] └── HotSearchConfig.java # 热门搜索配置 [新建] ``` --- ## 后端任务 ### Task 1: 创建搜索请求 VO **Files:** - Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/req/SearchReqVO.java` - [ ] **Step 1: 创建 SearchReqVO** ```java package com.css.txw.mhzc.pojo.req; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotBlank; @Data @Schema(description = "搜索请求") public class SearchReqVO { @Schema(description = "关键词", required = true) @NotBlank(message = "关键词不能为空") @Length(max = 50, message = "关键词不能超过50个字符") private String keyword; @Schema(description = "分类类型: all, carbon_cert, service, news") private String categoryType = "all"; @Schema(description = "页码") private Integer page = 1; @Schema(description = "每页条数") private Integer pageSize = 10; } ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/req/SearchReqVO.java git commit -m "feat(search): 添加搜索请求VO" ``` --- ### Task 2: 创建搜索结果 VO **Files:** - Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/SearchResultVO.java` - [ ] **Step 1: 创建 SearchResultVO** ```java package com.css.txw.mhzc.pojo.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @Schema(description = "搜索结果") public class SearchResultVO { @Schema(description = "内容ID") private String id; @Schema(description = "标题(关键词高亮)") private String title; @Schema(description = "摘要") private String summary; @Schema(description = "缩略图URL") private String thumbnail; @Schema(description = "分类标签(展示用)") private String category; @Schema(description = "分类类型(筛选用)") private String categoryType; @Schema(description = "来源名称") private String source; @Schema(description = "来源类型") private String sourceType; @Schema(description = "发布时间") private String publishTime; @Schema(description = "跳转链接") private String url; } ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/SearchResultVO.java git commit -m "feat(search): 添加搜索结果VO" ``` --- ### Task 3: 创建搜索服务接口和实现 **Files:** - Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/SearchService.java` - Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java` - [ ] **Step 1: 创建 SearchService 接口** ```java package com.css.txw.mhzc.service; import com.css.txw.mhzc.pojo.req.SearchReqVO; import com.css.txw.mhzc.pojo.vo.SearchResultVO; import java.util.List; public interface SearchService { /** * 搜索 */ List search(SearchReqVO reqVO); /** * 获取热门搜索词 */ List getHotSearchKeywords(); /** * 获取搜索建议 */ List getSearchSuggestions(String keyword); /** * 获取搜索历史 */ List getSearchHistory(); /** * 清除搜索历史 */ void clearSearchHistory(); } ``` - [ ] **Step 2: 创建 SearchServiceImpl 实现** ```java package com.css.txw.mhzc.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.css.ggzc.framework.common.pojo.CommonResult; import com.css.txw.mhzc.pojo.req.SearchReqVO; import com.css.txw.mhzc.pojo.vo.SearchResultVO; import com.css.txw.mhzc.pojo.vo.SyzxxxVO; import com.css.txw.mhzc.service.SearchService; import com.css.txw.mhzc.service.TxwMhzcZxxxbService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Service @Slf4j public class SearchServiceImpl implements SearchService { @Resource private TxwMhzcZxxxbService zxxxbService; private static final String HIGHLIGHT_START = ""; private static final String HIGHLIGHT_END = ""; @Override public List search(SearchReqVO reqVO) { // 调用资讯接口获取数据 CommonResult>> result = zxxxbService.zxxx(); List resultList = new ArrayList<>(); if (result != null && result.getData() != null) { java.util.Map> data = result.getData(); // 从资讯数据中筛选 data.forEach((category, list) -> { list.forEach(zxxx -> { // 简单匹配逻辑,实际应更复杂 if (matchKeyword(zxxx.getTitle(), reqVO.getKeyword()) || matchKeyword(zxxx.getContent(), reqVO.getKeyword())) { SearchResultVO vo = new SearchResultVO(); vo.setId(zxxx.getId()); vo.setTitle(highlightKeyword(zxxx.getTitle(), reqVO.getKeyword())); vo.setSummary(highlightKeyword(truncate(zxxx.getContent(), 200), reqVO.getKeyword())); vo.setCategory(category); vo.setCategoryType(getCategoryType(category)); vo.setSource("碳信网"); vo.setSourceType("news"); vo.setPublishTime(zxxx.getPublishTime()); vo.setUrl("/mhzc/news/" + zxxx.getId()); resultList.add(vo); } }); }); } // 分页 int start = (reqVO.getPage() - 1) * reqVO.getPageSize(); int end = Math.min(start + reqVO.getPageSize(), resultList.size()); if (start >= resultList.size()) { return new ArrayList<>(); } return resultList.subList(start, end); } @Override public List getHotSearchKeywords() { // TODO: 从数据库读取运营配置的热门搜索词 return Arrays.asList("江苏电厂配额", "林业碳汇开发", "CBAM 报告", "零碳展会"); } @Override public List getSearchSuggestions(String keyword) { if (!StringUtils.hasText(keyword)) { return new ArrayList<>(); } List suggestions = new ArrayList<>(); String lowerKeyword = keyword.toLowerCase(); // TODO: 实际应从数据库查询匹配的建议词 // 模拟返回 suggestions.add(keyword); return suggestions.stream() .filter(s -> s.toLowerCase().contains(lowerKeyword)) .limit(10) .collect(Collectors.toList()); } private boolean matchKeyword(String text, String keyword) { if (!StringUtils.hasText(text) || !StringUtils.hasText(keyword)) { return false; } return text.contains(keyword); } private String highlightKeyword(String text, String keyword) { if (!StringUtils.hasText(text) || !StringUtils.hasText(keyword)) { return text; } return text.replace(keyword, HIGHLIGHT_START + keyword + HIGHLIGHT_END); } private String truncate(String text, int maxLength) { if (text == null || text.length() <= maxLength) { return text; } return text.substring(0, maxLength) + "..."; } private String getCategoryType(String category) { if ("行业专题".equals(category)) { return "news"; } else if ("碳证中心".equals(category)) { return "carbon_cert"; } else if ("服务中心".equals(category)) { return "service"; } return "all"; } @Override public List getSearchHistory() { // Phase 1: 返回空列表(前端使用localStorage) // TODO: 用户认证后从数据库或Redis获取 return new ArrayList<>(); } @Override public void clearSearchHistory() { // Phase 1: 前端localStorage已清除 // TODO: 用户认证后清除数据库或Redis中的数据 } } ``` - [ ] **Step 3: 提交代码** ```bash git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/SearchService.java git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java git commit -m "feat(search): 添加搜索服务接口和实现" ``` --- ### Task 4: 创建搜索控制器 **Files:** - Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/SearchController.java` - [ ] **Step 1: 创建 SearchController** ```java package com.css.txw.mhzc.controller; import com.css.ggzc.framework.common.pojo.CommonResult; import com.css.txw.mhzc.pojo.req.SearchReqVO; import com.css.txw.mhzc.pojo.vo.SearchResultVO; import com.css.txw.mhzc.service.SearchService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import java.util.List; import java.util.Map; @RestController @RequestMapping("/search") @Tag(name = "搜索接口") @Validated @Slf4j public class SearchController { @Resource private SearchService searchService; @GetMapping @Operation(summary = "搜索", description = "全站搜索") public CommonResult> search(@Valid SearchReqVO reqVO) { List list = searchService.search(reqVO); // 构建返回结果 // 注意:实际应计算 categoryCount,这里简化处理 Map result = new java.util.HashMap<>(); result.put("total", list.size()); result.put("list", list); Map categoryCount = new java.util.HashMap<>(); categoryCount.put("all", list.size()); categoryCount.put("carbon_cert", 0); categoryCount.put("service", 0); categoryCount.put("news", list.size()); result.put("categoryCount", categoryCount); return CommonResult.success(result); } @GetMapping("/hot") @Operation(summary = "热门搜索", description = "获取热门搜索词") public CommonResult> getHotSearch() { return CommonResult.success(searchService.getHotSearchKeywords()); } @GetMapping("/suggest") @Operation(summary = "搜索建议", description = "获取搜索建议") public CommonResult>> getSuggest(@RequestParam String keyword) { List suggestions = searchService.getSearchSuggestions(keyword); Map> result = new java.util.HashMap<>(); result.put("suggestions", suggestions); return CommonResult.success(result); } @GetMapping("/history") @Operation(summary = "搜索历史", description = "获取用户搜索历史") public CommonResult> getSearchHistory() { return CommonResult.success(searchService.getSearchHistory()); } @DeleteMapping("/history") @Operation(summary = "清除搜索历史", description = "清除用户搜索历史") public CommonResult clearSearchHistory() { searchService.clearSearchHistory(); return CommonResult.success(true); } } ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/SearchController.java git commit -m "feat(search): 添加搜索控制器" ``` --- ## 前端任务 ### Task 5: 创建搜索 API **Files:** - Create: `txw-mhzc-web/src/pages/index/api/search.js` - [ ] **Step 1: 创建 search.js API** ```javascript import { fetchSso } from '@/core/request'; const basurl = ''; export default { /** * 搜索 * @param {Object} params - { keyword, categoryType, page, pageSize } */ search(params) { return fetchSso({ url: `${basurl}/search`, method: 'get', loading: true, params, }); }, /** * 获取热门搜索词 */ getHotSearch() { return fetchSso({ url: `${basurl}/search/hot`, method: 'get', loading: false, }); }, /** * 获取搜索建议 * @param {String} keyword - 关键词 */ getSuggest(keyword) { return fetchSso({ url: `${basurl}/search/suggest`, method: 'get', loading: false, params: { keyword }, }); }, /** * 获取搜索历史 */ getSearchHistory() { return fetchSso({ url: `${basurl}/search/history`, method: 'get', loading: false, }); }, /** * 清除搜索历史 */ clearSearchHistory() { return fetchSso({ url: `${basurl}/search/history`, method: 'delete', loading: false, }); }, }; ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/api/search.js git commit -m "feat(search): 添加搜索API" ``` --- ### Task 6: 创建搜索区域组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchArea.vue` - [ ] **Step 1: 创建 SearchArea.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchArea.vue git commit -m "feat(search): 添加搜索区域组件" ``` --- ### Task 7: 创建搜索建议组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchSuggestion.vue` - [ ] **Step 1: 创建 SearchSuggestion.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchSuggestion.vue git commit -m "feat(search): 添加搜索建议组件" ``` --- ### Task 8: 创建搜索历史横条组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchHistoryBar.vue` - [ ] **Step 1: 创建 SearchHistoryBar.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchHistoryBar.vue git commit -m "feat(search): 添加搜索历史横条组件" ``` --- ### Task 9: 创建分类 Tab 组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchCategoryTabs.vue` - [ ] **Step 1: 创建 SearchCategoryTabs.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchCategoryTabs.vue git commit -m "feat(search): 添加分类Tab组件" ``` --- ### Task 10: 创建搜索结果项组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchResultItem.vue` - [ ] **Step 1: 创建 SearchResultItem.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchResultItem.vue git commit -m "feat(search): 添加搜索结果项组件" ``` --- ### Task 11: 创建搜索结果列表组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchResultList.vue` - [ ] **Step 1: 创建 SearchResultList.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchResultList.vue git commit -m "feat(search): 添加搜索结果列表组件" ``` --- ### Task 12: 创建空状态组件 **Files:** - Create: `txw-mhzc-web/src/pages/index/components/search/SearchEmpty.vue` - [ ] **Step 1: 创建 SearchEmpty.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/components/search/SearchEmpty.vue git commit -m "feat(search): 添加空状态组件" ``` --- ### Task 13: 创建搜索结果页 **Files:** - Create: `txw-mhzc-web/src/pages/index/views/search/index.vue` - [ ] **Step 1: 创建搜索结果页 index.vue** ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/views/search/index.vue git commit -m "feat(search): 添加搜索结果页" ``` --- ### Task 14: 添加搜索路由 **Files:** - Modify: `txw-mhzc-web/src/pages/index/router/routes.js` - [ ] **Step 1: 添加搜索路由** 在 routes.js 中添加: ```javascript // 搜索 function search() { return import(/* webpackChunkName: "search" */ '@/pages/index/views/search/index.vue'); } ``` 在 export default 的路由数组中添加: ```javascript { name: 'search', path: '/search', component: search, meta: { title: '搜索结果', isShowSideBar: false, hasHome: true, breadCrumbs: [{ title: '首页', to: '/home' }, { title: '搜索结果', to: '/search' }], disableBack: true, }, }, ``` - [ ] **Step 2: 提交代码** ```bash git add txw-mhzc-web/src/pages/index/router/routes.js git commit -m "feat(search): 添加搜索路由" ``` --- ## 联调与测试 ### Task 15: 前后端联调 - [ ] **Step 1: 验证后端接口** 启动后端服务,访问 Swagger 文档: ``` http://localhost:xxxx/swagger-ui.html ``` 测试接口: - `GET /search?keyword=碳排放&categoryType=all&page=1&pageSize=10` - `GET /search/hot` - `GET /search/suggest?keyword=碳` - [ ] **Step 2: 验证前端页面** 启动前端项目: ```bash cd txw-mhzc-web yarn dev ``` 访问搜索页面: ``` http://localhost:8080/#/search?keyword=碳排放 ``` 测试功能: 1. 搜索框输入关键词,点击搜索 2. 点击热门搜索词 3. 分类 Tab 切换 4. 分页功能 5. 搜索历史展示和清除 - [ ] **Step 3: 提交联调代码** ```bash git add . git commit -m "fix(search): 联调修复" ``` --- ## 完成标准 - [ ] 所有组件正常渲染 - [ ] 搜索功能返回正确结果 - [ ] 分类筛选正常工作 - [ ] 分页功能正常 - [ ] 搜索历史本地存储正常 - [ ] 热门搜索词展示正常 - [ ] 移动端适配正常(可选)