每个做毕业设计的人都会遇到同一个问题:题目看着简单,一打开编辑器反而不知道从哪下手。这个基于springboot+vue的毕业生导师双选系统,恰好是那种“看起来就是一个管理系统,实际做起来全是细节”的典型项目。我用前后端分离的方式完整做了一遍,从业务模型设计到数据库建表,从接口联调到演示答辩,走完了一圈之后最大的体会是:这个题的核心不在“增删改查”,而在双选流程的设计。这篇就把整个项目的设计思路、关键实现和踩过的坑完整梳理一遍,给正在做同类毕业设计或者工作中要快速搭建类似业务系统的人一个可以直接参考的版本。
1. 双选系统的业务逻辑拆解
1.1 核心用户角色与权限边界
毕业生导师双选系统里有三类角色,这个定位必须一开始就定清楚,因为几乎后面所有的表结构、页面路由、接口权限都是从这三个角色派生出来的:
- 学生(毕业生):浏览导师和课题、填报志愿、查看匹配状态。
- 导师(教师):维护个人信息、发布课题、查看选择自己的学生、进行录取。
- 管理员(学院教务):创建双选批次、管理用户和角色分配、监控报名进度、导出最终结果。
权限边界看着简单,但实际设计时有很多细节。学生只能看到一个开启的批次下、允许自己参与的课题列表;导师只能看到选择自己的学生,而且要在批次允许的时间范围内操作;管理员不参与具体业务,但要能纵览全局。这些“边界”如果不在后端接口层做控制,只靠前端隐藏按钮,那演示的时候很容易被人点穿。
一个非常重要的细节是:导师和学生都是用户表里的一类人,但跟账号是一对一关系。我的做法是冗余设计——直接在用户表里加一个 role 字段区分身份,随后建 student_profile 和 teacher_profile 两张扩展表存放各自的具体信息(学号、工号、职称、研究方向等)。好处是登录认证统一走一套逻辑,不需要在认证阶段区分身份,只有进入具体业务模块才去查对应的扩展表。
1.2 从课题发布到结果确认的完整流程
整个双选流程我用一个业务闭环来串,这也是我在设计阶段花时间最多的地方。完整流程是这样的:
- 管理员创建“双选批次”,设置批次名称、报名起止时间、每人最多填报几个志愿、每个导师最多录取几人。
- 导师在批次开启后发布课题,课题信息包括标题、简介、人数上限、研究方向标签。
- 学生在批次时间内浏览课题,按优先级填报志愿(第一志愿、第二志愿,依此类推)。
- 报名时间截止后,导师进入系统查看报了自己课题的学生列表,按志愿顺序和自己的判断进行“录取”操作。
- 学生可以在一定时间内确认或拒绝导师的录取,双选完成。
- 管理员随时可以查看整体进度,批次结束后导出结果表。
这里有个关键设计——批次(batch)这个概念。很多第一次做这个题目的人容易忽略,直接做成“随时可以选”,导致业务逻辑变成简单的一对多选择数据库查询。加上批次之后,所有操作都受时间窗口约束,数据状态变得更清晰,也更容易做权限控制。批次的状态我设计成三个:未开始、进行中、已结束。
双选系统真正的规则核心是:学生可以报多个志愿,但导师只能录取有限人数;导师可以录取多个学生,学生最终只能归属于一个导师。这个“一对多对一”的结构,决定了志愿表要设计成多行记录,而不是在学生表里放一个 teacher_id 字段。
1.3 这个系统比普通CRUD复杂在哪
如果只看表面的增删改查接口,这个项目不算难。但真正让很多人在答辩时被问住的是下面这几个问题:
- 如果超过10个学生选了同一位导师,而导师最多只带6个人,怎么处理?
- 如果两个导师同时对一个学生点了“录取”,系统会不会出现一个学生被录取两次的情况?
- 批次截止时间是周末凌晨2点,服务端怎么保证恰好在这个时间关闭?
这三个问题分别对应超员校验、并发控制和定时任务,任何一个环节处理得不严谨,演示时就会翻车。
我在项目中用了下面这套状态机来管理报名记录的状态:已报名 -> 导师已录取 -> 学生已确认(或者学生已拒绝)。同时在一个导师的录取操作里,先查询当前已录取人数,再跟配置的上限比对,用事务包裹整个逻辑,防止并发提交造成超员。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是SpringBoot+Vue
2.1 SpringBoot后端的实际考量
这个项目用SpringBoot几乎是必然选择,因为毕业设计要兼顾“有技术含量”和“不给自己挖坑”两个目标。SpringBoot的自动配置让自己不用花大量篇幅去写Spring MVC的配置文件,内嵌Tomcat也让部署变得简单——打完一个jar包直接 java -jar 就能跑起来,不会在环境配置上浪费宝贵时间。
我用的版本组合是 SpringBoot 2.7.x + JDK 1.8,这里有个很现实的考量:很多学校的老旧实验室机器、答辩演示用的电脑上装的可能还是JDK 8,SpringBoot 3.x已经强制要求JDK 17,一旦现场环境不匹配,整个项目起不来,场面会很尴尬。所以在选题阶段就要考虑演示环境的兼容性。
ORM层我选了MyBatis Plus,而不是纯MyBatis或Spring Data JPA。原因很简单:MyBatis Plus提供了通用的单表CRUD方法,写起来效率高,ServiceImpl 和BaseMapper 直接用,同时保留了手写SQL的能力。像双选系统里“统计导师当前已录取人数并按时间排序”这种复杂查询,手写SQL反而比JPA的派生查询直观得多。
2.2 Vue前端结构与关键依赖
前端选择Vue是另一个稳妥的决定。用Vue 2 + Element UI做后台管理界面是近年毕设最常见的组合,案例多、组件齐、遇到问题搜得到答案。虽然Vue 3 + Element Plus更主流,但Vue 2的生态稳定性对毕设来说更友好,不推荐为了赶新潮给自己制造不必要的学习成本。
我的前端目录结构按标准Vue CLI工程来组织:
code复制src/
├── api/ // axios 请求封装,按模块划分文件
├── assets/ // 静态资源
├── components/ // 公共组件(上传组件、分页组件等)
├── router/ // 路由配置,带核心守卫做登录验证
├── store/ // 登录状态、用户角色信息(vuex)
├── views/
│ ├── admin/ // 管理员页面(用户管理、批次管理、结果导出)
│ ├── teacher/ // 导师页面(课题发布、学生录取)
│ └── student/ // 学生页面(课题浏览、志愿填报)
这里有两个依赖要重点说明:axios 和 vue-router。axios负责所有HTTP请求,需要在拦截器里统一加Token;vue-router要配合后端权限机制做路由守卫,未登录用户一律重定向到登录页。Element UI的 el-table、el-dialog、el-form 三个组件覆盖了90%的后台页面场景,没必要额外引入太多UI库。
2.3 前后端分离下的接口设计与跨域处理
前后端分离之后,接口规范从一开始就要定清楚。我用的是RESTful风格,资源用名词复数表示,操作映射到HTTP方法上:GET /api/teacher 获取导师列表,POST /api/apply 提交志愿,PUT /api/apply/{id}/confirm 确认录取。
所有接口统一返回一个 Result<T> 结构:
java复制public class Result<T> {
private Integer code; // 200成功,401未登录,500业务异常
private String message;
private T data;
}
这个结构非常省事,前端axios拦截器里面直接根据 code 做统一处理——401就跳登录页,500就弹错误提示,完全不用每个页面单独写错误处理。
跨域问题用SpringBoot的 @CrossOrigin 注解配合全局配置解决。开发环境我直接在WebMvcConfigurer里写了跨域配置,允许所有来源,允许的请求方法包含GET、POST、PUT、DELETE。生产环境把前端dist目录拷贝到SpringBoot的static目录下,由同一个Tomcat提供服务,天然同源,跨域配置只需要在开发期生效。
3. 数据库设计与核心接口实现
3.1 表结构设计中的关键决策
数据库设计直接决定后期编码的顺畅程度。我用MySQL 5.7,核心表共五张:tb_user、tb_teacher_profile、tb_student_profile、tb_batch、tb_topic,以及一张业务关系表 tb_application(选报记录)。后一张表是双选系统的“脐带”,所有核心流程都挂在它上面。
tb_application 的字段设计如下:
| 字段 | 说明 |
|---|---|
| id | 主键,自增 |
| batch_id | 批次ID,关联tb_batch |
| student_id | 学生ID |
| topic_id | 课题ID,关联tb_topic |
| teacher_id | 导师ID(冗余,方便查询和校验) |
| priority | 志愿优先级(1表示第一志愿) |
| status | 状态:0已报名,1导师已录取,2学生已确认,3学生已拒绝 |
| create_time | 报名时间 |
这里我特意把 teacher_id 冗余到选报表里,因为“查询某个导师收到哪些学生的报名”是最频繁的业务查询,冗余字段可以省掉一次关联。而且这个字段在初始化时就知道——课题表里本身就有 teacher_id。
tb_topic 表里需要有一个 quota(人数上限)字段,这是后面超员校验的数据来源。批次表tb_batch里的 max_apply_count(每人最多报几个志愿)和 max_choose_count(导师最多录几人)也是核心配置项,这两个字段要跟业务代码里的逻辑对应起来。
3.2 志愿填报与导师确认的接口逻辑
志愿填报接口是整个系统的第一个核心方法。一个学生一次提交多个志愿,通常是一个List形式传入。这里必须用事务包裹,保证“要么全部提交成功,要么全部失败”。实现时需要注意以下三个校验:
- 当前批次是否处于“进行中”状态。
- 提交的志愿数是否在配置范围内。
- 学生是否重复选择了同一课题。
这些校验如果漏了任何一个,演示时就会出现“在截止时间之后还能报名”“同一个课题报了两遍”这种低级错误。我在代码里用 @Transactional 注解给方法加事务,所有校验通过与数据写入在同一个事务里执行,只要有一个异常就整体回滚。
导师确认录取的接口逻辑相对复杂一些,核心伪代码如下:
java复制@Transactional
public Result confirmStudent(Long applicationId, Long teacherId, Long batchId) {
// 1. 校验批次是否处于可录取状态
Batch batch = batchMapper.selectById(batchId);
if (batch.getStatus() != BatchStatus.RUNNING) {
return Result.error("当前批次不允许录取操作");
}
// 2. 校验申请人状态还是"已报名"
Application app = applicationMapper.selectById(applicationId);
if (app.getStatus() != ApplyStatus.APPLIED) {
return Result.error("该学生已不处于待录取状态");
}
// 3. 校验该课题名额未满(使用行锁)
int count = applicationMapper.countByTeacherAndStatus(teacherId, ApplyStatus.TEACHER_CONFIRMED);
if (count >= batch.getMaxChooseCount()) {
return Result.error("录取人数已达上限");
}
// 4. 更新状态
app.setStatus(ApplyStatus.TEACHER_CONFIRMED);
applicationMapper.updateById(app);
return Result.success();
}
这里面最有讲究的是第三步的防止超员。最开始的版本我是先 select count(*) 再在事务里更新,但并发压测时发现了问题——两个请求同时进来,都读到当前人数少了名额不够的判断条件,结果都执行了更新,超员了。后来给查询加上了 FOR UPDATE 行锁,让同时进来的请求排队执行,才真正解决。
3.3 双选匹配与人数上限控制的深入补充
双选匹配逻辑,如果愿意可以做到非常复杂,例如在导师录取阶段提供“一键自动分配”功能:把所有报了某导师的学生按志愿优先级和报名时间排序,优先第一志愿,同等条件下先到先得,自动为导师选择符合条件的学生。这个功能在答辩时展示非常出彩,一来说明你想过程序化分配的规则,二来说明你考虑了导师工作负担。
实现思路很简单——写一个定时或手动触发的任务,遍历所有未分配的课题,把报了该课题且状态为“已报名”的学生拉出来,按 priority ASC, create_time ASC 排序,依次更新状态为“导师已录取”,直到人数达到 quota 或名单遍历完。
这里需要提示一点:自动分配和导师手动录取两种方式不能同时跑。我的解决办法是加了一个开关,管理员创建批次时选择“允许导师手动录取”还是“批次结束后自动分配”,这样避免数据互相覆盖。
4. 实操过程中最容易踩的坑
4.1 并发提交导致超员
这个问题在前面提到了,但值得单独拿出来说。每年毕设答辩,都有学生现场演示时被人反复点击提交按钮,结果后台数据出现重复记录或者超员。除了数据库层面加锁,前端也要做好提交按钮的防重复处理——loading 状态在请求完成前不消失,用 el-button 的 :loading 属性来控制。
另外,志愿服务器的唯一约束也要设置好:alternate key 可以用一个组合唯一索引 uk_student_topic_batch (student_id, topic_id, batch_id) 来防止同一学生在同一批次内重复提交同一课题。这属于数据库层面的最后一道防线,就算前端和后端业务代码都出现漏洞,数据库也能兜住。
4.2 批次时间校验只做前端
这是新手最容易犯的错。许多毕设的批次时间校验写在Vue里,用当前时间和批次起止时间做比较,超过时间就隐藏按钮。但前端的时间判断是摆设——只要请求被绕过(比如用Postman直接调接口),就会发现系统根本没有做二次校验。
后端要在两个层面校验时间:
- 接口层:通过AOP或全局拦击,读取当前时间判断批次状态,状态不是“进行中”就直接返回错误。
- 业务层:每个涉及写操作的服务方法里,校验一处
batch.status。
我的做法是写了一个 @BatchOpen 自定义注解,配合AOP切面,在带有该注解的接口执行前自动校验批次状态,代码看起来干净,做起来也方便。
4.3 权限过滤器的遗漏
权限控制是很多毕设项目的重灾区。如果一个普通学生可以调用导师的录取接口,整个系统的演示价值就消失了。我用的是SpringBoot的拦截器配合自定义注解,结构如下:
AuthInterceptor:拦截/api/**路径,解析请求头里的Authorization字段,把Token解析成用户信息放进ThreadLocal。@RequireRole(role = "teacher"):自定义注解,在拦截器里读取请求处理器的注解,校验当前用户的角色是否匹配,不匹配返回403。
把角色校验放在注解里而不是写死在代码里,能减少重复代码,后续想扩展“管理员也能帮导师提交录取”这种功能也更灵活。
4.4 前后端联调时的典型问题
联调阶段,我遇到的几个问题频率极高,总结下来也最值得分享:
时间格式不一致。后端返回的是 LocalDateTime 默认ISO格式,前端用的是 2024-01-01 00:00:00 这种格式,造成表格里显示T字分隔符。解决方法是直接在 application.yml 里配置时间格式化:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
Token过期失效。axios拦截器里放了一个401跳转逻辑,但Token过期和后端抛异常返回401会混在一起,导致前端偶尔误跳登录页。后来我把Token过期的响应码单独改成403,前端在401时才跳转登录页面,403就提示重新登录弹窗。
跨域配置和拦截器的顺序。这里有一个特别隐蔽的坑:如果没在WebMvcConfigurer里注册拦截器,且跨域配置顺序不对,浏览器发出的预检(OPTIONS请求)会被拦截器拦下来导致跨域失败。解决办法是在拦截器里放行OPTIONS请求,或者把跨域配置通过 addCorsMappings 注册好,保证预检请求能正确返回。
5. 系统的演示效果与准备要点
5.1 演示流程怎么讲出亮点
毕业设计答辩时,现场演示环节控制在8到10分钟最合适。我的建议是不要从头到尾点每一个页面,而是按照“问题驱动”的思路走:
- 用管理员账号创建批次,设置志愿个数和导师录取上限,截图留底。
- 切换学生账号,填三个志愿,重点展示志愿优先级和当前状态。
- 切换导师账号,展示“待录取学生列表”和录取操作,这时候一定要现场点开“录取”按钮,展示系统对超员情况的拦截。
- 回到学生账号,展示确认/拒绝录取的结果。
- 最后用管理员视角导出表格,展示完整结果。
这套流程把系统的每个核心功能都演到了,并顺带展示了你对双选规则的思考。答辩老师最核心的疑问基本就三个——并发怎么处理、时间窗口怎么控制、权限怎么管理,上面每一步都覆盖了这几个点。
5.2 答辩时高频问题和应对思路
除了技术细节,答辩时老师经常会问一些“为什么这么设计”的问题。我整理了这几个高频问题的应对思路:
- 为什么用SpringBoot而不是SSH/SSM? 回答要突出“减少配置、快速开发、内嵌容器”三个优势,顺便提一句“SpringBoot是当前企业主流的敏捷开发框架”,不要只说“因为简单方便”。
- Vue和JSP有什么区别? 重点讲前后端分离的思想——前端专注于交互展示,后端专注于数据接口,并行开发,也可以提到组件化复用和虚拟DOM带来的性能优势。
- 如果学生揣两个志愿怎么办? 用“志愿优先级排序,导师看到的是按优先级排好的候选列表,录取时系统只允许选择一个”来说明规则层面已经做了设计。
- 为什么选择MySQL? 从题目业务数据量、选型成本、与SpringBoot生态的适配性三个角度回答即可,不用展开讲分布式数据库。
只要把问题背后的业务逻辑想通,答辩就容易变为你展示思考的窗口。
6. 一些实用的小配置和经验补充
6.1 环境配置与版本选型建议
给还在准备阶段的人一个环境配置清单,这些版本组合我实测下来很稳:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容性好,教学环境基本都有 |
| Maven | 3.6+ | 依赖管理,SpringBoot项目标配 |
| Node.js | 14+ | Vue CLI构建需要 |
| MySQL | 5.7 / 8.0 | 学校一般5.7多 |
| IDE | IDEA + VSCode | 后端IDEA,前端VSCode |
如果本机JDK版本过高(比如JDK 17),用SpringBoot 2.7.x也可以运行,但要注意Maven的编译器插件需要跟随调整。
6.2 一个细节技巧:多环境配置
开发期间我用 application-dev.yml 连接本地数据库,答辩之前切换成 application-prod.yml 指向服务器上的MySQL。这个配置实战起来非常有用,避免现场演示时数据库里没有测试数据而当场卡住。同时团队协作时,多环境配置文件也能避免“我本地能跑,到你那边就报错”的问题。
配置的切换只用在启动时指定:
bash复制java -jar double-select-system.jar --spring.profiles.active=prod
6.3 打包部署中容易被忽视的点
前端打包完的直接产物是dist目录,里面是静态文件。很多人在本地开发时前端后端分开跑,部署时就直接把前端项目挂到nginx上,再把后端jar包挂一个端口。这样确实行,但如果想要简化部署,可以直接改用SpringBoot的static目录作为前端文件存放位置。具体操作是把 npm run build 生成的目标目录拷贝到 src/main/resources/static/ 下,再重新打包,访问 http://localhost:8080 就能直接打开系统,连nginx都不用装。
需要注意的一点是,前端路由如果用history模式直接由后端托管,刷新时会404更常见。此时改用 hash 模式路由(URL带 #)能规避这个问题,放下演示时,完全不影响讲解。
做这个项目的过程里我最深的体会是:毕业设计真正考察的不是代码量,而是你对一个完整业务流程的理解程度。双选系统技术栈主流、业务闭环清晰、扩展空间大,看起来是一个普通的管理系统,做下来其实覆盖了状态机设计、并发控制、权限安全、定时任务这些平时面试都在问的知识点。如果能像我上面梳理的一样,把规则想清楚,把边界校验做扎实,答辩的底气自然就有了。
