ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot + Vue学生选课系统开发实战:从数据库设计到部署上线

Spring Boot + Vue学生选课系统开发实战:从数据库设计到部署上线 学生选课系统是Java全栈开发里最常被拿来练手的项目之一每逢毕业设计和课程设计季节问得最多的一定是它。原因很简单业务模型清晰学生、教师、管理员三种角色选课、退课、排课这些场景贴近校园生活逻辑复杂度又刚好能撑起一套完整的前后端分离项目。这篇东西我打算直接用做项目的思路来讲。从需求拆解到数据库设计从后端接口实现到前端页面联调再到最后的部署上线和文档整理一套走完。不管你是拿这套源码做课程设计、毕业设计还是想彻底搞懂Spring Boot Vue前后端分离项目的完整玩法应该都能找到你需要的部分。1. 项目整体设计先搞清楚选课系统到底在解决什么问题很多初学者拿到选课系统的题目上来就建表、写代码结果做着做着发现自己不知道在实现什么。做任何系统之前先把业务梳理清楚后面每一步才有依据。1.1 核心角色与业务场景梳理学生选课系统本质上是解决一个“资源分配”的问题。课程有容量上限学生有选课需求系统要保证资源分配公平、数据准确、操作可追溯。围绕这个核心业务场景可以拆成以下几块学生端浏览可选课程、查看课程详情教师、时间、地点、学分、容量、选课、退课、查看自己已选的课表、查看成绩。教师端查看自己负责的课程、查看选课学生名单、录入成绩。管理员端课程信息管理新增、修改、上下架、学生和教师账号管理、统计选课人数、处理特殊调整需求。这三个角色对应三种权限前端路由要和权限对应起来后端接口也要有拦截校验。我见过很多学生项目把所有接口都开放出来没有任何鉴权这样的系统虽然能跑但在答辩时很容易被问到“你怎么保证一个学生不能改别人的成绩”这类问题。比较好的做法是引入JWT做登录态管理。用户登录后后端签发一个token前端把token存在localStorage或者内存里每次请求带上 Authorization 头后端通过拦截器统一校验token并解析出用户身份和角色。角色不同能访问的接口不同这就是最简单的权限控制模型。1.2 技术栈选型为什么是Spring Boot Vue MySQL这套组合在校园项目和中小型企业项目中都非常主流原因在于每一层选的都是“刚好够用且生态成熟”的技术。后端用Spring Boot理由非常实在。它内嵌Tomcat不需要单独部署容器打包成jar就能直接跑。自动配置机制省掉了大量XML配置Spring Security、MyBatis-Plus、Validation等组件都有一键集成的starter。对学生来说这意味着可以把更多精力放在业务逻辑上而不是浪费在环境配置上。更重要的是Spring Boot的面试题非常多做完这个项目再去复习IoC、AOP、自动装配这些概念理解深度会完全不同。前端用Vue全家桶核心是Vue 2或Vue 3 Vue Router Pinia/Vuex Element UI。Vue的组件化开发方式特别适合后台管理系统这种“页面结构相似、逻辑重复”的场景。表格、表单、弹窗、分页这些UI组件用Element UI一行就能引入开发效率非常高。而且Vue生态的文档和社区资料全遇到问题几乎都能搜到解决方案。数据库选择MySQL本身是关系型数据库的经典选择。选课系统里有明显的数据关联关系学生选课表要关联学生表和课程表成绩表要关联选课记录和教师表。MySQL的ACID事务特性也正好能解决选课时的并发一致性问题这块后面会详细讲。不做技术选型对比的话很多同学会纠结“要不要用Redis做缓存”“要不要用MyBatis-Plus还是JPA”“要不要前后端不分离”。我的建议是如果你是课程设计或者毕设不要为了炫技引入过多组件。项目里用到的每一项技术你都要能解释清楚它解决什么问题这才是答辩时加分的点。我这套系统里核心就是Spring Boot MyBatis-Plus MySQL Vue 2 Element UI每一样都能讲清楚为什么选它。2. 数据库设计与后端核心接口把选课逻辑落进代码数据库设计是整套系统的地基。表结构如果设计得不合理后面写接口、做联调、应对并发都会遇到大量麻烦。我见过很多选课系统的表设计最常见的问题是选课关系表设计得太随意没有唯一约束、没有级联关系、字段类型不合理。2.1 数据库表结构设计与关系说明一套标准的选课系统核心表至少有这几张表名作用关键字段student学生信息id、student_no、name、password、major、class_nameteacher教师信息id、teacher_no、name、password、departmentcourse课程信息id、course_no、name、teacher_id、credit、max_student、selected_count、week_time、classroomstudent_course选课关系表id、student_id、course_id、status、score、create_timestudent表和teacher表可以各自独立也可以统一做成user表加角色字段。我倾向于独立建表因为字段差异比较大而且业务角色清晰。course表和teacher表通过teacher_id建立关联一门课程对应一个教师一个教师可以有多门课程这是很典型的一对多关系。student_course表是整套系统的核心枢纽。它通过student_id关联学生、course_id关联课程形成多对多关系的中间表。这张表的设计有几个关键点student_id和course_id必须加联合唯一索引。这个索引的作用是从数据库层面保证同一个学生不能重复选择同一门课程。光靠代码判断是不安全的并发请求下代码判断可能失效但数据库约束不会。score字段默认设为0或者NULL。成绩还没录入时前端展示应该显示“未录入”不要用0去表示因为0分和未录入在语义上完全不同。status字段建议保留。虽然基本只有选课和退课两个状态但保留这个字段以后要加“待选”“已选”“退课中”等状态时不用改表结构。课程表里需要特别注意max_student和selected_count这两个字段。一个表示课程容量上限一个表示已选人数。每次选课成功selected_count就加1退课就减1。这就是最朴素的“库存扣减”模型。2.2 后端接口设计与核心业务实现后端接口设计我建议遵循RESTful风格用HTTP方法表达操作意图。以课程和选课相关的核心接口为例GET /api/course/page分页查询课程列表GET /api/course/{id}查询课程详情POST /api/course新增课程管理员PUT /api/course/{id}修改课程信息管理员DELETE /api/course/{id}删除课程管理员POST /api/student/course/{courseId}学生选课DELETE /api/student/course/{courseId}学生退课GET /api/student/course/list查询我的选课列表PUT /api/teacher/score教师录入成绩工程结构用标准的Controller-Service-Mapper三层架构。Controller层只做参数接收和结果封装不写业务逻辑Service层写核心业务逻辑事务注解加载这里Mapper层用MyBatis-Plus单表CRUD不需要写SQL复杂查询用LambdaQueryWrapper或者注解SQL解决。选课这个接口是最核心的业务逻辑大致是这样校验课程是否存在、是否在上架状态。校验学生是否已经选过这门课联合唯一索引兜底。校验当前已选人数是否小于课程容量。校验课程时间是否和其他已选课程冲突。插入选课记录课程已选人数加1。这五步必须在同一个事务里要么全部成功要么全部回滚。用Transactional注解即可。注意第5步的“已选人数加1”不能先查出数量再在代码里加应该用一条原子SQL去更新UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count max_student这种写法的好处是即使两个学生同时发起选课请求数据库行锁也会保证只有一个请求能更新成功另一个请求会因为条件不满足而影响行数为0从而在数据库层面防止超选。这是这套系统里我认为最核心的一个细节。2.3 并发选课与事务控制要点学生选课系统的并发量不像电商秒杀那么夸张但在选课高峰期比如开放选课的第一分钟几百个学生同时点选课按钮是非常正常的。如果代码写得不讲究很容易出现两个问题一是超选二是重复选课。重复选课靠数据库唯一索引解决前面已经说了。超选问题必须靠事务和原子更新解决。我见过很多同学这样写Course course courseMapper.selectById(courseId); if (course.getSelectedCount() course.getMaxStudent()) { return 课程已满; } course.setSelectedCount(course.getSelectedCount() 1); courseMapper.updateById(course);这种写法在并发请求下必出问题。两个线程同时读到selected_count49容量是50两个请求都判断通过然后都执行加1最后selected_count变成51超卖了。根源在于“读”和“写”之间没有锁保护。解决办法就是前面提到的条件更新SQL把判断和更新合并成一个原子操作。事务控制上还要注意一个细节Transactional默认只在RuntimeException和Error时回滚如果方法里catch了异常但没有重新抛出事务是不会回滚的。我当时就踩过这个坑日志里明明看到异常了但数据也插进去了。正确的做法是异常往上抛或者手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。3. 前端Vue实现与前后端联调从写页面到真正跑通后端接口写好后前端就是把这些接口一个一个对接到页面上。很多同学第一次做前后端分离项目最迷茫的不是Vue语法而是“怎么把一个后端接口变成页面上能点的按钮”。3.1 前端工程结构与路由设计前端工程我用Vue CLI创建目录结构大致如下src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 状态管理 ├── views/ # 页面组件 │ ├── admin/ │ ├── student/ │ └── teacher/ ├── utils/ # 工具函数 └── App.vueapi目录下按照业务模块拆文件比如course.js、student.js、user.js。每个文件里统一用axios发请求导出对应的方法。这样做的好处是页面里不需要直接写请求路径改后端地址时只需要修改一个配置文件。路由设计上用Vue Router的嵌套路由把管理员、学生、教师三个角色的页面分开。每个角色的页面放在对应的layout里再加上路由守卫。路由守卫的作用是用户没登录时跳转登录页登录后访问没有权限的页面时跳转403页。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(store.state.user.role)) { next(/403) } else { next() } } })这里有个很容易被忽略的点路由守卫只是前端体验层面的控制真正的权限校验必须以后端接口为准。前端隐藏掉按钮不代表接口不能调该做的后端拦截一个都不能少。3.2 页面开发与组件封装页面开发的核心思路是“先搭骨架再填细节”。以学生选课页为例这个页面通常是整个系统最复杂的页面需要同时处理课程表格展示、搜索条件、分页、选课操作、已选课状态展示。我习惯先把页面分为三个板块搜索区、表格区、分页区。搜索区用el-form和el-input做条件输入表格区用el-table渲染数据操作列放选课/退课按钮。分页用el-pagination组件。数据获取逻辑写在methods里页面加载时在created生命周期中调用。选课按钮的处理要特别注意交互体验。一个学生选过课之后按钮应该显示“已选”且置灰不能等用户点下去才提示。这要求后端返回的数据里包含当前用户和课程的关系状态或者前端在拿到选课列表后做一个id集合渲染表格时判断一下。后端课程列表接口返回的字段里我建议加上一个selected布尔字段表示当前登录用户是否已经选了这门课以及countLeft剩余名额。这样前端渲染起来非常顺畅不用做二次请求。表单处理上Element UI的el-form提供了很方便的校验规则但是重置表单时有个经典坑this.$refs.form.resetFields()重置的是初始值而不是清空所有值。如果你在dialog打开时才通过接口异步赋值resetFields可能不会生效。解决办法是在打开弹窗时用nextTick重新设置初始值或者直接手动清空数据对象。3.3 前后端联调与跨域处理前后端分离项目里跨域是最常见的联调问题。前端跑在8080端口后端跑在8081端口浏览器的同源策略会把请求拦下来。解决方式有两种常用方案。第一种是在后端配置CORS写一个WebMvcConfigurer实现类配置允许的跨域来源。第二种是前端利用Vue CLI的devServer代理把请求代理到后端地址。我个人推荐第二种因为开发环境和生产环境的配置可以完全一致前端请求直接写相对路径。// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }配置好后前端请求/api/course/page开发服务器会把它转发到http://localhost:8081/api/course/page。这样前端代码里不需要写任何绝对地址以后生产环境部署时也只需要配置Nginx指向后端服务前端代码一行都不用改。联调过程中一定要让后端和前端约定好统一的返回结构。我用的统一结构是这样{ code: 200, message: 操作成功, data: {} }前端在axios响应拦截器里统一处理codecode为200时正常返回datacode不为200时弹出message提示。这样前端业务代码里不用每个请求都写错误处理代码会干净很多。4. 部署上线与文档交付源码、数据库、文档缺一不可项目写完之后很多同学就抱着“跑起来就算完事”的心态其实部署和文档同样是项目非常重要的一部分。尤其当你拿这个项目作为课程设计或毕设答辩时老师一定会问部署过程也会翻看文档。一个能跑、能部署、有完整文档的项目和一个只能在本机IDE里跑的项目评分完全不在一个层级。4.1 环境配置与本地运行步骤本地运行需要的环境清单如下软件版本建议用途JDK1.8或11后端运行环境Maven3.6后端依赖管理和打包Node.js14前端运行和构建MySQL5.7或8.0数据库Navicat或DBeaver任意数据库管理工具拿到源码之后第一步不是启动而是检查配置文件。后端有一个application.yml重点检查数据库连接配置。我建议数据库密码不要写在代码里用明文虽然在项目中图省事可以这样做但至少要做到每个环境用一个独立的配置文件通过Spring Boot的多环境配置功能切换。开发环境用application-dev.yml生产环境用application-prod.yml启动时通过--spring.profiles.activeprod指定使用哪个环境。数据库导入时有个容易踩坑的地方脚本文件的字符集。如果你的SQL脚本里包含中文注释或者中文数据导入前确认文件编码是UTF-8否则会导入乱码数据。导入时选择好目标数据库注意脚本里如果有CREATE DATABASE语句可能会和你本地已有的数据库重名部分导入工具会提示覆盖。4.2 打包部署jar dist Nginx生产环境部署是整个项目最见功力的一步。后端用Maven打包mvn clean package -DskipTests打包完成后target目录下会生成一个jar文件。用java -jar xxx.jar就能直接启动。注意Spring Boot内嵌了Tomcat不需要额外安装。如果你想在服务器上长期运行不推荐直接用java -jar命令挂在前台。用nohup命令放到后台nohup java -jar student-course-system.jar --spring.profiles.activeprod app.log 21 这样启动的好处是即使断开SSH连接服务也不会停止。日志输出到app.log文件排查问题时可以随时查看。前端构建npm run build构建完成后生成dist目录里面是纯静态文件。把这些文件交给Nginx托管Nginx配置里要同时做两件事一是将根路径指向dist目录二是把/api开头的请求反向代理到后端jar服务。server { listen 80; server_name your-domain.com; location / { root /opt/student-course/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }有个细节一定要记住try_files $uri $uri/ /index.html。这一行是解决前端路由刷新404的问题。Vue Router的history模式下浏览器访问/student/courses这个地址时如果Nginx没有配置try_files它会去找服务器上是否存在/student/courses这个文件找不到就返回404。加上try_files后所有找不到的路径都会回退到index.html由前端路由接管。4.3 数据库脚本与项目文档的整理源码和数据库之外文档是整个交付包里最容易被忽视、却最能体现专业度的部分。一套完整的学生选课系统文档至少应该包含需求分析文档角色分析、功能列表、用例图或用例描述。数据库设计文档ER图、每张表的字段说明、索引说明、核心SQL逻辑。接口文档每个接口的请求方式、请求路径、参数说明、返回示例。部署文档环境要求、初始化步骤、启动步骤、常见问题。测试报告功能测试用例、测试结果、发现的问题和修复情况。数据库脚本建议拆成两个文件schema.sql负责建表data.sql负责插入初始数据。初始数据至少要包含一个管理员账号、一个测试教师账号、一个测试学生账号以及若干门课程数据。评分老师拿过去直接就能用这个体验会非常好。文档并不是写完就完事了我见过很多同学在项目答辩前才临时补文档写出来的东西跟代码严重脱节。正确的做法是边写代码边记录每完成一个功能模块就同步更新对应文档。如果你是从零开始做这个项目我建议你把文档写进git的提交记录里每次提交代码时顺手更新一下文档这样保证文档和代码永远同步。5. 常见问题与排查技巧实录这部分是我自己实际做项目时遇到的问题汇总。很多问题网上也能搜到但我把最典型的场景和排查思路直接列出来供你参考。5.1 高频报错与解决方案速查表问题现象可能原因解决方案前端请求接口报404请求路径和后端接口路径不一致或Nginx代理配置错误先在浏览器直接访问后端接口地址确认后端接口是否存在再检查前端请求路径和代理配置。报错Access-Control-Allow-Origin前端直接访问了后端地址跨域拦截开发环境用代理生产用Nginx代理后端也可以配置CORS作为兜底。数据库连接失败数据库地址、端口、密码配置错误检查application.yml中jdbc连接串用Navicat或命令行工具先测一下数据库能否连接。MyBatis-Plus查询结果为空表名、字段名大小写或下划线映射问题检查实体类字段和表字段的驼峰映射配置确认map-underscore-to-camel-case配置为true。前端打包后访问空白页资源路径配置不对vue.config.js中publicPath改为相对路径./或部署时保证dist资源路径正确。部署后刷新页面404没有配置try_files在Nginx的location /中配置try_files或者改成hash路由模式。这里重点说一下MyBatis-Plus的字段映射问题。数据库字段通常用下划线命名比如student_noJava实体类字段用驼峰命名比如studentNo。MyBatis-Plus默认开启了驼峰映射但如果你自定义SQL里写的是SELECT *一般情况下没问题。如果你自己写了带别名的SQL一定要给列名起别名否则字段会映射不上。这个坑排查起来很麻烦因为不报错就是字段为null。5.2 并发选课时容易忽略的几个坑前面讲接口时我已经说了超选问题的核心解法这里再补充几个容易被忽视的细节。第一选课按钮没有做防重复点击。用户快速点两下选课按钮请求发了两次即使数据库有唯一索引第二次请求会报重复选课的错这个可以接受。但如果你的代码是先查再插两次请求可能都通过查询然后第一次插入成功第二次插入失败。用户体验上用户看到第一次请求成功第二次请求弹出“重复选课”的提示会觉得很奇怪。解决办法是把选课请求在接口层面做到幂等最简单的方式是前端在请求未返回时把按钮禁用加上loading状态。第二退课和选课并发。如果一个学生选课时课程满了恰好另一个学生正在退课此时退课还没提交事务选课请求查询到的剩余名额还没变选课就失败了。这个场景在实际中概率不高但确实是存在的。如果要对这个场景做优化可以在退课接口里用同步锁或者悲观锁但课程设计阶段一般不需要做到这一步能应对典型的超选问题就已经足够了。第三跨事务的缓存问题。如果你用了Redis缓存课程剩余名额缓存和数据库之间的一致性就是一个大坑。我建议初级项目不要引入Redis缓存课程信息直接在数据库层做原子扣减性能和一致性都能满足需求还能把代码逻辑保持在可解释的范围内。5.3 提升答辩和评分的几个细节答辩时老师通常不会只看功能是否齐全更多会考察你对项目的理解深度和细节的处理。有几个细节我认为能明显拉开差距。第一项目里要有日志。你可以在关键业务节点加上日志记录比如选课成功、退课成功、成绩录入等操作打印请求参数和耗时。老师问“你怎么排查线上问题”的时候你说“我在关键操作上加了日志可以通过日志追踪用户操作链路”这个回答会非常加分。第二异常提示要够具体。不要统一的“系统错误”而是区分参数缺失、数据不存在、状态不允许、操作冲突等不同场景给用户明确的提示。这能说明你真的考虑过业务的边界情况。第三代码要有注释但不要过度注释。关键的算法逻辑、事务边界、防超卖设计这些地方加注释说明原因简单的方法不需要注释。老师翻代码时看到关键地方有解释会觉得这个项目是你认认真真写的。第四项目里要有一两个“亮点设计”。比如联合唯一索引防重复选课、原子更新防超选、统一返回结构、路由守卫做权限控制、Nginx部署时配置try_files解决刷新404这些都是能拿得出手的细节答辩前可以把它们提前准备好组织好语言思路就好。6. 一些个人总结学生选课系统这个项目我带过不少学生做过也自己完整地开发过好几版。我越来越觉得这类项目的价值不在于“技术有多新”而在于“它逼你把一套完整软件的流程走完”分析需求、设计库表、写后端接口、写前端页面、联调、部署、写文档。每一步都会遇到具体的问题每个问题都能在真实的开发场景里找到对应。如果你是在校学生做完这样一个项目简历上可以写成“基于Spring Boot Vue的学生选课系统涵盖了权限管理、事务控制、前后端分离开发、Nginx部署”。面试官问细节时你能把选课防超卖的原理讲清楚把跨域解决方案说清楚这已经比很多只背八股文的候选人有说服力了。最后说一个我的个人习惯开发和联调阶段尽量把错误信息完整地暴露出来不要在前端把错误全部吞掉。很多初学者觉得前端报错不好看把错误弹窗全部删掉了但这样反而让排查问题变得更困难。我一般都会保留错误提示哪怕是500错误也要在控制台打印出完整的堆栈信息。等项目稳定了再根据不同的错误类型去优化提示文案。这个习惯帮我节省了大量排查问题的时间希望对你也有效。
返回列表