每年一到毕设季,私信里求推荐Java题目的消息就没断过。电商、秒杀、校园商城这些题目早被写烂了,答辩时千篇一律。如果让我推一个既不会让工作量失控、又能把Spring Boot核心技术点都带出来的项目,我脑子里第一个蹦出来的就是"毕业设计双选系统"。名字不酷,但业务闭环完整,答辩故事线清晰,代码量也适中。这篇文章我按自己实际带项目的习惯,把这套系统的业务设计、数据库、核心逻辑、调试运行、论文写作全部复盘一遍,每个关键取舍后面都告诉你为什么。
1. 这个项目究竟解决什么问题
1.1 传统选题流程里的真实痛点
很多同学拿到题目第一反应是:双选系统不就是两个下拉框吗?学生选一下,老师确认一下,有什么好做的?有这种想法是因为没经历过真实的毕设选题现场。
在没有系统化管理之前,毕业设计选题基本靠人工流转:教学秘书收集各教研室课题,整理成Excel表格发下去;学生翻表格、找学长打听、给老师发邮件申请;老师这边收一堆邮件后逐个回复"名额满了,换一个吧"。整个流程里,有三类问题是必然出现的。
第一,信息不透明。公开的课题表经常过期,哪些课题还有名额、哪些已经被内定,学生完全不知道。第二,公平性难保证。选题先后顺序、老师个人偏好都会影响结果,而且没有任何留痕记录,出了问题说不清。第三,管理成本高。负责老师每天都在回答相同的问题:这个题还能选吗?我换了题目要不要重新申请?
这些痛点天然需要一个平台来承接。"双选"这个业务模式,本质上讲的就是学生选课题、教师选学生的双向匹配过程,它不是一个简单的列表,而是一套有规则、有状态、有审核机制的业务闭环。
1.2 系统角色与核心功能地图
整套系统按用户身份分成三条链路:学生、教师、管理员。三条链路各管一摊事,又通过课题和选题记录这两张核心表串在一起。
| 角色 | 核心职责 | 关键功能点 |
|---|---|---|
| 学生 | 选择课题、等待教师确认 | 浏览已审核通过的课题、提交选题申请、撤销申请、查看选题结果、接收通知 |
| 教师 | 发布课题、筛选学生 | 新增课题、提交审核、查看申请学生、确认或拒绝学生、管理已选名单 |
| 管理员 | 全局配置与监督 | 用户管理、课题审核、选题进度统计、通知发布、选题时间窗口设置 |
拿到任何一份本系统的源码,我建议你先按这三条链路去浏览工程结构。学生链路走select相关接口,教师链路走topic相关接口,管理员链路走audit相关接口。对应关系理清了,6000行代码也不会觉得乱。
1.3 对比商城类项目,为什么这个题目更适合毕设
很多同学一上来就奔着电商系统去,觉得购物车、订单、支付听起来有面子。但真做起来往往会遇到两个尴尬:一是支付肯定不能真接,只能模拟;二是前后台功能过于分散,最后写出来的代码七成是增删改查,论文里讲不出设计深度。
双选系统恰好相反,它的业务链条短而完整。教师发布课题、管理员审核、学生申请、教师确认、系统记录全过程,每一步都能讲出"为什么这么设计"的理由。答辩老师问这个项目有什么价值,你不需要吹技术多新颖,只需要把业务流程讲通:降低信息不透明、保证选题公平、过程可追溯。业务系统该回答的问题,它一个不少。
2. 从需求到设计:角色模型与双选状态机
2.1 三角色权限模型:口子要封住,但别过度设计
权限模型我用的是经典RBAC思路:用户表、角色表、用户角色关联表,加上接口级别的权限控制。很多教程喜欢把菜单权限、按钮权限、数据权限全铺开,但对本科课设来说,这个深度其实没必要。真正重要的是把资源访问边界封住:学生不能访问教师的课题管理接口,教师不能代替学生提交选题,管理员不直接干预具体选题结果,只做审核和统计。
权限框架我推荐Spring Security。实践中你会遇到一个典型问题:登录接口明明配了permitAll,为什么还是被拦截?其实permitAll只是允许匿名访问该路径,它依然会走完过滤器链。理清这一点,权限配置就不容易玄学。如果觉得Spring Security太重,用拦截器加注解也能实现同样效果,只是答辩的时候"安全性设计"这一块可讲的内容会少一些。
2.2 双选状态机:整个项目最该画清楚的一张图
这个系统最核心的设计,就是课题的状态流转。我把它定义为一条主链路:
草稿 -> 待审核 -> 已通过 -> 已被选 -> 双选完成
另外还有三个分支状态:审核驳回、教师下线、退选回流。每个状态变更都必须满足前置条件,不能乱跳:
- 待审核的课题,不会出现在学生的可选列表里;
- 已通过且名额未满的课题,学生才能提交申请;
- 学生提交申请后,课题进入"已被选"状态,此时其他学生不能再申请同一课题;
- 教师确认一名学生后,如果名额达到上限,课题自动变为"双选完成"。
有一个细节特别容易漏:学生退选后,状态回退到哪里?合理的做法是,撤销成功后课题状态回到"已通过",名额释放,其他学生可以继续申请。如果教师拒绝某学生的申请,也是同样的回退逻辑。把这条状态链画成一张状态图放进论文里,比写八百字描述都管用。
2.3 数据库表设计:核心五张表,别急着扩表
很多课设项目一上来就设计二十张表,结果光建表就花了三天。这套系统里真正参与核心流程的只有五张表:用户表、角色表、课题表、选题记录表、通知表。操作日志、班级专业这类辅助表,按论文需要再加就行。
关键字段这样设计:
sys_user:id、username、password(BCrypt加密)、real_name、role_type、class_name、create_time;topic:id、teacher_id、title、description、max_students、selected_count、status、audit_remark、create_time;selection_record:id、topic_id、student_id、status(申请中/已通过/已拒绝/已退选)、apply_time、confirm_time;notice:id、title、content、publisher_id、create_time、is_read。
这里有两个非常关键的细节。第一,topic表里的selected_count是冗余字段,用空间换查询效率,每次教师确认成功后在一个事务里对它加一。第二,selection_record表必须建(topic_id, student_id)联合唯一索引,这是防止学生重复申请的最后一道防线。前端按钮置灰只是体验优化,数据库约束才是最终保证。
3. 技术选型思路与工程结构搭建
3.1 服务端:Spring Boot 2.7 + MyBatis-Plus
服务端我推荐Spring Boot 2.7.x搭配MyBatis-Plus。原因很实在:Spring Boot把自动配置、内嵌Tomcat、日志、健康检查都封装好了,三分钟就能起一个Web工程;MyBatis-Plus对单表的CRUD、分页、条件构造器支持成熟,毕业设计九成场景就是单表查询加一两个联查,用它开发速度比JPA更可控,SQL也掌握在自己手里。
至于用JPA还是MyBatis-Plus,我的建议是跟就业市场走。现在Java后端岗位普遍要求MyBatis,课设保持技术栈一致,后面写简历也顺。JPA虽然也能写,但遇到复杂查询时,调试JPQL的成本会让很多人崩溃。MyBatis-Plus的LambdaQueryWrapper写条件像写普通Java一样直观,比如查出所有"已通过"且"名额未满"的课题,一行条件构造器就完事,不用拼字符串SQL。
3.2 前端方案:看你的时间包来决定
前端有两条路,各有适用场景。
第一种是Thymeleaf服务端渲染。Spring Boot天然支持,不用单独启动前端服务,页面和Controller之间直接传Model,学习成本低,项目结构紧凑。如果你只有三到四周时间,这条路最稳。权限判断放在Controller层,登录后根据role_type转发到不同首页,简单直接。
第二种是前后端分离,Vue3加Element Plus。界面更现代,简历上的观感更好,但你需要额外处理跨域、Token存储、请求拦截、前端打包部署,整条链路会复杂不少。如果本来就熟练Vue,没问题;如果是为了这个项目现学,我劝你衡量一下时间成本。
3.3 关键依赖与基础配置
pom.xml里最重要的几个依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>版本上要提醒一下:Spring Boot 3.x要求JDK17,而不少学校机房还是JDK8,用2.7.x兼容性更好;MyBatis-Plus别用太老的版本,和Spring Boot 2.7配合时建议3.5以上,否则可能出现分页插件失效这类问题。
数据源配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/grad_double_choose?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:mapper/**/*.xml注意serverTimezone=Asia/Shanghai,这是毕设项目里出现频率最高的坑。好多同学的连接串是从教程里直接抄的,没带时区参数,启动直接报时间连接错误,或者查询出来的时间差八个小时。这一步配置对了,能少踩一半的坑。
4. 核心功能实现:从课题发布到双选落定
4.1 课题发布与审核流程
教师端发布课题的核心就是插入一条status=待审核的记录,重点在管理员的审核接口。审核时必须先加载课题,校验它的当前状态,再决定是否放行。
// AdminServiceImpl.java @Transactional public Result audit(Long topicId, boolean pass, String remark) { Topic topic = topicService.getById(topicId); if (topic == null || !TopicStatus.PENDING.equals(topic.getStatus())) { return Result.error("该课题不处于待审核状态"); } if (pass) { topic.setStatus(TopicStatus.APPROVED); } else { topic.setStatus(TopicStatus.REJECTED); topic.setAuditRemark(remark); } topicService.updateById(topic); noticeService.record(topic.getTeacherId(), "课题审核结果", pass ? "通过" : "驳回:" + remark); return Result.success(pass ? "审核通过" : "已驳回"); }这里的核心是前置状态校验,很多同学写代码时容易漏掉。前端只管把topicId和pass传过来,如果没有状态判断,一个已经审核通过的课题可能被再次审核,数据就乱了。所有可能改变状态的接口,第一步都必须是加载最新数据并判断当前状态,这个习惯能帮你避免大量脏数据问题。
4.2 学生申请课题:靠事务和唯一索引拦住重复请求
学生申请接口是并发问题最集中的地方。需要考虑两个场景:同一个学生反复点按钮,和多个学生同时抢最后一个名额。
// SelectionServiceImpl.java @Transactional public Result apply(Long topicId, Long studentId) { Topic topic = topicService.getById(topicId); if (topic == null || !TopicStatus.APPROVED.equals(topic.getStatus())) { return Result.error("课题不存在或不可申请"); } if (topic.getSelectedCount() >= topic.getMaxStudents()) { return Result.error("课题名额已满"); } SelectionRecord exist = selectionMapper.selectOne(new LambdaQueryWrapper<SelectionRecord>() .eq(SelectionRecord::getTopicId, topicId) .eq(SelectionRecord::getStudentId, studentId)); if (exist != null) { return Result.error("你已经申请过该课题"); } SelectionRecord record = new SelectionRecord(); record.setTopicId(topicId); record.setStudentId(studentId); record.setStatus(SelectionStatus.APPLYING); selectionMapper.insert(record); topic.setStatus(TopicStatus.SELECTED); topicService.updateById(topic); return Result.success("申请成功"); }代码里这两次查询是常规防线。但极端并发下,两个请求可能同时通过判断,同时走到insert。这个时候就需要(topic_id, student_id)联合唯一索引兜底:第二个请求插入时数据库直接报DuplicateKeyException,事务回滚,流程结束。建议在全局异常处理器里把这类异常转成"你已经申请过该课题"的友好提示。能在答辩时说出"程序判断加数据库唯一索引双保险",这个深度远超普通课设。
4.3 教师确认与名额扣减:别让超卖问题发生
教师确认学生申请,本质上是把申请池里的一条记录转正,同时占一个名额。这里最怕的是"超卖"——两个教师同时操作,或者一个学生申请通过后其他人又插进来,导致最后确认的人数超过max_students。
我的处理方式是加一个冗余字段version做乐观锁,配合一段条件更新SQL:
UPDATE topic SET selected_count = selected_count + 1, version = version + 1, status = CASE WHEN selected_count + 1 >= max_students THEN 3 ELSE status END WHERE id = #{id} AND version = #{version} AND selected_count < max_students这段SQL的含义是:只有在我读到数据之后没人改过它、且名额还没满的情况下,才允许加一。update的返回值是影响行数。如果是0,说明条件不满足,直接提示"课题名额已满或状态已变更"。这种写法比SELECT FOR UPDATE更容易理解,也不用长期持有行锁,对课设项目来说足够优雅。
确认完成后,同一个课题下其他还在"申请中"的记录要批量置为"已拒绝"。这一步也是很多实现容易漏的,如果漏掉,学生会看到自己的申请一直停在"申请中",体验很差。
4.4 通知、统计与Excel导出:低成本高收益的加分功能
通知模块没什么玄机,核心就是在业务动作结束后调一次noticeService.record,给对应用户插入一条未读通知。学生端显示未读数量,点击已读后更新状态。代码量不大,但论文里能写,答辩能演示,属于性价比很高的模块。
统计功能用MyBatis写聚合查询,统计每个教师的选题人数、课题通过率、学生选课热度。前端用ECharts画几个图表摆在管理员首页,视觉效果一下就不一样了。不用多复杂,一两个柱状图加一个饼图就够。
Excel导出推荐EasyExcel,三行代码生成一个课题汇总表:
EasyExcel.write(response.getOutputStream(), TopicExcelVO.class) .sheet("课题汇总") .doWrite(topicList);相比原生POI,EasyExcel的写入内存占用更低,对答辩演示的稳定性有保障。建议把"课题汇总导出"和"选题名单导出"都加上,老师想看数据时直接导出Excel,这个细节非常加分。
5. 本地运行与调试实录
5.1 环境准备与初始化数据库
运行这套项目需要JDK8或JDK11、Maven 3.6以上、MySQL 5.7或8.0、IDEA。拿到工程后,先别急着启动,用命令行确认环境变量:java -version、mvn -v。很多人IDE里报错半天,最后发现是环境变量没配好,这一步检查只要一分钟,能省下半小时。
数据库初始化时,新建一个grad_double_choose库,字符集选utf8mb4,然后执行SQL脚本。脚本里除了建表语句,最好带上初始数据:三个角色的测试账号、八条以上课题记录、几条通知。这里有个建议:初始课题要覆盖不同状态,待审核的、已通过的、名额快满的都得有,这样演示双选流程时不用临时造数据,点几下就能把完整链路跑通。
5.2 配置文件与第一轮启动
启动前检查application.yml三样东西:数据源密码、端口号、Mapper扫描路径。端口冲突是启动失败最常见的原因,8080被占了就改成8081。
运行主类,看到Spring Boot的Banner打印出来,再看到Tomcat started on port 8080,服务就起来了。第一次启动后,用admin登录,应该进管理员工作台;退出后用teacher登录,看到课题管理菜单;再用student登录,看到可选课题列表。三条角色链路各走一遍,确认没有404和500,项目就算跑通了。
5.3 断点调试中值得停下的几个位置
拿到一份源码,最快的学习方式不是从头翻代码,而是跟着请求打断点。我建议重点看三个位置。
第一,Spring Security的UserDetailsService.loadUserByUsername,观察登录时用户信息如何从数据库加载、密码如何比对。第二,学生申请课题的apply方法,把topicId传成一个不存在的主键执行一遍,再传正常的执行一遍,观察前置校验和唯一索引两条防线的触发顺序。第三,管理员审核的audit方法,看审核通过后课题状态值如何变化、通知记录如何生成。
这三个断点串起来就是完整业务主链路,跑一遍之后,你对整个项目的理解会立刻清晰,比盲目翻代码高效得多。
6. 常见问题与排错速查表
6.1 启动阶段问题
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 启动失败,端口被占用 | 8080被其他程序占用 | 改server.port,或用netstat -ano查占用进程 |
| 数据库连接失败 | 密码错误、库不存在、时区字符串不对 | 核对url、账号密码,确认库已创建 |
| Mapper接口报Invalid bound statement | 没有加@Mapper注解 | 在启动类加@MapperScan或给接口加注解 |
编译缺少@Data相关方法 | 没装Lombok插件 | IDEA安装Lombok插件并开启注解处理 |
6.2 运行阶段问题
| 功能 | 遇到问题 | 排查方向 |
|---|---|---|
| 登录后进不去管理页 | 权限配置把所有请求都拦了 | 检查SecurityConfig的白名单和角色规则 |
| 学生申请后课题仍显示可选 | 课题状态不是已通过,或名额没扣减 | 检查status字段和selected_count计数 |
| 同一学生重复申请居然成功了 | 唯一索引没建 | 执行ALTER TABLE加联合唯一约束 |
| 上传附件超过1MB报错 | Spring默认单文件上限1MB | 配置spring.servlet.multipart.max-file-size |
6.3 部署阶段问题
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 打包后静态资源404 | 页面被放进jar包内,路径不对 | 确认templates和static目录结构正确 |
| MySQL8驱动报Public Key Retrieval | 连接校验方式问题 | 在url后面加allowPublicKeyRetrieval=true |
| 数据库时间差8小时 | 连接时区与数据库时区不一致 | JDBC url设置serverTimezone=Asia/Shanghai |
排查问题有个小经验:报错日志很长,别从第一行开始读,直接找Caused by后面的内容,那才是真正的根因。这套系统的报错基本集中在唯一索引冲突和状态判断异常两类,踩过一两次后定位速度会很快。
7. 论文写作与答辩的实战建议
7.1 论文结构怎么搭
学软件工程的同学都知道标准套路:绪论、需求分析、系统设计、系统实现、系统测试、总结。关键是不要在前几章翻来覆去介绍Spring Boot是什么,老师更想看你针对"毕业设计双选"这个业务的分析过程和设计决策。
三个写作技巧供参考。第一,把双选状态机画进系统设计章,一张"课题状态流转图"配上每步前置条件的文字说明,比八百字描述更直观。第二,数据库设计用表格列关键字段名、类型、约束和含义,老师一眼就能看出你做了认真设计。第三,测试部分写真实场景数据。比如"模拟10名学生并发申请同一课题(仅有2个名额),系统最终生成2条成功记录,其余返回名额已满",这种描述比"系统测试全部通过"有力得多。
7.2 答辩讲解节奏
答辩最常见的失误是从登录页开始一个功能一个功能演示,讲了十分钟才到重点。时间根本不够。正确的节奏是:先用两分钟讲清楚业务痛点,再用三分钟讲状态机和数据库设计,然后演示一条完整链路——教师发布、管理员审核、学生申请、教师确认、Excel导出,最后花一分钟讲并发控制和异常处理。
准备好这几个高频问题基本就稳了:
- "怎么防止学生重复选同一课题?"答联合唯一索引加事务校验;
- "如果两个人同时抢最后一个名额怎么办?"答乐观锁条件更新,数据库层面保证不超卖;
- "课题状态为什么不用0和1两个值?"答需要支撑多阶段流程,比如待审核驳回、退选回流;
- "你和传统Excel选题有什么区别?"答信息透明、流程可追溯、并发场景下保证公平。
回答的时候不要背概念,结合代码里的具体位置说,比如"selection_record表上有联合唯一索引,apply方法里做了状态判断",一句话就能证明代码是你写的,这个印象分很重要。
这套项目我前后带过不少学生跑完,最大的体会是:真正决定毕设高度的从来不是框架多新、页面多炫,而是你对自己业务状态图的理解程度。选这个题目的人,只要把"课题从草稿到双选完成"这条主链路走一遍,想明白每一处状态变化的前置条件,开发、论文、答辩就都有了底气。基础版本跑通之后,还可以继续扩展WebSocket通知推送、Excel批量导入课题,或者给教师端加一个选题分配的建议算法。在Spring Boot里做增量开发,比从零开始要舒服得多。