1. 为什么选"考研互助交流平台"当毕设题目
1.1 选题的三个现实理由
每年三月份,计算机专业的同学基本都开始焦虑毕业设计选题这件事。我当时的情况和大家差不多:不想选图书馆管理系统、学生选课系统这种被做烂的题目,又担心选太偏门的技术方向,答辩时讲不清楚,甚至做不完。
最后定了"考研互助交流平台",主要有三个考虑。
第一个理由是考研这个话题本身有真实需求。近几年考研报名人数一直居高不下,备考群体面临三个很实际的痛点:资料散落各处不好找、信息差严重(目标院校的报录比、专业课难度、复试风格都很难问到)、一个人复习容易心态崩。一个能把考研资料、经验帖、研友匹配集中起来的平台,听起来就有故事可讲,也有实际价值。
第二个理由是业务复杂度适中。和电商系统相比,它不需要处理订单、支付、库存那一套高并发逻辑;和纯粹的CMS内容管理系统相比,它又多了用户互动、文件上传、标签匹配这些功能。做完之后工作量饱满,答辩时不至于被导师问"你这个系统除了CRUD还有什么"。
第三个理由比较实在——参考资料充足。SpringBoot+Vue+MySQL这个技术组合,网上的教程数量非常多,遇到问题搜一下就能解决。考研互助平台类似的论文和源码也不少,但不是拿来抄,而是作为功能设计和技术方案的参考,能省掉很多从零摸索的时间。
1.2 先把功能边界划清楚
很多同学做毕设最大的问题不是不会写代码,而是需求想不清楚就开始建表,做到一半发现功能对不上,又推翻重来。我建议在动手之前,先把平台的用户角色和核心业务线画明白。
考研互助交流平台按我的设计,分了三种角色:
- 游客:只能浏览公开内容,比如帖子列表、资料列表的摘要信息
- 学生用户:注册登录后可以发帖、回帖、上传资料、下载资料、记录学习打卡、找研友
- 管理员:负责用户管理、帖子审核、资料审核、公告发布、数据统计
核心业务线归纳下来是五条:
- 内容社区:发帖求助、分享经验、评论回复
- 资料共享:上传考研资料、通过积分或下载权限控制下载
- 学习监督:每日学习打卡、打卡日历、专注时长记录
- 研友匹配:按目标院校、目标专业方向互相查找、私信联系
- 后台管理:内容审核和基础数据维护
整个系统围绕这五条线来做,数据库表结构和前端页面都跟着业务线走,逻辑非常清晰。实际做下来,五条业务线恰好对应了MySQL里主要的几张业务表——post、comment、resource、study_record、user,不会出现业务和表对不上的情况。
2. 技术选型:SpringBoot+Vue+MySQL这个组合怎么搭
2.1 前后端分离,但别分离得太痛苦
这个项目用的是前后端分离架构。前端是Vue,负责页面渲染和用户交互;后端是SpringBoot,提供JSON格式的RESTful API;MySQL负责数据持久化。前后端通过HTTP接口通信,用Axios发请求,后端统一返回{ code, message, data }格式的结果。
选前后端分离而不是传统的服务端渲染模板(比如Thymeleaf),原因很简单:Vue的组件化开发方式更适合社区类产品的复杂页面交互,比如帖子详情页、用户主页、消息列表这种频繁切换状态的界面。而且答辩的时候,你明确说出"前后端分离架构,通过接口层解耦"这句话,本身就是加分项。
但前后端分离也带来一个现实问题:开发和部署都要维护两套东西,端口不同还会遇到跨域。我一开始被跨域折磨了一阵子,后面通过后端配置CorsFilter一次性解决了。这块后面部署章节专门讲。
2.2 具体的版本组合和工具链
这套组合的版本,我建议选自己最熟悉的稳定版,不要盲目追新。我的环境是这样的:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定、兼容性好,SpringBoot 2.x全家桶都能跑 |
| SpringBoot | 2.7.x | 选2.x不要选3.x,3.x要求JDK17,不少同学本地环境还是8 |
| MyBatis-Plus | 3.5.x | 内置分页插件、代码生成器,省掉大量Mapper XML |
| MySQL | 5.7.44 | 资料多、兼容老项目的SQL写法 |
| Node.js | 14.x 或 16.x | 对应Vue CLI 4.x/Vue 2.x |
| Vue | 2.6.x + Element UI | Vue 3虽然新,但Element UI对Vue 2支持最成熟 |
| Maven | 3.6.x | 别用太高的版本,某些私服镜像对旧依赖解析有问题 |
之所以强调SpringBoot用2.x而不是3.x,是因为我见过不少同学图新鲜下载了SpringBoot 3.2,结果发现很多老教程里的配置全都变了——javax.servlet变成jakarta.servlet,拦截器、过滤器、静态资源映射的写法全不一样,网上搜到的解决方案大半不能用。毕设的核心目标是顺利做完安全落地,用你搜得到答案的版本比用最新的版本重要得多。
2.3 项目目录结构,前后端认真规划
后端的包结构我按职责分得很清楚,也是答辩时口径统一的基础:
com.example.kaoyan ├── controller # 接口层,只做参数接收和结果封装 ├── service # 业务逻辑层,处理具体规则 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端传参对象(登录、分页、搜索) ├── vo # 返回给前端的视图对象 ├── config # 配置类:跨域、拦截器、上传路径 ├── common # 统一返回结果、异常处理器、工具类 └── KaoYanApplication.java前端用Vue CLI创建项目,目录按页面和组件划分:
src ├── api # 按业务模块封装的请求函数 ├── assets ├── components # 公共组件(分页、上传、富文本) ├── router # 路由配置 ├── store # 用户状态、token管理(Vuex) ├── utils # axios实例、时间格式化等 ├── views # 页面:Home、PostDetail、UserCenter、Admin └── App.vue这样的结构看起来啰嗦,但后期排查bug和写论文时画架构图,你就知道好处了——每个类、每个文件都是清清楚楚的,论文里写"系统采用分层架构,表现层通过Vue组件实现,业务层通过Service接口隔离数据库访问细节"这些话,每句话都能对到具体的代码文件。
3. 数据库设计:考研场景下的数据模型怎么画
3.1 核心表结构和字段设计
数据库设计是整个项目的地基。表结构如果设计得合理,后面写后端接口会非常顺手;表结构有硬伤,后面改起来就相当于返工重建。我根据自己的业务线,设计了下面这几张主要表。
用户表(user):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(32) | 用户名,唯一索引 |
| password | varchar(128) | BCrypt加密后的密文 |
| nickname | varchar(32) | 昵称 |
| avatar | varchar(255) | 头像路径 |
| varchar(64) | 邮箱,找回密码用 | |
| role | tinyint | 0-学生,1-管理员 |
| target_school | varchar(64) | 目标院校,匹配研友用的关键字段 |
| target_major | varchar(64) | 目标专业,匹配研友用的关键字段 |
| status | tinyint | 0-正常,1-封禁 |
| create_time | datetime | 注册时间 |
帖子表(post):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 发帖人ID,外键逻辑关联user表 |
| title | varchar(128) | 帖子标题 |
| content | text | 帖子正文 |
| category | varchar(16) | 求助/经验分享/资料推荐/闲聊 |
| view_count | int | 浏览量,做热门排序用 |
| comment_count | int | 评论数,冗余字段,避免每次统计 |
| status | tinyint | 0-待审核,1-已发布,2-已删除 |
| create_time | datetime | 发布时间 |
评论表(comment)和帖子表结构类似,关键字段是post_id和user_id,再加一个parent_id来支持楼中楼回复。虽然平铺评论实现更简单,但导师普遍喜欢看到"支持二级评论"这个设计点,做起来也不算复杂,核心就是评论表加一个parent_id,前端根据parent_id渲染成树形结构。
资料表(resource):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 上传者ID |
| title | varchar(128) | 资料标题,比如"高等数学30年真题分类解析" |
| file_name | varchar(255) | 存储的文件名(重命名过的) |
| file_path | varchar(255) | 服务器存储路径 |
| file_size | bigint | 文件大小(字节) |
| category | varchar(16) | 数学/英语/政治/专业课 |
| download_count | int | 下载次数 |
| status | tinyint | 0-待审核,1-已发布 |
| create_time | datetime | 上传时间 |
学习打卡表(study_record):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 打卡用户 |
| record_date | date | 打卡日期,和user_id做联合唯一索引防重复 |
| duration_minutes | int | 当日学习时长(分钟) |
| content | varchar(255) | 学习内容简述 |
| create_time | datetime | 打卡时间 |
私信表(message)的字段更简单:id、send_id、receive_id、content、is_read、create_time。
3.2 几个重要的设计讲究
第一,所有表都带上create_time字段。这个不是凑字段,而是后面分页排序、后台统计都要用到。第二,状态字段尽量用tinyint而不是varchar,比如帖子status用0/1/2表示待审核/已发布/已删除,写业务代码时用数字判断比字符串判断速度更快。第三,逻辑外键就够用,不需要物理外键——答辩时说一句"系统设计追求高可用,采用逻辑外键避免级联操作对性能的影响"就够了。
索引设计上,我看到很多同学会在每个字段上建索引,这是错的。索引是查询辅助,写操作多的字段建索引反而是负担。我这个项目里,实际建索引的只有用户名(唯一)、帖子表create_time(热门帖子排序)、评论表post_id(楼中楼查询)、打卡表(user_id, record_date)联合唯一索引。热门帖子列表的SQL是WHERE status = 1 ORDER BY view_count DESC LIMIT 20,所以我当时在心里权衡过要不要给view_count加索引——最后没加,因为帖子量级到不了十万级别,全表扫描加上limit优化完全够用。
3.3 用Navicat还是命令行
我建表用的是Navicat for MySQL,图形化界面,导出SQL脚本也很方便。但建议你自己能手写几段建表SQL,这样答辩时如果导师问某个表是怎么建的,你能说得清楚。数据库导出的时候注意选utf8mb4字符集,不然存emoji表情和生僻字会变成乱码。
4. 核心功能实现:从用户登录到研友匹配
4.1 JWT登录鉴权,其实自己写一遍印象才深
登录模块我建议不要为了提高代码量而去用现成的Shiro或者Spring Security——对于这类中小型系统,用JWT自己实现一个轻量鉴权就够了,代码量三四百行,但你对整个认证流程的理解会透彻很多。
具体实现就三步:
- 用户输入用户名密码,后端用BCrypt校验密码。表里存的password字段就是BCrypt加密后的字符串,不是在数据库拿明文比对
- 校验通过后生成JWT字符串。我用的是
io.jsonwebtoken:jjwt这个库,在token里写入userId和role,设置过期时间为24小时 - 写一个拦截器,拦截除
/api/auth/login、/api/post/list等公开接口之外的所有请求。从请求头取Authorization: Bearer xxx,解析token,把userId放到ThreadLocal里,后面的接口直接从ThreadLocal取值
这里有一个很容易踩的坑:JWT的密钥是写在配置文件的,千万别硬编码在代码里。虽然毕设系统是本地跑,但答辩时如果有老师问"你的密钥怎么管理的",你说"硬编码在代码里"显然不好看,说"放在application.yml配置文件中,通过@Value注入"就专业多了。
还有一点建议,登录接口返回数据里,除了token,把用户的基本信息(昵称、头像、角色)一并返回。前端拿到这些信息存到Vuex里,页面顶部渲染头像和昵称就不需要再多发一次请求,这是实际开发中常见的"减少请求次数"优化手段。
4.2 帖子发布和评论回复,最常见的功能也值得认真做
帖子列表的接口我设计成分页查询:前端传pageNum和pageSize,后端通过MyBatis-Plus的分页插件返回IPage<PostVO>。排序策略是综合排序——浏览量高的排序靠前,同时通过ORDER BY create_time DESC让最新帖子能冒上来。
发帖子这里有个容易忽略的细节:帖子内容如果支持富文本,前端提交的content字段就包含HTML标签,不可避免存在XSS注入风险。我的处理方案是后端接口入口做了一个自定义注解@Sanitize,对富文本内容过滤掉<script>开头的危险标签。论文里你能写上"系统在输入层采用白名单过滤策略,防止存储型XSS攻击",这句话的分量比堆一百行CRUD代码都重。
帖子详情接口除了返回帖子本身,还会把评论列表一起返回。二级评论用parent_id字段做递归组装,返回给前端的VO结构是:每条评论有children数组,前端用递归组件渲染。这个功能是答辩时的"C位功能",因为面试官/导师都喜欢问:"如果评论有三级、四级,你这个方案还能用吗?"你要回答的是:实际业务场景限制最多两级,避免递归层级过深带来的查询性能问题,如果要做无限层级,考虑用path字段存储评论链路。
4.3 资料上传下载和文件存储策略
资料文件我用的是本地磁盘存储。SpringBoot配置里设置上传路径:
file: upload-dir: /home/kaoyan/upload/ access-prefix: /files/**上传接口用MultipartFile接收文件流,保存时做两件事:给文件名加UUID前缀防止重名,同时从文件扩展名做类型白名单校验——只允许pdf、zip、rar、docx这几种。像.exe、.jsp这些明摆着不安全的扩展名直接拒绝。
下载接口是重点,很多人开始接触这块后会因为中文文件名踩坑。当文件名是"高等数学真题.pdf",下载时都要做一次URL编码处理:
String fileName = URLEncoder.encode(resource.getFileName(), "UTF-8") .replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName);不这样做的话,前端拿到下载地址,浏览器会把中文文件名直接截断或者变成一堆乱码。而且我还对下载接口做了登录校验,游客不能直接通过URL访问文件,拦截器层就拦掉了。文件存储路径也不直接暴露给前端,前端拿到的只有一个文件ID,下载时通过/api/resource/download/{id}从后端数据库反查路径。这么设计主要是为了防御路径穿越攻击——攻击者传一个../的路径被人拼接到服务器路径里。
4.4 研友匹配:一个简单的"标签相似度"算法就能跑起来
研友匹配是这个项目里最有"平台特色"的功能,也是我答辩时导师重点问的部分。
我的实现思路不复杂:每个用户在注册或者个人设置里填写了目标院校(target_school)、目标专业(target_major)、所在城市(city)、考试科目组合(subjects)这几个标签。匹配时,后端算两个用户之间的标签相似度:
// 标签一模一样的字段越多,相似度越高 private int calculateScore(User a, User b) { int score = 0; if (StringUtils.equals(a.getTargetSchool(), b.getTargetSchool())) score += 40; if (StringUtils.equals(a.getTargetMajor(), b.getTargetMajor())) score += 30; if (StringUtils.equals(a.getCity(), b.getCity())) score += 20; if (StringUtils.equals(a.getSubjects(), b.getSubjects())) score += 10; return score; }然后取分数最高的前20个用户,排除掉自己,按分数倒序返回。匹配结果在用户详情页展示,点击"私信"按钮就能打开聊窗口。
这套简单的加权打分逻辑,胜在好讲清楚,也有优化空间。我记得当时还想过用协同过滤做推荐,但意识到考研平台本质是长尾需求,用户数量小,协同过滤数据稀疏问题特别严重,不如标签匹配效果好。答辩的时候,这段"为什么不用协同过滤而是用加权匹配"的分析,反而比实现本身更让导师满意。
5. 前端Vue的工程化细节
5.1 路由设计:嵌套布局路由和动态权限
前端如果用Vue 2 + Vue Router 3,路由结构建议设计成嵌套布局路由,把一路做下来会非常清晰:
const routes = [ { path: '/', component: Layout, // 包含顶部导航和侧边栏的主体布局 redirect: '/home', children: [ { path: '/home', name: 'Home', component: Home }, { path: '/posts', name: 'PostList', component: PostList }, { path: '/post/:id', name: 'PostDetail', component: PostDetail }, { path: '/resources', name: 'ResourceList', component: ResourceList }, { path: '/match', name: 'Match', component: Match }, { path: '/checkin', name: 'CheckIn', component: CheckIn }, { path: '/profile', name: 'Profile', component: Profile } ] }, { path: '/login', component: Login }, { path: '/admin', component: AdminLayout, meta: { requiresAdmin: true }, children: [ { path: 'users', component: AdminUsers }, { path: 'posts', component: AdminPosts }, { path: 'resources', component: AdminResources }, { path: 'stats', component: AdminStats } ] } ]Layout组件就是平台的框架页,里面放一个顶部导航栏(logo、搜索框、用户头像下拉菜单)和底部的路由出口。这样进入/home、/posts这些页面时,导航栏和页脚都不需要重复渲染。
路由守卫我这里写了两层逻辑:
router.beforeEach((to, from, next) => { const token = store.state.token; if (to.meta.requiresAdmin) { if (!token) { next('/login'); return; } if (store.state.user.role !== 1) { next('/home'); return; } } if (!token && to.path !== '/login') { next('/login'); } else { next(); } });一个很重要的细节:前台的一些页面(比如帖子列表、资料列表)是可以游客访问的,不要一股脑全部拦到登录页。我实际开发时只对"发帖、下载、打卡、匹配"这类需要登录后的操作做前端按钮级别的拦截(点击时提示"请先登录"),配合后端接口的鉴权,两套机制相互配合才是完整方案。
5.2 Axios拦截器和统一异常处理
前端的Api请求统一封装在src/utils/request.js里,核心就是创建axios实例:
const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }); // 请求拦截器:自动携带token service.interceptors.request.use(config => { const token = store.state.token; if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); // 响应拦截器:统一处理状态码和报错 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message); if (res.code === 401) { store.dispatch('logout'); router.push('/login'); } return Promise.reject(new Error(res.message)); } return res; }, error => { Message.error('网络异常,请稍后重试'); return Promise.reject(error); } );统一封装的收益是:项目里几十个接口,到处都不用写try-catch,也不用重复拿到token。需要注意的是baseURL别写死成http://localhost:8080,环境变量区分开发环境和生产环境:开发时用Vue CLI的proxy代理,生产时用Nginx反向代理。这样上线前不用改一行代码。
5.3 和SpringBoot联调时最常遇到的三个问题
第一个是跨域。开发时前端跑在8080端口(Vue默认),后端跑在9090(我自定义的),直接请求必然跨域。有两种解决方式——后端全局CorsFilter,把允许的来源配好;或者前端proxy方式。我生产用Nginx,开发用代理,不需要前端多配。后端配置如下:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:8080"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }第二个是时间格式。SpringBoot返回的LocalDateTime默认序列化成2025-03-10T12:30:00,Vue直接展示很别扭。我在application.yml里统一做格式化:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8第三个是参数传递。后端接口用@RequestBody接收JSON对象,前端用POST传;后端用@RequestParam或者@PathVariable的前端要用query参数或路径参数。很多同学在联调时遇到的400、404错误几乎都是这个原因。记住一个口诀:JSON数据一律放请求体,简单参数放URL,别混着传。
6. 部署实战:从本机到云服务器的完整过程
6.1 本地打包:前端命名的坑
先把本地能跑通的代码打包成生产包。前端打包:
npm run build打包之后在dist目录下生成一堆静态文件。有个非常常见的问题:这里如果用Vue CLI,直接跑npm run build后绝对路径是/js/app.js,部署到服务器的子路径时全部404,所以调整一下publicPath。我在vue.config.js里设置:
module.exports = { publicPath: './', // 关键配置:让资源走相对路径 outputDir: 'dist', assetsDir: 'static' }后端打包更简单:
mvn clean package -DskipTests在target目录下生成kaoyan-server.jar。打包之前注意检查application.yml里的数据库地址——本地用的localhost:3306,部署时要改成服务器的内网地址或公网地址,这个改漏了是最常见的部署失败原因。
6.2 服务器上的MySQL配置
服务器我用的CentOS 7.6,安装MySQL 5.7.44。安装教程网上很多,这里提三个特别重要的配置点。
第一,数据库导入时用Navicat导出的SQL脚本,一定要注意字符集。SQL文件头部要确保有:
SET NAMES utf8mb4;不然中文内容进去全变问号。导入后建议手动查一下user表里的中文昵称是否正常。
第二,MySQL默认只监听localhost,需要修改配置文件vim /etc/my.cnf,注释掉bind-address,或者改成0.0.0.0,然后重启MySQL服务。不然后端jar包在服务器上连不上数据库。
第三是登录用户的授权问题。因为项目部署是SpringBoot直接连MySQL,为了安全性,建议创建一个专用的应用账号,只授权这个业务库:
CREATE USER 'kaoyan'@'localhost' IDENTIFIED BY 'YourPassword'; GRANT ALL PRIVILEGES ON kaoyan_db.* TO 'kaoyan'@'localhost'; FLUSH PRIVILEGES;注意:部署的时候,不要直接用root账号甚至root系统账号连数据库,虽然毕设项目可能只有你自己在用,但这是一个习惯问题,也是论文里"安全设计"那一小节可以说一句的内容。
6.3 Nginx反向代理和前端路由history模式
前端dist目录放到服务器/home/kaoyan/dist目录下面,配置Nginx:
server { listen 80; server_name your_domain_or_ip; root /home/kaoyan/dist; index index.html; # 前端路由 history 模式,找不到文件时回退到index.html location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9090/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问 location /files/ { alias /home/kaoyan/upload/; } }三个关键点:
try_files $uri $uri/ /index.html是Vue Router history模式的标配,没有这行,刷新页面时会出现404location /api/把接口请求反代给后端,这样前端不需要处理跨域,因为浏览器看到的是同源访问- 上传文件访问用的是alias而不是root,这个区别容易搞混,alias是指定一个新的路径,root是拼接root+path
后端启动命令我用nohup放到后台:
nohup java -jar kaoyan-server.jar --server.port=9090 > /home/kaoyan/logs/server.log 2>&1 &第一次启动后,一定要看日志文件确认没有报错。最常见的错误是数据库连接被拒绝(账号密码不对/MySQL没监听)、端口被占用(9090被之前调试进程占了)、上传目录不存在(启动时会创建,但要注意权限)。
6.4 部署时一定会踩的几个坑
坑一:MySQL的SSL连接错误
我用的MySQL 5.7.44默认开启了SSL,SpringBoot的JDBC连接字符串如果不带useSSL参数会报Communications link failure或者类似SSL的握手错误。解决办法是在JDBC连接串上加:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/kaoyan_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai注意serverTimezone也必须带上,否则默认的时区和系统差8小时,所有时间字段显示都会错乱。
坑二:Nginx代理后上传文件过大导致413
SpringBoot默认带的最大请求没有限制,但Nginx默认client_max_body_size 1m,传一个10MB的考研资料PDF直接413。Nginx配置文件里加一行:
client_max_body_size 50m;后端SpringBoot这边同时也要设置:
spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB坑三:服务器时间不对导致JWT过期判断异常
如果服务器的时区没改成Asia/Shanghai,JWT过期时间按UTC算,可能刚登录就过期,或者过期时间相差8小时。执行:
timedatectl set-timezone Asia/Shanghai然后重启java进程,JWT校验就正常了。
坑四:忘了打开安全组/防火墙端口
阿里云/腾讯云的服务器除了系统防火墙,还有控制台上的安全组。安全组没放行80和9090端口,你怎么测都连不上。这个属于"部署文档里写清楚了,但实际最容易忘"的操作。我的部署文档里把这一段写在了最前面,粗体标注"先放端口再调试"。
7. 论文怎么写:如何把项目变成本科毕业论文
7.1 论文结构,跟着项目走就不难
毕业论文我按学校常规的七章结构走:绪论、需求分析、系统设计、系统实现、系统测试、总结与展望。在此之前,还要先写一个开题报告,内容基本是绪论和需求分析的略缩版。
绪论部分写得快的关键是先看15篇左右的参考文献,参考别人怎么表述"研究背景"和"国内外现状"。写的时候注意不要空话套话,比如"随着互联网技术的飞速发展"这句话虽然模板但可以用,但后面一定要落到具体数据上——考研人数、信息壁垒、资料分散。有数据支撑的绪论比任何虚话都要顺眼。
需求分析部分,不要只写文字描述,画用例图很重要。我用了用例图、活动图、类图,都是在draw.io里画的。答辩时导师扫一眼图表,比看十页文字更快速地明白你的系统边界在哪里。
7.2 系统设计和实现部分怎么写才能体现工作量
这两章是论文的核心,也最容易写得像代码说明书。我的经验是不要贴大段代码,而是围绕"模块讲解 + 流程图 + 核心代码片段"来写。
具体做法是:每个功能模块写一个小节,结构统一——先讲模块的业务流程是什么,画一个流程图;再贴一段关键的代码(比如JWT拦截器的校验逻辑、文件上传的处理逻辑、研友匹配算法的打分函数);最后给一两张系统运行截图。截图很关键,导师是真的会翻到系统实现章节看截图验证系统的真实性。
核心代码贴法有讲究:不要贴整个类,只贴最核心的方法。比如登录模块粘贴login()方法那几行 + JWT工具类的生成逻辑,不用贴Controller的所有代码。截图要有代表性,每个模块至少一张截图。
7.3 测试报告和论文防抄袭
系统测试章节,一般需要功能测试用例表和性能测试结果。功能测试用例我设计的是四五十条,覆盖所有角色和功能点,比如"用户登录成功""用户以不存在的账号登录""游客访问下载接口被拦截""管理员删除违规帖子"。每条用例包含用例编号、测试目标、操作步骤、预期结果、实际结果、是否通过。
性能测试有必要做一下。我用JMeter对登录接口和帖子列表接口分别做了100并发持续60秒的压测,记录响应时间的平均值,以及系统资源占用。这个数据写进论文里,整个测试章节的分量就上来了。我实测下来SpringBoot+MySQL在100并发下接口平均响应时间在300ms左右,数据库连接池配置成20,表现完全够用。
防抄袭这边特别提醒:论文查重是毕业论文答辩的硬性门槛。技术方案和需求描述部分可以参考其他论文的框架结构,但必须用自己的语言重写,尤其是系统实现和功能描述这些核心章节,保证是自己做的项目,写起来自然有自己的特点,技术路线完全一致,查重率就不会过高。
8. 答辩之前,建议你完成的三件事
答辩PPT和答辩稿是最后一道工序,但我观察到不少同学把精力全放在做PPT上,忽略了更重要的三个准备。
第一,把项目的关键参数背熟:技术栈的版本号、数据库表的数量、核心接口的响应时间、部署服务器的配置。导师问"你系统每秒能扛多少请求""你有几张表""JWT过期时间多久",回答得干脆利落,说明系统是你亲手做的,不是拿别人现成的东西敷衍。
第二,提前准备可能会被追问的拓展问题。我答辩时被问过的问题有:"如果用户数量变多,你系统最大的瓶颈在哪?"(回答方向:数据库连接池撑不住,需要加缓存和读写分离);"文件上传存本地磁盘,如果做多台服务器部署怎么办?"(回答方向:需要引入OSS对象存储或分布式文件系统);"帖子搜索接口用的LIKE模糊查询,数据量大有什么问题?"(回答方向:全表扫描效率低,需要引入Elasticsearch做全文检索)。这些问题不需要你在毕设里真的做,但你要能说出"目前方案的局限 + 业界常规的优化方向"。
第三,把项目亲自从零到部署再走一遍。答辩现场的演示环节通常是在你自己电脑上运行,但如果到时候电脑链接不上服务器,你要有备用方案。我当时特意准备了一个本机环境的备用启动包,把MySQL、前端、后端全部配置成localhost模式,这样即使现场网络出问题、服务器连不上,也能本地跑起来演示完整功能。
最后分享一点个人的感受:做毕业设计的过程,很多时候是在"用你已经会的东西做出来一个能跑的系统"和"学到一点新东西并且把自己的理解写进文档"之间找平衡。这个考研互助交流平台做完,我最满意的不是代码量,而是每一层自己都说得清楚——数据库为什么建这些字段、接口为什么这么设计、部署踩了哪些坑、论文里每句话对应的实际依据都在。这些东西才是毕业设计之后留给自己的真正积累。