搜索优化

czq
2991692032 1 month ago
parent 8b55f0c5b6
commit 945bbe25bd

@ -5,10 +5,11 @@ import request from './request';
* @param page
* @param size
* @param categoryId ID
* @param keyword
* @param sort latest, hot, likes, commentslatest
* @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

@ -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);
}
};

@ -17,6 +17,10 @@ export interface PostState {
loadingCategories: boolean;
errorCategories: string | 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;
}
}
});

@ -6,7 +6,7 @@
<el-input
v-model="searchKeyword"
size="large"
placeholder="搜索帖子、资源、用户..."
placeholder="搜索帖子、资源..."
@keyup.enter="handleSearch"
clearable
>
@ -21,16 +21,23 @@
<!-- 搜索过滤器 -->
<div class="search-filters">
<el-radio-group v-model="searchType" @change="handleSearch">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="post">帖子</el-radio-button>
<el-radio-button label="resource">资源</el-radio-button>
<el-radio-button label="user">用户</el-radio-button>
</el-radio-group>
<el-select v-model="sortBy" placeholder="排序方式" @change="handleSearch" style="width: 120px; margin-left: 10px;">
<el-option label="相关性" value="relevance" />
<el-option label="时间" value="time" />
<el-option label="热门度" value="popularity" />
<el-option label="最新" value="latest" />
<el-option label="热门" value="hot" />
<el-option label="点赞" value="likes" />
</el-select>
<el-select v-model="categoryId" placeholder="选择分类" @change="handleSearch" style="width: 150px; margin-left: 10px;" clearable>
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
</div>
</div>
@ -39,22 +46,20 @@
<div class="search-content" v-loading="loading">
<!-- 搜索统计 -->
<div class="search-stats" v-if="searchResult">
<span>找到 {{ searchResult.total }} 个结果</span>
<span class="search-time">耗时 {{ searchResult.searchTime }}ms</span>
<span>找到 {{ searchResult.total || 0 }} {{ searchType === 'post' ? '帖子' : '资源' }}</span>
<span v-if="searchResult.total" class="search-time">{{ searchResult.pages || 1 }}</span>
</div>
<!-- 搜索结果列表 -->
<div class="search-results" v-if="searchResult && searchResult.items.length > 0">
<!-- 帖子搜索结果 -->
<div class="search-results" v-if="searchType === 'post' && searchResult && searchResult.list && searchResult.list.length > 0">
<div
v-for="item in searchResult.items"
:key="`${item.type}-${item.id}`"
class="search-item"
@click="handleItemClick(item)"
v-for="item in searchResult.list"
:key="item.id"
class="search-item post-item"
@click="handlePostClick(item)"
>
<div class="item-header">
<el-tag :type="getTypeTagType(item.type)" size="small">
{{ getTypeText(item.type) }}
</el-tag>
<el-tag type="primary" size="small">帖子</el-tag>
<span class="item-title">{{ item.title }}</span>
</div>
@ -65,15 +70,52 @@
<div class="item-meta">
<div class="author-info">
<el-avatar :src="item.avatar" :size="20" />
<span class="author-name">{{ item.author }}</span>
<span class="author-name">{{ item.nickname }}</span>
</div>
<div class="meta-info">
<span v-if="item.categoryName" class="category">{{ item.categoryName }}</span>
<span class="category">{{ item.categoryName }}</span>
<span class="create-time">{{ formatTime(item.createdAt) }}</span>
<span class="stats">
<el-icon><StarFilled /></el-icon>{{ item.likeCount }}
<el-icon style="margin-left: 10px;"><View /></el-icon>{{ item.viewCount }}
<el-icon style="margin-left: 10px;"><ChatDotSquare /></el-icon>{{ item.commentCount }}
</span>
</div>
</div>
</div>
</div>
<!-- 资源搜索结果 -->
<div class="search-results" v-else-if="searchType === 'resource' && searchResult && searchResult.list && searchResult.list.length > 0">
<div
v-for="item in searchResult.list"
:key="item.id"
class="search-item resource-item"
@click="handleResourceClick(item)"
>
<div class="item-header">
<el-tag type="success" size="small">资源</el-tag>
<span class="item-title">{{ item.title }}</span>
</div>
<div class="item-content">
<p class="item-summary">{{ item.description }}</p>
</div>
<div class="item-meta">
<div class="author-info">
<el-avatar :src="item.avatar" :size="20" />
<span class="author-name">{{ item.nickname }}</span>
</div>
<div class="meta-info">
<span class="category">{{ item.categoryName }}</span>
<span class="file-info">{{ formatFileSize(item.fileSize) }} · {{ item.fileType }}</span>
<span class="create-time">{{ formatTime(item.createdAt) }}</span>
<span class="stats">
<el-icon><StarFilled /></el-icon>{{ item.likeCount }}
<el-icon style="margin-left: 10px;"><Download /></el-icon>{{ item.downloadCount }}
</span>
</div>
</div>
@ -82,36 +124,21 @@
<!-- 空状态 -->
<el-empty
v-else-if="searchResult && searchResult.items.length === 0"
description="没有找到相关内容"
v-else-if="searchResult && (!searchResult.list || searchResult.list.length === 0)"
:description="`没有找到相关${searchType === 'post' ? '帖子' : '资源'}`"
/>
<!-- 分页 -->
<div class="pagination" v-if="searchResult && searchResult.total > 0">
<div class="pagination" v-if="searchResult && (searchResult.total || 0) > 0">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="searchResult.total"
:total="searchResult.total || 0"
layout="prev, pager, next, jumper"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 热门搜索词 -->
<div class="hot-keywords" v-if="!searchResult">
<h3>热门搜索</h3>
<div class="keyword-tags">
<el-tag
v-for="keyword in hotKeywords"
:key="keyword"
@click="handleHotKeywordClick(keyword)"
style="margin: 5px; cursor: pointer;"
>
{{ keyword }}
</el-tag>
</div>
</div>
</div>
</template>
@ -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<number>()
const currentPage = ref(1)
const pageSize = ref(10)
//
const searchResult = ref<SearchResult>()
const hotKeywords = ref<string[]>([])
const searchResult = ref<any>()
const categories = ref<any[]>([])
//
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'
}
</script>
<style scoped>
.search-page {
max-width: 800px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.search-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
@ -282,32 +302,33 @@ const formatTime = (time: string) => {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.search-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
color: #666;
font-size: 14px;
.search-content {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 400px;
}
.search-time {
color: #999;
.search-stats {
padding: 20px 20px 10px;
color: #666;
border-bottom: 1px solid #f0f0f0;
}
.search-results {
margin-bottom: 20px;
padding: 20px;
}
.search-item {
border: 1px solid #ebeef5;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
padding: 20px;
margin-bottom: 15px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.3s ease;
}
.search-item:hover {
@ -318,14 +339,14 @@ const formatTime = (time: string) => {
.item-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.item-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-left: 10px;
color: #333;
}
.item-content {
@ -333,7 +354,7 @@ const formatTime = (time: string) => {
}
.item-summary {
color: #606266;
color: #666;
line-height: 1.6;
margin: 0;
}
@ -342,8 +363,8 @@ const formatTime = (time: string) => {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #909399;
font-size: 14px;
color: #999;
}
.author-info {
@ -352,22 +373,12 @@ const formatTime = (time: string) => {
gap: 8px;
}
.author-name {
color: #409eff;
}
.meta-info {
display: flex;
align-items: center;
gap: 15px;
}
.category {
background: #f4f4f5;
padding: 2px 8px;
border-radius: 4px;
}
.stats {
display: flex;
align-items: center;
@ -375,17 +386,26 @@ const formatTime = (time: string) => {
}
.pagination {
padding: 20px;
display: flex;
justify-content: center;
margin-top: 30px;
border-top: 1px solid #f0f0f0;
}
.hot-keywords {
margin-top: 40px;
@media (max-width: 768px) {
.search-page {
padding: 10px;
}
.hot-keywords h3 {
margin-bottom: 15px;
color: #303133;
.search-filters {
flex-direction: column;
align-items: stretch;
}
.item-meta {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>

@ -107,6 +107,20 @@
<!-- 帖子列表区域 -->
<section class="posts-section">
<!-- 搜索状态提示 -->
<div v-if="postStore.isSearching" class="search-status">
<el-alert
:title="`搜索 '${postStore.searchKeyword}' 的结果 (共 ${postStore.totalPosts} 个帖子)`"
type="info"
show-icon
:closable="false"
>
<template #default>
<el-button size="small" @click="clearSearch"></el-button>
</template>
</el-alert>
</div>
<!-- 加载状态 -->
<div v-if="postStore.loading && postStore.posts.length === 0" class="loading-container">
<div class="posts-skeleton">
@ -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) => {
if (postStore.isSearching && postStore.searchKeyword) {
// 使
postStore.searchPosts({
keyword: postStore.searchKeyword,
categoryId: postStore.selectedCategoryId,
pageNum: page
});
} else {
// 使
postStore.fetchPosts({ pageNum: page });
}
};
const handleSizeChange = (size: number) => {
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 {

@ -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设计原则。每个资源的搜索功能都在自己的控制器中处理避免了不必要的复杂性。

@ -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<ResponseType>('/posts', serverParams);
}
```
#### 资源搜索
```typescript
// 前端调用Front/vue-unilife/src/api/resource.ts
getResources(params: ResourceParams = {}) {
// ResourceParams 已包含 keyword 参数
return get<ResponseType>('/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. ✅ **减少了系统复杂度**
现在的搜索架构更加清晰、简洁、符合最佳实践。

@ -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
<!-- 搜索状态提示 -->
<div v-if="postStore.isSearching" class="search-status">
<el-alert
:title="`搜索 '${postStore.searchKeyword}' 的结果 (共 ${postStore.totalPosts} 个帖子)`"
type="info"
show-icon
:closable="false"
>
<template #default>
<el-button size="small" @click="clearSearch">清除搜索</el-button>
</template>
</el-alert>
</div>
```
- **优化分页处理**
- 在搜索状态下,分页使用搜索方法
- 在普通状态下,分页使用常规获取方法
## 功能特性
### 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 个帖子)"
- 下方显示匹配的帖子列表
- 分页功能正常工作
- 可以通过"清除搜索"按钮返回全部帖子列表

@ -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 = "更新帖子")

@ -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<SearchResultVO> 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<SearchResultVO> 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<SearchResultVO> 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<SearchResultVO> 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<List<String>> getSuggestions(
@Parameter(description = "关键词") @RequestParam String keyword) {
List<String> suggestions = searchService.getSuggestions(keyword);
return Result.success(suggestions);
}
@GetMapping("/hot-keywords")
@Operation(summary = "获取热门搜索词", description = "获取热门搜索关键词列表")
public Result<List<String>> getHotKeywords() {
List<String> hotKeywords = searchService.getHotKeywords();
return Result.success(hotKeywords);
}
}

@ -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;
}

@ -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<String> highlights;
}
/**
*
*/
private List<SearchItem> items;
/**
*
*/
private Long total;
/**
*
*/
private Integer page;
/**
*
*/
private Integer size;
/**
*
*/
private String keyword;
/**
*
*/
private Long searchTime;
}

@ -27,12 +27,13 @@ public interface PostService {
/**
*
* @param categoryId IDnull
* @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);
/**
*

@ -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<String> getSuggestions(String keyword);
/**
*
* @return
*/
List<String> getHotKeywords();
}

@ -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<Post> posts = postMapper.getListByCategory(categoryId, sort);
// 根据是否有关键词选择不同的查询方法
List<Post> posts;
if (StrUtil.isNotBlank(keyword)) {
// 有关键词,使用搜索方法
posts = postMapper.searchPosts(keyword, categoryId, sort);
} else {
// 无关键词,使用普通列表查询
posts = postMapper.getListByCategory(categoryId, sort);
}
// 获取分页信息
PageInfo<Post> pageInfo = new PageInfo<>(posts);

@ -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<SearchResultVO.SearchItem> 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<Post> posts = postMapper.searchPosts(
searchDTO.getKeyword(),
searchDTO.getCategoryId(),
searchDTO.getSortBy()
);
PageInfo<Post> pageInfo = new PageInfo<>(posts);
List<SearchResultVO.SearchItem> 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<Resource> resources = resourceMapper.searchResources(
searchDTO.getKeyword(),
searchDTO.getCategoryId(),
searchDTO.getSortBy()
);
PageInfo<Resource> pageInfo = new PageInfo<>(resources);
List<SearchResultVO.SearchItem> 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<User> users = userMapper.searchUsers(searchDTO.getKeyword());
PageInfo<User> pageInfo = new PageInfo<>(users);
List<SearchResultVO.SearchItem> 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<String> getSuggestions(String keyword) {
if (!StringUtils.hasText(keyword) || keyword.length() < 2) {
return new ArrayList<>();
}
// 从热门搜索词中匹配
Set<String> 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<String> getHotKeywords() {
Set<ZSetOperations.TypedTuple<String>> 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<SearchResultVO.SearchItem> sortAndPage(List<SearchResultVO.SearchItem> 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) + "...";
}
}

@ -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
如果还有问题,请检查浏览器控制台的调试日志。
Loading…
Cancel
Save