多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot+Vue考研互助交流平台:从需求到部署全解析

SpringBoot+Vue考研互助交流平台:从需求到部署全解析 刚答辩完这个SpringBootVue考研互助交流平台趁热把整个项目的源码思路、数据库设计、前后端联调和踩坑经历完整梳理一遍。先说清楚这项目是干什么的备考考研的人需要一个能分享复习资料、讨论疑难题目、寻找研友互相督促的地方市面上通用论坛用起来隔靴搔痒所以我做了一个针对考研场景的垂直交流平台。整套代码用Java Web最主流的SpringBootVue前后端分离架构搭建配好SQL脚本和接口文档修修改改就能直接当毕设交。文章会按我从0到1的开发顺序来讲先聊技术选型为什么是中这个组合再拆核心业务模块接着过数据库表和SQL脚本的关键点然后讲前端Vue的实现细节和联调过程最后把开发中遇到的高频问题和排查方法整理成速查表。不管你是打算照抄这个方向做毕设还是想学前后端分离项目的完整套路这篇都能给你省下不少弯路。1. 项目整体定位与技术选型1.1 需求分析考研场景到底需要什么功能做项目第一步不是写代码是把需求想透。我调研了一圈发现考研人群的核心痛点就三个找资料难、找研友难、疑难问题没人讨论。对应到功能上平台就必须有三大板块资源分享上传和下载复习资料、问答社区发帖求助、回复解答、研友匹配按学校和专业方向找研友。除了这些核心功能基础的账号体系肯定要有不然没法区分是谁在发帖子、谁在传资料。我额外加了公告管理和后台管理两个模块一个是给管理员发通知用的一个是用来审核内容和封禁违规账号的。毕设评审老师很吃这套——有前端展示、有后台管理、有权限区分整个系统的完整度直接拉高一个档次。1.2 为什么选SpringBootVue这套组合选型的时候其实纠结过几种方案最后敲定SpringBootVue不是因为它最前卫而是因为它最稳妥、性价比最高。我做了个简单对比方案开发效率学习成本企业认可度资料丰富度JSPServlet低低低高SpringMVCThymeleaf中中中高SpringBootVue前后端分离高中高高极高SpringBoot把Spring繁琐的XML配置全部干掉了内嵌Tomcat可以直接跑jar包写接口的效率比传统SpringMVC快一倍不止。Vue在前端这边也是同样的思路组件化开发让页面复用变得很简单。最关键的是这套组合是当下企业的标配技术栈毕设用这套写简历上写掌握前后端分离开发说服力比老技术强太多。1.3 工程结构前后端分离怎么组织代码工程分成两个独立目录后端kaoyan-server和前端kaoyan-web。后端是标准的Maven多模块单模块结构毕设没必要过度拆分按包名分层com.kaoyan ├── controller # 控制层接收请求、返回结果 ├── service # 业务层核心逻辑处理 ├── mapper # 持久层MyBatis-Plus的Mapper接口 ├── entity # 实体类对应数据库表 ├── dto # 数据传输对象接收前端参数 ├── vo # 视图对象返回给前端的数据 ├── config # 配置类跨域、拦截器、文件上传等 ├── common # 公共类统一返回结果、异常处理 └── util # 工具类JWT、MD5等前端用Vue CLI脚手架创建按views页面、components组件、router路由、api接口封装、utils工具组织。前后端通过HTTP接口通信后端返回统一格式的JSON前端用Axios拦截器统一处理。这套结构虽然简单但胜在清晰答辩讲架构的时候你一张图就能说清楚整个数据流。2. 核心业务模块拆解与实现2.1 用户模块JWT登录与权限控制用户模块是整个系统的地基我用了JWTJSON Web Token做登录态管理。为什么不用传统的Session因为前后端分离后后端不维护Session状态才对JWT把用户信息加密放进token里前端存到localStorage每次请求带在请求头里后端无状态验证就能识别身份。用户表设计上有个细节值得注意我没有做成单一的角色字段而是拆出角色表tb_role用户表只存role_id。虽然现在只有普通用户和管理员两种角色但以后想加版主督导这类中间角色不用改用户表结构直接往角色表插数据就行这个设计习惯在职场上也是很加分的。密码存储必须用MD5或者BCrypt加密。我最初图省事直接明文存后来想了想作为毕设可以被老师批评安全意识不到位但真上线就是事故了所以老老实实加了MD5加盐。注册接口的核心逻辑是这样的Override public Result register(RegisterDTO dto) { // 1. 校验用户名是否已存在 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, dto.getUsername()); if (userMapper.selectCount(wrapper) 0) { return Result.error(用户名已存在); } // 2. 密码加密后入库 User user new User(); user.setUsername(dto.getUsername()); user.setPassword(MD5Util.md5(dto.getPassword() SALT)); user.setRoleId(2); // 默认为普通用户 user.setCreateTime(new Date()); userMapper.insert(user); // 3. 签发Token并返回 return Result.success(JwtUtil.generateToken(user.getId(), user.getRoleId())); }2.2 资源分享模块文件上传与预览资源分享是这个平台最能体现考研垂直属性的模块。用户上传PDF、Word、图片等复习资料其他用户可以在线预览或下载。后端用SpringBoot自带的文件上传能力配置一个虚拟映射路径让外部能访问存储的静态文件。这里有个项目里最容易翻车的地方配置文件上传大小限制。SpringBoot默认只允许上传1MB考研资料动辄几十MB的PDF不调配置直接报错。我在application.yml里加了spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB文件存本地路径D:/kaoyan/files/用UUID重命名防止文件名乱码和冲突然后把原始文件名单独存到数据库tb_resource表里。预览功能前端用Vue-PDF插件实现文档类资源直接内嵌展示图片类就更简单了一个img标签搞定。下载就是拼一个后端接口的URL走HTTP流返回文件。2.3 问答讨论模块发帖、回复与置顶问答社区是用户粘性的核心我设计了帖子表和回复表两张表。帖子表存标题、内容、标签、浏览量、回复数、置顶标记回复表通过question_id关联帖子做成一对多的关系。这里有一个我自己踩过的性能坑浏览量计数每次查看详情就直接update一次view_count如果成了大流量项目数据库压力会很大。正确做法是先在缓存里累加定时批量同步到数据库。但毕设阶段并发量很小直接用数据库计数完全够用这个取舍要在答辩时能说清楚——你是知道有更优方案而不是不懂。标签字段用的是简单的字符串存储逗号分隔比如英语,数学,专业课。查询的时候用LIKE模糊匹配就行。这个设计不够优雅但务实。等数据量真的上去了再拆标签表也不迟前期开发效率优先。2.4 研友匹配模块标签筛选与申请研友匹配是我自己加的一个差异化功能。用户完善个人信息时可以填目标院校、专业方向、备考阶段、每日学习时长系统就基于这些标签做筛选匹配。实现思路不复杂匹配页面提供筛选项后端根据条件拼接SQL查询用户看到心仪的研友后点申请结伴对方那边会收到一条申请记录同意之后两人就建立了研友关系。这个模块用到了MyBatis-Plus的条件构造器关键查询长这样public PageUser matchUsers(Integer userId, String school, String major, Integer pageNum, Integer pageSize) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.ne(User::getId, userId) // 排除自己 .eq(StringUtils.isNotBlank(school), User::getTargetSchool, school) .eq(StringUtils.isNotBlank(major), User::getTargetMajor, major) .orderByDesc(User::getCreateTime); return userMapper.selectPage(new Page(pageNum, pageSize), wrapper); }这种动态条件查询是MyBatis-Plus的核心用法eq方法第一个参数是布尔条件只有满足时才追加这个查询条件。面试的时候这个点也常被问到务必吃透。3. 数据库设计与SQL脚本要点3.1 核心表结构设计思路数据库设计我遵循先梳理实体关系再建表的步骤。整个平台的核心实体包括用户、角色、资源、帖子、回复、研友申请、公告。表与表之间的关系主要有用户与帖子一对多、帖子与回复一对多、用户与资源一对多、用户与用户通过研友申请表多对多。我最终设计的表清单表名用途核心字段tb_user用户表username, password, nickname, avatar, target_school, target_majortb_role角色表role_name, role_codetb_resource资源表user_id, title, file_url, file_name, category, download_counttb_question帖子表user_id, title, content, tag, view_count, reply_count, is_toptb_answer回复表question_id, user_id, content, is_accepttb_partner_apply研友申请表from_user_id, to_user_id, status, messagetb_notice公告表title, content, create_time3.2 关键SQL脚本要点解析SQL脚本里我认为最值得说的是两个点。第一是建表语句要写全注释和字符集创建时间统一用DEFAULT CURRENT_TIMESTAMP这样插入数据的时候不用手动set时间减少代码出错面。第二是外键约束在毕设里不要过度使用。这个概念说出来可能有点反直觉但真实企业开发中很多团队的规范是表之间不建物理外键只在应用层维护逻辑关联。为什么因为物理外键会影响插入删除的性能而且在分库分表场景下外键直接失效。毕设里很多人建了外键反而被各种删除报错折腾得半死。我的做法是逻辑外键字段名叫user_id、question_id靠程序员自觉关联。答辩时老师要问就答从扩展性和性能角度考虑采用逻辑外键设计比单纯说我忘了专业一百倍。帖子表建表脚本展示一部分CREATE TABLE tb_question ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, user_id INT NOT NULL COMMENT 发帖用户ID, title VARCHAR(200) NOT NULL COMMENT 帖子标题, content TEXT COMMENT 帖子内容, tag VARCHAR(50) DEFAULT NULL COMMENT 标签英语/数学/政治等, view_count INT DEFAULT 0 COMMENT 浏览量, reply_count INT DEFAULT 0 COMMENT 回复数, is_top TINYINT DEFAULT 0 COMMENT 是否置顶0否 1是, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_user_id (user_id), KEY idx_tag (tag) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考研问答帖子表;注意到我给user_id和tag建了索引。索引是数据库调优里性价比最高的手段毕设数据量小看不出差别但你有这个意识和完全没有是两回事。4. 前端Vue实现与核心细节4.1 路由设计与登录守卫前端路由我用的是Vue Router页面结构拆成三块不需要登录的首页、登录注册页、需要登录的个人中心、发帖页、资源上传页、需要管理员权限的后台管理页。权限控制的核心是路由守卫加路由元信息配合router.beforeEach((to, from, next) { const token localStorage.getItem(token) const roleId localStorage.getItem(roleId) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.requiresAdmin roleId ! 1) { next(/) } else { next() } })这个逻辑不复杂但要注意一个细节用户登录后修改了个人信息localStorage里的roleId可能不是最新的。稳妥的做法是路由守卫里异步向后端请求一下当前用户信息再判断。但毕设为了省事我直接登录时把roleId存下来只在重新登录后更新。4.2 Axios封装与统一响应处理前端和后端联调最忌讳的就是每个页面各自写一堆axios.get(...)然后到处处理错误。我封装了一个统一的请求工具把基地址、超时时间、响应拦截器全部集中import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[token] token } return config }) // 响应拦截器统一处理业务错误和登录过期 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.clear() window.location.href /login } ElMessage.error(服务器开小差了稍后再试) return Promise.reject(error) } )这样封装有个立竿见影的好处后端返回格式统一为{code: 200, msg: 操作成功, data: {...}}前端拿数据永远从res.data里取业务错误和网络错误都被拦截器兜住了页面代码干净很多。4.3 核心页面组件化的思路Vue项目最核心的优势是组件化。我把博客首页的帖子列表抽成了独立组件因为帖子列表在首页、搜索结果页、标签筛选页都会用到。列表组件的props传入数据源的事件通过自定义事件向父组件通信。Element Plus的el-pagination分页组件配合后端返回的Page对象实现翻页每页数据量定10条用户体验比较合适。资源预览页是另一个比较有代表性的组件。PDF预览用的vue-pdf插件但是实测中发现这个插件对某些PDF版本兼容性一般有的文件会白屏。我兜底的做法是预览失败就自动切换成下载模式至少保证用户能拿到文件。这个降级方案的思路算是开发中比较实在的经验了。5. 前后端联调、部署与常见问题排查5.1 联调阶段三大坑前后端分离开发联调是必经的磨合期。第一个坑是跨域问题。前端跑在8080端口后端跑在9090端口浏览器直接拦截跨域请求。解决办法是后端写一个全局CORS配置类或者在网关层处理。我选了SpringBoot配置文件的方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }注意allowCredentials(true)和allowedOrigins(*)不能同时使用否则启动直接报错这个坑特别隐蔽。第二个坑是Vue项目打包后放在后端运行时刷新404。前端打包成静态文件扔进SpringBoot的static目录后直接访问首页没问题但是一刷新/question/detail/12这种地址就404了。原因是前端用的是history模式的History API而后端没有对应的路由。解决方法是后端加一个转发到首页的兜底Controller把所有非/api开头的请求都转发到index.html。第三个坑是日期格式不统一。后端返回Date类型默认序列化成时间戳前端要转成yyyy-MM-dd HH:mm:ss的字符串。我统一在后端加上Jackson的全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这种后端统一格式化的方式比每个前端页面自己转要省心太多。5.2 常见错误速查表我把开发期间的报错整理成了一张速查表这里按出现频次排个序错误现象根本原因解决办法启动报Port 8080 was already in use端口被占用结束占用进程或改server.port上传文件失败日志提示FileSizeLimitExceededException默认上传1MB限制修改max-file-size配置接口返回401token缺失或过期检查请求拦截器是否带token后端校验逻辑跨域请求被拦截CORS未配置添加全局CORS配置类刷新页面404history路由模式问题后端添加index.html兜底转发中文乱码编码不一致数据库表用utf8mb4连接URL加characterEncodingutf8这张表我打印出来贴在工位上答辩前的系统测试就是照着一张一条条过排查效率提高不少。5.3 打包部署环节的实际操作部署这块我走的是最朴素的路线后端打成jar包用java -jar启动前端npm run build生成dist目录后复制到后端static目录下前端页面和后端接口共用同一个端口这样既不用配置Nginx也不存在跨域。虽然是凑合方案但毕设演示时最稳定。如果后续想做成企业级的部署方案可以引入Nginx做反向代理前端静态资源挂在Nginx上后端接口单独用域名或路径代理。你需要在答辩的扩展展望环节提到这个才显得你不是只会写业务代码而是有生产环境思维。6. 用这个项目拿高分的经验整个项目从立项到写完调试通过前后用了大概三周时间。作为一个Java Web毕设它的难度适中——不是那种一眼看完就空洞的学生管理系统但也没有盲目堆砌技术栈。我给后续想做类似项目的同学几个非常具体的建议技术栈上不要为了追求花哨引入太多中间件。SpringBootMyBatis-PlusVueMySQL这套组合足够支撑一个完整的毕业设计Redis、ElasticSearch这些如果项目里没有非用不可的场景硬加进去反而会把自己卡住。我从一开始就克制住了加Redis做缓存的冲动事实证明是对的因为开发周期和精力都有限把核心业务做扎实远比堆技术名词重要。答辩的时候老师最常问的三个问题我提前给你们打个预防针第一个是你这个研友匹配的逻辑能再讲讲吗这时候你要把标签筛选的SQL构造过程说清楚第二个是为什么选JWT而不是Session要从无状态扩展性角度答第三个是项目有哪些可以优化的地方这时候抛出前面提过的浏览量缓存方案、索引优化、Nginx部署老师基本就满意了。最后再分享一个小技巧SQL脚本里一定要加几行测试数据。我当初往四张核心表里插了二十多条模拟数据包括不同专业的考研资料、几条带回复的帖子、几个标签不同的用户。这就让整个系统演示起来非常饱满老师打开页面就能看到效果而不是对着空表发呆。这个习惯到了真实的项目开发里也同样适用。
返回列表