十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot的毕业设计选题管理系统设计与实现

基于Spring Boot的毕业设计选题管理系统设计与实现 又到了一年毕业设计的季节。每年都有不少同学来找我聊选题问得最多的就是“做什么系统比较简单又能过”“有没有现成的源码参考”。说实话毕业设计这个事选系统类的项目核心不是“代码写得有多花哨”而是“业务逻辑是否清晰、技术栈是否主流、文档是否完整”。今天分享的这个选题方向——基于Java Spring Boot的Web毕业设计选题管理系统就是这类项目里很典型、也很适合本科阶段去实现的一个。它不是单纯做个CRUD而是把高校毕业设计选题这一整条业务流程搬到了线上老师发题、学生选题、管理员审核、名额冲突处理、改题申请、数据统计。麻雀虽小五脏俱全。这篇内容我会从需求梳理、技术选型、数据库设计、核心模块实现、常见踩坑记录这几个维度把这个系统怎么从零搭起来、每个环节为什么这么做完整拆开讲一遍。无论你是正在纠结毕设选题还是打算拿这套思路去做课设、甚至放到简历上当项目经历都值得花几分钟把这篇文章看完。1. 系统到底要解决什么问题重新梳理一下选题这件事1.1 选题管理的真实痛点是什么很多同学没有在学校里跑过这个流程可能觉得毕业选题不就是老师发个题目学生挑一个就行了吗至于做个系统吗但真实情况比这个复杂得多。一个中等规模的学院每年可能有几十位老师参与带毕业设计每位老师带6到10个学生意味着要发布几十甚至上百个题目。题目不是一次性发完的有些老师会分批次发布有些老师会对题目描述做调整有些老师带的学生人数有上限热门题目可能一瞬间就被抢光。如果纯粹靠人工登记、Excel汇总结果就是学生不知道还剩什么题目可选老师不知道谁选了自己的题教务员要花大量时间做人工协调经常出现「两个人选了同一个题目」「某个老师被选了12个人但上限只有8个」这类混乱情况。所以这个系统的第一个核心价值是把整个选题流程从“线下人工”搬到“线上规范化。系统的核心业务流程是这样的管理员创建用户账号、设置选题时间窗口、维护基础数据。教师在选题时间内发布题目设置可选人数上限。学生在开放时间内浏览题目、提交选题申请。教师审核学生的选题申请通过或退回。学生被退回后可以重新选择其他题目。管理员全程查看进度导出统计报表。这里面最关键的交互关系就是名额和审核状态。题目有人数上限学生申请后要等老师审核审核通过才算真正选上选满之后该题目自动关闭。这一套状态流转如果只在脑子里想可能会觉得简单但真正写代码的时候你会发现状态管理、并发控制、权限校验这些细节才是大头。1.2 三种角色与权限划分这个系统涉及的账号角色是三类管理员、教师、学生。不要小看这个角色划分它是整个系统权限设计的地基。管理员负责系统配置、账号管理、流程监督、数据统计。教师负责发布题目、审核申请、查看自己的选题名单。学生负责浏览题目、提交申请、查看审核结果。在Spring Boot里实现这种多角色权限控制通常用拦截器或者Spring AOP就能解决。核心思路是用户在登录时把角色信息放进Session请求进入Controller之前通过拦截器校验当前Session里的角色是否允许访问该接口。我在实际写这类系统时比较推荐用一个自定义注解RequireRole(ADMIN)标注在Controller方法上拦截器统一做校验。比在每个方法里手写判断要干净得多代码也更好维护。2. 技术选型和架构拆解为什么这套组合最顺手2.1 Spring Boot MySQL MyBatis的搭配考量技术上选什么直接决定了你后面写代码是舒服还是难受。Spring Boot目前已经是Java Web开发的绝对主流。它的自动配置机制极大减少了XML配置量内嵌Tomcat让你本地开发不用单独装Web容器打成一个Jar包就能跑。这对毕设来说太重要了——答辩前部署演示一个Jar包传上去启动就行不用折腾一整天环境。而且从就业角度Spring Boot几乎是Java后端岗位的必备技能写在简历上是加分项。持久层框架用MyBatis原因主要有两点。第一SQL是可控的你可以手写复杂查询比如“统计每个题目的申请人数”“关联查询学生选题记录和题目信息”这类需求写SQL反而比ORM的自动生成更清晰高效。第二它的学习成本低入门比JPA快得多适合在有限时间内完成毕设开发。MySQL就不用多说了免费、稳定、网上教程一抓一大把还不用担心商业授权问题。唯一要提醒的是本地开发建议直接用MySQL 8.0同时把驱动版本和连接串配置对齐后面我会在踩坑部分专门讲版本不匹配的问题。前端部分如果追求效率不建议上前后端分离。让我说实话毕设阶段用Thymeleaf模板引擎、配合Bootstrap做页面一套代码直接搞定前后端工作量要小三分之一。你非要用Vue Spring Boot分离开发也不是不行但要额外处理跨域、Token鉴权、接口联调这些事时间成本真的会高出不少。2.2 分层架构要理清楚项目结构上强烈建议严格分层。我在帮人梳理代码时见过太多“所有代码堆在Controller里”的情况那种代码不是不能跑是你后期改需求、写文档、答辩讲解的时候会非常痛苦。推荐的包结构是这样的com.example.graduation ├── controller // 接收请求返回视图或JSON ├── service // 业务逻辑层处理核心流程 │ └── impl // Service实现类 ├── mapper // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互的数据传输对象 ├── config // 配置类拦截器、WebMvc配置等 ├── common // 通用工具类、统一返回结果封装 └── interceptor // 登录拦截器、权限拦截器Controller只负责参数接收和响应封装Service层写具体的业务判断Mapper层跟数据库打交道。这么做的最大好处在于出问题的时候你知道去哪儿改代码不需要在三百行的Controller里翻。尤其是答辩时老师问你“你这个选题名额超了怎么办”你可以直接说出“在Service层的submitSelection方法里做了容量校验”一听就是有条理的开发者。数据库层面核心表我建议至少设计5张用户表、角色表、题目表、选题记录表、系统参数表。如果还想要更完整的统计功能可以再加一张院系列表或者专业表不过作为毕设来说前面的5张表已经能覆盖全部主要业务流程了。3. 数据库表结构怎么设计这一步千万别偷懒3.1 基础表字段设计思路先看用户表这是三个角色共用的一张表用role字段区分身份。统一设计的好处是登录逻辑只需要查一次表不用三张用户表来回切换。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密存储, real_name VARCHAR(50) COMMENT 真实姓名, role VARCHAR(20) NOT NULL COMMENT 角色ADMIN/TEACHER/STUDENT, department VARCHAR(100) COMMENT 所属院系, major VARCHAR(100) COMMENT 专业, class_name VARCHAR(100) COMMENT 班级, phone VARCHAR(20) COMMENT 联系电话, email VARCHAR(100) COMMENT 邮箱, status TINYINT DEFAULT 1 COMMENT 状态1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );这里要提醒一句密码一定要加密存储千万别用明文。Spring Security的BCryptPasswordEncoder是现成的选择不需要引入完整Spring Security框架单独拿BCrypt的工具类来做加密也能用。答辩时老师极大概率会问密码安全的问题你用了一句“密码通过BCrypt加盐哈希存储”面试观感完全不一样。3.2 题目表和选题记录表是核心题目表的设计要注意三个关键字段max_students表示这个题目最多能选几人selected_count可以冗余记录当前已选人数status表示当前题目的审核状态。为什么要冗余一个selected_count因为统计列表页要显示“已选/总人数”每次都用count(*)实时统计当然也行但列表数据量大时效率会差很多。用冗余字段每次选题成功时事务里把它加一查询的时候直接读就行性能更好。CREATE TABLE topic ( id BIGINT PRIMARY KEY AUTO_INCREMENT, teacher_id BIGINT NOT NULL COMMENT 发布教师ID, title VARCHAR(200) NOT NULL COMMENT 题目名称, description TEXT COMMENT 题目详细描述, requirements TEXT COMMENT 能力要求, max_students INT DEFAULT 1 COMMENT 可选人数上限, selected_count INT DEFAULT 0 COMMENT 当前已选人数, status TINYINT DEFAULT 0 COMMENT 0待审核 1已发布 2已满员 3已下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (teacher_id) REFERENCES sys_user(id) );选题记录表的核心是状态管理。一条记录从学生提交申请那一刻开始经历的状态可能是待审核、已通过、已退回、学生主动取消。注意这里最好有一个is_active字段因为一个学生可能选过多个题目但最终只保留一个有效的我们不能把历史记录删掉要用逻辑状态来管理而不是物理删除。CREATE TABLE selection_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, topic_id BIGINT NOT NULL, student_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL COMMENT 冗余教师ID方便查询, status TINYINT DEFAULT 0 COMMENT 0待审核 1已通过 2已退回 3已取消, apply_reason VARCHAR(500) COMMENT 申请理由, audit_remark VARCHAR(500) COMMENT 审核意见, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME COMMENT 审核时间, UNIQUE KEY uk_student_active (student_id, is_active), FOREIGN KEY (topic_id) REFERENCES topic(id), FOREIGN KEY (student_id) REFERENCES sys_user(id) );这个表设计里有几个细节值得解释。冗余teacher_id字段是为了方便教师查看“我的题目收到的所有申请”时少一次联表查询。每个学生的有效选题只能有一条所以建议加上唯一索引的逻辑实际实现里可以配合is_active字段做部分唯一索引MySQL 8.0支持函数索引很好用。3.3 状态流转把业务规则在数据库层面想清楚再写代码我在给同学们梳理表结构时最常说的一句话是表和表的关系、状态与状态之间的流转决定了你的业务逻辑好不好写。如果这一步偷懒后面写Service会非常痛苦。举例说明一个题目的状态变化是什么教师发布题目时status 0待审核。管理员审核通过后status 1已发布。当已选人数等于上限时自动变成status 2已满员。教师可以主动下架自己的题目status 3。再说选题记录的状态流转学生提交申请生成status 0的记录。教师审核通过status变为1同时topic的selected_count加1。教师审核不通过status变为2学生可以重新申请其他题目。学生在待审核状态下主动取消status变为3。在这个流转过程中最容易出Bug的地方就是第2步“审核通过”和“名额加一”必须是一个原子操作。如果名额加一成功但状态更新失败或者反过来就会出现一个题目显示还能选但实际已经超员了。所以这里必须加事务用Transactional把审核逻辑包起来。4. 核心功能模块怎么实现从登录到选题的完整链路4.1 登录认证与权限拦截登录逻辑其实很简单但有几个细节值得写一下。账号密码从前端提交到ControllerService层根据username查出用户然后用BCrypt校验密码通过后往Session里存用户信息。Service public class UserServiceImpl implements UserService { Autowired private SysUserMapper userMapper; Override public SysUser login(String username, String rawPassword) { SysUser user userMapper.findByUsername(username); if (user null) { throw new RuntimeException(用户不存在); } if (user.getStatus() ! 1) { throw new RuntimeException(账号已被禁用请联系管理员); } BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); if (!encoder.matches(rawPassword, user.getPassword())) { throw new RuntimeException(密码错误); } return user; } }权限拦截用HandlerInterceptor实现。这里要说明一个点Spring Boot里拦截器要做两件事一个是判断用户是否已经登录另一个是判断当前用户角色是否能访问这个接口。比较常见的做法是在自定义注解里声明需要的角色拦截器里通过反射拿到注解信息做判断。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole null) { return true; } SysUser loginUser (SysUser) request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } String[] allowedRoles requireRole.value(); for (String role : allowedRoles) { if (loginUser.getRole().equals(role)) { return true; } } response.setStatus(403); return false; } }4.2 教师发布选题与管理员审核教师登录后在个人中心看到“发布选题”按钮填写题目、描述、人数上限提交后存到topic表状态是待审核。这里注意教师发布题目的界面上应该默认带出教师ID而不是让学生自己填教师是谁。管理员审核列表里会看到待审核的题目点进去查看详情然后选择通过或退回。退回时会在页面上触发填写退回意见的逻辑系统把它存到audit_remark字段里也就是审核意见教师可以在自己的题目列表里看到。这里有个实际开发中的细节管理员审核通过后应不应该给教师发通知如果有站内信模块可以触发一条消息如果没有那么至少题目列表页要用状态标识展示当前审核状态避免教师反复猜测到底过没过审。我在自己的毕设项目里没有做站内信而是做了一行状态提示“审核通过学生可以申请”,虽然不是多高级的功能但体验上比只改数据库状态强得多。4.3 学生选题与并发控制学生进入选题大厅可以看到所有已经审核通过的题目分页展示支持按关键词搜索。点“申请选题”后会弹出确认框要求学生填写申请理由然后提交。后台对应这样一个方法Transactional public void applySelection(Long studentId, Long topicId, String reason) { Topic topic topicMapper.selectById(topicId); if (topic null || topic.getStatus() ! 1) { throw new RuntimeException(该题目当前不可选); } if (topic.getSelectedCount() topic.getMaxStudents()) { throw new RuntimeException(该题目已选满不能再申请); } int currentCount selectionRecordMapper.countActiveByStudent(studentId); if (currentCount 1) { throw new RuntimeException(你已有进行中的选题记录不能重复申请); } SelectionRecord record new SelectionRecord(); record.setTopicId(topicId); record.setStudentId(studentId); record.setTeacherId(topic.getTeacherId()); record.setStatus(0); record.setApplyReason(reason); selectionRecordMapper.insert(record); }逻辑写着简单但有一个经典坑“多人同时申请同一个题目超选了怎么办”。两个学生同时点了申请都读到selected_count是4max_students是5都判断可以申请插入成功后selected_count变成6超员了。解决办法在数据库层面加限制——UPDATE语句带上条件int rows topicMapper.increaseSelectedCountIfAvailable(topicId, maxStudents); if (rows 0) { throw new RuntimeException(手慢了名额已经没了); }对应的SQL是UPDATE topic SET selected_count selected_count 1 WHERE id #{topicId} AND selected_count max_studentsMyBatis的update方法返回受影响的行数如果affected rows为0说明条件不成立名额已经满了。这种CAS风格的更新在并发场景下比重写业务判断靠谱得多也是行业里处理秒杀类需求的基本思路。答辩时把这个点讲清楚老师会认为你真的理解并发。4.4 教师审核与学生改选教师的待办列表里展示所有status0的申请记录。点“通过”时会调一个事务方法更新申请记录状态为1同时把topic的selected_count加一。注意这一步不能直接在Service里写死要看一下当前题目剩余名额是否大于0逻辑跟学生申请那道判断是一致的。学生被退回后原有记录status变为2学生可以重新提交新申请。这里又要防一个Bug——学生之前的记录还在“已通过”状态时能不能再次申请肯定不行。所以学生端每一次提交前必须校验该学生是否存在status1的有效记录。有则不允许重复申请提示先联系老师取消或退还。4.5 管理员后台与统计导出说句实话很多毕设做不出亮点就是有学生选题老师审核然后没下文了。而你只要在管理员后台加一个统计分析模块项目的完整度会提升一个档次。具体做什么三个就够按院系统计选题人数分布。按题目维度统计申请人数和最终选上的人数。导出Excel格式的选题汇总表。Excel导出用Apache POI版本注意和项目依赖对齐我后面会说版本冲突的事。提供一个Controller接口把查询结果写入Workbook再写到响应流前端一个按钮调这个接口就能下载。这个功能在答辩演示的时候非常抓眼球老师会认为你考虑到了实际使用场景而不只是完成了基本功能。5. 实操踩坑记录这些问题我几乎每次都会遇到5.1 MySQL版本和驱动的兼容性问题很多新手用MySQL 8.0但pom里引的驱动还是5.1.x的老版本导致启动时爆NoClassDefFoundError、或连接成功但查询中文乱码。建议直接用mysql-connector-java 8.0.x版本同时连接串加上时区参数spring: datasource: url: jdbc:mysql://localhost:3306/graduation_design?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver这一条建议能帮你省掉一大半的数据库连接报错。5.2 Thymeleaf页面刷新不生效Spring Boot项目改完前端页面默认静态资源有缓存刷新浏览器看不到效果。开发阶段可以在application.yml里关掉缓存spring: thymeleaf: cache: false另外页面里如果用本地js文件建议加个版本号后缀比如/js/app.js?v20240601不然改了js但浏览器还是旧缓存排查起来很浪费时间。5.3 字段命名大小写问题MySQL在Windows上对表名大小写不敏感Linux上敏感。开发时用Windows部署到Linux服务器如果建表时表名是SysUser代码里查sys_user本地没问题服务器上就报表不存在。所以约定俗成所有表名、字段名统一用小写加下划线代码里的实体类属性用驼峰通过MyBatis的map-underscore-to-camel-case配置自动映射不要手动在SQL里给每个字段起别名减少维护成本。5.4 时间字段的显示格式LocalDateTime字段直接序列化返回给前端默认格式是带T的ISO格式比如2024-06-01T10:30:00很多浏览器显示出来非常丑。解决办法是加一个jackson全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT85.5 事务不生效的问题在Service里加Transactional有时候并不生效。最常见的原因是方法被同类内部调用——private方法调用this.xxx()Spring的代理不会拦截这种调用。排查方法检查方法是不是public、入口是不是从外部通过代理对象进来、有没有异常被try-catch吞掉了导致事务无法感知。这个坑如果不提前知道调试会很痛苦。6. 交付物和答辩准备源码之外的事同样重要很多同学以为把代码写完毕设就算大功告成其实不是。毕设的交付物通常不只是源码还包括数据库脚本、操作说明文档、答辩PPT、演示视频。这个项目的数据库脚本要写清楚创建数据库、建表、初始化管理员账号的语句让人拿到手5分钟内能把系统跑起来。我在交付时会额外放一份README.md里面写清环境要求、启动步骤、默认账号密码、常见问题这样无论是老师看还是未来面试官看第一印象都会好很多。答辩的时候要准备一段流畅的演示流程。我的建议是先进管理员端展示用户管理、题目审核、统计报表再切教师端演示发布题目、审核学生申请最后切学生端演示浏览题目、提交申请、查看结果。这条逻辑线把三类角色的核心功能全部串起来老师看完对你的系统就有了整体认知。还有一点代码讲解不要照着Controller从头读到尾而是挑一两个核心难点讲比如并发名额控制、状态流转设计、权限拦截。讲清楚为什么这么设计、解决了什么问题远比把几百行代码念一遍要加分。最后说点实际的。毕设选题系统这类项目技术上不算难但它好就好在业务场景真实、流程完整、可扩展性强。想提升难度你可以加一个消息通知模块或者接入WebSocket做选题状态的实时推送想走数据方向可以在现有数据基础上做选题热度的可视化管理。但作为本科毕设先把核心流程做到逻辑严谨、代码整洁、文档完整就已经是很好的项目了。我最想提醒你的一件事是写代码前一定先把表结构和角色权限梳理清楚。我见过太多同学拿到题目就直接上手写写了一半发现权限校验补不全、状态流转对不上然后又开始重构。磨刀不误砍柴工数据模型是这一类业务系统的地基地基稳了后面的代码怎么都好写。
返回列表