资讯动态

Spring Boot学生选课系统实战:从数据库设计到并发控制

发布时间:2026/10/14 16:37:29 来源:尧图企业网站定制
又是一个被毕设和实训项目反复“光顾”的经典题目基于Spring Boot的学生选课系统。说它经典是因为它几乎覆盖了Java Web开发的核心知识点从权限控制到事务处理从表结构设计到接口分层一套做下来基本就能摸清一个单体业务系统的全貌。网上和各类课程里确实有大量版本但很多要么代码过于陈旧要么只放了源码和SQL脚本文档一笔带过部署时踩坑全靠自己摸索。我前前后后帮人看过、改过、重构过好几套这种选课系统也独立做过一版比较完整的设计这篇就把整个项目的核心拆解和实操记录梳理出来从数据库设计、选课并发控制到前后端联调、部署上线含有源码的模块逻辑和文档组织方式给正在做同题项目或者想学习Spring Boot业务开发的朋友一份可以直接参考的落地方案。先说清楚这套东西解决什么问题。高校教务场景里学生要选课、退课、查成绩老师要维护课程、录入成绩管理员则要管理学生、教师和课程基础数据。一个结构清楚的选课系统就是把这三类角色放到同一个系统里用权限区分开用数据库表和事务逻辑保证选课数据不出错。说白了它就是一个带角色权限、带核心业务流程、带基础CRUD的完整小系统。适合用来做课程设计、毕业设计的底子也适合刚学完Spring Boot基础、想系统做一遍真实项目的开发者参考。1. 项目整体设计与模块拆解1.1 为什么用Spring Boot单体会话模式我见过不少项目一上来就整微服务、Redis缓存、MQ消息队列对选课系统这种场景来说属于典型的大炮打蚊子。选课系统最核心的诉求是业务逻辑清晰、开发效率高、部署运维简单。Spring Boot单体应用在这个量级下优势非常明显。Spring Boot最直观的价值是自动配置和Starter机制。以前用SSM搭项目要写一堆XML配置配置数据源、配置MyBatis扫描、配置事务管理器、配置拦截器。Spring Boot把这些东西大部分通过starter依赖和自动配置抹平了开发者的精力可以集中在业务代码上。对选课系统来说用户角色固定、接口量有限、并发量不大单体完全够用而且排查问题的时候特别直接日志一条线拉下来就能定位。等以后真要扩展了再按模块拆服务也不迟。这个系统采用的分层结构是标准的Controller-Service-Mapper三层外加entity和dto。Controller层只做参数接收和结果封装不写业务逻辑。Service层承载核心业务比如选课事务、权限判断、课程容量扣减。Mapper层负责和数据库打交道。这样做的好处是每个层次的职责单一出了问题或者要加功能直接定位到对应层不会出现“为了查一个选课报错把Controller和Service翻个底朝天”的尴尬局面。还有一点值得提这套项目用了Maven进行依赖管理。建议所有版本依赖都集中到pom.xml里用父级spring-boot-starter-parent统一管理尽量不用手动指定一些互相冲突的版本号。版本冲突是新手最容易踩的坑比如Spring Boot 2.x和3.x的javax与jakarta命名空间差异一旦混用启动直接报错。1.2 核心功能模块与角色权限划分学生选课系统听起来核心就是“选课”但真要落地围绕选课前后还有一堆配套功能。我梳理的需求清单通常包含以下几块学生端功能登录后查看个人课表、浏览可选课程列表、选择课程、退选课程、查看已选课程和成绩。教师端功能登录后维护课程信息新增、修改、设置容量和上课时间、查看选课学生名单、录入和修改成绩。管理员端功能管理学生账号增删改查、管理教师账号、管理课程分类、维护系统基础数据学期、院系、查看整体选课统计。这个权限模型其实就是三张用户基础表学生表、教师表、管理员表。但为了登录鉴权统一通常会额外设计一张用户表或者用角色字段区分。我在设计时采用的方式是学生和教师各持一套账号表管理员单独一套账号表三张表共用一个登录入口登录时先判断用户类型再分别走不同逻辑。这种方式对业务更直观后续统计报表写SQL也简单。权限控制在实现上有两个层面一个是接口层面的权限校验一个是按钮/菜单级的前端显示控制。使用Spring Boot实现时可以引入spring-boot-starter-security或写拦截器HandlerInterceptor来处理。我在做这套系统时考虑到学习成本和代码可读性选了拦截器方式根据请求路径前缀比如/student/**、/teacher/**、/admin/**来校验当前登录用户角色。这种方式足够清晰也容易讲清楚整个过程。如果是追求“更标准”的效果Spring Security的注解式权限控制在业务代码里会更优雅但配置复杂度会高一些可以根据自己的讲解需求选择。1.3 技术选型中容易被忽略的细节再展开说说技术栈里的隐藏知识点。Spring Boot版本选择我习惯用2.7.x系列原因是稳定且社区资料最丰富网上搜问题一搜一个准。JDK版本对应也建议用JDK 8或11不要一上来上JDK 17除非你能确定所有依赖都兼容。前端页面这块这套项目我用的是模板引擎Thymeleaf配合Bootstrap框架做界面。为什么不用前后端分离的Vue原因很简单单体项目里Thymeleaf直接在服务端渲染部署时一个jar包打完不用再单独处理前端打包产物对课程设计和毕设来说演示过程更顺畅。如果你确实想用前后端分离那建议把后端做成纯RESTful API前端单独用Vue工程但部署时就要处理跨域和静态资源分离问题工作量会大不少。数据库用的MySQL 8.0。连接配置里有一点要注意serverTimezoneAsia/Shanghai必须加上否则日期字段在插入和查询时会和本地时间差8个小时。这个坑我见太多了明明数据库里存的是9点页面显示变成17点排查半天才发现是连接串没有指定时区。2. 数据库设计与核心表结构2.1 表设计的原则与核心表分析选课系统的数据库设计是整个项目的地基。表设计得好后续写SQL和业务逻辑都顺畅。我总结的核心表有这几张学生表、教师表、课程表、选课记录表、成绩表、学期表、用户登录表。课程表的关键字段包括课程编号、课程名称、授课教师ID、课程容量、已选人数、上课时间、上课地点、学分、课程分类、学期ID。选课记录表的核心字段包括选课记录ID、学生ID、课程ID、选课时间、选课状态。这里有一个设计上的重点已选人数这个字段要不要冗余在课程表里我强烈建议要。很多新手会问为什么不直接select count(*)在选课表里数一下确实可以但每节课程页面的访问都要做一次count数据量大了以后会很吃力更重要的是选课操作时要判断“是否还有容量”在课程表的字段上做比较和更新比先count再判断要高效得多也更容易通过一条SQL实现原子性扣减。选课记录表还需要加一个唯一约束UNIQUE KEY uk_student_course (student_id, course_id)保证同一个学生同一门课只能选一次。这个约束是在数据库层面做的最后一道防线业务层即使写漏了数据库也不会允许重复数据写入。2.2 各表字段设计与建表要点建表过程中有几个具体的字段设计细节值得拿出来聊。先看建表SQL截取核心CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher_id BIGINT NOT NULL COMMENT 授课教师ID, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT NOT NULL DEFAULT 60 COMMENT 课程容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, schedule_time VARCHAR(100) COMMENT 上课时间, location VARCHAR(100) COMMENT 上课地点, semester_id BIGINT NOT NULL COMMENT 所属学期ID, status TINYINT DEFAULT 1 COMMENT 状态1-可选0-已关闭, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_teacher (teacher_id), KEY idx_semester (semester_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里把capacity和selected_count都设为NOT NULL并给默认值目的就是避免选课逻辑里出现空值导致的NPE。schedule_time我用了字符串类型存的是“周一 3-4节”这种人类可读的格式对课程时间冲突判断来说够用而且展示方便。选课记录表的设计CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1-已选0-已退选, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩教师录入, UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;特别注意这个status字段与唯一索引的关系。选了同一门课再退选然后再选同一门课这样就会产生两条“曾经选过”的记录如果唯一索引建立在(student_id, course_id)上第二次选课就会违反约束。解决的方案有两种第一种退选时物理删除记录这样唯一索引不会受影响第二种保留记录并更新状态但把唯一索引改成(student_id, course_id, status)加上一个细节上的妥协。实际业务中学生的选课历史是有保留价值的所以我偏向保留记录但把唯一索引设计成针对“有效选课记录”的逻辑通过查询时限制status1来配合同时物理删除会被作为退选的兜底方式。这里值得你在做表设计时好好想清楚能体现出对真实场景的理解。2.3 数据初始化的准备与注意事项源码里通常附带一份init.sql里面包含建库、建表、初始数据。初始数据一般要有几类管理员账号admin/admin123方便登录后直接进入管理端。5~10个测试学生账号密码统一为123456身份证或学号做账号。5个左右教师账号关联课程使用。10~20门覆盖不同分类的课程最好分布在上午、下午、晚上不同时段便于测试选课冲突逻辑。初始化数据不要太少也不能太多。太少了整个系统跑起来像是空架子演示不直观。太多了测试选课功能时列表翻页都要翻很久。我个人经验是每张业务表初始化15~20条左右就非常合适。另外初始化SQL里建议加上SET FOREIGN_KEY_CHECKS 0;和SET FOREIGN_KEY_CHECKS 1;包裹体避免因为删表顺序问题导致导入失败。用DROP TABLE IF EXISTS写在前面保证脚本可以重复执行这对课程设计和毕设答辩前的多次重置数据非常有用直接跑一遍脚本就能恢复初始状态。3. 核心功能代码实现与选课逻辑3.1 选课主流程的完整逻辑拆解选课是这个系统的灵魂功能也是最容易出问题的地方。正常情况下选课的基本动作包含学生登录、浏览课程列表、点击选课、系统校验、写入选课记录、更新课程已选人数。看似简单但要把“正确性”做到位有几个环节不能省。一个完整的选课校验流程应该是参数校验课程ID不为空、学生已登录这些基本校验放在Controller参数里处理。课程存在性检查通过ID查出课程不存在则提示“课程不存在”。状态检查课程状态必须为可选不是可选则提示“课程已关闭”。容量检查已选人数小于容量等于或大于则提示“课程已满”。冲突检查检查该学生的课程表里是否已有同一时间段的其他课程有则提示“上课时间冲突”。重复选课检查查询选课记录表若已存在有效选课记录则提示“请勿重复选课”。事务性写入插入选课记录同时更新课程已选人数1。第7步必须放在一个数据库事务里保证两步操作要么全部成功要么全部失败。如果中途异常事务回滚不会出现“选课记录写了但人数没加”或者“人数加了但记录没有”的数据不一致问题。为了更贴近真实场景我在实现时把上述核心逻辑放在了Service方法里并且用Transactional注解标记。方法内部的代码顺序很重要先做所有查询校验最后才执行插入和更新尽量减少事务占用的时间这在并发情况下的表现会更好。3.2 并发控制与防超卖设计选课系统最常见的教学追问是你怎么保证多人同时选最后一门课时不会超卖对应的就是电商的“库存超卖”问题。实际上一个这么量级的单体系统在几十到几百人同时抢同一门课时不加任何措施也确实会有超卖风险。原因在于并发时多个请求同时读到selected_count59然后都去执行“加1”最终课程容量明明是60却有65人选上了。解决思路从简单到复杂有几种第一种方案把容量判断和更新合并成一条SQL。这条SQL就是核心。UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count capacity然后检查受影响行数如果为0说明更新失败即没有容量了事务回滚返回“课程已满”。这个方案思路清晰代码改动小而且天然防并发超卖因为数据库的行锁保证了同一时刻只有一个事务能成功执行这个UPDATE。第二种方案悲观锁。在查询课程时加上FOR UPDATE把数据行锁住其他事务要等当前事务提交后才能继续执行。这种方式控制粒度最细但会有锁等待问题并发量大时性能会下降。第三种方案乐观锁。在课程表加一个version字段更新时比较版本号。适合冲突率低的场景但选课高峰期容易造成大量无效重试。我在项目里用的是第一种方案这也是我见过的高校课程设计和真实业务里最常用的方案。它对并发量要求不高的单体系统来说完全够用也好向别人解释。关键在于校验容量的重点已经从“先查后写”的流程式校验改成了“写时校验”的原子性操作。课程时间冲突的检查也存在并发问题但考虑到冲突检查是基于学生已有选课记录的单个学生的并发选课操作频率极低所以只需要在事务里做普通查询判断即可。如果你真的想做到万无一失极端情况下可以对这个学生的记录行加锁但实际项目里很少需要如此苛刻。做项目讲究的是把资源和代码用在正确的场景上而不是为几乎不会发生的极端事件过度设计。3.3 代码分层实现与关键接口写核心代码的时候接口切分要遵循RESTful风格路径命名清晰让前端调用和后端维护都省心。我定义的主要接口如下模块接口路径请求方式说明课程/course/listGET分页查询课程列表课程/course/detail/{id}GET课程详情与剩余容量选课/selection/selectPOST提交选课选课/selection/cancelPOST退选课程选课/selection/myGET我的已选课程列表成绩/score/listGET学生查询成绩成绩/teacher/score/updatePOST教师录入/修改成绩登录/loginPOST登录入口Controller层统一用Result对象封装返回结构类似public class ResultT { private Integer code; private String message; private T data; }前端或调用方通过code判断请求是否成功比如200成功500失败400参数错误。这样处理好处是不需要返回HTTP层那种晦涩的状态码业务层面的错误信息也统一。Service层里选课的事务方法核心逻辑伪代码如下Transactional(rollbackFor Exception.class) public ResultVoid selectCourse(Long studentId, Long courseId) { Course course courseMapper.selectById(courseId); if (course null) { return Result.error(课程不存在); } if (course.getStatus() ! 1) { return Result.error(课程已关闭); } // 校验是否已选 Integer count selectionMapper.countValid(studentId, courseId); if (count ! null count 0) { return Result.error(请勿重复选课); } // 校验时间冲突 ListCourse myCourses courseMapper.selectCoursesByStudentAndSemester(studentId, course.getSemesterId()); for (Course myCourse : myCourses) { if (hasTimeConflict(myCourse.getScheduleTime(), course.getScheduleTime())) { return Result.error(上课时间冲突); } } // 容量原子更新 int rows courseMapper.increaseSelectedCount(courseId, course.getCapacity()); if (rows 0) { return Result.error(课程已满); } // 插入选课记录 CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setStatus(1); selectionMapper.insert(selection); return Result.success(); }注意这里有个细节increaseSelectedCount更新的courseId时即使选课记录插入失败导致整体回滚更新操作也一并回滚所以不用担心。事务把两步绑定成原子操作之后就算插入记录时数据库突然报错已选人数也不会被改坏。3.4 退课与成绩录入的实现细节退课的逻辑比选课简单一些但也有一些边界要考虑。我的实现是先查询选课记录确认状态是已选再更新状态为0同时把课程表的selected_count减1。有些系统会在这里判断“选课后超过X小时不能退课”或“开课后不能退课”这个就看你的业务需求了。教务场景里通常有具体的退课规则我当时在这个项目里加了一个简单的限制课程状态为“已开课”的课程不允许退选避免课程已结束还要处理退选导致的成绩和人数不一致。成绩录入模块老师的操作入口在教师端课程管理页面里可以进入一门课并看到所有选了该课的学生列表然后逐条录入分数。成绩字段放在选课记录表里有它的好处一位学生同一门课只对应一条选课记录可能有多条历史退选记录教师录入时定位到特定的course_selection记录即可。成绩的校验规则也要注意分数范围0~100小数最多1位录入或修改时直接在前端和后端都做校验。前端校验提升用户体验后端校验保证数据绝对安全。有些人只做了后端校验结果用户提交非法数据时前端没有即时提示体验很生硬只做前端校验更危险懂行的人绕过前端直接发请求就能写入脏数据。4. 权限控制、登录鉴权与安全设计4.1 登录状态管理与密码加密方案登录模块很基础但我把它单独拿出来说是因为很多人在这个看似简单的环节上犯过低级错误。比如密码明文存储无论是教学项目还是演示项目这都不是好习惯。密码加密我选择了BCrypt算法Spring Security自带的BCryptPasswordEncoder可以直接用。如果你不想引入Spring Security全家桶也可以单独引入spring-security-crypto依赖只使用它的加密工具类。BCrypt的好处是每次加密的盐值随机同样的密码两次加密后的密文不同彩虹表攻击对它基本无效。登录成功的会话管理用HttpSession存用户对象就够用了关键是存的对象要选好。不要直接存整个学生或教师对象因为里面可能包含敏感字段比如手机号、邮箱。建议只存一个LoginUser对象包含用户ID、用户名、真实姓名、角色类型这几个必要字段。展示当前登录人的信息时需要更多数据再按ID重新查询。退出登录直接session.invalidate()这一步千万不要省略成单纯的跳转页面否则会话还在后端保持别人用同一浏览器就能直接访问你登录过的接口。4.2 基于拦截器的统一权限校验这部分的实现要覆盖两个层面的问题一是未登录用户不能访问内部接口二是低权限用户不能越权访问高权限接口。我的做法是定义一个LoginInterceptor在preHandle方法里读取当前请求的Session判断用户是否为空。为空则直接重定向到登录页或者返回统一的未登录JSON提示。然后通过请求路径的前缀做角色匹配/admin/**需要角色为管理员/teacher/**需要角色为教师/student/**需要角色为学生/course/list这类公共接口对所有登录用户开放注册拦截器的时候注意放行路径的设置登录接口、静态资源、错误页面都要放行。这个细节很容易被忽略很多人拦截器写好了结果页面的CSS、JS加载不了表单提交也因为静态资源被拦而出问题。拦截器代码里还有一个实用技巧在postHandle方法里记录请求的处理耗时。给每个请求加上X-Process-Time开发时能快速定位到慢接口。当然这个只是在开发调试时用生产环境一般会接更完整的监控系统来统计。4.3 越权操作的防护思路权限校验做完了还要考虑越权问题。什么意思一个学生登录后理论上他可以手动构造请求去调教师的成绩录入接口。如果后端只做了“必须是教师角色”的校验没做“这个教师能不能给这门课录成绩”的数据级权限校验就会出现越权。所以我在Service层加了一层校验逻辑教师录入成绩时先根据teacher_id和course_id查课程表确认这门课确实是当前教师负责的才允许操作。同理学生查询成绩时只能查到自己的成绩不能在请求参数里传一个别人的student_id就把别人成绩拉出来。越权防护是很多新手项目忽略的硬伤也是评审老师或者面试官最爱追问的点。建议你在写代码时形成习惯每个查询和操作除了做身份认证还要做数据归属校验。这种“数据级权限”的概念体现在代码里项目的安全设计就超过大多数同类作业了。5. 页面实现与前后端交互要点5.1 Thymeleaf模板引擎的实际运用使用Thymeleaf作为页面层时有几个高频用法和易错点需要重点说明。一是th:each循环渲染列表tr th:eachcourse : ${courseList} td th:text${course.courseName}/td td th:text${course.teacherName}/td td th:text${course.capacity - course.selectedCount}/td td a th:href{/selection/select(courseId${course.id})} th:if${course.selectedCount course.capacity} classbtn btn-sm btn-primary选课/a span th:if${course.selectedCount course.capacity} classtext-danger已满/span /td /tr列表接口返回的VO对象中建议把展示需要的字段都放进去比如teacherName、selectedCount而不是在页面上通过course.teacher.name层层取对象。虽然Thymeleaf支持OGNL表达式取嵌套属性但有些懒人把整个Teacher对象塞进去前端页面只取一个name数据传输量有些冗余。把常用字段复合成一个CourseVO页面代码干净传参也更明确。二是表单提交的CSRF问题。使用Thymeleaf时如果引入Spring Security的CSRF保护需要在表单里加上input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} /忘了加这个所有的POST表单提交都会得到403。这个坑我印象很深第一次集成Security的时候排查了很久才发现是CSRF令牌缺失。5.2 分页查询与条件搜索课程列表页上除了全量展示还要支持按课程名称模糊搜索、按课程类别筛选。分页我用的是PageHelper插件配置简单使用方便。一个容易被忽视的坑是PageHelper.startPage()只会作用于紧接着的第一个查询语句。如果你的Mapper方法里先执行了一个其他查询再做真正的列表查询分页就会失效。所以写Service时要确保startPage紧跟在对应查询方法前一行调用。分页信息和列表数据用PageInfo封装返回前端取数据时直接遍历list分页参数用pageNum、pageSize、total。Bootstrap的pagination组件可以直接用这些参数渲染成页码。5.3 前端交互与提示信息的统一页面上的操作反馈要统一。选课成功还是失败都要有明确的提示不要只是跳转或者刷新页面。我的做法是在Controller里把操作结果封装成Result如果操作失败携带错误信息重定向到原来的列表页同时在重定向参数里带上错误消息页面顶部统一显示。Thymeleaf里读取重定向参数可以用${param.msg}或${session.msg}。如果有多个提示类型比如成功、失败、警告可以统一用alert alert-success、alert alert-danger、alert alert-warning来渲染。交互设计上还有一个细节选课按钮的点击要防止重复提交。在页面点击选课后按钮立即置灰并且显示“处理中”。快速双击按钮在真实场景里太常见了如果不做按钮防抖用户一激动点了两次系统就会触发两遍选课逻辑。虽然数据库唯一索引能拦截住重复选课但每一次失败都会给用户弹一次“请勿重复选课”的报错体验非常差。6. 部署运行与完整文档组织6.1 本地环境搭建与启动步骤这套系统要跑起来前提条件不复杂JDK 8、Maven 3.6、MySQL 5.7/8.0、IDEA或者Eclipse。启动步骤我整理成了一份清单照着做基本不会出问题创建数据库执行init.sql脚本建库course_selection_db导入表和初始数据。修改配置按你自己的MySQL账号密码去调整application.yml里的数据源配置。启动项目在IDEA里直接运行主类或者用mvn spring-boot:run。访问系统浏览器打开http://localhost:8080默认端口可以在配置文件里改。启动过程如果报错九成以上是三个原因数据库账号密码不对、MySQL版本兼容问题、端口被占用。MySQL 8.0以上的驱动类名是com.mysql.cj.jdbc.Driver老驱动才是com.mysql.jdbc.Driver配置里写错也会报错。6.2 常见启动异常与处理方案我整理了一份启动和运行过程中的常见问题速查表每一条都是实测遇到的现象可能原因处理办法启动直接报Failed to configure a DataSource数据源配置错误或未配置检查application.yml的url/username/password访问页面报Whitelabel Error Page路径映射错误或Controller未注册检查Controller/RequestMapping注解中文乱码数据库连接未指定编码连接串加characterEncodingutf8mb4页码参数解析报错pageNum不是数字前端传参与后端类型保持一致图片/样式加载不出来静态资源路径被拦截器拦截放行/static/**、/css/**、/js/**点击选课无反应表单提交CSRF校验失败检查是否漏加CSRF隐藏域事务回滚不生效Service层方法被同类调用自调用不走代理确保跨Bean调用或用AopContext第7条多说一句。Transactional注解是依赖Spring的AOP代理实现的当你在同一个类里写一个方法内部直接this.selectCourse()调用另一个带事务注解的方法时这个调用不会经过Spring的代理对象事务就会失效。新手改代码时很容易踩这个坑把原本分开的Service方法合并了一下事务就静默失效了选课记录和人数更新就会在多步操作中途报错时留下半截数据。解决方法很简单确保事务方法的调用发生在不同的Bean之间或者把目标方法放到另一个Service里。6.3 文档组织与答辩讲解思路这套项目配套的文档算不上多厚但组织方式很有讲究。我的文档结构分四部分每一部分都有明确的定位。第一部分是系统概述交代背景、开发环境、技术栈、功能清单解释系统解决了什么问题。注意不要长篇大论抄需求文档两三页讲清楚即可。第二部分是需求分析包含用例图和流程说明。重点描述三类角色各自能做什么以及核心流程选课、退课、成绩录入的步骤顺序。建议画业务流程图辅助说明。如果文档中有画图使用可以嵌入Markdown的图片路径或draw.io导出的PNG不要用Mermaid因为有些评审工具不支持渲染。第三部分是数据库设计列出所有表的字段说明、ER关系、建表SQL重点说明核心表之间的关联关系。这一部分是评审老师看的最多的一定要把每张表的作用讲清楚特别是course_selection表为什么是链接学生和课程的桥梁表。第四部分是系统实现挑核心功能讲代码思路和关键方法展示操作截图和测试结果。不需要贴所有代码只贴几个代表性方法比如selectCourse、拦截器校验、分页查询。配图比代码更重要界面截图结果截图能让讲解生动很多。讲解或答辩的时候建议按这个顺序展开先讲清楚角色和业务场景再演示主要功能最后重点讲解选课逻辑的边界处理和并发控制方案。选课逻辑是这个项目里最能体现思考深度的部分花最多时间在上面是值得的。6.4 项目扩展方向的思考这个项目跑通之后如果想继续深挖或者丰富内容有几个方向性价比很高第一个是引入Redis缓存课程列表。高频访问的课程列表接口通过缓存可以明显降低数据库压力。同时Redis还可以用来做分布式锁替换掉之前单库场景下简单SQL的防超卖方案。不过注意项目里有分布式锁的应用只适用于真正部署多实例的场景单机部署时不用勉强加这个复杂度。第二个是增加选课时间批次控制。真实教务场景中不同年级、不同院系会分批次开放选课。在课程表或学期表上增加“选课开始时间”“选课结束时间”字段选课时校验当前时间是否在批次窗口内。第三个是增加统计报表。按课程、按院系统计选课人数和选课率用ECharts展示柱状图和饼图项目演示效果会立体很多。ECharts静态引入js方式对Thymeleaf很友好几行代码就能画出好看的图表。第四个是引入消息通知。选课成功后给学生的站内消息里推一条通知课程容量剩余不足时给管理员发消息。这样系统就多了事件驱动的影子扩展性上的话题性也更足。我个人在实际动手中的体会是做选课系统这类项目最大的收获不是把CRUD写完而是把“数据一致性”和“用户权限”这两条线彻底想明白。把这两条线想透了以后做任何管理系统都会顺手很多。最后再分享一个小技巧编码时先写Service层的核心逻辑再用接口测试工具把所有情况跑一遍最后再写Controller和页面。这样代码结构会非常干净调试时也不会被前端代码干扰。这套项目做完源码、数据库、文档一整套是齐的可以直接跑也可以在此基础上按自己的需求加功能希望对正在做同题项目的朋友有所帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑