1. 项目概述与核心需求拆解高校学生选课系统这可能是国内计算机专业学生接触最频繁、毕业设计选题率最高的一类项目了。每年到毕业季我都能收到大量关于选课系统的咨询——有的人是拿来当课设交差有的人是想跑通后改成自己的毕设还有人是真心想通过这个项目把 SpringBoot 和 Vue 的整套开发流程吃透。不管你是哪种目的这套springboot vue高校学生选课系统都值得认真看一下。先说这个项目是干什么的。简单来说它模拟了高校里最真实的教务场景学生登录系统、浏览课程列表、选择自己感兴趣的课程、查看已选课程和成绩教师登录系统、发布课程、录入成绩管理员维护基础数据比如学生信息、教师信息、课程信息等。整个系统分三个角色覆盖了从课程发布到学生选课再到成绩管理的完整闭环。它适合谁来学、谁来用如果你是Java 后端方向的学生这个项目能让你把 SpringBoot 的自动装配、MyBatis-Plus 的数据库操作、事务管理等知识点串起来如果你是前端方向的学生Vue 的组件化开发、Axios 请求封装、路由守卫这些核心技能也都能练到如果你只是想快速跑通一个毕设系统那这份源码配合数据库脚本基本可以做到开箱即用把精力集中在论文和答辩准备上。这里我多说一句很多同学拿到源码第一件事就是急着启动项目结果被各种环境问题卡住然后开始怀疑人生。我见过太多这样的情况——不是代码有问题而是对项目结构缺乏基本认知。所以这篇博文我不打算只贴几个文件路径就完事而是把整个系统的架构思路、数据库设计、后端核心逻辑、前端页面实现、常见坑点都拆开揉碎了讲。你照着走一遍不仅能把项目跑起来还能在答辩时说得头头是道。2. 系统架构设计与技术选型分析2.1 为什么是 SpringBoot Vue 这个组合先聊技术选型。现在高校课设里SpringBoot Vue 基本上是事实标准了但很多同学其实没想明白为什么是它。先说后端。传统的 SSMSpring SpringMVC MyBatis不是不行但配置文件一堆光是 Spring 和 MyBatis 的 XML 配置就能劝退一大半新手。SpringBoot 的核心思想是“约定优于配置”把大量常规配置直接内置了你只需要在application.yml里写几行关键配置再配合起步依赖Starter就能快速搭建一个可运行的 Web 服务。对课设项目来说这大幅降低了搭建成本让你把时间花在业务逻辑上而不是配置文件上。再说前端。Vue 火到今天不是没道理的它有几个特点特别适合这类管理系统组件化开发让代码复用率极高比如课程卡片、分页组件、表单弹窗写一次就能在多个页面复用响应式数据绑定让界面和数据的联动变得非常自然后端返回的数据一赋值页面自动更新再加上 Element UI 或者 Element Plus 这类组件库表格、表单、弹窗、消息提示这些后台管理的常客都有现成的界面做出来干净整洁答辩时视觉分是有的。2.2 系统角色与核心功能模块在动数据库之前先把需求理清楚。虽然是课设项目但功能设计要能自圆其说。我按角色来拆学生端登录注册账号密码登录使用 JWT 做身份认证课程浏览按课程名称、教师、上课时间等条件筛选选课操作选中课程后提交选课需检测时间冲突、学分上限、限选人数我的课表查看已选课程支持退选成绩查询查看课程考核成绩教师端课程管理发布新课程、修改课程信息、查看选课学生名单成绩管理给选了课的学生录入成绩管理员端用户管理对学生的增删改查与账号启用禁用课程管理审核或调整课程信息选课统计查看每门课的选课人数与选课率这个功能划分不算复杂但覆盖面足够广——CRUD、权限控制、一对多关联查询、事务处理全都有了作为课设项目来说是“心机”很重的设计。它不会难到让你做不出来但每一个模块你都能讲出技术点来。2.3 核心流程梳理选课系统最关键的业务流程用大白话讲是这样的教师发布一门课程指定课程名称、学分、上课时间、上课地点、限选人数。学生登录系统浏览所有可选的课程列表点击选课时系统要立刻做几道检查这个学生是否已经选过这门课不能重复选、选课人数是否已满不能超选、这门课的上课时间是否和已选课程冲突不能时间重叠、加上这门课之后总学分是否超过限制。所有检查都通过才写入一条选课记录。这里面的门道在于选课系统是一个典型的高并发写场景尤其在选课高峰期几百个学生同时点选同一门课如果代码里不做并发控制很容易出现“显示还有名额但提交时提示已满”或者更严重的“实际选课人数超过限选人数”的情况。等下我在后面专门讲这个问题的解决方案这里先不展开。3. 数据库设计——选课系统的地基3.1 核心数据表结构与关系数据库是选课系统的地基。表结构设计得合理后面所有业务逻辑都顺畅表设计得不好写 SQL 的时候你会怀疑人生。这个项目的数据库一共用了 5 张核心表我来逐个拆解。用户表sys_user这是用户的基础表学生、教师、管理员统一存放在这里用role字段区分角色。CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 登录密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) NOT NULL COMMENT 角色STUDENT/TEACHER/ADMIN, status tinyint(1) DEFAULT 1 COMMENT 状态0禁用 1启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个很常见的设计问题为什么把学生、教师、管理员合成一张表而不是拆成三张表我的答案是——对课设项目来说合表会让登录逻辑异常简洁。你只需要查询一次sys_user就能完成身份验证然后根据role字段决定跳转到哪个页面。如果拆成三张表登录时你得先判断输入的用户名属于哪类角色再查对应表多一步判断不说用户体验也更差。当然如果想做得更专业可以把公共字段放用户表学生师和教师的专属字段单独扩展这属于进阶优化课设阶段不需要。课程表courseCREATE TABLE course ( id int(11) NOT NULL AUTO_INCREMENT, course_name varchar(100) NOT NULL COMMENT 课程名称, credit decimal(3,1) DEFAULT 0.0 COMMENT 学分, teacher_id int(11) NOT NULL COMMENT 授课教师ID, course_time varchar(100) DEFAULT NULL COMMENT 上课时间, course_location varchar(100) DEFAULT NULL COMMENT 上课地点, max_students int(11) DEFAULT 50 COMMENT 限选人数, selected_count int(11) DEFAULT 0 COMMENT 已选人数, description text COMMENT 课程简介, status tinyint(1) DEFAULT 1 COMMENT 状态0下架 1上架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;选课表student_course这张表是学生和课程之间的关联表也是整个系统业务量最大的表。CREATE TABLE student_course ( id int(11) NOT NULL AUTO_INCREMENT, student_id int(11) NOT NULL COMMENT 学生ID, course_id int(11) NOT NULL COMMENT 课程ID, score decimal(5,1) DEFAULT NULL COMMENT 成绩, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;请注意这里唯一键的设计uk_student_course的用途后面讲防重复选课时会用到这是一个很重要的细节。教师信息表和学生信息表可扩展字段我就不单独列了它们在sys_user基础上做一对一扩展保存学号、工号、学院、专业等额外信息。3.2 让数据表之间“有机关联”表关系用文字描述就是sys_user教师与course是一对多关系一个教师可以开多门课sys_user学生与course是多对多关系一个学生可以选多门课一门课可以被多个学生选这个多对多通过student_course中间表来维系。表之间要建立外键关联吗我的建议是业务上用逻辑外键不建物理外键。逻辑外键指的是在代码里通过JOIN或关联查询来维护表间关系而不是在数据库层面ALTER TABLE ADD CONSTRAINT FOREIGN KEY。原因很简单物理外键在删除和更新时会因为约束产生很多限制课设项目里经常要改数据物理外键容易导致误删受阻或者连锁删除异常。合理的做法是Java 实体类中关联字段仍然保留SQL 查询时用JOIN来拿关联数据。3.3 数据库设计中的两个关键决策第一个是selected_count字段的问题。有些设计者喜欢在选课表里COUNT(*)统计已选人数而不在课程表里冗余一个selected_count字段。但从性能角度讲每次选课都COUNT(*)全表统计在高并发场景下很吃力。冗余这个字段选课时直接UPDATE course SET selected_count selected_count 1性能会好很多。当然这也带来了数据一致性的问题等下讲并发控制时一起解决。第二个是时间冲突检测的数据支撑。course_time字段我用的是字符串存储比如周一 3-4节、周三 5-6节。这种设计对课设来说是最简单的方案虽然它能做的事有限但应付查询和冲突判断足够了。如果你想做得更专业可以把星期和节次拆成独立字段存储甚至按节次建一张时间表但那就会让选课冲突检测的代码复杂一个量级。课设嘛能讲清楚现状方案的优缺点答辩时反而显得你考虑过拓展。4. 后端核心功能实现与关键代码解析4.1 工程结构与启动流程拿到源码后的第一件事是先看工程结构做到心中有数。一个典型的 SpringBoot MyBatis-Plus 选课系统包结构大概是这样的com.example.course ├── config # 配置类CORS跨域、拦截器、JWT过滤器 ├── controller # 控制器层接收前端请求 ├── service # 业务逻辑层核心业务处理 │ └── impl # 业务实现类 ├── mapper # 数据访问层MyBatis-Plus的Mapper接口 ├── entity # 实体类对应数据库表 ├── dto # 数据传输对象接收前端参数、返回给前端的封装 ├── common # 通用类统一返回结果、异常处理、常量定义 ├── utils # 工具类JWT工具、密码加密工具 └── CourseApplication.java # 启动类application.yml里的核心配置长这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意到map-underscore-to-camel-case: true这个配置没有它让数据库里的下划线字段名real_name能自动映射到 Java 实体里的驼峰属性realName少写一堆TableField注解。4.2 选课接口的核心实现整个系统里最有技术含量的是选课接口。我的代码逻辑是这样的Override Transactional(rollbackFor Exception.class) public Result addCourse(SelectedCourseDTO dto, Long studentId) { // 1. 查询课程信息 Course course courseMapper.selectById(dto.getCourseId()); if (course null) { return Result.error(课程不存在); } if (course.getStatus() 0) { return Result.error(该课程已下架); } // 2. 带条件更新实现乐观锁防止超选 Integer maxStudents course.getMaxStudents(); Integer selectedCount course.getSelectedCount(); if (selectedCount maxStudents) { return Result.error(该课程选课人数已满); } // 关键利用条件更新保证并发安全的选课人数递增 int updated courseMapper.updateSelectedCount(course.getId(), maxStudents); if (updated 0) { return Result.error(选课人数已满请选择其他课程); } // 3. 检查是否重复选课 Long count studentCourseMapper.selectCount( new LambdaQueryWrapperStudentCourse() .eq(StudentCourse::getStudentId, studentId) .eq(StudentCourse::getCourseId, dto.getCourseId()) ); if (count 0) { return Result.error(你已选过该课程请勿重复选择); } // 4. 检查时间冲突 ListStudentCourse selectedCourses studentCourseMapper.selectList( new LambdaQueryWrapperStudentCourse() .eq(StudentCourse::getStudentId, studentId) ); ListLong courseIds selectedCourses.stream() .map(StudentCourse::getCourseId).collect(Collectors.toList()); if (!courseIds.isEmpty()) { ListCourse selectedCourseList courseMapper.selectBatchIds(courseIds); for (Course selected : selectedCourseList) { if (courseTimeConflict(course.getCourseTime(), selected.getCourseTime())) { // 选课失败了要把刚才递增的已选人数还原 courseMapper.decrementSelectedCount(course.getId()); return Result.error(选课时间与[ selected.getCourseName() ]冲突); } } } // 5. 写入选课记录 StudentCourse studentCourse new StudentCourse(); studentCourse.setStudentId(studentId); studentCourse.setCourseId(course.getId()); studentCourseMapper.insert(studentCourse); return Result.success(选课成功); }这个接口的代码逻辑不算复杂但有一个细节非常关键就是第 2 步和第 4 步之间的配合。我先通过乐观锁把selected_count递增掉相当于先占一个位置再去检查时间冲突和重复选课。如果后面检查不通过就把人数还原。这套“先占位再检查”的思路比“先检查再占位”在并发场景下稳得多——试想一下如果先检查名额足够再写选课记录最后才增加已选人数那两个并发请求可能同时通过检查最后导致超选。对应的 Mapper 里写的那个条件更新 SQL 长这样update idupdateSelectedCount UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count #{maxStudents} /update这段 SQL 比 Java 层判断更可靠因为它是数据库层面的原子操作。多个请求同时到来时数据库会串行化执行这个UPDATEselected_count maxStudents条件确保最多只有max - min个请求能真正成功递增。4.3 JWT 认证与登录流程选课系统的接口不能裸奔必须做权限控制。我用的方案是 JWT SpringBoot 拦截器。登录接口的流程是接收用户名密码在sys_user表里查询用 BCrypt 做密码比对密码在注册时就加密存储绝不能明文保存比对通过后生成一个 JWT token 返回给前端。前端后续每个请求都在Authorization请求头带上这个 token。后端有一个拦截器统一处理 token 校验Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token将用户信息放入request Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }JWT 有个好处是服务端不需要存 session天然适合前后端分离。但有几个坑你要注意token 有效期要设置合适太短了用户频繁掉线太长了不安全密钥别写死在代码里至少放到配置文件中前端要配合路由守卫做跳转否则 token 过期后用户会卡在页面里没有反应。4.4 统一结果返回与全局异常处理项目里控制层接口的返回类型是Result这是我自己封装的一个统一返回体public class Result { private Integer code; // 200成功500失败 private String message; // 提示信息 private Object data; // 返回数据 public static Result success() { ... } public static Result success(Object data) { ... } public static Result error(String message) { ... } }为什么非得包一层 Result因为如果不统一返回结构前端处理逻辑会非常痛苦——有的接口成功时返回对象失败时返回字符串前端代码里全是if (typeof res.data string)这种丑陋判断。统一之后前端只需要判断code 200就能决定后续操作出错信息统一从message里取。异常处理方面我写了一个RestControllerAdvice全局异常处理器把业务异常和系统异常分开处理。业务异常比如选课冲突返回 code 500 具体提示文案系统异常统一记录日志返回“系统繁忙请稍后重试”避免把数据库异常详情暴露给前端这是安全性的基本要求。5. 前端 Vue 模块实现与对接细节5.1 前端工程结构与路由设计Vue 这边我用的是 Vue 2 生态 Element UI如果你拿到的是 Vue 3 版本那就是 Element Plus原理是相通的。工程结构大概是这样的src ├── api # 接口请求模块 │ ├── course.js # 课程相关接口 │ ├── user.js # 用户相关接口 │ └── select.js # 选课相关接口 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 │ └── index.js # 路由及守卫 ├── store # Vuex状态管理 ├── views # 页面视图 │ ├── login.vue # 登录页 │ ├── student # 学生端页面 │ │ ├── CourseList.vue │ │ ├── MyCourse.vue │ │ └── Score.vue │ ├── teacher # 教师端页面 │ └── admin # 管理端页面 ├── App.vue └── main.js路由用到了动态路由的思路根据登录用户的角色向后端请求对应的菜单权限然后动态添加路由规则。这样学生登录后看不到教师的管理菜单安全性和体验都更好。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } if (token to.path /login) { next(/) return } next() })5.2 Axios 请求封装与拦截器前端要和后端对接Axios 封装是必不可少的。我的request.js大致长这样import axios from axios import { ElMessage } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动附带token 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然后通过 devServer 的代理转发到后端 8080 端口这样前后端联调时不会出现跨域问题另一个是响应拦截器里对401状态码的统一处理——当 token 过期时自动清理本地 token 并跳转登录页用户不需要手动刷新页面。你可能会有疑问请求拦截器里已经带了 token为什么后端还需要单独配置 CORS这是因为开发环境下用 webpack 代理可以绕过跨域但如果你把前端打包后部署到 Nginx或者直接用 IP 访问后端接口跨域问题就会重新出现。所以后端 CORS 配置还是要加上两条腿走路才稳。5.3 学生选课页面的交互设计选课页面是这个系统前端最核心的页面。我用的布局是左侧课程列表展示点击“选课”按钮弹出确认框。选课按钮的交互逻辑分两层页面加载时先请求后端拿当前学生的已选课程 ID 列表渲染列表时如果课程 ID 在已选集合里按钮就置灰显示“已选”点击选课按钮时调用后端接口根据Result.code判断是否弹成功提示还是错误提示。这里有个体验细节选课失败时的错误提示要具体不能只弹一个“选课失败”。比如后端返回“该课程选课人数已满”前端就要直接把这个 message 展示出来让用户明确知道失败原因。这依赖于后端 Result 封装时的 message 字段所以前后端联调时的接口文档非常重要字段含义要对齐。时间冲突的提示也需要做在前端。我用了 Element UI 的MessageBox确认弹窗用户点“选课”后首先前端校验时间是否冲突——前端先用已加载的课程时间做一次快速判断能提前拦截一部分错误请求后端再二次校验保证数据正确性。这种“前端体验优先、后端数据兜底”的思路在答辩时是加分项。5.4 打包与部署注意事项前端开发完成后npm run build会生成dist目录里面是纯静态文件。部署方式主要有两种一是把dist目录丢到 Nginx 下Nginx 同时把/api路径反向代理到后端的 8080 端口二是直接把前端打包后的static目录放进 SpringBoot 项目的src/main/resources/static下这样前端和后端就合并成一个服务了一个 Java 进程跑起来整个系统都能用。我个人的建议是课设答辩时用第二种方式部署简单省得现场演示时还要在评委面前启动两个服务。代码里再配合WebMvcConfigurer配置一下路径映射让 SpringBoot 把前端路由的 history 模式正确回退到index.html就不会出现页面刷新后 404 的问题了。6. 常见问题排查与避坑实录6.1 数据库连不上多半是这三个原因拿到源码后最常出现的问题不是代码报错而是项目启动直接挂在数据库连接上。我总结下来无非三个原因第一是 MySQL 版本和驱动版本不匹配。如果你用的是 MySQL 8.x驱动必须要用com.mysql.cj.jdbc.DriverserverTimezone也得写成Asia/Shanghai而 MySQL 5.7 及以下用com.mysql.jdbc.Driver都行记不住就全用com.mysql.cj.jdbc.Driver它向下兼容。第二是密码没改application.yml里的password一定要换成你自己的 MySQL 密码前阵子有人问“为什么报 Access denied”我一问才发现密码还是默认的123456。第三是数据库没导入course_system数据库需要先用 Navicat 或命令行执行项目自带的course_system.sql脚本没有数据表项目自然起不来。这三个问题按照“驱动版本 → 账号密码 → 数据是否存在”的顺序排查基本都能解决。排查速度最快的方式是把mybatis-plus.configuration.log-impl打开控制台能看到真实的 SQL 和异常堆栈比瞎猜快得多。6.2 跨域问题的终极处理方案前后端分离项目绕不开跨域。开发环境下的跨域通常用 Vue CLI 的 devServer proxy 解决在vue.config.js里写一段代理配置就能屏蔽跨域但如果代理配置没错还是跨域可以看看是不是axios的baseURL写成了完整地址http://localhost:8080正确写法是只写/api让浏览器把请求发到前端自己的域名下由代理转发。生产环境下的跨域最稳的方案是后端加 CorsFilter 配置放行所有来源和所有请求头。课设项目不需要太精打细算allowedOriginPatterns(*)就够了。注意是allowedOriginPatterns不是allowedOrigins后者在高版本 Spring Boot 里配合allowCredentials(true)会出问题。6.3 SpringBoot 版本太高引发的依赖问题这个坑我必须单独拎出来讲。热词里有个“springboot版本太高”搜得很高频说明很多人都栽在这上面。SpringBoot 3.x 和 2.x 有一个最大的区别3.x 基于 Jakarta EE包名从javax.*换成了jakarta.*。如果你从网上找的教程或博客是在 SpringBoot 2.x 下写的里面的javax.servlet、javax.annotation这些导入在 3.x 环境里直接编译不过。另外MyBatis-Plus 的版本也要配套——SpringBoot 3.x 必须用 MyBatis-Plus 3.5.3 以上的适配版本旧版连启动都会报ClassNotFoundException。我建议课设项目不要盲目追求新版本。如果你的目标是把项目跑起来、顺利答辩用 SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 8.x 是比较稳定的组合。网上资源最多遇到问题搜解决方案一搜一大把这比用最新版但到处踩坑要划算得多。6.4 选课并发超选——理论与实践选课系统最容易在答辩现场露怯的是评委问“如果几百个人同时选同一门课程你怎么防止超选”。很多同学在演示时因为是单人操作看不出来问题一被问到就紧张。这里我给大家梳理清楚数据库层面UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count max_students这个条件更新是并发安全的它是靠数据库的行锁来保证的——同一门课的行在同一时刻只能有一个事务在执行这个 UPDATE。业务层面Transactional保证选课记录插入和人数递增要么同时成功要么同时失败。这两层配合超选问题就基本控制了。如果你还想再进阶一点可以在student_course表加一个student_id course_id的唯一索引从数据库层面扼杀重复选课的可能性这正是我前面提到的uk_student_course的作用。这种“数据库兜底 应用层业务校验”的双保险写法在技术答辩时是很有说服力的。7. 从零到一跑通项目的实操指南最后是实操环节按这个步骤走基本能一次跑通前置环境确认JDK 1.8 或 11推荐 1.8兼容性最好Maven 3.6MySQL 5.7 或 8.xNode.js 14Vue 2 项目不建议用 Node 18 以上版本IDEA 或 Eclipse推荐 IDEA社区版就够用步骤一导入数据库用 Navicat 或命令行进入 MySQL执行项目根目录下的sql/course_system.sql。执行完毕后确认sys_user、course、student_course这几张表都创建成功。步骤二修改后端配置用 IDEA 以 Maven 工程的方式导入course-backend目录修改application.yml中的数据库账号密码在 Maven 面板里先clean再package然后启动CourseApplication。看到“Started CourseApplication”字样后端就没问题了。步骤三启动前端用 VSCode 或 WebStorm 打开course-frontend目录依次执行npm install和npm run serve浏览器访问http://localhost:8081就能看到登录页面。步骤四测试登录管理员账号在数据库初始化脚本里通常有一个admin/admin123教师账号比如teacher/123456学生账号比如student/123456。用不同角色登录可以体验不同的功能模块。整个流程正常情况下一小时内能跑通遇到问题不要慌按上面第六节的排查方法逐个对照。8. 写在最后的一点经验我带过的学生里有人三天跑通项目有人卡在环境配置上整整一周。最大的区别不在于基础好坏而在于调试的思路——是盲目百度、瞎猜乱试还是先看日志、定位问题、再搜解决方案。所以我特别建议你拿到源码之后不要急着改代码先把数据库脚本和执行日志过一遍你就能对这个系统有一个全局认识。另外如果你打算拿这个项目作为毕业设计我强烈建议你往里加一个自己的功能点。哪怕只是在原来的基础上多做一个“选课排行榜”或者“课程收藏”功能写进毕业论文里都是你独立完成的亮点。做一个你完全能讲清楚的项目远比塞一个华丽但讲不明白的系统要踏实得多。希望这篇拆解能帮你在选课系统这个项目上少走弯路。你照着搭一遍遇到问题再回来对照排查应该都能顺利解决。