Spring Boot毕业设计双选系统:从状态机到并发控制完整实战
每年一到毕设季私信里求推荐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_userid、username、passwordBCrypt加密、real_name、role_type、class_name、create_timetopicid、teacher_id、title、description、max_students、selected_count、status、audit_remark、create_timeselection_recordid、topic_id、student_id、status申请中/已通过/已拒绝/已退选、apply_time、confirm_timenoticeid、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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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注意serverTimezoneAsia/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 LambdaQueryWrapperSelectionRecord() .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做乐观锁配合一段条件更新SQLUPDATE 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);相比原生POIEasyExcel的写入内存占用更低对答辩演示的稳定性有保障。建议把课题汇总导出和选题名单导出都加上老师想看数据时直接导出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-size6.3 部署阶段问题现象原因解决方案打包后静态资源404页面被放进jar包内路径不对确认templates和static目录结构正确MySQL8驱动报Public Key Retrieval连接校验方式问题在url后面加allowPublicKeyRetrievaltrue数据库时间差8小时连接时区与数据库时区不一致JDBC url设置serverTimezoneAsia/Shanghai排查问题有个小经验报错日志很长别从第一行开始读直接找Caused by后面的内容那才是真正的根因。这套系统的报错基本集中在唯一索引冲突和状态判断异常两类踩过一两次后定位速度会很快。7. 论文写作与答辩的实战建议7.1 论文结构怎么搭学软件工程的同学都知道标准套路绪论、需求分析、系统设计、系统实现、系统测试、总结。关键是不要在前几章翻来覆去介绍Spring Boot是什么老师更想看你针对毕业设计双选这个业务的分析过程和设计决策。三个写作技巧供参考。第一把双选状态机画进系统设计章一张课题状态流转图配上每步前置条件的文字说明比八百字描述更直观。第二数据库设计用表格列关键字段名、类型、约束和含义老师一眼就能看出你做了认真设计。第三测试部分写真实场景数据。比如模拟10名学生并发申请同一课题仅有2个名额系统最终生成2条成功记录其余返回名额已满这种描述比系统测试全部通过有力得多。7.2 答辩讲解节奏答辩最常见的失误是从登录页开始一个功能一个功能演示讲了十分钟才到重点。时间根本不够。正确的节奏是先用两分钟讲清楚业务痛点再用三分钟讲状态机和数据库设计然后演示一条完整链路——教师发布、管理员审核、学生申请、教师确认、Excel导出最后花一分钟讲并发控制和异常处理。准备好这几个高频问题基本就稳了怎么防止学生重复选同一课题答联合唯一索引加事务校验如果两个人同时抢最后一个名额怎么办答乐观锁条件更新数据库层面保证不超卖课题状态为什么不用0和1两个值答需要支撑多阶段流程比如待审核驳回、退选回流你和传统Excel选题有什么区别答信息透明、流程可追溯、并发场景下保证公平。回答的时候不要背概念结合代码里的具体位置说比如selection_record表上有联合唯一索引apply方法里做了状态判断一句话就能证明代码是你写的这个印象分很重要。这套项目我前后带过不少学生跑完最大的体会是真正决定毕设高度的从来不是框架多新、页面多炫而是你对自己业务状态图的理解程度。选这个题目的人只要把课题从草稿到双选完成这条主链路走一遍想明白每一处状态变化的前置条件开发、论文、答辩就都有了底气。基础版本跑通之后还可以继续扩展WebSocket通知推送、Excel批量导入课题或者给教师端加一个选题分配的建议算法。在Spring Boot里做增量开发比从零开始要舒服得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →