简介基于Vue.js高校学生选课系统是一套完整的前后端分离项目面向高校学生、毕业设计选题者及正在学习Spring Boot与Vue开发的技术人员可帮助理解课程管理、学生选课、密码修改等典型业务模块的真实实现方式。压缩包共579个文件约16.16MB以131个Java后端源码、98个Vue前端组件、161个SVG图标和38个JS脚本为主并包含SQL数据库脚本、批处理运行与构建脚本便于快速部署和二次开发。前端采用模块化组件设计主体内容、侧边栏、面包屑导航与头部导航等区域均由独立组件承担页面结构清晰、便于维护整体按管理端和学生端分目录组织便于对照学习两端实现。项目源码已通过测试验证可正常启动运行已有45人学习下载适合用来对照研究前后端分离架构下选课系统的设计思路以及Spring Boot与Vue.js在真实项目中的整合方式。1. 高校选课系统为什么选 Vue.js 来写前端每年的选课开放日某高校教务系统总要在早八点准时卡成白屏学生一边刷新一边吐槽教务处老师一边接电话一边解释。见过这个场景的开发者多半会冒出一个念头如果前端用 Vue.js 重写能不能让抢课体验好一点《基于Vue.js高校学生选课系统.rar》这个标题之所以在开发者之间流传就是因为它指向一个能直接解压、导入数据库、跑起来的前后端项目而不是一份只讲概念的PPT。把系统做成.rar形式意味着它已经整理好源码、SQL脚本和配置文件适合三类人需要课程设计或毕业设计的高校学生、想找个完整前后端项目练手的前端开发者、以及准备把选课系统二次开发成教务工具的运维或全栈工程师。这个标题背后是一个典型的业务闭环学生登录、浏览课程、选课、退课、查看已选列表管理员维护课程信息、查看选课统计。Vue.js 负责的正是这个闭环里所有用户看得见的部分而数据存储和业务规则靠后端接口与数据库兜底。看懂“选课系统”四个字不难难的是把它拆成一张能跑的架构图前端哪些组件负责什么、接口如何约定、数据库表怎么设计才能撑住选课高峰。这篇文章就沿着这条路径从环境搭建讲到核心模块再讲数据库与并发最后落在避坑清单和改写技巧上。2. 把解压包变成能跑的系统环境搭建与启动路径2.1 先看包内结构判断这是一个前后端分离的项目还是一个单体应用拿到一个.rar源码包别急着npm install先花两分钟看目录结构。一个典型的基于Vue.js选课系统根目录下通常会有frontend或client目录放Vue.js前端代码backend或server目录放后端接口服务还可能有一个database或sql目录专门放初始化脚本。如果只看到一层目录所有Vue组件和后端代码混在一起那大概率是一个用Vue CLI搭建了前端壳、但后端逻辑写在Spring Boot或Node.js框架里的前后端分离项目只是文件组织比较随意而已。判断标准很简单找有没有package.json和vue.config.js有就是标准Vue工程找有没有application.yml或app.js有就是后端服务入口。前端和后端是两个独立进程意味着开发时要在两个终端分别启动调试时也要先确认接口服务在监听端口而不是只启动一个npm run dev就等页面出现。某同学第一次跑这类项目时只启动了前端打开页面空白F12之后发现所有API请求都指向localhost:8080但服务根本没起这就是没看懂“分离”二字的典型翻车现场。如果包里有README或环境说明文档先读它那里通常会写明要求的Node版本、MySQL版本和初始化顺序。没有文档也不要慌按下面的流程走同样能把系统拉起来。2.2 前端最小启动流程依赖安装、端口配置与代理设置无论后端是Spring Boot还是Express前端的起步命令基本一致。进入前端目录后执行依赖安装需要注意的是最好锁定npm版本避免不同Node版本解析依赖树时产生冲突。项目里如果自带了package-lock.json文件只执行npm install即可如果没有可以考虑用npm install --legacy-peer-deps规避部分新版npm严格peer依赖检查导致的安装失败。cd frontend npm install npm run dev执行完这三步终端会打印出本地访问地址通常是http://localhost:8080或者http://localhost:3000取决于项目的Vue CLI配置。这一步只能证明前端页面在跑登录接口大概率还是废的因为前端默认请求的后端地址可能和你本机不一致。接着要改vue.config.js里的proxy配置这是整个启动流程里第一个真正的坑点。// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这段配置的含义是前端开发服务器拦截所有以/api开头的请求转发到本机9090端口的后端服务。changeOrigin设置为true是为了把请求头中的Host字段改写成目标地址避免部分后端框架的域名校验拦截。pathRewrite一般保持原样除非后端接口路径本身不带/api前缀。我一般会先把target指向后端控制台用于确认端口是9090还是8080这是新手拿到源码包后最容易翻车的地方——前端8080端口和后端8080端口撞车接口全部404还以为是代理没生效。2.3 后端启动与数据初始化SQL先于代码顺序不能乱后端服务的启动方式取决于框架选型。Spring Boot项目用Maven或Gradle打包后启动Node.js项目直接node app.js或npm start。但无论哪种框架前提都是MySQL里已经有库和表。.rar包里大概率带.sql初始化文件按文件名排序先执行建库脚本再执行测试数据脚本。mysql -u root -p database/init_schema.sql mysql -u root -p database/init_data.sql关于MySQL的字符集执行前建议先确认脚本文件本身的编码格式。Windows环境下直接双击导入.sql文件经常遇到乱码导致建表失败解决办法是用Navicat或MySQL Workbench指定UTF-8编码导入。表创建成功后检查一下数据库连接配置Spring Boot项目看application.ymlNode项目看config目录下的db.js常见的错误是密码没改成本地MySQL的密码、host写成了服务器地址而非localhost。# application.yml 部分内容 server: port: 9090 spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8 username: root password: 123456这里的url后缀非常关键。如果不加useUnicode和characterEncoding参数插入中文课程名时很容易出现乱码。password改成你本地MySQL的真实密码后保存再重新启动后端服务。启动成功的标志是控制台出现“Started Application”或“服务启动成功”之类的日志而非只是进程不退出。后端起来之后回到浏览器访问前端地址尝试登录这一步能通说明环境搭建已经过了最难的关口。3. Vue.js 前端的四大核心模块登录鉴权、课程展示、选课操作、状态管理3.1 登录与token持久化路由守卫决定谁能进选课页选课系统的前端第一个模块是登录页。登录流程有三件事表单校验、调用后端接口换取token、把token存起来供后续请求携带。一个常见的做法是用Vuex管理用户状态配合localStorage做持久化刷新页面之后路由守卫还能根据token的存不存在决定放不放行。// store/modules/user.js 部分逻辑 const userStore { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), mutations: { setToken(state, token) { state.token token localStorage.setItem(token, token) }, clearToken(state) { state.token localStorage.removeItem(token) } }, actions: { async login({ commit }, form) { const res await request.post(/api/login, form) commit(setToken, res.data.token) commit(setUserInfo, res.data.userInfo) }, async logout({ commit }) { commit(clearToken) commit(setUserInfo, {}) } } }这里的request.post是统一封装过的axios实例重点是后端返回的数据结构必须和代码里一致。有的后端返回{code: 200, data: {token: xxx}}有的返回{status: 0, result: xxx}字段名不同前端就有不同的取值逻辑。如果登录成功后依然跳转不了页面先打开浏览器Network面板看响应体长什么样再回头改这里的取值字段这是前后端联调的血泪经验。3.2 课程列表与搜索筛选组件化拆解让页面更清爽选课主页面通常包含一个课程列表、一个搜索框和若干个筛选下拉框。Vue.js做这类页面非常顺手课程卡片抽成一个子组件列表页只负责数据加载与状态管理。template div el-input v-model.trimkeyword placeholder搜索课程名称 clearable inputhandleSearch/el-input el-select v-modelselectedCredit placeholder学分筛选 changehandleSearch el-option v-forc in creditOptions :keyc.value :labelc.label :valuec.value/el-option /el-select course-card v-forc in filteredCourses :keyc.id :coursec selecthandleSelectCourse/course-card /div /template script export default { data() { return { keyword: , selectedCredit: , allCourses: [], filteredCourses: [] } }, methods: { async loadCourses() { const res await request.get(/api/courses) this.allCourses res.data.list this.filteredCourses this.allCourses }, handleSearch() { this.filteredCourses this.allCourses.filter(course { const matchKeyword course.name.includes(this.keyword) const matchCredit this.selectedCredit || course.credit Number(this.selectedCredit) return matchKeyword matchCredit }) }, async handleSelectCourse(payload) { const res await request.post(/api/select, { courseId: payload.courseId }) this.$message.success(res.data.message || 选课成功) this.loadCourses() } }, mounted() { this.loadCourses() } } /script这里的筛选逻辑放在前端做是因为选课系统课程数据量通常在几千条以内前端过滤完全够快且筛选条件切换时不需要反复请求后端体验更流畅。几个注意点v-model.trim修饰符去掉搜索关键词两端的空格学分对比时选了字符串类型还是数字类型要前后端统一否则会出现筛选结果永远为空。选课成功之后重新拉一遍课程列表是为了同步剩余名额数据这比局部更新某个卡片的字段要稳妥得多。3.3 选课与退课的交互细节并发和多标签页下怎么保证数据不混乱选课系统最容易出体验问题的地方是重复提交。学生手快点了两次选课按钮或者开了两个浏览器标签页操作后端如果没做幂等处理就会出现同一个人同一门课选了两条记录或者明明没有名额了前端还认为选上了。前端层面的兜底方案是节流和提交状态标志。在选课按钮的点击事件里加一个isSubmitting标志位提交期间禁用按钮更严谨的做法是在请求发出但未返回时不允许再次调用同一个接口。async handleSelectCourse(course) { if (this.submitting) return this.submitting true try { await request.post(/api/select, { courseId: course.id }) this.$message.success(选课成功) this.loadCourses() } catch (e) { this.$message.error(e.response.data.message || 选课失败) } finally { this.submitting false } }显式的submitting控制比单纯的按钮disabled更可靠因为按钮disabled状态在某些组件库的异步渲染下可能更新不及时。多个标签页的同步问题前端无法彻底解决只能在登录时记录一个session标识后端在写入选课记录时用该标识做限定条件同一个学生同一门课程在数据库层面只会存在一条有效记录。前端做到的是保证请求不带重、报错有提示、成功后数据刷新把并发压力留给后端事务去抵御。3.4 Vuex还是Pinia用不用状态管理要看项目的规模早期基于Vue.js的选课系统大多用Vuex项目直接内置了Vuex实例的也很多。但对于一个选课流程来说全局共享状态其实不算多无非是当前登录用户的信息和已选课程列表。如果已选课程只在“我的选课”页面用放到组件内部的data里完全足够如果侧边栏的已选数量需要在多个页面同步显示才值得放进全局状态。我的习惯是先看包里的router配置幅度。页面超过5个、页面间有跨组件数据联动用Vuex合理页面就三四个、数据请求都从后端拉硬套Vuex只会让代码多了store和mutations的样板。Vue 3项目可以切换到Pinia它去掉了mutations概念、API更简洁但如果是Vue 2老项目继续用Vuex就好不要为了新而新。选课系统这种规模的项目状态管理的体量控制在100行以内是正常的超过这个数说明有些数据原本放组件state里就够了。4. 数据库设计与后端接口一张选课关系表怎样扛住高峰4.1 三张核心表和一个关系表字段设计决定业务边界选课系统的数据模型围绕两条主线课程信息和选课关系。课程表存课程的固定属性学生表存学生基本信息选课关系表存“某学生选了某门课”这个动作以及动作产生的成绩、选课时间等状态。下面是最常见的建表语句。CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARSET utf8mb4; USE course_selection; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, grade_year INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, teacher VARCHAR(50), credit DECIMAL(3, 1), capacity INT DEFAULT 0, selected_count INT DEFAULT 0, start_week INT, end_week INT, schedule VARCHAR(100) ); CREATE TABLE student_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5, 2) DEFAULT NULL, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) );这个设计里要注意的点student_course表上的uk_student_course唯一约束是选课防重的第一道闸门它保证同一学生同一课程绝对不会有重复记录这个约束比前端任何判断都硬。course表里同时存capacity和selected_count两个字段是为了让查询无需关联子表就能知道某门课还剩多少名额代价是每次选课成功后要多执行一条UPDATE语句维护计数。用course_no代替name做关联可以避免因为某门课改名导致的历史数据歧义。4.2 选课接口的事务边界先查后写为什么会超选选课接口的核心逻辑是“检查名额是否满→插入选课记录→更新已选人数”。这三步操作如果在没有事务保护的代码里依次执行并发请求时序错开时就会出现超选。典型场景课程容量只剩1个两个学生同时发起选课请求后端线程A查询时发现selected_count0未满线程B在A插入之前也做了同样的检查两人都执行了插入逻辑最终该课程选上了2名学生超了容量。Transactional public synchronized String selectCourse(Long studentId, Long courseId) { Course course courseMapper.selectByPrimaryKey(courseId); if (course.getSelectedCount() course.getCapacity()) { return 课程已满; } StudentCourse sc new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); try { studentCourseMapper.insert(sc); } catch (DuplicateKeyException e) { return 你已经选过这门课了; } courseMapper.increaseSelectedCount(courseId); return 选课成功; }这段代码里有两个细节值得说清楚。synchronized关键字作用于方法时锁的是当前service实例单实例部署下能有效防止并发穿破门槛但多实例部署时synchronized就失效了需要换分布式锁或数据库层面的悲观锁。increaseSelectedCount执行的是UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity这样的条件更新只影响1行才代表更新成功这比先查后更新更安全。唯一约束捕获到DuplicateKeyException后直接返回“已经选过”从数据库级挡住了重复提交。4.3 接口设计规范统一返回体让前后端联调少一半时间前端登录时一直在跟后端打交道返回结构的统一程度直接影响开发效率。一个稳定的返回体结构应该长这样{ code: 200, message: success, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., userInfo: { id: 1, studentNo: 20240001, name: 张三 } } }code表示业务状态码200是成功非200是失败message是给前端直接展示的提示信息data里放真正的业务数据。后端封装这个结构时要注意异常分支也要返回同样的格式而不是抛一个默认的500页面给前端。前端封装axios响应拦截器时先判断code再返回到组件组件里只需要处理data不需要每个页面都做error分支。// request.js 统一请求封装部分逻辑 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { this.$message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { this.$message.error(error.response.data.message || 网络异常) return Promise.reject(error) } )我还习惯在响应拦截器里加一个401处理当后端返回未认证或token过期时清除本地token并跳转登录页。很多源码包里这部分是缺的导致会话过期后页面还在点选课弹报错用户体验非常糟糕。一段不到十行的处理能省掉后续一堆问题。5. 避坑指南四个让新手疯狂折腾的细节5.1 前端端口与后端端口的“撞车事故”现象前端npm run dev启动成功但登录时接口全部404Network面板里请求地址显示的端口是8080而后端明明在9090。原因vue.config.js里的proxy没有配置或者配置了但改错了作用目标。前端页面是Vue CLI的devServer启动的它默认监听8080接口请求也打到同一个地址这个地址上没有后端服务自然404。解决将后端启动端口改成非8080的端口比如9090在vue.config.js里配置proxy把所有/api请求转发到9090。另一个连带问题如果后端框架本身占用了8080前端devServer启动会失败提示端口被占用或自动切换到8081这时候访问地址变成了8081不是你想的8080后端接口地址也要同步改。5.2 SQL文件编码不统一中文课程名全部变问号现象导入建库脚本后表结构正常但执行数据脚本后课程表里的中文变成了“???”。原因Windows环境下以GBK编码写出的.sql文件被MySQL以UTF-8字符集读取中文二字节被截断成了乱码。查字符集设置会发现表本身是utf8mb4问题出在文件的字节流上。解决用文本编辑器打开.sql文件另存为UTF-8 without BOM格式再重新导入。不要直接在命令行mysql -u root -p data.sql而是用Navicat的“运行SQL文件”功能并指定UTF-8编码。BOM头有时候也会导致MySQL第一行报错另存为不要带BOM是关键。5.3 密码字段用MD5明文存储登录接口“永远成功”但查不到人现象前端登录提示密码错误但数据库里确认密码字段是对的或者注册新账号后立刻登录成功但重启服务后又登录失败。原因源码包里用户表的密码字段预置了密文但你通过SQL直接插入的测试用户存的是明文前端提交的密码经过加密算法处理后与库里明文不匹配自然报错。解决先看代码里的密码处理逻辑。如果前端登录时用md5或bcrypt加密后提交那SQL插入测试账号时也要用同样的加密方式生成密码字段。对测试数据没兴趣的直接用源码包里初始化的学生账号登录管理员账号同理别自己去INSERT一条新记录当管理员。5.4 Vue 2与Vue 3的组件库冲突Element UI和Element Plus不能混用现象前端npm install之后页面能打开但表单组件渲染不出来控制台报错“Unknown custom element: ”。原因项目基于Vue 2写的组件模板却因为新安装的依赖把Vue 3的组件库拉进来或者项目原本用的Element UI被升级成Element Plus两者注册方式不同导致组件标签无法识别。解决打开package.json看vue版本。vue版本是2.x就用element-uivue是3.x就用element-plus。安装组件库时手动锁版本比如npm install element-ui2.15.14不要裸装最新版。装错之后的卸载重装是最快修复途径另外检查main.js里是否同时注册了新旧两个组件库有的话删掉一个。6. 拿到这个源码包后怎么才能高质量地变成“自己的系统”选课系统的课程设计答辩现场评委最爱问的是“哪些是你自己写的、系统有什么难点”。直接照搬源码包的风险在于老师翻过太多同款项目一眼就能认出原生代码的长相。我的建议是不要重写而是在现有骨架上做三个可见的增量改动。第一个增量给课程冲突检测加进选课逻辑。现成的系统可能没处理“周一第3节选了A课就不能选在同一时段的B课”。在course表里增加week和period字段选课时写一段查询判断该学生已选课程中是否存在相同week且period重叠的条目有则拒绝选课。这段逻辑虽然不长但它牵涉到数据库查询设计和接口改造答辩时讲得出查询步骤和边界情况含金量比“我熟练使用npm install”高得多。第二个增量把选课结果导出功能补上。管理员需要统计某门课的选课学生名单用前端生成CSV导出是最快实现路径。// 导出选课名单为CSV部分代码 const exportCsv (rows) { const header [学号, 姓名, 课程名称, 选课时间] const content rows.map(r [r.studentNo, r.name, r.courseName, r.selectTime].join(,)) const csv [header.join(,), ...content].join(\n) const blob new Blob([\ufeff csv], { type: text/csv;charsetutf-8 }) const url URL.createObjectURL(blob) const a document.createElement(a) a.href url a.download 选课名单.csv a.click() URL.revokeObjectURL(url) }这段代码里值得讲的细节是前面拼接了\ufeff也就是UTF-8 BOM不加它的话生成的CSV用Excel打开时中文列名会乱码。实际开发里这个细节经常被忽略我在真做过一个导出工具踩过这个坑后来每个导出功能都自动带上BOM。第三个增量把前端打包部署到生产环境。开发模式用npm run dev答辩演示或部署到校园服务器时要用构建模式。npm run build之后dist目录是一堆静态文件用Nginx托管再把/api反向代理到后端端口即可。server { listen 80; server_name localhost; location / { root /opt/course_selection/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files指令在history模式下极其重要不写的后果是前端路由刷新后出现404页面。这个坑我在部署一个内部管理系统时遇到过刷新URL直接白屏排查半天才发现是Nginx没有fallback到index.html。把前后端分离部署跑通之后再去本地把后端改造成“多实例部署时选课不超卖”的版本这段分布式事务经验完全可以支撑毕业设计或者技术面试的深入追问。这套源码下载下来第一遍先照搬跑通第二遍按上述三个方向改改完再回读一遍自己的代码能讲清楚每个文件为什么存在这门课的作业就不只是“一个系统”了而是一次完整的工程训练。希望帮到你。本文还有配套的精品资源点击获取