ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue学生选课系统:从数据库设计到部署答辩全攻略

SpringBoot+Vue学生选课系统:从数据库设计到部署答辩全攻略 又是一个被“学生选课系统”折磨的毕业季。说实话每年这时候找我聊这个题目的学弟学妹特别多原因是它看起来简单但真要做到能答辩、能演示、能写进论文牵扯出来的细节比你想象的多得多。这次我把基于SpringBoot Vue的学生选课系统整个从零到一的思路、建表、后端接口、前端页面、联调部署全部捋一遍源码、数据库脚本和文档的坑也一并说清楚这篇基本就是可以直接照做的完整参考。如果手里已经有一套源码但不知道怎么讲清楚、不知道怎么改成自己的这篇同样适用——很多时候答辩没高分不是因为代码不行是因为你自己没吃透项目。我下面会从“设计思路”开始一路说到“项目启动时最容易炸的地方”你耐心看至少能少走三天的弯路。1. 项目整体设计先想清楚要做什么再动手写代码1.1 这道毕设题到底在考什么很多同学看到“学生选课系统”第一反应是不就是登录、选课、退课嘛有什么难的结果一上手就发现要做的功能细碎且联动极多而且越做到后面越发现自己在重复造轮子。这道题放在毕业设计里核心考核点并不是“你会不会写增删改查”而是你懂不懂用户角色权限设计学生、教师、管理员各自看到的界面和操作完全不一样你懂不懂数据库表之间的关联关系选课记录、课程表、学生表、教师表如何通过外键或逻辑键串联你懂不懂前后端分离架构下接口怎么定义、数据怎么交互、跨域怎么解决你懂不懂选课业务里的冲突场景比如课程容量满了怎么办、同一时间冲突怎么处理、并发选课怎么防止超卖。所以拿到题目第一件要做的事不是打开IDEA新建项目而是把功能架构图画出来。哪怕你准备用现成源码改也得先搞清楚每个模块的边界不然答辩时老师随便抽一个功能问你是怎么设计的你支支吾吾评分直接就下去了。1.2 为什么选SpringBoot Vue这套组合这套技术栈在近几年的毕业设计里几乎是“统治级”的存在原因很实际SpringBoot把SpringMVC那套繁琐的配置全部自动化了你不用再写一堆XML配置文件内置Tomcat打包成jar直接跑对毕设来说省掉部署的很多麻烦事Vue使用组件化开发写前端页面比传统JSP加jQuery那种方式清晰太多数据和页面是双向绑定的表单、列表、弹窗这些交互写起来效率极高前后端分离以后前端可以单独启动一个开发服务器后端单独跑一个端口联调时出了问题很容易定位是前端还是后端的事网上资料、现成开源项目、教程视频极其丰富哪怕你基础一般遇到问题也能很快找到解决方案。如果你单纯为了“好过”那确实可以继续用JSP Servlet那套老技术但说实话等你去实习或者找工作会发现面试官对SpringBoot和Vue的熟悉程度几乎是默认要求了。借毕设这个机会把这套流程跑通对你自己的收益远大于学分本身。1.3 系统角色与功能模块拆解一个标准的学生选课系统通常包含三类用户每种用户的权限和功能边界必须一开始就界定清楚否则后面接口设计时会乱套。角色核心功能说明管理员教师管理、学生管理、课程管理、选课日志查看、系统统计一切基础数据的录入和维护入口教师查看个人课程、查看选课学生名单、录入成绩只操作与自己相关的数据学生浏览课程、在线选课、退课、查看课表、查询成绩系统使用频率最高的人群除此之外还有一个【未登录用户只能看到登录页】的约束这个也是Spring Security或拦截器要处理的核心逻辑。我在写的时候习惯单独抽一个“公共模块”用于放常量、工具类、统一返回结果封装类这样三个角色共用一套底层代码不会到处重复。功能模块我建议分成六个登录认证模块、学生管理模块、课程管理模块、选课管理模块、成绩管理模块、个人中心模块。前面两个是基础中间三个是核心最后一个用来做一些个人信息维护和密码修改。模块划清楚了数据库设计就有依据了。2. 数据库设计选课系统真正花心思的地方2.1 数据表到底需要几张我见过不少同学的设计方案一上来就建了二十几张表看着很唬人实际上大部分都是空的或者冗余的。选课系统的核心其实就是五张表学生表student存储学生学号、姓名、密码、专业、班级教师表teacher存储教师工号、姓名、密码、职称、所属院系课程表course存储课程编号、课程名称、学分、上课时间、上课地点、容量、已选人数、授课教师ID选课表student_course记录学生和课程的对应关系也就是中间表管理员表admin管理员账号和密码数量一般只有一两条。如果你想做得更完整一点可以再加一张“学生成绩表”记录某学生某门课的成绩这样学生在选课系统里就能查看成绩教师也能录入成绩。但要注意有的同学把成绩字段直接放在选课表里也可以——看答辩时你怎么讲道理。选课表本身就包含了“某学生选了某门课”这个事实成绩本质上是这个事实的扩展属性所以放在选课表里一点问题都没有反而少一张表少一次联表查询。2.2 关键字段设计要点这里我说几个我踩过的坑希望你不要再踩。课程表的“已选人数”这个字段很多同学容易忽略。没有这个字段你就必须在查询课程列表时统计选课表的记录数数据量小无所谓但列表页每次请求都做一次group by体验会明显下降。加上这个字段后选课成功时在事务里做update course set selected_count selected_count 1退课时减一配合容量字段去做校验稍微注意下并发就行。学生表和教师表的密码字段用的是varchar(100)而不是varchar(20)因为密码加密后长度会变长。很多人在数据库阶段没考虑这事到后端联调时发现密码总是对不上后悔都来不及。直接用BCrypt加密存储长度预留64位以上就稳了。再一个就是主键的选择。学生表和教师表不建议用自增ID做主键因为他们的业务编号——学号和工号——本身就具有唯一性。你直接用学号做主键后面关联查询时会省掉一次关联。当然如果你用MyBatis-Plus它对字符串主键的支持也很友好TableId(type IdType.INPUT)就能搞定。2.3 选课冲突检测的三层保护选课系统里最容易出问题的一个点就是学生选了一门课又选了另一门课结果两门课上课时间相同。这个问题在数据库层面很难直接通过字段约束解决因为“上课时间”是一个文本字段比如“周一第3-4节”数据库不知道A课和B课是否冲突。常规做法是分三层去控制第一层是前端校验拿到学生的已有课程列表和待选课程对比上课时间有重叠就直接弹窗提示 第二层是后端接口校验这是最关键的因为前端可以绕过后端必须在事务里查出学生的已有课程逐一比对上课时间 第三层是数据库兜底对选课表建立student_id和course_id的联合唯一索引保证同一个学生选同一门课只能有一条记录。时间冲突检查注意一个细节课程表里最好把上课时间拆成两个字段比如week_day星期几和class_section第几节到第几节而不是直接存“周一第3-4节”这种字符串。拆开之后冲突判断就可以通过星期几相等且节次区间重叠来判断用代码写条件判断也比解析字符串靠谱得多。这个设计如果你能在数据库设计说明书里写清楚答辩时讲出来绝对是一个加分点——因为这体现的是你对业务冲突场景的理解而不是只会照搬教程。3. 后端SpringBoot实现接口设计和核心业务的落地3.1 项目初始化和依赖选型我用的是SpringBoot 2.7.x版本配合MyBatis-Plus 3.5.x、MySQL 8.x、JDK 1.8。为什么不用SpringBoot 3因为3.x最低要求JDK 17很多学校机房电脑上的JDK版本还是1.8你写的时候也许没问题但答辩演示时环境不兼容就糟糕了。版本选择遵循“能稳定跑起来的版本就是好版本”不要在毕设里去追新。依赖方面核心需要引入这些spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、spring-boot-starter-validation以及一个JWT库我用的是jjwt 0.9.1。有两个点容易踩坑lombok在JDK 8版本下必须用1.18.24或更低版本高版本会报不兼容jjwt较老版本对JDK模块化不太友好如果你用JDK 11以上可能需要换用io.jsonwebtoken:jjwt-api:0.11.5那套新API。项目包结构上我习惯按业务模块分包而不是按技术层分包大概长这样com.example.selectcourse ├── config // 配置类跨域、拦截器删除线等 ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── common // 公共返回结果、异常处理、常量 └── utils // JWT工具类等按业务模块分包比如把student、teacher、course、selection分别作为子包也可以看个人习惯。对毕设来说按技术层分包更容易向老师解释“三层架构”的概念而且代码量不算大不会乱。3.2 统一返回结果和全局异常处理前后端分离项目里后端接口返回的数据格式最好统一方便前端axios封装后统一处理。我这里给一个非常常见的约定{ code: 200, message: 操作成功, data: { } }code为200表示成功其他值表示各类业务异常。写一个Result类提供静态方法Result.success(data)、Result.error(msg)所有controller都返回Result前端拿到数据后先判断code是否为200再决定是否把data渲染到页面上。然后是全局异常处理这个往往被很多初学者忽略导致接口报错时返回一堆英文堆栈信息给前端。用RestControllerAdvice加ExceptionHandler可以统一捕获业务异常和系统异常业务异常比如“课程容量已满”“选课时间冲突”在service层直接throw new BusinessException(具体原因)前端拿到这个message直接弹提示框体验会好很多。我现在还记得第一次做完这步时前端同学说“这次报错信息终于不是500了”那种感觉真的很爽。3.3 JWT登录认证和拦截器配置登录认证我推荐JWT方案原因很简单毕设项目不需要引入Spring Security全家桶那套东西功能强大但配置复杂对一个小型选课系统来说属于杀鸡用牛刀还容易把自己绕晕。JWT的逻辑很直白用户提交账号密码后端校验通过后生成一个token返回给前端前端把token存在localStorage里之后每次请求都在请求头加Authorization: token后端拦截器里解析token能读到用户信息就放行读不到或无token就返回401状态码前端收到后跳转登录页。JWT里我一般只放三个信息userId、role角色、expire时间。不放密码、不放手机号这些敏感信息因为JWT只是签名不是加密别人拿到token可以解码看到payload内容。拦截器这里容易出的问题是放行路径配置不对导致登录接口本身也被拦截或者静态资源被拦截。我自己的做法是配置一个白名单集合包含/login、/captcha、/error这些路径其他所有接口默认拦截。为了调试方便可以先用一个简单的注释开关控制拦截器是否生效联调时如果发现“莫名其妙的所有接口都401”先检查一下排除路径写对没有。3.4 选课接口的核心业务逻辑选课是系统的核心接口这段代码也最能体现你的业务思考。我先给出完整流程Transactional(rollbackFor Exception.class) public Result selectCourse(Long studentId, Long courseId) { // 1. 查课程信息 Course course courseMapper.selectById(courseId); if (course null) { throw new BusinessException(课程不存在); } // 2. 校验容量 if (course.getSelectedCount() course.getCapacity()) { throw new BusinessException(课程容量已满); } // 3. 查学生已有的选课记录 ListStudentCourse selectedList studentCourseMapper .selectList(new LambdaQueryWrapperStudentCourse() .eq(StudentCourse::getStudentId, studentId)); // 4. 检查是否重复选课 boolean duplicate selectedList.stream() .anyMatch(sc - sc.getCourseId().equals(courseId)); if (duplicate) { throw new BusinessException(不能重复选课); } // 5. 检查上课时间冲突 for (StudentCourse sc : selectedList) { Course selected courseMapper.selectById(sc.getCourseId()); if (timeConflict(selected.getWeekDay(), selected.getSection(), course.getWeekDay(), course.getSection())) { throw new BusinessException(上课时间冲突 selected.getCourseName()); } } // 6. 插入选课记录并更新已选人数 StudentCourse sc new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); sc.setSelectTime(new Date()); studentCourseMapper.insert(sc); course.setSelectedCount(course.getSelectedCount() 1); courseMapper.updateById(course); return Result.success(); }这段逻辑的关键在于事务插入选课记录和更新已选人数这两个操作必须放在同一个事务里要么同时成功要么同时回滚。我用了Transactional(rollbackFor Exception.class)注意rollbackFor必须写上否则默认只能拦截RuntimeException而如果你在事务里抛的是自定义的受检异常事务会完全不生效。关于并发如果你的毕设需要在文档里体现“高并发处理意识”可以再加一个乐观锁字段version更新已选人数时用update course set selected_count selected_count 1, version version 1 where id ? and version ?如果更新行数为0就说明数据被别人改了回滚并提示“选课人数发生变化请重试”。这个点讲出来老师会觉得你考虑到了实际生产中的超卖问题比单纯的增删改查有深度得多。4. 前端Vue实现路由、状态管理和选课页面交互4.1 前端项目结构和依赖前端我推荐用Vue 3 Vite Element Plus不要用Vue 2了毕竟Vue 2已经停止维护Vite也比webpack那套开发体验快得多。如果你手里拿到的是Vue 2 Element UI的旧源码可以继续用但自己新建项目时建议直接用Vue 3。项目基本结构src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Pinia状态管理 ├── utils // request封装token处理 ├── views // 页面视图 │ ├── login.vue │ ├── admin │ ├── teacher │ └── student └── App.vueElement Plus是按需引入还是全量引入毕设项目我建议全量引入。虽然按需引入能减小打包体积但多一步配置而且毕设项目对性能要求没那么高全量引入省心老师在演示时也不会因为一个组件没注册导致页面空白。4.2 Axios封装和路由守卫axios封装的核心就是把baseURL配好、token自动加上、响应统一拦截处理。我这里给出核心配置// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, // 开发环境通过Vite代理到后端 timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(服务器异常请稍后重试) } return Promise.reject(error) } ) export default request这里有个细节baseURL配成/api而不是直接配http://localhost:8080是为了配合Vite的代理配置。开发时前端跑在5173端口后端跑在8080端口如果直接跨域请求浏览器会报CORS错误。解决方式有两种后端加CrossOrigin配置CORS或者前端配置Vite代理。我推荐Vite代理因为这样做后端不需要额外处理跨域生产部署时也比较统一。Vite代理配置在vite.config.js里加一段server.proxy即可server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这样前端请求/api/course/list会代理到http://localhost:8080/course/list跨域问题直接被开发服务器解决了。路由守卫方面用beforeEach判断localStorage里有没有token没有token直接跳转登录页。同时根据路由的meta.role字段来判断角色的访问权限比如/admin开头的页面只允许管理员访问这样即使学生手动输入URL也进不了管理页面。4.3 选课页面的展示与交互学生选课页面是系统里最常被展示的功能。我用Element Plus的el-card加el-table实现一个课程列表每行显示课程名称、教师、学分、上课时间、容量和已选人数最后一列是一个“选课”按钮。这里有一个很实用的设计已选人数达到容量时按钮要变成disabled状态颜色变为灰色并显示“已满”如果该课程已经被当前学生选了按钮变成“已选”并禁用有空位但时间冲突的可以不做前置判断让用户点击后由后端返回错误信息——因为时间冲突要根据学生的全部选课记录来判断前端判断逻辑会和后端重复没必要。前端点击选课后调接口成功就刷新当前列表同时更新该行的已选人数成本很低。退课则加上一个确认弹窗用ElMessageBox.confirm提示“确定要退选这门课程吗”防止误操作——这个细节在演示时会被老师当作“用户体验考虑”的加分项。教师端页面相对简单教师登录后看到“我的课程”列表点击某门课可以查看选课学生名单然后给每个学生录入成绩。这里对权限的控制要注意教师只能看到自己创建的课程查询时必须在SQL里带上teacherId条件不能用一个课程列表给所有教师共用否则就是越权访问。4.4 前端调试的常用技巧前端出问题时第一件事是按F12打开开发者工具看Network面板里请求的实际请求地址、请求头和响应体。我遇到最多的情况是接口明明通了但页面不更新十有八九是数据格式没对上比如后端返回的是Result包装对象前端却直接当数组用了或者字段名大小写不一致MyBatis-Plus的驼峰映射没开启导致courseName在JSON里变成了coursename。如果页面白屏先看Console有没有报错。Vue项目里最常见的几个报错找不到组件、读不到undefined的属性、路由未匹配到。这些大多数是代码笔误根据报错信息定位到具体文件行数并不难。还有一种情况登录进不去一直跳回登录页检查一下axios拦截器是不是把业务code非200当成401处理了或者路由守卫里判断token的逻辑写反了——这种问题我调试了整整一个下午才想起来去看路由守卫的代码。5. 联调、部署和文档撰写从能跑到能交的最后一公里5.1 本地联调的完整流程前后端都写完之后第一次联调是最痛苦的。常规流程是这样的先在本地启动MySQL执行项目提供的sql脚本确认表都建好了再启动SpringBoot后端用Postman或Apifox测试登录接口确认返回token最后启动前端Vite开发服务器浏览器打开登录页输入测试账号走一遍完整流程。联调这里我建议一定要准备几个测试账号管理员、教师、学生各至少一个并且提前造好数据比如至少5门课程、一个满足条件的学生账号。我就是因为只在数据库里插了一条admin账号第一次演示时临时想注册学生账号发现注册接口一堆问题场面一度非常尴尬。数据库SQL脚本建议直接用Navicat或MySQL Workbench导出一份完整的sql文件包括建表语句和初始数据。这份sql文件要随源代码一起提交并且文档里要写清楚“导入数据库脚本的前提是已经安装了MySQL 8.x并设置了utf8mb4编码”。很多同学数据库报错就是因为建库时没有指定utf8mb4中文字段全是乱码或直接报“Incorrect string value”。5.2 生产环境部署方案毕设答辩一般是在自己电脑上演示但有些学校要求部署到服务器或者提交一个可演示的URL那就要掌握最基本的部署流程。后端的SpringBoot项目打包成jar包在项目根目录执行mvn clean package -DskipTests然后java -jar target/xxx.jar启动。注意如果你用了自定义的端口或数据库连接配置可以在启动命令上加参数覆盖比如java -jar xxx.jar --server.port8081部署文档里最好写清楚这些可调整的配置项。前端打包是npm run build生成dist目录里面是纯静态文件。部署方式有两种一种是把dist塞进Nginx配置一个server块指向dist目录同时把/api反向代理到后端端口另一种简单粗暴点直接把dist文件夹放到后端项目的src/main/resources/static目录下重新打包让SpringBoot同时提供静态页面和接口服务。第二种方式对毕设来说反而更稳妥因为不用单独部署Nginx一个jar包跑起来就什么都有了。但有个坑前端build时baseURL如果写成/api并且用Vite代理代理掉的打包后代理就不存在了你需要把请求地址改成后端接口的真实地址或者在SpringBoot里加一个/api开头的转发Controller。最省事的方案是把前端请求的baseURL配成和后端接口路径完全一致的一个相对路径比如后端接口是/course/list前端baseURL就配置成空字符串然后生产环境直接把dist放进static目录开发环境用Vite代理配一个/api前缀。如果这块觉得绕最简单的办法就是本地演示永远用Vite开发模式跑前端不打包。5.3 文档撰写的核心逻辑毕业设计的文档重要性完全不亚于代码。但很多人的文档是怎么写的呢从网上拼拼凑凑把系统功能描述一遍代码贴一堆答辩时老师翻了两页就放下了——因为没看到“为什么”的论述。我的经验是文档里至少要在三个地方体现你的独立思考。第一处是技术选型论证为什么用SpringBoot而不是SSH为什么用Vue而不是JSP从开发效率、维护成本、市场需求三个角度说。第二处是数据库设计说明每一张表的用途、表与表之间的关系、为什么选课表要加联合唯一索引这些都要用ER图和文字说清楚。第三处是核心功能的设计思路选课接口的时序图、事务处理方式、并发冲突的解决方案这部分的文字量可以少但一定要逻辑闭环。文档结构上按绪论、需求分析、系统设计、数据库设计、系统实现、系统测试、总结与展望这个标准模板来写这是学校普遍接受的格式。测试部分不要只写“功能正常”要给出一张测试用例表包括测试项、测试步骤、预期结果、实际结果最后所有用例都打勾通过——这个表格一出来文档的严谨性立刻就拉上去了。5.4 答辩时的高频问题清单答辩前最好把下面这些问题自己过一遍我整理了这几年被问得最多的为什么选课表要用联合唯一索引它解决了什么问题选课时如果两个学生同时选最后一门课你怎么保证不超卖JWT登录和传统的Session登录有什么区别、为什么选JWT前端路由守卫的原理是什么、放在前端做权限控制安全吗项目里有没有遇到跨域问题是怎么解决的课程时间冲突你是怎么判断的、如果上课时间跨多节怎么处理如果选课人数很多这个系统有哪些可以优化的点前两个问题涉及并发和数据库约束第三个涉及认证机制第四个考的是前端安全认知需要你诚实回答“前端控制权限有限真正的安全控制应该在后端”这个答案比强行圆场要加分。第七个问题是开放性提问你可以说加Redis缓存热点课程数据、用消息队列削峰、接口限流等不用真的做出来但要让老师知道你有扩展意识。实操中容易踩的坑和我的解决经验最后集中说几个我在做这类项目时实实在在遇到过的坑都是网上教程不会主动提醒你的细节。第一个坑是Maven依赖下载极慢或直接卡住。解决方式是使用阿里云镜像在settings.xml里配置mirror节点这一步能节省大量等待时间。如果你用的是2023年之后的IDEA版本自带的Maven可能默认走maven中央仓库网络不好时项目创建半天没反应换镜像后明显改善。第二个坑是删除线MySQL时区问题。SpringBoot连接MySQL时jdbcUrl里的serverTimezone参数不配或者配错启动或查询时就会报The server time zone value错误。我一般直接写jdbc:mysql://localhost:3306/select_course?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai一串全带上省得后面来回折腾。第三个坑是前端表单验证。Element Plus的form组件需要给表单项设置prop属性并且rules里的字段名必须和表单数据字段名一致否则验证规则完全不生效。这个问题非常隐蔽因为不报错、页面也没异常就是点了提交没反应很多人排查半天才发现是prop写错了。第四个坑是课程表里如果有删除操作删除课程前必须检查该课程是否已经有人选了有人选的课程不能物理删除否则选课表里会出现悬空引用。最稳妥的做法是不提供物理删除功能而是用一个status字段标记该课程为“已下架”学生端查询时自动过滤掉下架课程。这个设计给数据库保留完整历史答辩时也讲得通。其实做完这个项目你会发现编码本身占用的时间只占三成剩下七成都在“和环境搏斗”以及“理解业务边界”。这些经验学校不会教你网上也很难有人系统总结但都是真实项目里最值钱的东西。希望这篇东西能帮你在整个过程中少烧掉几次CPU不管你是打算自己从零写还是拿到源码后准备改造把上面的设计思路和避坑清单过一遍心里都会踏实很多。
返回列表