尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot + Vue.js + MySQL 高校选课系统开发实战与避坑指南

SpringBoot + Vue.js + MySQL 高校选课系统开发实战与避坑指南 选课系统这个选题几乎所有做毕设的同学都绕不开。业务量级不大不小角色清晰流程完整而且前后端都能充分展示用来应付毕业设计答辩或者课程设计验收非常稳妥。但问题也恰恰出在“稳妥”上——代码烂大街的太多如果你的项目从数据库到权限到并发处理都能讲出点门道答辩时的效果完全不一样。这篇就聊聊我用 SpringBoot Vue.js Java MySQL 从零搭一套高校学生选课系统管理平台的完整经验重点是那些容易被忽略但一讲就加分的细节以及我踩过的坑。跟大多数同学一样我先想清楚一个问题这套系统要交付给谁用用到什么程度。选课系统的核心用户是三个角色——管理员、教师、学生。管理员管课程和基础数据教师管开课和成绩录入学生负责选课退课查成绩。这个边界听起来简单但实际建模时很多细节需要反复推敲比如“课程容量”和“已选人数”怎么保持一致“选课冲突”到底靠什么机制拦截。这些问题如果看源码时没想透答辩被老师追问到就很容易卡住。1. 为什么选课系统适合作为毕设题目技术选型背后的逻辑1.1 选课系统的业务复杂度其实拿捏得很准选课系统的业务复杂度很微妙——它比纯增删改查的图书管理难一点但又不像电商秒杀那么高并发。这种“中间地带”特别适合作为毕设够你展示技术深度又不会把自己绕晕在业务细节里。具体来说选课系统包含的东西不少多角色权限管理、课程与选课数据的完整性约束、选课时间窗口的状态控制、不同时段的冲突检测还有成绩录入与统计展示。这些功能模块拆给三个角色后前后端加起来大概有大几十个页面/接口的规模工作量刚好卡在一个人能完成、又能写满毕业论文的区间。更难得的是选课系统天然带“状态”和“截止时间”的概念。这促使你在设计时必须考虑时间维度怎么处理——是前端禁用按钮还是后端直接拒绝选课请求还是两者同时校验这些问题一旦展开项目的深度就比“简单增删改查”高出了一个档次。1.2 SpringBoot Vue.js 这套组合经验的验证选这套组合并不是因为它是最先进的而是因为它最适合“一个人独立完成答辩展示”的场景。后端选 SpringBoot理由很直接Java 生态的资料多、社区成熟SpringBoot 的自动装配和 starter 机制让项目初始化成本非常低。哪怕你以前只写过 JavaWeb 和 Servlet上手 SpringBoot 也就是一两周的事而且它能直接暴露你对依赖注入、事务管理、拦截器这套核心机制的理解答辩时能聊的内容很多。再加上 Java 本身是主流开发语言毕业后找 Java 开发工程师相关工作也用得上属于“毕设顺便积累面试素材”。前端选 Vue.js 也是同样的逻辑。Vue 上手曲线平滑组件化思路直观不用太挣扎就能写出可交互的界面。配合 Element Plus 一类的组件库学生端选课页面、教师端成绩录入表格、管理端课程管理列表都能快速搭建得像个正经系统。对非前端专业的同学来说Vue 比 React 更友好也比单纯用 JSP 模板渲染更配得上“前后端分离”这个答辩关键词。数据库自然就是 MySQL。这个不用纠结SpringBoot 对 MySQL 的整合是最顺滑的资料铺天盖地遇到连接问题、乱码问题、ssl 报错都能快速搜到解决方案。我使用的是 MySQL 8.x 版本配 mysql-connector-j驱动版本保持一致基本就没什么坑。1.3 加分的关键点到底是“能跑”还是“能讲清楚”很多同学做毕设只追求“能跑通”但答辩老师看重的往往是你对这个系统的理解深度。我自己的经验是把下面几个问题想清楚答辩就很稳为什么课程表要有teacher_id外键而不直接存教师名字字符串选课接口用什么保证同一个学生不会重复选同一门课课程人数满了以后前端展示的已选人数和后端实际入库的数据如何保持一致选课时间截止后学生强行用工具发请求会不会被后端拦截这些问题在本文后面的章节都会逐步拆解。每回答一个你的项目就从“演示品”变成了“设计品”。2. 系统功能边界与角色权限拆解2.1 三个角色的核心业务流我习惯把整套系统的功能画成三张角色清单再动手写代码这样前后端做起来都不会乱。管理员端的核心动作是维护院系信息、维护教师账号、维护学生账号、审核课程开课申请、设置学期选课时间窗口。其中“设置选课时间窗口”是管理员端最容易出彩的功能因为你要用一个全局配置表来控制选课的开启和关闭这个业务逻辑本身就有讲究。教师端则是提交开课申请、查看自己课程的学生名单、录入成绩。注意教师不能自己创建课程直接上线要走管理员审核——这个流程增加了一层真实感也平添了一次外键关联的机会写论文时“业务流程分析”这一章会好写很多。学生端是查看可选课程、选课、退课、查看自己的课表、查成绩。这里有个细节很关键学生选课时看到的是“还有空位”的课程已经满的课程前端要置灰或标满但后端选课接口同样要再校验一次容量防止极端并发下把课程选爆。另外三端共用的模块还包括登录认证和统一权限拦截。我使用的方案是 JWTJSON Web Token做无状态认证登录成功后由后端签发 token前端存储到 localStorage 并在 axios 拦截器中附加到请求头。后端用 HandlerInterceptor 统一校验 token并基于角色判断接口访问权限。2.2 权限控制要怎么做才能不像玩具项目一个很常见的反面案例是前端判断用户角色然后隐藏按钮后端接口则完全不设防。平时演示没问题但老师一句“我用 Postman 直接调用你的删除接口能删掉吗”就能把你问住。所以我坚持后端权限必须可验证。实现上我在 SpringBoot 里写了一个基于拦截器的权限控制方案不用引入 Spring Security毕设阶段引入它会显著增加学习成本和配置复杂度反而容易被纠缠在细节里。具体的做法是用户登录时后端生成 JWT并把角色信息如ROLE_STUDENT写入 token拦截器解析 token把用户信息放入ThreadLocal或请求属性中定义接口时用自定义注解标记接口需要的角色拦截器里做匹配校验。为了演示方便我会按角色划分接口路径前缀比如/api/student/**、/api/teacher/**、/api/admin/**然后按前缀做统一拦截校验。同时每个接口内部也尽量再做一次身份与数据归属校验比如学生A只能查自己的选课记录不能把选课接口里的 studentId 改成学生B的——这就是防越权也是完整性约束的一部分。前端也不能完全不设防但前端的意义主要在体验而非安全。Vue Router 配置全局前置守卫从 localStorage 取 token 和角色信息没有 token 直接跳转到 /login角色不匹配则跳转对应角色的首页并给出提示。这样用户不会白点一遍按钮再收到接口报错。2.3 业务状态的流转规则选课系统的业务状态最核心的就是选课窗口。我设计了一张系统配置表sys_config里面存semester、start_time、end_time等字段。选课接口在被调用时第一时间读取配置判断当前时间是否在窗口内不在窗口内直接抛出业务异常“当前不在选课时间内”。这个设计有一个值得答辩时提的点配置读取有一个很小的性能开销但换来的是灵活性——管理员改配置无需重启服务也无需改代码。每次请求都查库虽然多一次 IO但这种系统的并发量根本谈不上性能瓶颈把状态控制放在后端统一收敛是正确取舍。退课也有状态控制。学生退课不能没有限制地退比如选课窗口结束后退课应当被禁止否则教师已经录了成绩学生还能退课数据就乱套了。我用一个简单的规则做约束退课的允许条件和选课的允许条件一致即窗口开启时间内可以退课。这个规则并不复杂但能引出“为什么前后端都要校验”的讨论答辩老师通常吃这一套。3. 数据库设计里那些必须想清楚的细节3.1 核心表结构与字段设计数据库设计是整个项目的地基。我建表的时候反复调整过几次最后落地的核心表大致如下表名用途关键字段sys_user用户表学生/教师/管理员统一账号id, username, password, role, statusstudent学生扩展信息id, user_id, student_no, name, major, gradeteacher教师扩展信息id, user_id, teacher_no, name, titlecourse课程表id, course_name, course_code, credit, teacher_id, capacity, selected_count, week_day, start_section, end_section, semestercourse_selection选课记录表id, student_id, course_id, select_time, statussys_config系统配置表id, config_key, config_value, description学生和教师字段为什么要单独拆一张扩展表很多同学图省事直接在sys_user表里放学生号、专业、职称。这样其实也能跑但一个sys_user表既要有student_no又要空着teacher_no的同类字段数据库的范式设计会很难看答辩提到数据库规范化的时候容易露怯。拆开后sys_user只负责认证和角色student和teacher负责业务扩展信息关联关系用user_id外键连接逻辑清晰也符合“一个角色一套扩展属性”的设计预期。课程表是整个系统里查询最多的表。teacher_id外键关联教师信息是为了保证开课信息的一致性——如果直接存教师名字字符串教师改了姓名或系统里删除了教师记录课程数据就变成脏数据了。外键约束虽然对性能稍有影响但这个体量完全可以忽略得到的收益是数据完整性有保障。3.2 选课冲突与超选的防线选课冲突有两类时间冲突和容量冲突。时间冲突是指同一学生的两门课上课时间重叠。光靠前端判断是不够的所以我在后端选课接口里做了这么一步查询该学生所有已选课程的上课时间与准备选的课程做时段重叠判断。这里的核心字段是week_day星期几、start_section第几节开始、end_section第几节结束用区间重叠公式判断即可也就是 newStart oldEnd newEnd oldStart。容量冲突则需要靠并发控制。想象一下课程容量是 40 人当前已选 39 人两个学生同时点击选课理论上第 40 和第 41 个请求都可能通过“已选人数 容量”的检查最终课程被选到 41 人。这个问题怎么解决我的方案是分两层数据库层增加唯一约束uk_student_course(student_id, course_id)确保同一个学生不能重复选同一门课然后选课接口使用数据库行级锁SELECT ... FOR UPDATE锁定课程行在锁定状态下判断selected_count capacity并更新selected_count。这样并发请求会串行化处理超选问题从根上解决。你可能会问用乐观锁加版本号是不是也可以可以但选课场景下悲观锁更直观也更容易在答辩时讲清楚。选择悲观锁牺牲了一点并发性能但这个系统的业务峰值可能只是全班几十个人同时选课悲观锁完全扛得住。3.3 冗余字段与查询性能的平衡course.selected_count就是一个典型的冗余字段。它其实可以从course_selection表里 count 出来但每次查询课程列表都要统计选课人数SQL 又复杂展示又慢。所以我直接在课程表里冗余了一个selected_count每次选课成功 1退课成功 -1。这里要特别注意冗余字段的更新必须和业务操作在同一个事务里完成否则就会出现选课记录插入了但数量没更新上或者反过来。我用Transactional保证原子性数据库层面的事务隔离级别用默认的REPEATABLE_READ即可——至少在 MySQL 8 的 InnoDB 引擎下配合行锁不会出现幻读问题。另外一个小心思是课程表里我存了week_day、start_section、end_section而不是直接存一个“星期几第几节”的字符串。买这个细节是为了方便写冲突检测 SQL 时做数值比较——存成整数区间重叠公式才能高效计算。4. 后端核心接口的落地方式4.1 选课接口的完整事务实现选课接口是整套系统的灵魂也是答辩老师最喜欢追问的地方。我贴出核心逻辑的伪代码结构帮助理解整体流程public Result selectCourse(Long studentId, Long courseId) { // 1. 校验选课时间窗口 SysConfig config sysConfigMapper.selectByKey(SELECT_WINDOW); if (now before config.startTime || now after config.endTime) { throw new BusinessException(当前不在选课时间内); } // 2. 查询课程并锁定行 Course course courseMapper.selectByIdForUpdate(courseId); if (course null) { throw new BusinessException(课程不存在); } // 3. 校验课程容量 if (course.getSelectedCount() course.getCapacity()) { throw new BusinessException(该课程已满); } // 4. 校验时间冲突 ListCourse selectedCourses courseMapper.selectByStudentId(studentId); for (Course c : selectedCourses) { if (timeOverlap(c, course)) { throw new BusinessException(与课程[ c.getCourseName() ]上课时间冲突); } } // 5. 插入选课记录并更新数量 CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setSelectTime(new Date()); courseSelectionMapper.insert(selection); courseMapper.increaseSelectedCount(courseId); return Result.success(选课成功); }这个接口里最关键的一行是第 2 步的selectByIdForUpdate。它会在数据库层面给这条课程记录加排他锁让其他同时处理该课程选课的事务进入等待状态。虽然对性能有一点影响但在毕设这个体量下完全没问题——你反而能详细讲出“行锁避免超卖”这个经典方案这是面试和答辩都非常青睐的考点。4.2 退课与成绩录入的前后端一致性退课接口也值得琢磨。它的核心逻辑是校验当前仍处于选课窗口期内然后删除选课记录同时把课程的selected_count减一其他同学的选课记录不受影响。这里有个细节删除选课记录后如果学生想再次选同一门课理论上也不会受唯一约束拦截了——因为旧的选课记录已经删掉新插入的记录和旧记录并没有同时存在。这个行为符合预期但要确保删除与减数在同一事务内。教师录入成绩是另一个完整性的考验。教师录入成绩本质是对一个课程下的所有选课记录做批量更新。我是按course_id查选课记录然后逐条回填成绩字段。成绩也是要加状态约束的只有选课状态为“已选”的才能录成绩已退课的记录直接过滤掉避免教师录到已经退课学生的成绩。4.3 通用返回体和全局异常处理的细节为了让前后端对接顺畅我写了一套统一的返回体ResultTpublic class ResultT { private Integer code; // 200 成功400 业务异常500 系统异常 private String message; private T data; }业务异常用一个自定义BusinessException配合RestControllerAdvice全局异常处理器把所有的 IllegalArgumentException、SQLException、Exception 统一转换成标准返回结构。这个设计的妙处在于前端 axios 拦截器只需要判断res.code 200即可进入成功分支其余全部进入错误提示分支。整个系统的错误处理逻辑非常干净后续加功能也不需要到处 try-catch。密码存储我建议用 BCrypt 哈希而不是明文。虽然毕设阶段不搞 HTTPS、不搞对公网暴露但良好的安全习惯本身就能写进论文里的“系统安全设计”章节是送分项。5. 前端页面结构与状态管理的实用取舍5.1 页面组织与组件划分Vue 项目我使用的是 Vue 3 Vite Element Plus。页面按角色拆目录/views/login登录页/views/student学生端主页、选课页、课表页、成绩页/views/teacher教师端主页、开课申请、我的课程、成绩录入/views/admin管理端主页、用户管理、课程审核、选课窗口配置页面分组清晰进一步的好处是 Vue Router 的嵌套路由和动态菜单可以直接按目录映射。我采用的菜单结构是登录后根据角色渲染不同的侧边栏菜单项前端根据角色字段动态生成菜单不用写三分冗余的路由配置。这也是很多同学容易做笨的地方——直接手写三个路由表费时且容易漏不如在一个路由配置里通过 meta.role 来控制访问。组件复用方面我做了两个通用组件一个是CourseTable.vue——用来展示课程列表含分页、搜索、筛选学生端选课和教师端课程管理都用它另一个是TimeGrid.vue——用来展示个人课表学生端查课表、教师端查教学任务都能复用。组件化能极大减少重复代码也让前端代码结构显得更专业。5.2 前后端联调中的跨域与代理配置前后端分离项目遇到的第一道坎就是跨域。我在 Vite 的配置里设置了开发代理把/api前缀的请求转发到http://localhost:8080// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })同时后端也配置了全局跨域支持。为什么要两层都配置开发阶段用前端代理最省事前端代码里写相对路径/api/...部署到生产时也能平滑切换到后端静态资源模式不用改前端代码。这个细节在答辩时提一句“开发期走代理、生产期走同源部署”老师会觉得你考虑得很全面。axios 封装上我会在请求拦截器里统一附加 tokenaxios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })响应拦截器里则统一处理 code 非 200 的情况直接弹出 Element Plus 的 Message 提示并捕获 401 状态跳转登录页。这样业务页面里就不用每个请求都写错误分支代码会清爽很多。5.3 三个前端常见的坑都替你们踩过了第一个坑时间格式化。后端返回的 LocalDateTime 默认是一长串序列化文本如果不对齐前端显示会很难看。我的做法是后端在application.yml里配置全局 Jackson 格式化yyyy-MM-dd HH:mm:ss这样前端拿到的就是可直接展示的字符串不用每个字段手动格式化。第二个坑刷新后登录状态丢失。localStorage 存 token 和用户信息后刷新页面本来不会丢但如果初始化路由时用的是 Promise 异步加载角色菜单刷新瞬间 key 还没有被赋值的话会闪到空白。我的解决方式是 router.beforeEach 里做个同步判断localStorage 有信息就直接设置到 store不要再走异步初始化避免路由守卫和状态初始化打架。第三个坑Element Plus 表格的跨页多选。如果学生选课支持分页每页勾选几门课跨页勾选后状态会被清掉。需要在表格选择列上设定 reserve-selection 并用 row-key 保持粒度才能保证跨页选择不丢。这个坑很经典很多源码里都没有处理好你踩过之后能讲出来就是一个实践亮点。6. 联调、部署与答辩演示的实战经验6.1 本地联调阶段最容易出的问题本地开发时最容易遇到的三类问题我单独列出来第一类是数据库连接报错。MySQL 8 的驱动如果是老版本或者时区参数没配会被The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这类报错搞疯。我在application.yml的连接串里加了serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue问题迎刃而解。allowPublicKeyRetrieval 这个参数如果不加MySQL 8 有时会因为 RSA 公钥检索被拒而连不上很隐蔽。第二类是前后端端口冲突或者代理不生效。确认 SpringBoot 端口是 8080前端 Vite 端口是 3000代理配置没写错一般就不会出问题。如果代理不生效先检查changeOrigin: true是否设置这个参数漏了会导致后端收到的请求头里还是前端地址在某些场景下会被拦截。第三类是数据初始化的问题。自己手动造数据太痛苦我在 resources 目录下放了data.sql脚本用 SpringBoot 的spring.sql.init.modealways在启动时自动初始化管理员账号、测试教师、测试学生和二十多门课程数据。这些数据要刻意设计成能演示的场景比如有几门课的容量已经接近满员、有几门课上课时间重叠方便直接演示“满课不可选”和“时间冲突提示”。6.2 部署方案怎么选本地演示用什么部署方案我推荐两种方案一前后端完全分离部署后端 SpringBoot 跑在 8080前端通过 Nginx 或静态托管跑在 80。这种方案最贴近企业真实部署方式但答辩现场如果网络环境不好反向代理配置出现问题容易翻车。方案二前端打包后放进 SpringBoot 的静态资源目录打成单 Jar 运行。这个方案我实际使用得更多因为操作简单、稳定、方便演示——前端npm run build生成的 dist 目录拷贝到后端的src/main/resources/static下再配合 SpringBoot 的 Web 配置处理 SPA 路由最终只需要java -jar xxx.jar启动即可。答辩现场只依赖一个 jar 包运行天然自带前端页面演示过程几乎零配置。单 Jar 有一个细节要注意Vue 路由使用的是 history 模式刷新页面会 404。解决办法是在后端写一个 Controller 把非/api开头的、无扩展名的路径转发到index.html或者使用 SpringBoot 提供的WebMvcConfigurer对 SPA 做 forward 处理。这样刷新页面不会白屏。生产环境建议还是方案一但毕设答辩现场方案二能帮你省下大把折腾时间。6.3 答辩演示前的检查和演示流程最后说说答辩演示环节的准备。我给自己列过一个检查清单先以管理员身份登录展示用户管理和选课窗口配置把窗口配置成“当前时间处于选课时间内”切到学生身份演示选课——选一门正常课程成功、选一门满的课程提示已满、选一门时间冲突的课程提示冲突切到教师身份看到学生名单录入两个成绩回到学生身份查课表和查成绩确认刚才录入的成绩可见最后演示退课流程再切到管理员确认人数变化。这套流程贯穿三个角色把系统所有的核心功能串成一条完整的业务叙事。演示的时候我还会打开开发者工具的 Network 面板展示选课请求返回的 JSON 数据和状态码并顺手指出请求头里带了 JWT ——这些“小动作”能向现场老师直观证明项目的真实性和自己的掌握程度。关于垃圾数据的清理也要提前做。反复演示选课退课之后数据库里的数据会比较乱答辩前我通常会重新初始化一次数据库确保演示数据干净有序。避免现场出现“课程明明满员却显示 40/40 但名单里只有 10 个人”这种尴尬情况。这套系统从数据库设计到前后端联调我前后花了三周左右完成。真正花时间的不是写代码——SpringBoot 和 Vue 的脚手架已经能省掉大半重复工作——而是把业务边界想清楚、把并发冲突和权限控制这些细节打磨扎实。做选课系统的过程里我最大的收获不是跑通一个 Demo 时的成就感而是终于意识到一个靠谱的项目靠的不是贴更多新技术而是把业务逻辑和数据一致性真正落到每一天的编码细节中。下次再做类似管理系统哪怕业务换成了图书馆借阅或实验室预约这套“角色-权限-状态-事务”的思考框架我也还是底层的那一套。
返回列表