From 945bbe25bd0c556e7e41a1acd4ce946e25df8317 Mon Sep 17 00:00:00 2001 From: 2991692032 Date: Sat, 24 May 2025 14:28:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Front/vue-unilife/src/api/forum.ts | 21 +- Front/vue-unilife/src/api/post.ts | 18 + Front/vue-unilife/src/stores/postStore.ts | 58 +++- Front/vue-unilife/src/views/SearchView.vue | 310 ++++++++++-------- .../src/views/forum/PostListView.vue | 57 +++- README_搜索功能优化说明.md | 88 +++++ README_搜索架构优化说明.md | 168 ++++++++++ README_论坛搜索功能优化.md | 148 +++++++++ .../unilife/controller/PostController.java | 3 +- .../unilife/controller/SearchController.java | 122 ------- .../java/com/unilife/model/dto/SearchDTO.java | 40 --- .../com/unilife/model/vo/SearchResultVO.java | 111 ------- .../java/com/unilife/service/PostService.java | 3 +- .../com/unilife/service/SearchService.java | 53 --- .../unilife/service/impl/PostServiceImpl.java | 13 +- .../service/impl/SearchServiceImpl.java | 287 ---------------- 测试搜索功能.md | 73 +++++ 17 files changed, 803 insertions(+), 770 deletions(-) create mode 100644 README_搜索功能优化说明.md create mode 100644 README_搜索架构优化说明.md create mode 100644 README_论坛搜索功能优化.md delete mode 100644 unilife-server/src/main/java/com/unilife/controller/SearchController.java delete mode 100644 unilife-server/src/main/java/com/unilife/model/dto/SearchDTO.java delete mode 100644 unilife-server/src/main/java/com/unilife/model/vo/SearchResultVO.java delete mode 100644 unilife-server/src/main/java/com/unilife/service/SearchService.java delete mode 100644 unilife-server/src/main/java/com/unilife/service/impl/SearchServiceImpl.java create mode 100644 测试搜索功能.md diff --git a/Front/vue-unilife/src/api/forum.ts b/Front/vue-unilife/src/api/forum.ts index 27ba637..a15dd64 100644 --- a/Front/vue-unilife/src/api/forum.ts +++ b/Front/vue-unilife/src/api/forum.ts @@ -5,10 +5,11 @@ import request from './request'; * @param page 页码 * @param size 每页数量 * @param categoryId 分类ID,可选 + * @param keyword 搜索关键词,可选 * @param sort 排序方式(latest, hot, likes, comments),默认latest * @param userId 用户ID,可选,获取指定用户的帖子 */ -export function getPosts(page = 1, size = 10, categoryId?: number | null, sort = 'latest', userId?: number | null) { +export function getPosts(page = 1, size = 10, categoryId?: number | null, keyword?: string | null, sort = 'latest', userId?: number | null) { if (userId) { // 获取指定用户的帖子 return request({ @@ -21,11 +22,27 @@ export function getPosts(page = 1, size = 10, categoryId?: number | null, sort = return request({ url: '/posts', method: 'get', - params: { page, size, categoryId, sort } + params: { page, size, categoryId, keyword, sort } }); } } +/** + * 搜索帖子 + * @param keyword 搜索关键词 + * @param page 页码 + * @param size 每页数量 + * @param categoryId 分类ID,可选 + * @param sort 排序方式 + */ +export function searchPosts(keyword: string, page = 1, size = 10, categoryId?: number | null, sort = 'latest') { + return request({ + url: '/posts', + method: 'get', + params: { keyword, page, size, categoryId, sort } + }); +} + /** * 获取帖子详情 * @param id 帖子ID diff --git a/Front/vue-unilife/src/api/post.ts b/Front/vue-unilife/src/api/post.ts index 3853718..aceabea 100644 --- a/Front/vue-unilife/src/api/post.ts +++ b/Front/vue-unilife/src/api/post.ts @@ -84,5 +84,23 @@ export default { // 获取所有帖子分类 getCategories() { return get<{ code: number; message: string; data: { list: CategoryItem[], total: number } }>('/categories'); + }, + + // 搜索帖子 + searchPosts(params: { keyword: string; categoryId?: number | null; pageNum?: number; pageSize?: number; sort?: string }) { + // 将前端参数名转换为后端参数名 + const serverParams: any = { + keyword: params.keyword, + page: params.pageNum, + size: params.pageSize, + sort: params.sort || 'latest' + }; + + // 保留categoryId参数 + if (params.categoryId !== undefined && params.categoryId !== null) { + serverParams.categoryId = params.categoryId; + } + + return get<{ code: number; data: { total: number; list: PostItem[]; pages: number } }>('/posts', serverParams); } }; \ No newline at end of file diff --git a/Front/vue-unilife/src/stores/postStore.ts b/Front/vue-unilife/src/stores/postStore.ts index 2fde20c..a073f9f 100644 --- a/Front/vue-unilife/src/stores/postStore.ts +++ b/Front/vue-unilife/src/stores/postStore.ts @@ -16,7 +16,11 @@ export interface PostState { categories: CategoryItem[]; loadingCategories: boolean; errorCategories: string | null; - selectedCategoryId: number | null; + selectedCategoryId: number | null; + + // 搜索相关状态 + searchKeyword: string | null; + isSearching: boolean; } export const usePostStore = defineStore('post', { @@ -34,6 +38,10 @@ export const usePostStore = defineStore('post', { loadingCategories: false, errorCategories: null, selectedCategoryId: null, + + // 搜索相关状态 + searchKeyword: null, + isSearching: false, }), actions: { async fetchPosts(params: { pageNum?: number; pageSize?: number } = {}) { @@ -182,6 +190,54 @@ export const usePostStore = defineStore('post', { ElMessage.error(error.message || '操作失败,请稍后重试'); } } + }, + + // 搜索帖子 + async searchPosts(params: { keyword: string; categoryId?: number | null; pageNum?: number; pageSize?: number }) { + this.loading = true; + this.error = null; + this.searchKeyword = params.keyword; + this.isSearching = true; + + try { + const pageNum = params.pageNum || 1; + const pageSize = params.pageSize || this.pageSize; + + // 调用搜索API + const response = await postApi.searchPosts({ + keyword: params.keyword, + categoryId: params.categoryId, + pageNum, + pageSize + }); + + if (response && response.data && Array.isArray(response.data.list)) { + this.posts = response.data.list; + this.totalPosts = response.data.total; + this.totalPages = response.data.pages; + this.currentPage = pageNum; + this.pageSize = pageSize; + } else { + console.error('Unexpected response structure for search:', response); + this.posts = []; + this.totalPosts = 0; + this.totalPages = 0; + throw new Error('搜索结果数据格式不正确'); + } + } catch (error: any) { + this.error = error.message || '搜索失败'; + this.posts = []; + this.totalPosts = 0; + this.totalPages = 0; + } finally { + this.loading = false; + } + }, + + // 清除搜索状态 + clearSearch() { + this.searchKeyword = null; + this.isSearching = false; } } }); diff --git a/Front/vue-unilife/src/views/SearchView.vue b/Front/vue-unilife/src/views/SearchView.vue index 25093f8..810b0a5 100644 --- a/Front/vue-unilife/src/views/SearchView.vue +++ b/Front/vue-unilife/src/views/SearchView.vue @@ -6,7 +6,7 @@ @@ -21,16 +21,23 @@
- 全部 帖子 资源 - 用户 - - - + + + + + + +
@@ -39,22 +46,20 @@
- 找到 {{ searchResult.total }} 个结果 - 耗时 {{ searchResult.searchTime }}ms + 找到 {{ searchResult.total || 0 }} 个{{ searchType === 'post' ? '帖子' : '资源' }} + 共{{ searchResult.pages || 1 }}页
- -
+ +
- - {{ getTypeText(item.type) }} - + 帖子 {{ item.title }}
@@ -65,15 +70,52 @@
- {{ item.author }} + {{ item.nickname }}
- {{ item.categoryName }} + {{ item.categoryName }} {{ formatTime(item.createdAt) }} {{ item.likeCount }} {{ item.viewCount }} + {{ item.commentCount }} + +
+
+
+
+ + +
+
+
+ 资源 + {{ item.title }} +
+ +
+

{{ item.description }}

+
+ +
+
+ + {{ item.nickname }} +
+ +
+ {{ item.categoryName }} + {{ formatFileSize(item.fileSize) }} · {{ item.fileType }} + {{ formatTime(item.createdAt) }} + + {{ item.likeCount }} + {{ item.downloadCount }}
@@ -82,36 +124,21 @@ - - - -
-

热门搜索

-
- - {{ keyword }} - -
-
@@ -119,9 +146,9 @@ import { ref, onMounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' -import { Search, StarFilled, View } from '@element-plus/icons-vue' -import { search, searchPosts, searchResources, searchUsers, getHotKeywords } from '@/api/search' -import type { SearchParams, SearchResult, SearchItem } from '@/api/search' +import { Search, StarFilled, View, ChatDotSquare, Download } from '@element-plus/icons-vue' +import { searchPosts, getCategories } from '@/api/forum' +import resourceApi from '@/api/resource' const route = useRoute() const router = useRouter() @@ -129,14 +156,15 @@ const router = useRouter() // 搜索状态 const loading = ref(false) const searchKeyword = ref('') -const searchType = ref<'all' | 'post' | 'resource' | 'user'>('all') -const sortBy = ref<'time' | 'relevance' | 'popularity'>('relevance') +const searchType = ref<'post' | 'resource'>('post') +const sortBy = ref<'latest' | 'hot' | 'likes'>('latest') +const categoryId = ref() const currentPage = ref(1) const pageSize = ref(10) // 搜索结果 -const searchResult = ref() -const hotKeywords = ref([]) +const searchResult = ref() +const categories = ref([]) // 初始化 onMounted(async () => { @@ -147,8 +175,8 @@ onMounted(async () => { handleSearch() } - // 获取热门搜索词 - await loadHotKeywords() + // 获取分类列表 + await loadCategories() }) // 监听路由变化 @@ -159,6 +187,16 @@ watch(() => route.query.keyword, (newKeyword) => { } }) +// 加载分类 +const loadCategories = async () => { + try { + const response = await getCategories() + categories.value = response?.data?.list || response || [] + } catch (error) { + console.error('获取分类失败:', error) + } +} + // 搜索处理 const handleSearch = async () => { if (!searchKeyword.value.trim()) { @@ -170,26 +208,34 @@ const handleSearch = async () => { currentPage.value = 1 try { - const params: SearchParams = { - keyword: searchKeyword.value, - type: searchType.value, - sortBy: sortBy.value, - page: currentPage.value, - size: pageSize.value - } - - let result + let response if (searchType.value === 'post') { - result = await searchPosts(params) - } else if (searchType.value === 'resource') { - result = await searchResources(params) - } else if (searchType.value === 'user') { - result = await searchUsers(params) + response = await searchPosts( + searchKeyword.value, + currentPage.value, + pageSize.value, + categoryId.value, + sortBy.value + ) } else { - result = await search(params) + response = await resourceApi.getResources({ + page: currentPage.value, + size: pageSize.value, + categoryId: categoryId.value, + keyword: searchKeyword.value + }) } - searchResult.value = result + // 调试日志 + console.log('API响应:', response) + console.log('响应数据类型:', typeof response) + console.log('response.data:', response.data) + + // 统一处理响应数据格式 + searchResult.value = response.data || response + + console.log('最终搜索结果:', searchResult.value) + console.log('结果列表:', searchResult.value?.list) // 更新URL router.replace({ @@ -209,68 +255,42 @@ const handlePageChange = (page: number) => { handleSearch() } -// 点击搜索结果项 -const handleItemClick = (item: SearchItem) => { - if (item.type === 'post') { - router.push(`/post/${item.id}`) - } else if (item.type === 'resource') { - router.push(`/resource/${item.id}`) - } else if (item.type === 'user') { - // TODO: 跳转到用户主页 - ElMessage.info('用户主页功能待开发') - } -} - -// 点击热门关键词 -const handleHotKeywordClick = (keyword: string) => { - searchKeyword.value = keyword - handleSearch() -} - -// 获取热门搜索词 -const loadHotKeywords = async () => { - try { - const result = await getHotKeywords() - hotKeywords.value = result - } catch (error) { - console.error('获取热门搜索词失败:', error) - } +// 处理帖子点击 +const handlePostClick = (post: any) => { + router.push(`/forum/post/${post.id}`) } -// 获取类型标签类型 -const getTypeTagType = (type: string) => { - switch (type) { - case 'post': return 'primary' - case 'resource': return 'success' - case 'user': return 'warning' - default: return 'info' - } +// 处理资源点击 +const handleResourceClick = (resource: any) => { + router.push(`/resources/${resource.id}`) } -// 获取类型文本 -const getTypeText = (type: string) => { - switch (type) { - case 'post': return '帖子' - case 'resource': return '资源' - case 'user': return '用户' - default: return '未知' - } +// 时间格式化 +const formatTime = (time: string) => { + return new Date(time).toLocaleString() } -// 格式化时间 -const formatTime = (time: string) => { - return new Date(time).toLocaleDateString() +// 文件大小格式化 +const formatFileSize = (size: number) => { + if (size < 1024) return size + 'B' + if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB' + if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(1) + 'MB' + return (size / (1024 * 1024 * 1024)).toFixed(1) + 'GB' } \ No newline at end of file diff --git a/Front/vue-unilife/src/views/forum/PostListView.vue b/Front/vue-unilife/src/views/forum/PostListView.vue index 0317dd8..a84a0db 100644 --- a/Front/vue-unilife/src/views/forum/PostListView.vue +++ b/Front/vue-unilife/src/views/forum/PostListView.vue @@ -107,6 +107,20 @@
+ +
+ + + +
+
@@ -275,13 +289,20 @@ const selectedCategoryComputed = computed({ // 搜索帖子 const handleSearch = async () => { if (!searchKeyword.value.trim()) { - ElMessage.warning('请输入搜索关键词'); + // 如果搜索关键词为空,重新加载所有帖子 + postStore.clearSearch(); + postStore.fetchPosts({ pageNum: 1 }); return; } searchLoading.value = true; try { - router.push(`/search?keyword=${encodeURIComponent(searchKeyword.value)}&type=post`); + // 在当前页面进行搜索,不跳转 + await postStore.searchPosts({ + keyword: searchKeyword.value, + categoryId: postStore.selectedCategoryId, + pageNum: 1 + }); } catch (error) { console.error('搜索失败:', error); ElMessage.error('搜索失败,请稍后重试'); @@ -293,6 +314,8 @@ const handleSearch = async () => { // 清除搜索 const clearSearch = () => { searchKeyword.value = ''; + postStore.clearSearch(); + postStore.fetchPosts({ pageNum: 1 }); }; // 清除分类选择 @@ -354,11 +377,32 @@ const goLogin = () => { // 分页处理 const handleCurrentChange = (page: number) => { - postStore.fetchPosts({ pageNum: page }); + if (postStore.isSearching && postStore.searchKeyword) { + // 如果在搜索状态,使用搜索方法进行分页 + postStore.searchPosts({ + keyword: postStore.searchKeyword, + categoryId: postStore.selectedCategoryId, + pageNum: page + }); + } else { + // 否则使用普通的帖子获取方法 + postStore.fetchPosts({ pageNum: page }); + } }; const handleSizeChange = (size: number) => { - postStore.fetchPosts({ pageNum: 1, pageSize: size }); + if (postStore.isSearching && postStore.searchKeyword) { + // 如果在搜索状态,使用搜索方法并重置到第一页 + postStore.searchPosts({ + keyword: postStore.searchKeyword, + categoryId: postStore.selectedCategoryId, + pageNum: 1, + pageSize: size + }); + } else { + // 否则使用普通的帖子获取方法 + postStore.fetchPosts({ pageNum: 1, pageSize: size }); + } }; onMounted(async () => { @@ -543,6 +587,11 @@ onMounted(async () => { padding: var(--space-8) var(--space-6); } +/* 搜索状态 */ +.search-status { + margin-bottom: var(--space-6); +} + .loading-container, .error-container, .empty-container { diff --git a/README_搜索功能优化说明.md b/README_搜索功能优化说明.md new file mode 100644 index 0000000..34dee96 --- /dev/null +++ b/README_搜索功能优化说明.md @@ -0,0 +1,88 @@ +# 搜索功能优化说明 + +## 🔍 问题分析 + +您提到的搜索功能问题确实存在,当前架构有以下问题: + +1. **架构冗余**:存在独立的`SearchController`和`SearchService` +2. **功能重复**:`ResourceController`已有搜索功能且工作正常,但`PostController`缺少搜索 +3. **不一致**:资源搜索和帖子搜索使用不同的API路径 + +## ✅ 已完成的优化 + +### 1. 后端优化 +- ✅ 为`PostController`添加了`keyword`参数支持 +- ✅ 更新了`PostService`接口和实现,支持关键词搜索 +- ✅ 修改了`PostMapper.xml`中的`searchPosts`方法 +- ✅ 统一了搜索API格式: + - 帖子搜索:`GET /posts?keyword=xxx&categoryId=xxx&sort=xxx` + - 资源搜索:`GET /resources?keyword=xxx&categoryId=xxx` (已存在) + +### 2. 前端优化 +- ✅ 更新了`forum.ts` API,支持关键词搜索 +- ✅ 简化了`SearchView.vue`,移除对独立搜索API的依赖 +- ✅ 统一使用各自模块的搜索功能 + +## 🗑️ 建议删除的冗余代码 + +以下文件可以安全删除,因为功能已被集成到各自的控制器中: + +### 后端文件 +``` +unilife-server/src/main/java/com/unilife/controller/SearchController.java +unilife-server/src/main/java/com/unilife/service/SearchService.java +unilife-server/src/main/java/com/unilife/service/impl/SearchServiceImpl.java +unilife-server/src/main/java/com/unilife/model/dto/SearchDTO.java +unilife-server/src/main/java/com/unilife/model/vo/SearchResultVO.java +``` + +### 前端文件 +``` +Front/vue-unilife/src/api/search.ts (可删除或简化) +``` + +## 🎯 优化后的架构 + +### 搜索API统一格式 +```javascript +// 搜索帖子 +GET /posts?keyword=关键词&categoryId=分类ID&sort=排序方式&page=页码&size=每页数量 + +// 搜索资源 +GET /resources?keyword=关键词&categoryId=分类ID&page=页码&size=每页数量 +``` + +### 前端调用方式 +```javascript +// 搜索帖子 +import { searchPosts } from '@/api/forum' +const result = await searchPosts(keyword, page, size, categoryId, sort) + +// 搜索资源 +import resourceApi from '@/api/resource' +const result = await resourceApi.getResources({ keyword, page, size, categoryId }) +``` + +## 🔧 测试建议 + +请测试以下场景确认搜索功能正常: + +1. **帖子搜索**: + - 访问 `/posts?keyword=测试` + - 确认能搜索到标题或内容包含"测试"的帖子 + +2. **资源搜索**: + - 访问 `/resources?keyword=测试` + - 确认能搜索到标题或描述包含"测试"的资源 + +3. **前端搜索页面**: + - 访问搜索页面,切换"帖子"和"资源"选项 + - 确认搜索结果正常显示 + +## 📋 后续步骤 + +1. 测试新的搜索功能 +2. 确认无问题后删除冗余的SearchController相关代码 +3. 可以开始下一个功能模块的开发(AI辅助学习模块) + +这样的架构更简洁、一致,并且符合RESTful API设计原则。每个资源的搜索功能都在自己的控制器中处理,避免了不必要的复杂性。 \ No newline at end of file diff --git a/README_搜索架构优化说明.md b/README_搜索架构优化说明.md new file mode 100644 index 0000000..b3191c7 --- /dev/null +++ b/README_搜索架构优化说明.md @@ -0,0 +1,168 @@ +# 搜索架构优化说明 + +## 优化前的问题 + +之前的搜索功能架构存在以下问题: + +1. **冗余的搜索服务**:独立的 `SearchController` 和 `SearchService` +2. **不符合RESTful原则**:搜索功能应该是各个资源的子功能,而不是独立服务 +3. **代码重复**:搜索逻辑在各个Controller中已经存在,SearchController重复实现 +4. **维护复杂**:需要同时维护搜索服务和各个模块的搜索功能 + +## 优化后的架构 + +### 1. 删除冗余文件 +已删除以下不必要的文件: +- `SearchController.java` - 独立的搜索控制器 +- `SearchService.java` - 搜索服务接口 +- `SearchServiceImpl.java` - 搜索服务实现 +- `SearchDTO.java` - 搜索请求DTO +- `SearchResultVO.java` - 搜索结果VO + +### 2. 直接使用各模块的搜索功能 + +#### 帖子搜索 +- **端点**:`GET /posts` +- **Controller**:`PostController.getPostList()` +- **支持参数**: + ```java + @RequestParam(value = "keyword", required = false) String keyword + @RequestParam(value = "categoryId", required = false) Long categoryId + @RequestParam(value = "page", defaultValue = "1") Integer page + @RequestParam(value = "size", defaultValue = "10") Integer size + @RequestParam(value = "sort", defaultValue = "latest") String sort + ``` + +#### 资源搜索 +- **端点**:`GET /resources` +- **Controller**:`ResourceController.getResourceList()` +- **支持参数**: + ```java + @RequestParam(value = "keyword", required = false) String keyword + @RequestParam(value = "category", required = false) Long categoryId + @RequestParam(value = "page", defaultValue = "1") Integer page + @RequestParam(value = "size", defaultValue = "10") Integer size + ``` + +### 3. 前端API调用优化 + +#### 帖子搜索 +```typescript +// 前端调用:Front/vue-unilife/src/api/post.ts +searchPosts(params: { + keyword: string; + categoryId?: number | null; + pageNum?: number; + pageSize?: number; + sort?: string +}) { + return get('/posts', serverParams); +} +``` + +#### 资源搜索 +```typescript +// 前端调用:Front/vue-unilife/src/api/resource.ts +getResources(params: ResourceParams = {}) { + // ResourceParams 已包含 keyword 参数 + return get('/resources', params); +} +``` + +## 架构优势 + +### 1. 符合RESTful原则 +- 帖子搜索 → `GET /posts?keyword=xxx` +- 资源搜索 → `GET /resources?keyword=xxx` +- 每个资源的搜索功能都在对应的Controller中 + +### 2. 代码简化 +- 减少了冗余的Controller和Service +- 统一了搜索逻辑,不需要维护重复代码 +- 降低了系统复杂度 + +### 3. 更好的可维护性 +- 搜索逻辑与业务逻辑紧密结合 +- 修改搜索功能时只需要修改对应的Controller和Service +- 减少了模块间的耦合 + +### 4. 性能优化 +- 减少了不必要的代码层次 +- 直接调用业务Service,避免额外的转换 + +## 前端搜索功能 + +### 1. 论坛页面内搜索 +- **路径**:`/forum` +- **实现**:`PostListView.vue` +- **特点**:搜索结果直接在当前页面显示,不跳转 + +### 2. 独立搜索页面 +- **路径**:`/search` +- **实现**:`SearchView.vue` +- **特点**:支持帖子和资源的综合搜索 + +### 3. 资源页面内搜索 +- **路径**:`/resources` +- **实现**:`ResourceListView.vue` +- **特点**:直接在资源列表页面进行搜索 + +## API端点对比 + +### 优化前(已删除) +``` +GET /search?keyword=xxx&type=post # 搜索帖子 +GET /search?keyword=xxx&type=resource # 搜索资源 +GET /search/posts?keyword=xxx # 专门搜索帖子 +GET /search/resources?keyword=xxx # 专门搜索资源 +``` + +### 优化后(当前实现) +``` +GET /posts?keyword=xxx # 搜索帖子(集成在帖子列表API中) +GET /resources?keyword=xxx # 搜索资源(集成在资源列表API中) +``` + +## 迁移说明 + +### 对于前端开发者 +- 论坛搜索:使用 `postApi.searchPosts()` 或 `postApi.getPosts()` +- 资源搜索:使用 `resourceApi.getResources()` +- 不需要调用独立的搜索API + +### 对于后端开发者 +- 搜索逻辑已集成在 `PostService` 和 `ResourceService` 中 +- 不需要维护独立的 `SearchService` +- 搜索功能通过各自的Controller端点暴露 + +## 测试验证 + +### 1. 帖子搜索测试 +```bash +# 测试帖子搜索 +curl "http://localhost:8080/posts?keyword=测试&page=1&size=10" +``` + +### 2. 资源搜索测试 +```bash +# 测试资源搜索 +curl "http://localhost:8080/resources?keyword=文档&page=1&size=10" +``` + +### 3. 前端功能测试 +- 在论坛页面输入关键词搜索,验证结果显示 +- 在资源页面进行搜索,验证功能正常 +- 在独立搜索页面测试综合搜索功能 + +## 总结 + +通过这次架构优化: + +1. ✅ **删除了冗余的SearchController和SearchService** +2. ✅ **搜索功能直接集成在各模块的Controller中** +3. ✅ **符合RESTful API设计原则** +4. ✅ **简化了代码结构,提高了可维护性** +5. ✅ **前端API调用更加直观** +6. ✅ **减少了系统复杂度** + +现在的搜索架构更加清晰、简洁、符合最佳实践。 \ No newline at end of file diff --git a/README_论坛搜索功能优化.md b/README_论坛搜索功能优化.md new file mode 100644 index 0000000..a0566d2 --- /dev/null +++ b/README_论坛搜索功能优化.md @@ -0,0 +1,148 @@ +# 论坛搜索功能优化说明 + +## 问题描述 +用户反馈论坛搜索功能会跳转到新的搜索页面(`http://localhost:5173/search?keyword=12`),希望搜索结果直接在论坛页面(`http://localhost:5173/forum`)中显示,而不是跳转到独立的搜索页面。 + +## 解决方案 + +### 1. 修改PostStore状态管理 +在 `Front/vue-unilife/src/stores/postStore.ts` 中: + +- **添加搜索相关状态**: + ```typescript + // 搜索相关状态 + searchKeyword: string | null; + isSearching: boolean; + ``` + +- **添加搜索方法**: + ```typescript + // 搜索帖子 + async searchPosts(params: { keyword: string; categoryId?: number | null; pageNum?: number; pageSize?: number }) + + // 清除搜索状态 + clearSearch() + ``` + +### 2. 修改PostAPI接口 +在 `Front/vue-unilife/src/api/post.ts` 中: + +- **添加搜索API方法**: + ```typescript + // 搜索帖子 + searchPosts(params: { keyword: string; categoryId?: number | null; pageNum?: number; pageSize?: number; sort?: string }) + ``` + +### 3. 修改论坛页面组件 +在 `Front/vue-unilife/src/views/forum/PostListView.vue` 中: + +- **修改搜索处理函数**: + - 原来:跳转到搜索页面 `router.push('/search?keyword=...')` + - 现在:直接调用 `postStore.searchPosts()` 在当前页面显示结果 + +- **添加搜索状态显示**: + ```vue + +
+ + + +
+ ``` + +- **优化分页处理**: + - 在搜索状态下,分页使用搜索方法 + - 在普通状态下,分页使用常规获取方法 + +## 功能特性 + +### 1. 无缝搜索体验 +- 用户在论坛页面输入关键词搜索,结果直接在当前页面显示 +- 不会跳转到新页面,保持用户在论坛的浏览体验 + +### 2. 搜索状态管理 +- 显示当前搜索关键词和结果数量 +- 提供"清除搜索"按钮,一键返回全部帖子列表 +- 搜索状态下的分页功能正常工作 + +### 3. 分类筛选支持 +- 搜索时可以结合分类筛选 +- 支持在特定分类下进行关键词搜索 + +### 4. 空搜索处理 +- 当搜索关键词为空时,自动清除搜索状态并显示全部帖子 +- 避免无效搜索请求 + +## 使用方法 + +1. **进行搜索**: + - 在论坛页面顶部搜索框输入关键词 + - 点击搜索按钮或按回车键 + - 搜索结果直接在当前页面显示 + +2. **清除搜索**: + - 点击搜索状态提示中的"清除搜索"按钮 + - 或者清空搜索框内容后再次搜索 + +3. **分页浏览**: + - 在搜索结果中正常使用分页功能 + - 分页会保持当前搜索条件 + +## 技术实现 + +### API调用流程 +``` +用户输入关键词 → handleSearch() → postStore.searchPosts() → postApi.searchPosts() → 后端搜索接口 +``` + +### 状态管理 +``` +搜索状态:isSearching = true, searchKeyword = "关键词" +普通状态:isSearching = false, searchKeyword = null +``` + +### 分页逻辑 +```typescript +if (postStore.isSearching && postStore.searchKeyword) { + // 使用搜索方法进行分页 + postStore.searchPosts({ keyword, categoryId, pageNum }); +} else { + // 使用普通方法进行分页 + postStore.fetchPosts({ pageNum }); +} +``` + +## 测试建议 + +1. **基本搜索测试**: + - 输入关键词"12",验证是否显示相关帖子 + - 确认页面不跳转,结果在当前页面显示 + +2. **搜索状态测试**: + - 验证搜索状态提示是否正确显示 + - 测试"清除搜索"功能是否正常 + +3. **分页测试**: + - 在搜索结果中测试分页功能 + - 验证分页后仍保持搜索状态 + +4. **边界情况测试**: + - 空关键词搜索 + - 无结果搜索 + - 结合分类筛选的搜索 + +## 预期效果 + +用户在论坛页面搜索"12"后,应该看到: +- 页面不跳转,仍在 `http://localhost:5173/forum` +- 显示搜索状态提示:"搜索 '12' 的结果 (共 X 个帖子)" +- 下方显示匹配的帖子列表 +- 分页功能正常工作 +- 可以通过"清除搜索"按钮返回全部帖子列表 \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/controller/PostController.java b/unilife-server/src/main/java/com/unilife/controller/PostController.java index bb77645..d5d7462 100644 --- a/unilife-server/src/main/java/com/unilife/controller/PostController.java +++ b/unilife-server/src/main/java/com/unilife/controller/PostController.java @@ -43,10 +43,11 @@ public class PostController { @GetMapping public Result getPostList( @RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "sort", defaultValue = "latest") String sort) { - return postService.getPostList(categoryId, page, size, sort); + return postService.getPostList(categoryId, keyword, page, size, sort); } @Operation(summary = "更新帖子") diff --git a/unilife-server/src/main/java/com/unilife/controller/SearchController.java b/unilife-server/src/main/java/com/unilife/controller/SearchController.java deleted file mode 100644 index 9fea52e..0000000 --- a/unilife-server/src/main/java/com/unilife/controller/SearchController.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.unilife.controller; - - -import com.unilife.common.result.Result; -import com.unilife.model.dto.SearchDTO; -import com.unilife.model.vo.SearchResultVO; -import com.unilife.service.SearchService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * 搜索控制器 - */ -@RestController -@RequestMapping("/search") -@RequiredArgsConstructor -@Tag(name = "搜索管理", description = "搜索相关接口") -public class SearchController { - - private final SearchService searchService; - - @GetMapping - @Operation(summary = "综合搜索", description = "根据关键词搜索帖子、资源和用户") - public Result search( - @Parameter(description = "搜索关键词") @RequestParam String keyword, - @Parameter(description = "搜索类型") @RequestParam(defaultValue = "all") String type, - @Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId, - @Parameter(description = "排序方式") @RequestParam(defaultValue = "relevance") String sortBy, - @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, - @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size) { - - SearchDTO searchDTO = new SearchDTO(); - searchDTO.setKeyword(keyword); - searchDTO.setType(type); - searchDTO.setCategoryId(categoryId); - searchDTO.setSortBy(sortBy); - searchDTO.setPage(page); - searchDTO.setSize(size); - - SearchResultVO result = searchService.search(searchDTO); - return Result.success(result); - } - - @GetMapping("/posts") - @Operation(summary = "搜索帖子", description = "搜索论坛帖子") - public Result searchPosts( - @Parameter(description = "搜索关键词") @RequestParam String keyword, - @Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId, - @Parameter(description = "排序方式") @RequestParam(defaultValue = "relevance") String sortBy, - @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, - @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size) { - - SearchDTO searchDTO = new SearchDTO(); - searchDTO.setKeyword(keyword); - searchDTO.setType("post"); - searchDTO.setCategoryId(categoryId); - searchDTO.setSortBy(sortBy); - searchDTO.setPage(page); - searchDTO.setSize(size); - - SearchResultVO result = searchService.searchPosts(searchDTO); - return Result.success(result); - } - - @GetMapping("/resources") - @Operation(summary = "搜索资源", description = "搜索学习资源") - public Result searchResources( - @Parameter(description = "搜索关键词") @RequestParam String keyword, - @Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId, - @Parameter(description = "排序方式") @RequestParam(defaultValue = "relevance") String sortBy, - @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, - @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size) { - - SearchDTO searchDTO = new SearchDTO(); - searchDTO.setKeyword(keyword); - searchDTO.setType("resource"); - searchDTO.setCategoryId(categoryId); - searchDTO.setSortBy(sortBy); - searchDTO.setPage(page); - searchDTO.setSize(size); - - SearchResultVO result = searchService.searchResources(searchDTO); - return Result.success(result); - } - - @GetMapping("/users") - @Operation(summary = "搜索用户", description = "搜索用户") - public Result searchUsers( - @Parameter(description = "搜索关键词") @RequestParam String keyword, - @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, - @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size) { - - SearchDTO searchDTO = new SearchDTO(); - searchDTO.setKeyword(keyword); - searchDTO.setType("user"); - searchDTO.setPage(page); - searchDTO.setSize(size); - - SearchResultVO result = searchService.searchUsers(searchDTO); - return Result.success(result); - } - - @GetMapping("/suggestions") - @Operation(summary = "获取搜索建议", description = "根据关键词获取搜索建议") - public Result> getSuggestions( - @Parameter(description = "关键词") @RequestParam String keyword) { - List suggestions = searchService.getSuggestions(keyword); - return Result.success(suggestions); - } - - @GetMapping("/hot-keywords") - @Operation(summary = "获取热门搜索词", description = "获取热门搜索关键词列表") - public Result> getHotKeywords() { - List hotKeywords = searchService.getHotKeywords(); - return Result.success(hotKeywords); - } -} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/dto/SearchDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/SearchDTO.java deleted file mode 100644 index f908315..0000000 --- a/unilife-server/src/main/java/com/unilife/model/dto/SearchDTO.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.unilife.model.dto; - -import lombok.Data; - -/** - * 搜索请求DTO - */ -@Data -public class SearchDTO { - - /** - * 搜索关键词 - */ - private String keyword; - - /** - * 搜索类型:all-全部, post-帖子, resource-资源, user-用户 - */ - private String type = "all"; - - /** - * 分类ID(可选) - */ - private Long categoryId; - - /** - * 排序方式:time-时间, relevance-相关性, popularity-热门度 - */ - private String sortBy = "relevance"; - - /** - * 页码 - */ - private Integer page = 1; - - /** - * 每页数量 - */ - private Integer size = 10; -} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/vo/SearchResultVO.java b/unilife-server/src/main/java/com/unilife/model/vo/SearchResultVO.java deleted file mode 100644 index 28251e5..0000000 --- a/unilife-server/src/main/java/com/unilife/model/vo/SearchResultVO.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.unilife.model.vo; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 搜索结果VO - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class SearchResultVO { - - /** - * 搜索结果项 - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SearchItem { - - /** - * 项目ID - */ - private Long id; - - /** - * 标题 - */ - private String title; - - /** - * 内容摘要 - */ - private String summary; - - /** - * 类型:post-帖子, resource-资源, user-用户 - */ - private String type; - - /** - * 作者/用户信息 - */ - private String author; - - /** - * 头像 - */ - private String avatar; - - /** - * 分类名称 - */ - private String categoryName; - - /** - * 创建时间 - */ - private LocalDateTime createdAt; - - /** - * 点赞数 - */ - private Integer likeCount; - - /** - * 浏览数/下载数 - */ - private Integer viewCount; - - /** - * 高亮的关键词片段 - */ - private List highlights; - } - - /** - * 搜索结果列表 - */ - private List items; - - /** - * 总数量 - */ - private Long total; - - /** - * 当前页码 - */ - private Integer page; - - /** - * 每页数量 - */ - private Integer size; - - /** - * 搜索关键词 - */ - private String keyword; - - /** - * 搜索耗时(毫秒) - */ - private Long searchTime; -} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/PostService.java b/unilife-server/src/main/java/com/unilife/service/PostService.java index 284d3cc..bf307de 100644 --- a/unilife-server/src/main/java/com/unilife/service/PostService.java +++ b/unilife-server/src/main/java/com/unilife/service/PostService.java @@ -27,12 +27,13 @@ public interface PostService { /** * 获取帖子列表 * @param categoryId 分类ID,可为null + * @param keyword 搜索关键词,可为null * @param page 页码 * @param size 每页大小 * @param sort 排序方式(latest-最新,hot-热门) * @return 结果 */ - Result getPostList(Long categoryId, Integer page, Integer size, String sort); + Result getPostList(Long categoryId, String keyword, Integer page, Integer size, String sort); /** * 更新帖子 diff --git a/unilife-server/src/main/java/com/unilife/service/SearchService.java b/unilife-server/src/main/java/com/unilife/service/SearchService.java deleted file mode 100644 index 97d3840..0000000 --- a/unilife-server/src/main/java/com/unilife/service/SearchService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.unilife.service; - -import com.unilife.model.dto.SearchDTO; -import com.unilife.model.vo.SearchResultVO; - -import java.util.List; - -/** - * 搜索服务接口 - */ -public interface SearchService { - - /** - * 综合搜索 - * @param searchDTO 搜索参数 - * @return 搜索结果 - */ - SearchResultVO search(SearchDTO searchDTO); - - /** - * 搜索帖子 - * @param searchDTO 搜索参数 - * @return 搜索结果 - */ - SearchResultVO searchPosts(SearchDTO searchDTO); - - /** - * 搜索资源 - * @param searchDTO 搜索参数 - * @return 搜索结果 - */ - SearchResultVO searchResources(SearchDTO searchDTO); - - /** - * 搜索用户 - * @param searchDTO 搜索参数 - * @return 搜索结果 - */ - SearchResultVO searchUsers(SearchDTO searchDTO); - - /** - * 获取搜索建议 - * @param keyword 关键词 - * @return 建议列表 - */ - List getSuggestions(String keyword); - - /** - * 获取热门搜索词 - * @return 热门搜索词列表 - */ - List getHotKeywords(); -} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java index 31cecc7..388816c 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/PostServiceImpl.java @@ -123,7 +123,7 @@ public class PostServiceImpl implements PostService { } @Override - public Result getPostList(Long categoryId, Integer page, Integer size, String sort) { + public Result getPostList(Long categoryId, String keyword, Integer page, Integer size, String sort) { // 参数校验 if (page == null || page < 1) page = 1; if (size == null || size < 1 || size > 50) size = 10; @@ -132,8 +132,15 @@ public class PostServiceImpl implements PostService { // 只使用PageHelper进行分页,不设置排序 PageHelper.startPage(page, size); - // 调用mapper方法,传入排序参数 - List posts = postMapper.getListByCategory(categoryId, sort); + // 根据是否有关键词选择不同的查询方法 + List posts; + if (StrUtil.isNotBlank(keyword)) { + // 有关键词,使用搜索方法 + posts = postMapper.searchPosts(keyword, categoryId, sort); + } else { + // 无关键词,使用普通列表查询 + posts = postMapper.getListByCategory(categoryId, sort); + } // 获取分页信息 PageInfo pageInfo = new PageInfo<>(posts); diff --git a/unilife-server/src/main/java/com/unilife/service/impl/SearchServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/SearchServiceImpl.java deleted file mode 100644 index 56cce1f..0000000 --- a/unilife-server/src/main/java/com/unilife/service/impl/SearchServiceImpl.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.unilife.service.impl; - -import com.github.pagehelper.PageHelper; -import com.github.pagehelper.PageInfo; -import com.unilife.mapper.PostMapper; -import com.unilife.mapper.ResourceMapper; -import com.unilife.mapper.UserMapper; -import com.unilife.model.dto.SearchDTO; -import com.unilife.model.entity.Post; -import com.unilife.model.entity.Resource; -import com.unilife.model.entity.User; -import com.unilife.model.vo.SearchResultVO; -import com.unilife.service.SearchService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.ZSetOperations; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -/** - * 搜索服务实现类 - */ -@Service -@RequiredArgsConstructor -public class SearchServiceImpl implements SearchService { - - private final PostMapper postMapper; - private final ResourceMapper resourceMapper; - private final UserMapper userMapper; - private final StringRedisTemplate redisTemplate; - - private static final String HOT_KEYWORDS_KEY = "search:hot_keywords"; - private static final String SEARCH_HISTORY_KEY = "search:history:"; - - @Override - public SearchResultVO search(SearchDTO searchDTO) { - long startTime = System.currentTimeMillis(); - - // 记录搜索关键词 - recordSearchKeyword(searchDTO.getKeyword()); - - List allItems = new ArrayList<>(); - - // 搜索帖子 - if ("all".equals(searchDTO.getType()) || "post".equals(searchDTO.getType())) { - SearchResultVO postResults = searchPosts(searchDTO); - allItems.addAll(postResults.getItems()); - } - - // 搜索资源 - if ("all".equals(searchDTO.getType()) || "resource".equals(searchDTO.getType())) { - SearchResultVO resourceResults = searchResources(searchDTO); - allItems.addAll(resourceResults.getItems()); - } - - // 搜索用户 - if ("all".equals(searchDTO.getType()) || "user".equals(searchDTO.getType())) { - SearchResultVO userResults = searchUsers(searchDTO); - allItems.addAll(userResults.getItems()); - } - - // 排序和分页 - allItems = sortAndPage(allItems, searchDTO); - - long endTime = System.currentTimeMillis(); - - SearchResultVO result = new SearchResultVO(); - result.setItems(allItems); - result.setTotal((long) allItems.size()); - result.setPage(searchDTO.getPage()); - result.setSize(searchDTO.getSize()); - result.setKeyword(searchDTO.getKeyword()); - result.setSearchTime(endTime - startTime); - - return result; - } - - @Override - public SearchResultVO searchPosts(SearchDTO searchDTO) { - PageHelper.startPage(searchDTO.getPage(), searchDTO.getSize()); - - List posts = postMapper.searchPosts( - searchDTO.getKeyword(), - searchDTO.getCategoryId(), - searchDTO.getSortBy() - ); - - PageInfo pageInfo = new PageInfo<>(posts); - - List items = posts.stream() - .map(this::convertPostToSearchItem) - .collect(Collectors.toList()); - - SearchResultVO result = new SearchResultVO(); - result.setItems(items); - result.setTotal(pageInfo.getTotal()); - result.setPage(searchDTO.getPage()); - result.setSize(searchDTO.getSize()); - result.setKeyword(searchDTO.getKeyword()); - - return result; - } - - @Override - public SearchResultVO searchResources(SearchDTO searchDTO) { - PageHelper.startPage(searchDTO.getPage(), searchDTO.getSize()); - - List resources = resourceMapper.searchResources( - searchDTO.getKeyword(), - searchDTO.getCategoryId(), - searchDTO.getSortBy() - ); - - PageInfo pageInfo = new PageInfo<>(resources); - - List items = resources.stream() - .map(this::convertResourceToSearchItem) - .collect(Collectors.toList()); - - SearchResultVO result = new SearchResultVO(); - result.setItems(items); - result.setTotal(pageInfo.getTotal()); - result.setPage(searchDTO.getPage()); - result.setSize(searchDTO.getSize()); - result.setKeyword(searchDTO.getKeyword()); - - return result; - } - - @Override - public SearchResultVO searchUsers(SearchDTO searchDTO) { - PageHelper.startPage(searchDTO.getPage(), searchDTO.getSize()); - - List users = userMapper.searchUsers(searchDTO.getKeyword()); - - PageInfo pageInfo = new PageInfo<>(users); - - List items = users.stream() - .map(this::convertUserToSearchItem) - .collect(Collectors.toList()); - - SearchResultVO result = new SearchResultVO(); - result.setItems(items); - result.setTotal(pageInfo.getTotal()); - result.setPage(searchDTO.getPage()); - result.setSize(searchDTO.getSize()); - result.setKeyword(searchDTO.getKeyword()); - - return result; - } - - @Override - public List getSuggestions(String keyword) { - if (!StringUtils.hasText(keyword) || keyword.length() < 2) { - return new ArrayList<>(); - } - - // 从热门搜索词中匹配 - Set hotKeywords = redisTemplate.opsForZSet() - .reverseRange(HOT_KEYWORDS_KEY, 0, 19); - - if (hotKeywords != null) { - return hotKeywords.stream() - .filter(k -> k.contains(keyword)) - .limit(5) - .collect(Collectors.toList()); - } - - return new ArrayList<>(); - } - - @Override - public List getHotKeywords() { - Set> tuples = redisTemplate.opsForZSet() - .reverseRangeWithScores(HOT_KEYWORDS_KEY, 0, 9); - - if (tuples != null) { - return tuples.stream() - .map(ZSetOperations.TypedTuple::getValue) - .collect(Collectors.toList()); - } - - return new ArrayList<>(); - } - - private void recordSearchKeyword(String keyword) { - if (StringUtils.hasText(keyword)) { - // 增加关键词热度 - redisTemplate.opsForZSet().incrementScore(HOT_KEYWORDS_KEY, keyword, 1); - // 设置过期时间 - redisTemplate.expire(HOT_KEYWORDS_KEY, 30, TimeUnit.DAYS); - } - } - - private List sortAndPage(List items, SearchDTO searchDTO) { - // 根据排序方式排序 - switch (searchDTO.getSortBy()) { - case "time": - items.sort((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())); - break; - case "popularity": - items.sort((a, b) -> Integer.compare(b.getLikeCount(), a.getLikeCount())); - break; - default: // relevance - // 相关性排序可以基于关键词匹配度等 - break; - } - - // 简单分页 - int start = (searchDTO.getPage() - 1) * searchDTO.getSize(); - int end = Math.min(start + searchDTO.getSize(), items.size()); - - if (start >= items.size()) { - return new ArrayList<>(); - } - - return items.subList(start, end); - } - - private SearchResultVO.SearchItem convertPostToSearchItem(Post post) { - SearchResultVO.SearchItem item = new SearchResultVO.SearchItem(); - item.setId(post.getId()); - item.setTitle(post.getTitle()); - item.setSummary(getSummary(post.getContent())); - item.setType("post"); - - // 通过userId查询用户信息 - User user = userMapper.getUserById(post.getUserId()); - item.setAuthor(user != null ? user.getNickname() : "未知用户"); - item.setAvatar(user != null ? user.getAvatar() : null); - - // 分类名称暂时设为空,需要通过categoryId查询 - item.setCategoryName("未知分类"); - item.setCreatedAt(post.getCreatedAt()); - item.setLikeCount(post.getLikeCount()); - item.setViewCount(post.getViewCount()); - return item; - } - - private SearchResultVO.SearchItem convertResourceToSearchItem(Resource resource) { - SearchResultVO.SearchItem item = new SearchResultVO.SearchItem(); - item.setId(resource.getId()); - item.setTitle(resource.getTitle()); - item.setSummary(resource.getDescription()); - item.setType("resource"); - - // 通过userId查询用户信息 - User user = userMapper.getUserById(resource.getUserId()); - item.setAuthor(user != null ? user.getNickname() : "未知用户"); - item.setAvatar(user != null ? user.getAvatar() : null); - - // 分类名称暂时设为空,需要通过categoryId查询 - item.setCategoryName("未知分类"); - item.setCreatedAt(resource.getCreatedAt()); - item.setLikeCount(resource.getLikeCount()); - item.setViewCount(resource.getDownloadCount()); - return item; - } - - private SearchResultVO.SearchItem convertUserToSearchItem(User user) { - SearchResultVO.SearchItem item = new SearchResultVO.SearchItem(); - item.setId(user.getId()); - item.setTitle(user.getNickname()); - item.setSummary(user.getBio()); - item.setType("user"); - item.setAuthor(user.getUsername()); - item.setAvatar(user.getAvatar()); - item.setCategoryName(user.getDepartment()); - item.setCreatedAt(user.getCreatedAt()); - item.setLikeCount(user.getPoints()); - item.setViewCount(0); - return item; - } - - private String getSummary(String content) { - if (content == null) return ""; - if (content.length() <= 100) return content; - return content.substring(0, 100) + "..."; - } -} \ No newline at end of file diff --git a/测试搜索功能.md b/测试搜索功能.md new file mode 100644 index 0000000..fafe788 --- /dev/null +++ b/测试搜索功能.md @@ -0,0 +1,73 @@ +# 搜索功能修复测试 + +## 🐛 问题分析 + +根据后端日志和代码分析,发现问题出在前端数据访问逻辑: + +### 后端返回格式 +```json +{ + "code": 200, + "message": "success", + "data": { + "total": 1, + "pages": 1, + "list": [...] + } +} +``` + +### 问题原因 +前端`request.ts`响应拦截器返回完整的响应对象,但SearchView.vue直接访问`searchResult.list`,应该访问`searchResult.data.list`。 + +## ✅ 已修复内容 + +1. **修正数据访问路径**: + ```javascript + // 修复前 + searchResult.value = result + + // 修复后 + searchResult.value = response.data || response + ``` + +2. **添加调试日志**: + - 打印API响应 + - 打印最终数据结构 + - 帮助排查数据流问题 + +3. **优化显示逻辑**: + - 搜索统计显示添加默认值 + - 分页组件防止undefined错误 + +## 🔧 测试步骤 + +1. **启动服务**: + ```bash + # 后端已启动在8087端口 + # 前端启动: + cd Front/vue-unilife + npm run dev + ``` + +2. **测试搜索**: + - 访问搜索页面 + - 输入关键词"12" + - 切换帖子/资源搜索 + - 查看浏览器控制台调试日志 + +3. **验证结果**: + - 搜索统计正确显示"找到 1 个帖子" + - 帖子列表正确显示搜索结果 + - 分页组件正常工作 + +## 📋 预期结果 + +搜索"12"应该显示: +- 标题:123456 +- 作者:caizq +- 分类:兴趣爱好 +- 浏览数:25 +- 点赞数:1 + +如果还有问题,请检查浏览器控制台的调试日志。 \ No newline at end of file