你从头到尾把“计算机毕设”这几个字换成“实战项目”来看的话这个题目的内核其实非常扎实一套基于 SpringBoot 做后端接口、Vue 做前端展示的新闻发布与推送系统外加用户互动留言模块。作为毕业设计它的技术栈覆盖面很完整工作量也足够支撑一篇像样的论文和答辩演示。我最近正好帮一位朋友梳理过类似结构的项目今天就把整个实现思路、核心代码逻辑、数据库设计以及我实际踩过的一些坑一次性讲清楚。1. 项目核心拆解这到底是一个什么样的系统1.1 核心需求与功能边界新闻推送平台这个题目从功能上看通常包含三个大块。第一是新闻内容的发布与管理。这部分主要由后台管理员完成包括新闻分类维护、新闻编辑与发布、新闻下线或删除、置顶操作、封面图上传等。前端面向普通访客和注册用户展示新闻列表、新闻详情、分类筛选和关键词搜索。第二是实时推送。这里的“实时”体现在两个方面一是当管理员在后台发布一条新新闻后正在访问前台页面的用户不需要手动刷新就能看到新条目出现二是系统可以向在线用户推送通知比如“您关注的分类有新内容”。这个需求的实现级里业界常用 WebSocket 做服务端主动推送而不是让前端定时轮询接口。第三是互动留言系统。注册用户可以在新闻详情页下方发表评论留言可以回复他人的留言管理员可以管理留言删除违规内容。这个模块的业务逻辑和评论系统是一样的只是叫法不同。1.2 技术选型的合理性判断SpringBoot 加 Vue 这一组合在近几年的毕业设计中几乎是“标准答案”。SpringBoot 负责提供 RESTful API内置 Tomcat简化了配置过程Vue 负责页面渲染和数据交互配合 Element UI 或类似组件库可以快速做出页面效果不错的后台管理界面和用户端界面。从评分角度讲这套组合的加分项在于前后端分离架构清晰体现现代 Web 开发的主流模式SpringBoot 框架本身包含安全认证Spring Security 或 Shiro、数据持久化MyBatis Plus / JPA、实时通信WebSocket等多个知识点论文和技术答辩都有东西可讲Vue 部分包含组件化开发、路由管理、状态管理、Axios 封装、WebSocket 客户端集成前端知识量也充足1.3 适合谁做这个题目我接触到的做这类题目的同学主要有三类有一定 Java 基础想通过一个完整项目把前后端知识串联起来的本科生需要快速完成毕设同时希望项目有一定技术亮点的同学想往全栈方向发展的同学用这个项目作为求职作品集的一部分如果你是零基础或者 Java 仅停留在 SSH 阶段也不用太担心这个题目的难点主要在后端 WebSocket 和权限控制上前面照着写一遍理解原理答辩时能说清“为什么这么设计”就足够了。2. 数据库设计五张表撑起整个平台2.1 核心表结构与字段说明数据库设计是这类项目最先要做好的环节表建不好后面写代码到处别扭。我推荐用下面这一组表结构经过实际项目验证覆盖功能完整且不会过度设计。用户表user字段类型说明idbigint主键自增usernamevarchar(50)登录用户名唯一passwordvarchar(100)加密后的密码nicknamevarchar(50)显示昵称avatarvarchar(255)头像地址roletinyint角色0为普通用户1为管理员statustinyint状态0正常1禁用create_timedatetime注册时间新闻分类表category字段类型说明idbigint主键namevarchar(50)分类名称如科技、体育、财经sortint排序权重数字越小越靠前新闻表news字段类型说明idbigint主键category_idbigint分类ID外键关联分类表titlevarchar(200)新闻标题summaryvarchar(500)摘要列表页显示contenttext正文内容支持富文本cover_imagevarchar(255)封面图authorvarchar(50)发布人viewsint浏览数is_toptinyint是否置顶statustinyint状态0草稿1已发布2已下线publish_timedatetime发布时间留言表comment字段类型说明idbigint主键news_idbigint所属新闻IDuser_idbigint留言用户IDcontentvarchar(500)留言内容parent_idbigint父留言ID0表示顶级留言reply_user_idbigint被回复的用户IDstatustinyint状态0正常1隐藏create_timedatetime留言时间用户关注表user_follow——可选如果希望“实时推送”有更多场景可以加一张关注表用户关注分类后该分类有新新闻时推送通知。2.2 设计要点说明留言表的 parent_id 和 reply_user_id 这两个字段是评论回复功能的关键。parent_id 指向的是顶级留言的 ID这样前端在渲染时可以很简单地做两级评论展示reply_user_id 记录“这条回复是回复谁的”用户看到回复时能知道消息是发给自己的。新闻表的 content 字段建议使用 MEDIUMTEXT 类型富文本编辑器生成的 HTML 内容通常不会太小TEXT类型只有 64KB遇到长文会报错。别问我是怎么知道的本地测试数据看不出问题一换正式数据就炸了。status 字段统一使用 tinyint 数字而不是字符串这是最简单也最高效的设计方式。不同模块的 status 含义可以不同后端代码里用常量类统一管理避免魔法数字散落各处。3. 后端实现SpringBoot 核心代码怎么写3.1 项目依赖与基础配置实际做这个毕设我建议直接用 Spring Initializr 生成基础工程依赖勾选 Spring Web、MyBatis Framework或 MyBatis Plus、MySQL Driver、Lombok、Spring Security 或直接导入自定义的拦截器。WebSocket 依赖后面用到时再加。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependencyMyBatis Plus 简化了单表 CRUD 的代码量对于毕设来说非常友好。几乎不需要写 XML 就能完成 80% 的数据库操作让新手把精力集中在业务逻辑上。application.yml 里的关键配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/news_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB字符编码和时区这两个参数必须配不然中文乱码、时间差 8 小时的问题够你查一上午。3.2 用户登录与权限拦截用户认证这块毕业设计不推荐直接上 Spring Security 全家桶因为配置复杂且学习成本高。更务实的做法是用 JWTJSON Web Token 自定义拦截器实现既轻量又能讲清楚原理。登录成功后后端生成 Token 返回给前端String token JWT.create() .withClaim(userId, user.getId()) .withClaim(role, user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .sign(Algorithm.HMAC256(your-secret-key));前端拿到 Token 后存到 localStorage 里每次请求在请求头带上Authorization: xxx。后端的拦截器里做个统一校验public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); // 校验token失败则返回401 return true; } }这里有一个细节要注意CORS 预检请求是 OPTIONS 方法不会携带 Token所以拦截器必须放行 OPTIONS否则前端跨域请求直接失败。3.3 新闻发布与管理模块新闻管理和普通系统的 CRUD 没有太大区别但有两个操作需要多写一些逻辑。一是发布新闻时除了插入新闻记录还要把这条新闻推送给所有在线用户这个放到后面 WebSocket 部分讲。二是新闻列表的分页查询需要按条件动态拼接 SQLMyBatis Plus 的 LambdaQueryWrapper 可以搞定LambdaQueryWrapperNews wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(categoryId), News::getCategoryId, categoryId) .eq(News::getStatus, 1) .orderByDesc(News::getIsTop) .orderByDesc(News::getPublishTime); PageNews page newsMapper.selectPage(new Page(pageNum, pageSize), wrapper);3.4 WebSocket 实时推送的实现实时推送是这个项目的技术亮点。SpringBoot 中集成 WebSocket 的流程分三步。第一步写一个 WebSocket 配置类Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new NewsWebSocketHandler(), /ws/news) .setAllowedOrigins(*); } }第二步定义处理器。这里有两种流行方案一种是直接用TextWebSocketHandler另一种是用 STOMP 协议加MessageMapping注解。对于毕设来讲直接使用WebSocketHandler更直观易懂。我维护一个在线用户会话集合保存所有连接public class NewsWebSocketHandler extends TextWebSocketHandler { private static final CopyOnWriteArraySetWebSocketSession SESSIONS new CopyOnWriteArraySet(); Override public void afterConnectionEstablished(WebSocketSession session) { SESSIONS.add(session); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { SESSIONS.remove(session); } public static void sendMessageToAll(String message) { for (WebSocketSession session : SESSIONS) { if (session.isOpen()) { synchronized (session) { session.sendMessage(new TextMessage(message)); } } } } }第三步在新闻发布接口中调用推送方法NewsWebSocketHandler.sendMessageToAll(JSON.toJSONString(new PushMessage(news, news)));前端收到消息后根据消息类型做不同处理如果是 news 类型就刷新新闻列表如果是 notice 类型就弹通知提示。关于 WebSocket 的认证有一个常见的坑WebSocket 握手时不会自动携带 HTTP Header 中的 Token。如果你需要区分“是哪个用户连接了 WebSocket”可以在建立连接时通过 URL 参数传递 Token例如ws://localhost:8080/ws/news?tokenxxx服务端在握手拦截器中校验Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { String token ((ServletServerHttpRequest) request).getServletRequest().getParameter(token); // 校验token并储存用户信息 attributes.put(userId, userId); return true; }3.5 留言互动模块留言接口的设计相对标准提交留言、留言列表按新闻 ID 查询、删除留言管理员可用。但提交留言时需要注意一个细节留言内容必须做 HTML 转义防止用户输入script标签造成 XSS 攻击。这是个“说出来很基础但很多人会忘”的点。留言列表查询时需要一次性返回用户昵称和头像所以要用多表联查。MyBatis Plus 中可以用注解写 SQLSelect(SELECT c.*, u.nickname, u.avatar FROM comment c LEFT JOIN user u ON c.user_id u.id WHERE c.news_id #{newsId} ORDER BY c.create_time DESC) ListCommentVO selectCommentsByNewsId(Long newsId);前端渲染留言时根据 parent_id 是否为 0 判断是顶级留言还是回复留言做成两级嵌套的 UI 结构。4. 前端实现Vue 项目中需要重点把握的部分4.1 工程化结构与路由设计Vue 建议直接使用 Vue CLI 或 Vite 创建工程配合 Vue Router 做页面路由。典型的页面结构如下/ 首页新闻列表 分类导航/news/:id 新闻详情页内容 留言区/login 登录页/register 注册页/admin 后台管理页嵌套路由分类管理、新闻管理、留言管理路由守卫这块后台管理页面需要校验登录状态和管理员角色router.beforeEach((to, from, next) { if (to.path.startsWith(/admin)) { const user JSON.parse(localStorage.getItem(userInfo) || {}); if (!user.token || user.role ! 1) { next(/login); return; } } next(); });4.2 Axios 封装与状态管理前端最基础的是把 Axios 实例统一封装在请求拦截器中自动附加 Token在响应拦截器里统一处理后端返回的状态码和错误信息import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization token; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { router.push(/login); } return Promise.reject(error); } ); export default request;状态管理方面如果用 Vue 2就上 Vuex用 Vue 3就推荐 Pinia。其实对于这个项目全局共享的状态不多用户信息算一个未读通知数算一个所以用 Vuex/Pinia 管理这两个状态就足够。4.3 首页新闻列表与实时刷新体验首页是本项目的“门面”体验做得好演示效果直接上一个档次。新闻列表组件在 mounted 钩子里发起请求获取第一页数据然后创建 WebSocket 连接mounted() { this.fetchNews(); this.initWebSocket(); }, methods: { initWebSocket() { const ws new WebSocket(ws://localhost:8080/ws/news?token${localStorage.getItem(token)}); ws.onmessage (event) { const data JSON.parse(event.data); if (data.type news) { // 把新新闻插入到列表最前面并弹出提示 this.newsList.unshift(data.news); this.$message.info(有新新闻发布); } }; ws.onclose () { // 断线重连3秒后重试 setTimeout(() { this.initWebSocket(); }, 3000); }; } }断线重连是 WebSocket 必须处理的逻辑。因为网络切换、服务端重启都可能导致连接断开不做重连的话功能演示到一半就“失联”了会很被动。4.4 新闻详情页与留言组件通讯新闻详情页包含正文展示和留言区两个部分。留言组件建议独立成一个子组件接收newsId作为 prop内部维护留言列表数据。发布留言后清空输入框重新拉取留言列表。回复留言时把回复目标用户昵称显示在输入框中提交时带上 parentId 和 replyUserId。Element UI 的 el-input 的 typetextarea 可以方便地做多行输入配合字数限制maxlength500和show-word-limit属性体验比较完整。5. 前后端联调与部署从本地跑通到上线演示5.1 跨域问题的正确姿势本地开发时前端跑在 8081 或 5173 端口后端跑在 8080 端口跨域问题必然出现。解决方案有两个方案一后端配置全局跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方案二前端配置 Vite 或 Vue CLI 的代理把/api开头的请求转发到后端地址。这样浏览器看到的请求是同源的不需要后端处理 CORS。对于毕设答辩演示我推荐方案一加方案二一起上。方案一解决浏览器跨域方案二解决 WebSocket 在开发环境的地址拼接问题。5.2 图片上传与静态资源映射新闻封面和管理员头像都需要上传图片。开发阶段通常直接存到服务器本地磁盘然后配置静态资源映射对外提供访问Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); }上传接口接收 MultipartFile生成唯一文件名UUID 加原扩展名保存到 upload 目录返回访问路径。需要注意这个方案只适合开发和毕设场景。如果正式部署一般建议使用云存储服务但毕业设计通常不需要做到这一步。5.3 打包部署流程后端打包用 Maven 的 package 命令生成 jar 包然后java -jar news-platform.jar直接运行。前端打包用npm run build生成 dist 目录。部署时有两种常见选择。一种是把 dist 目录直接放到 Nginx 下Nginx 监听 80 端口前端请求/api时反向代理到后端的 8080 端口。另一种是在后端加一层静态资源映射把前端打包产物放进 SpringBoot 的 static 目录里一个 jar 包搞定所有。对于毕设演示第二种方式更省事因为只需要在一台机器上启动一个服务。不过如果你打算把项目放进简历里展示还是建议用 Nginx 方式因为有独立的 Nginx 配置可以讲也符合真实生产环境的部署习惯。6. 实操中常见的坑与排错清单6.1 WebSocket 连接被拒绝或频繁断开表现前端报WebSocket connection to ws://... failed。排查步骤先确认后端 WebSocket 配置类的路径和前端连接地址是否一致再用浏览器 DevTools 的 Network 面板看握手响应码如果出现 403基本可以断定是跨域拦截问题检查.setAllowedOrigins(*)是否设置最后确认后端服务是否真的启动了 WebSocket 端点有些同学把监听端口和 WebSocket 路径搞混导致前端连了一个不存在的地方。6.2 中文乱码问题表现数据库存进去是乱码、接口返回是问号。排查方向按顺序走数据库连接 URL 是否带characterEncodingutf8MySQL 表本身的字符集是否为 utf8mb4前端页面 meta 标签字符集是否为 UTF-8是否有字段在插入时被转码。90% 的情况是前两类问题。小知识MySQL 8.0 默认字符集已经是 utf8mb4但很多同学用的是 5.7 版本或者建表时没指定字符集所以建库建表时统一写上DEFAULT CHARSETutf8mb4最稳妥。6.3 留言列表日期格式不对表现详情页显示的时间比实际时间早 8 小时。原因是 MySQL 连接串里没有指定serverTimezoneAsia/ShanghaiSpringBoot 返回 JSON 时又用了默认时区解析。解决办法是在配置里指定时区同时在后端实体类的日期字段上加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。6.4 富文本内容安全过滤这是很多人在答辩时被问到的一个薄弱点。如果新闻正文支持富文本那么你在详情页渲染时如果直接用v-html会有 XSS 风险。实际做法是接入类似 js-xss 这样的过滤库服务端和前端都做一层过滤。这个点一旦被问到你能答出完整的防线设计就是加分项。在服务端也有一个等效方案新闻内容的提交接口里在入库前对脚本标签做白名单过滤。6.5 并发场景下浏览数不准确新闻详情页每访问一次views 就加 1如果用UPDATE news SET views views 1 WHERE id ?这种写法并没有什么问题因为 MySQL 行锁会保证原子性。但如果你先查出来再加回去在并发场景下就会丢更新。使用原生自增语句是正确做法。绝不要用先查后改的逻辑。7. 这个项目还能怎么扩展做毕设时如果你在原基础上再多加一两个扩展点答辩时讲起来就有了独特的亮点。我这里列几个我实际评估过性价比的扩展方向。第一个方向是“用户行为分析”。在现有表结构上加一张行为日志表记录用户的浏览量、留言数、关注分类。然后做一个简单的统计面板用 ECharts 展示最近 7 天新闻发布量和留言量趋势。这里用到的知识点包括定时任务、数据聚合查询、ECharts 集成工作量适中但视觉效果好。第二个方向是“消息中心”。把 WebSocket 推送的用户个人消息抽象成站内信模块用户登录后可以看到未读消息列表点击已读后消息状态更新。这个扩展可以和留言回复打通别人回复了你的留言给你发一条站内信。第三个方向是“极简推荐逻辑”。根据用户浏览过的新闻分类推出“猜你喜欢”模块。实现方式不用上机器学习算法一条 SQL 统计用户浏览量最多的分类然后推荐同分类的其他新闻就够了。这个扩展好实现也好讲解。以上三个方向里我建议想拿高分的同学至少做一个。项目规模和知识点密度刚刚好既不会太复杂影响完成度又能让评审老师眼前一亮。最后说一个个人体会做这类全栈毕业设计最忌讳的不是技术难而是“每个模块都浅尝辄止”。与其把登录、新闻 CRUD、留言、推送全部做成“能跑就行”不如挑一到两个模块做到“能讲清设计原理、边界处理完整、异常情况有兜底”。答辩的时候老师问的问题往往不是“你怎么实现功能”而是“为什么这样做”、“如果并发高怎么办”、“这样设计有什么缺陷”。把这些想明白了你的收获会比写完整套代码还大。
阅读完成 · 觉得有帮助?