1. 项目背景与技术选型
作为一个计算机专业的毕业设计项目,"基于SpringBoot+Vue3新闻点赞收藏评论系统"是一个典型的Web应用开发案例。这类系统在当今互联网环境中具有广泛的应用场景,从新闻门户到内容社区,用户互动功能已成为标配。
1.1 为什么选择这个技术栈
SpringBoot和Vue3的组合是目前企业级应用开发的主流选择之一。SpringBoot简化了Java后端开发的配置过程,提供了自动配置、起步依赖等特性,让开发者能够快速搭建稳定的后端服务。而Vue3作为前端框架,其组合式API和更好的性能使其成为现代Web开发的首选。
我在技术选型时主要考虑了以下几个因素:
- 市场需求:SpringBoot和Vue3都是当前就业市场最受欢迎的技术
- 学习曲线:相比其他框架组合,这个技术栈的学习资源丰富,社区活跃
- 开发效率:SpringBoot的约定优于配置原则和Vue3的组件化开发能显著提升开发效率
- 性能表现:Vue3的虚拟DOM优化和SpringBoot的高效运行机制保证了系统性能
1.2 系统核心功能概述
本系统主要实现以下功能模块:
- 用户认证与权限管理
- 新闻内容展示与分类
- 用户互动(点赞、收藏、评论)
- 数据统计与分析
- 后台管理界面
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 前后端分离架构
系统采用典型的前后端分离架构:
code复制前端(Vue3) ← HTTP/HTTPS → 后端(SpringBoot) ← JDBC → 数据库(MySQL)
这种架构的优势在于:
- 前后端可以并行开发
- 前端可以使用更专业的工具链
- 后端API可以被多种客户端复用
- 更易于实现负载均衡和扩展
2.2 后端技术架构
SpringBoot应用采用分层架构设计:
code复制Controller层 → Service层 → Repository层 → 数据库
关键依赖包括:
- Spring Web MVC:处理HTTP请求
- Spring Data JPA:数据库访问
- Spring Security:认证与授权
- Lombok:减少样板代码
- Jackson:JSON处理
2.3 前端技术架构
Vue3项目采用以下技术组合:
- Vue Router:页面路由
- Pinia:状态管理
- Axios:HTTP客户端
- Element Plus:UI组件库
- VueUse:常用工具集
3. 数据库设计与实现
3.1 核心数据模型
系统主要包含以下实体及其关系:
- 用户(User)
- 新闻(News)
- 评论(Comment)
- 点赞(Like)
- 收藏(Favorite)
3.2 数据库表结构
以下是核心表的DDL定义:
sql复制-- 用户表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 新闻表
CREATE TABLE `news` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`content` text NOT NULL,
`cover_image` varchar(255) DEFAULT NULL,
`author_id` bigint NOT NULL,
`category` varchar(50) DEFAULT NULL,
`view_count` int DEFAULT '0',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_author` (`author_id`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 评论表
CREATE TABLE `comment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`content` text NOT NULL,
`user_id` bigint NOT NULL,
`news_id` bigint NOT NULL,
`parent_id` bigint DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_news` (`news_id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 点赞表
CREATE TABLE `like` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`news_id` bigint NOT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_news` (`user_id`,`news_id`),
KEY `idx_news` (`news_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 收藏表
CREATE TABLE `favorite` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`news_id` bigint NOT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_news` (`user_id`,`news_id`),
KEY `idx_news` (`news_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.3 索引优化策略
为了提高查询性能,我在设计中采用了以下索引策略:
- 为所有外键字段添加普通索引
- 为用户名添加唯一索引
- 为点赞和收藏表添加(user_id, news_id)联合唯一索引
- 为新闻分类字段添加索引
4. 后端核心实现
4.1 SpringBoot应用配置
首先创建SpringBoot应用的基本配置:
java复制@SpringBootApplication
@EnableJpaAuditing
@EnableTransactionManagement
public class NewsApplication {
public static void main(String[] args) {
SpringApplication.run(NewsApplication.class, args);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2 用户认证实现
使用Spring Security实现基于JWT的认证:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/news/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
4.3 新闻服务实现
新闻服务核心业务逻辑:
java复制@Service
@RequiredArgsConstructor
@Transactional
public class NewsService {
private final NewsRepository newsRepository;
private final UserRepository userRepository;
private final LikeRepository likeRepository;
private final FavoriteRepository favoriteRepository;
public Page<News> getNewsList(int page, int size, String category) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
if (category != null && !category.isEmpty()) {
return newsRepository.findByCategory(category, pageable);
}
return newsRepository.findAll(pageable);
}
public News getNewsDetail(Long id) {
return newsRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("News not found"));
}
public News createNews(NewsCreateRequest request, Long authorId) {
User author = userRepository.findById(authorId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
News news = News.builder()
.title(request.getTitle())
.content(request.getContent())
.coverImage(request.getCoverImage())
.category(request.getCategory())
.author(author)
.build();
return newsRepository.save(news);
}
public void likeNews(Long newsId, Long userId) {
if (likeRepository.existsByUserIdAndNewsId(userId, newsId)) {
throw new BusinessException("You have already liked this news");
}
News news = newsRepository.findById(newsId)
.orElseThrow(() -> new ResourceNotFoundException("News not found"));
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
Like like = Like.builder()
.user(user)
.news(news)
.build();
likeRepository.save(like);
}
// 其他业务方法...
}
4.4 评论功能实现
评论功能支持多级回复:
java复制@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final NewsRepository newsRepository;
private final UserRepository userRepository;
public Comment createComment(CommentCreateRequest request, Long userId) {
News news = newsRepository.findById(request.getNewsId())
.orElseThrow(() -> new ResourceNotFoundException("News not found"));
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
Comment parent = null;
if (request.getParentId() != null) {
parent = commentRepository.findById(request.getParentId())
.orElseThrow(() -> new ResourceNotFoundException("Parent comment not found"));
}
Comment comment = Comment.builder()
.content(request.getContent())
.user(user)
.news(news)
.parent(parent)
.build();
return commentRepository.save(comment);
}
public Page<Comment> getCommentsByNews(Long newsId, int page, int size) {
News news = newsRepository.findById(newsId)
.orElseThrow(() -> new ResourceNotFoundException("News not found"));
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return commentRepository.findByNewsAndParentIsNull(news, pageable);
}
}
5. 前端核心实现
5.1 Vue3项目初始化
使用Vite创建Vue3项目:
bash复制npm create vite@latest news-system-frontend --template vue
cd news-system-frontend
npm install vue-router pinia axios element-plus @element-plus/icons-vue
5.2 前端路由配置
javascript复制import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/news/:id',
name: 'NewsDetail',
component: () => import('@/views/NewsDetailView.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/RegisterView.vue')
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/ProfileView.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
5.3 新闻列表组件
vue复制<template>
<div class="news-list">
<el-card v-for="news in newsList" :key="news.id" class="news-card">
<template #header>
<div class="card-header">
<router-link :to="`/news/${news.id}`" class="news-title">
{{ news.title }}
</router-link>
<span class="news-category">{{ news.category }}</span>
</div>
</template>
<div class="news-content">
<img v-if="news.coverImage" :src="news.coverImage" class="news-cover" />
<p class="news-summary">{{ getSummary(news.content) }}</p>
</div>
<div class="news-footer">
<div class="news-meta">
<span>{{ formatDate(news.createdAt) }}</span>
<span>浏览 {{ news.viewCount }} 次</span>
</div>
<div class="news-actions">
<el-button type="text" @click="handleLike(news.id)">
<el-icon><Star /></el-icon>
{{ news.likeCount }}
</el-button>
<el-button type="text" @click="handleFavorite(news.id)">
<el-icon><Collection /></el-icon>
{{ news.favoriteCount }}
</el-button>
</div>
</div>
</el-card>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="fetchNews"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Star, Collection } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/date'
import { useNewsStore } from '@/stores/news'
const newsStore = useNewsStore()
const newsList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const fetchNews = async () => {
const res = await newsStore.getNewsList({
page: currentPage.value - 1,
size: pageSize.value
})
newsList.value = res.content
total.value = res.totalElements
}
const getSummary = (content) => {
return content.length > 100 ? content.substring(0, 100) + '...' : content
}
const handleLike = async (newsId) => {
await newsStore.likeNews(newsId)
await fetchNews()
}
const handleFavorite = async (newsId) => {
await newsStore.favoriteNews(newsId)
await fetchNews()
}
onMounted(fetchNews)
</script>
5.4 评论功能实现
vue复制<template>
<div class="comment-section">
<h3>评论 ({{ comments.length }})</h3>
<el-form @submit.prevent="submitComment">
<el-input
v-model="commentContent"
type="textarea"
:rows="3"
placeholder="写下你的评论..."
/>
<el-button type="primary" native-type="submit">发表评论</el-button>
</el-form>
<div class="comment-list">
<div v-for="comment in comments" :key="comment.id" class="comment-item">
<div class="comment-header">
<el-avatar :src="comment.user.avatar" />
<span class="comment-author">{{ comment.user.username }}</span>
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
</div>
<div class="comment-content">{{ comment.content }}</div>
<div class="comment-actions">
<el-button type="text" @click="replyToComment(comment.id)">回复</el-button>
</div>
<div v-if="comment.replies && comment.replies.length > 0" class="reply-list">
<div v-for="reply in comment.replies" :key="reply.id" class="reply-item">
<div class="comment-header">
<el-avatar :src="reply.user.avatar" />
<span class="comment-author">{{ reply.user.username }}</span>
<span class="comment-date">{{ formatDate(reply.createdAt) }}</span>
</div>
<div class="comment-content">
<span class="reply-mention">@{{ reply.parent.user.username }}</span>
{{ reply.content }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { formatDate } from '@/utils/date'
import { useCommentStore } from '@/stores/comment'
const props = defineProps({
newsId: {
type: Number,
required: true
}
})
const commentStore = useCommentStore()
const comments = ref([])
const commentContent = ref('')
const replyingTo = ref(null)
const fetchComments = async () => {
comments.value = await commentStore.getCommentsByNews(props.newsId)
}
const submitComment = async () => {
if (!commentContent.value.trim()) return
await commentStore.createComment({
newsId: props.newsId,
content: commentContent.value,
parentId: replyingTo.value
})
commentContent.value = ''
replyingTo.value = null
await fetchComments()
}
const replyToComment = (commentId) => {
replyingTo.value = commentId
document.querySelector('.el-textarea__inner').focus()
}
onMounted(fetchComments)
</script>
6. 系统部署与测试
6.1 后端部署
使用Docker部署SpringBoot应用:
dockerfile复制# Dockerfile
FROM openjdk:17-jdk-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
构建并运行容器:
bash复制mvn clean package
docker build -t news-system-backend .
docker run -d -p 8080:8080 --name news-backend news-system-backend
6.2 前端部署
使用Nginx部署Vue3应用:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
6.3 系统测试
功能测试用例
-
用户注册与登录
- 测试新用户注册流程
- 测试用户登录和JWT令牌获取
- 测试受保护路由的访问控制
-
新闻功能测试
- 测试新闻创建、编辑和删除
- 测试新闻列表分页和分类筛选
- 测试新闻详情页展示
-
互动功能测试
- 测试点赞和取消点赞
- 测试收藏和取消收藏
- 测试评论和回复功能
性能测试
使用JMeter进行负载测试:
- 模拟100并发用户访问新闻列表
- 测试点赞功能的并发处理
- 测试评论提交的响应时间
安全测试
- 测试SQL注入防护
- 测试XSS攻击防护
- 测试CSRF防护机制
- 测试JWT令牌的过期和刷新机制
7. 开发经验与优化建议
7.1 开发中遇到的挑战
-
前后端数据格式不一致
- 解决方案:建立统一的DTO规范和使用MapStruct进行对象转换
-
Vue3响应式系统的理解
- 解决方案:深入学习Composition API和ref/reactive的区别
-
JWT令牌的刷新机制
- 解决方案:实现双令牌机制(access token + refresh token)
7.2 性能优化实践
-
数据库查询优化
- 使用JPA的@EntityGraph解决N+1查询问题
- 添加适当的数据库索引
- 对复杂查询使用原生SQL或QueryDSL
-
前端性能优化
- 实现组件懒加载
- 使用Vue的keep-alive缓存组件
- 对图片进行压缩和懒加载
-
缓存策略
- 对新闻列表使用Redis缓存
- 实现Spring Cache抽象层
- 对热点数据设置合理的过期时间
7.3 项目扩展方向
-
增加实时通知功能
- 使用WebSocket实现点赞和评论的实时通知
-
引入推荐算法
- 基于用户行为实现个性化新闻推荐
-
多端适配
- 开发移动端APP(使用Uniapp或React Native)
- 实现PWA支持
-
微服务化改造
- 将单体应用拆分为用户服务、新闻服务、互动服务等
- 引入Spring Cloud生态
8. 项目总结
这个毕业设计项目让我全面实践了从需求分析到系统部署的完整开发流程。通过这个项目,我不仅掌握了SpringBoot和Vue3的核心技术,还学到了许多实际开发中的经验:
- 前后端协作:清晰的API文档和接口约定能极大提高开发效率
- 错误处理:统一的错误处理机制能让系统更健壮
- 测试重要性:自动化测试能显著减少回归bug
- 性能意识:从设计阶段就要考虑性能问题,而不是事后优化
对于学弟学妹们的建议:
- 不要过度追求新技术,先把基础打牢
- 重视代码质量和规范
- 学会使用调试工具和性能分析工具
- 多阅读优秀开源项目的代码
- 保持文档更新的习惯
这个项目还有很多可以改进的地方,我会继续完善它,也欢迎各位提出宝贵意见。完整的项目源码已经上传到GitHub,可以在我的个人主页找到链接。
