简介:这是一套面向计算机专业本科生的毕业设计级学生网上选课系统实战资源,基于Spring Boot与Vue实现前后端分离架构,解决高校课程管理数字化与学生选课流程线上化的核心需求。资源包共417个文件,含109个Java后端业务逻辑与控制器代码、56个Vue组件及页面源码、161个SVG图标资源、19个PNG/JPG界面素材、13个XML配置与Mapper映射文件,以及SQL建表脚本、YML配置、BAT一键部署脚本和万字设计文档(含数据库ER图、接口说明与系统流程图),整体压缩包仅21.82MB,结构清晰、模块完整。已有33人下载学习,适合Java Web开发初学者通过可运行项目掌握Spring Boot整合MyBatis、Vue CLI工程搭建、RESTful接口联调及MySQL事务处理等关键技术点,配套文档详述选课冲突检测、权限分级控制与课程余量实时更新等核心业务实现逻辑,助力毕设落地与工程能力提升。
1. 项目缘起:从“手动排课”到“在线选课”的必然演进
每到新学期,教务处的老师就忙得焦头烂额。我还记得几年前在一所高校做技术支持时,亲眼目睹的场景:办公室里堆满了纸质选课单,几个老师围着一张大表格,用不同颜色的笔勾画、计算,电话铃声此起彼伏,全是学生来咨询课程冲突、名额已满的问题。一个简单的调课,可能就需要手动翻阅几十张表格,耗时大半天。这种传统的人工选课模式,不仅效率低下、容易出错,更无法满足学生个性化的学习需求,也无法为教学管理提供及时、准确的数据支持。
正是这种“痛点”,催生了学生网上选课系统的需求。它本质上是一个将教务管理流程数字化的信息平台,核心目标就三个:让学生选课更自主、更便捷;让老师排课、管理更轻松、更高效;让教务部门的数据统计更精准、决策更有依据。而“基于SpringBoot+Vue”的技术栈,几乎是当前实现这类前后端分离管理系统的“黄金组合”。SpringBoot以其“约定大于配置”的理念,能让我们快速搭建稳定、可扩展的后端服务;Vue.js则以其渐进式的特性和友好的学习曲线,让构建交互流畅、用户体验良好的前端页面变得简单。这个组合拳,完美契合了选课系统对高并发处理、前后端解耦、快速迭代的要求。
所以,当我们要动手实现一个这样的系统时,我们不仅仅是在写代码,更是在设计一套完整的业务流程和用户体验。接下来,我将以一个实际构建者的视角,拆解这个“学生网上选课系统”从设计到实现的全过程,分享其中的技术选型思考、核心模块设计、以及那些在文档里不会写的“踩坑”经验。
2. 技术栈深度剖析:为什么是SpringBoot + Vue?
在开始动手之前,我们必须彻底理解手中工具的特性与边界。选择SpringBoot和Vue,并非盲目跟风,而是基于项目实际需求和技术栈成熟度做出的综合判断。
2.1 后端基石:SpringBoot的“快速启动”与“生态护航”
SpringBoot的核心价值在于它极大地简化了基于Spring框架的应用初始搭建和开发过程。对于选课系统这类典型的Web应用,它提供了开箱即用的解决方案。
2.1.1 依赖管理与自动配置:告别繁琐的XML传统的Spring项目需要大量XML配置来定义Bean、数据源、事务管理等。SpringBoot通过spring-boot-starter-*系列依赖,将这些配置“打包”好了。对于我们的选课系统,核心依赖通常包括:
spring-boot-starter-web: 提供嵌入式Tomcat和Spring MVC,用于处理HTTP请求。spring-boot-starter-data-jpa: 或mybatis-spring-boot-starter,用于简化数据库操作。JPA更适合快速原型和标准CRUD,MyBatis则对复杂SQL和精细化控制更友好。选课系统的查询(如按条件筛选课程、统计选课人数)可能比较复杂,我个人更倾向于使用MyBatis,灵活性更高。spring-boot-starter-security: 用于实现认证和授权。选课系统涉及学生、教师、管理员等多角色,权限控制(如学生只能选课,教师只能管理自己课程,管理员能操作所有)是重中之重。spring-boot-starter-validation: 用于参数校验,确保从前端接收的数据(如学号、课程编号)符合规则。
在pom.xml中,我们只需引入这些starter,版本由spring-boot-dependencies统一管理,极大减少了依赖冲突的可能性。
2.1.2 嵌入式容器与一键部署SpringBoot内置了Tomcat、Jetty或Undertow等Servlet容器。这意味着我们开发完的应用程序是一个可以直接通过java -jar运行的、独立的JAR文件。这对于后期部署到Linux服务器至关重要,我们不需要额外配置复杂的Web容器环境。在application.yml或application.properties中,可以轻松配置服务器端口、上下文路径、SSL等。
server: port: 8080 servlet: context-path: /course-api spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver2.1.3 生产级特性:监控与健康检查这是SpringBoot容易被忽略但极其重要的优势。通过引入spring-boot-starter-actuator,系统会自动暴露一系列监控端点(如/actuator/health,/actuator/metrics)。在选课系统面临高并发选课(比如热门课程开放瞬间)时,我们可以通过这些端点快速了解应用状态(数据库连接是否正常、内存使用情况等),这对于运维和快速排错至关重要。
2.2 前端利刃:Vue.js的“响应式”与“组件化”
选课系统的前端需要频繁地与用户交互:点击选课按钮、筛选课程列表、查看课表等。Vue.js的响应式数据绑定和组件化开发模式,让这些交互的实现变得直观且高效。
2.2.1 核心特性:响应式数据驱动视图Vue的核心是一个响应式系统。我们定义一个数据对象(例如,一个包含课程列表的数组courseList),当这个数组发生变化时(比如从后端API获取到新数据),Vue会自动更新依赖此数据的DOM部分。这意味着我们不需要手动操作DOM(如jQuery时代常用的$(‘#list’).append(...)),只需关心数据逻辑。在选课场景中,当学生点击“选课”后,我们只需向后端发送请求,成功后更新本地courseList中对应课程的状态(如selected: true),页面上的按钮状态和已选课程列表会自动刷新。
2.2.2 工程化与开发体验:Vue CLI与Vite现在很少有人直接从<script>标签引入Vue来开发项目了。Vue CLI(或现在更流行的Vite)为我们提供了标准化的项目脚手架。通过几条命令,就能得到一个集成了Babel、ESLint、Webpack/Vite等现代前端工具链的项目结构。这对于团队协作和代码规范统一非常重要。例如,使用Vite创建项目:
npm create vue@latest然后按照提示选择需要的特性(TypeScript, Router, Pinia等)。这保证了我们项目的起点就是工程化、模块化的。
2.2.3 生态核心:Vue Router与状态管理
- Vue Router:管理前端路由。选课系统至少需要:登录页(
/login)、学生主页(/student/dashboard)、课程列表页(/student/courses)、我的课表页(/student/timetable)、教师管理页(/teacher/...)、管理员后台(/admin/...)等。Vue Router使得构建单页面应用(SPA)成为可能,页面切换无需刷新,体验流畅。 - 状态管理(Pinia/Vuex):当应用变得复杂,多个组件需要共享状态时(例如,当前登录的用户信息
userInfo),就需要状态管理库。Pinia是当前Vue官方推荐的状态管理库,比Vuex更简洁。我们可以创建一个authStore来集中管理登录状态、用户角色和权限信息,任何组件都可以方便地获取和修改这些共享状态。
2.2.4 UI组件库的选择:提升开发效率从头编写所有按钮、表格、表单是不现实的。选择一个成熟的UI组件库能事半功倍。国内常用的有:
- Element Plus:适用于中后台管理系统,组件丰富,文档齐全,风格与选课系统这类管理平台很搭。
- Ant Design Vue:同样强大,设计风格自成体系。
- Vuetify:遵循Material Design规范。
以Element Plus为例,实现一个课程列表表格非常简单:
<template> <el-table :data="courseList" style="width: 100%"> <el-table-column prop="courseCode" label="课程代码" width="120"/> <el-table-column prop="courseName" label="课程名称"/> <el-table-column prop="teacherName" label="任课教师"/> <el-table-column prop="credit" label="学分" width="80"/> <el-table-column prop="selectedCount" label="已选/容量" width="120"/> <el-table-column label="操作" width="120"> <template #default="scope"> <el-button size="small" :disabled="scope.row.selected || scope.row.selectedCount >= scope.row.capacity" @click="handleSelect(scope.row)"> {{ scope.row.selected ? '已选' : '选课' }} </el-button> </template> </el-table-column> </el-table> </template>这段代码清晰地展示了数据驱动视图:courseList数据变化,表格自动更新;按钮状态根据selected和容量逻辑动态禁用。
3. 系统核心模块设计与数据库建模
在技术栈明晰后,我们需要将业务需求转化为具体的软件模块和数据库表结构。这是系统稳定性的基础。
3.1 业务实体与关系分析
一个最简化的选课系统,至少包含以下几个核心实体:
- 用户(User):所有系统参与者的基类。通常包含
id,username,password,realName,role等字段。通过role字段(如:STUDENT,TEACHER,ADMIN)区分身份。 - 学生(Student):继承或关联User,扩展学号(
studentNo)、所属院系(department)、年级(grade)等信息。 - 教师(Teacher):继承或关联User,扩展工号(
teacherNo)、职称(title)等信息。 - 课程(Course):课程的核心信息。包括课程编号(
courseCode)、课程名称(courseName)、学分(credit)、总容量(capacity)、课程描述(description)、上课时间(classTime)、上课地点(location)等。这里有一个关键设计点:上课时间如何存储?简单的方式是用字符串存储,如"周一 1-2节",但这不利于后续的冲突检测。更规范的做法是设计一个Schedule表,记录每周几、第几节开始、持续几节。为了简化初始版本,我们可以先用字符串,但心里要清楚这是未来可以优化的地方。 - 选课记录(SelectionRecord):这是连接学生和课程的纽带,也是系统的核心事务表。字段包括:
id,student_id,course_id,selection_time(选课时间),status(可选状态,如‘成功’,‘待审核’,‘已退选’)。
它们之间的关系是:
- 一个学生可以选多门课程,一门课程可以被多个学生选。这是多对多关系,通过选课记录表实现。
- 一个教师可以讲授多门课程,一门课程通常由一位教师负责(或教学团队)。这是多对一或一对多关系,在
Course表中加一个teacher_id外键即可。
3.2 数据库表结构设计示例
基于以上分析,我们可以设计出以下核心表(以MySQL为例):
-- 用户表(基表) CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名/登录账号', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL COMMENT '角色:STUDENT/TEACHER/ADMIN', `avatar` varchar(500) DEFAULT NULL COMMENT '头像', `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 学生信息表 CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '关联用户ID', `student_no` varchar(20) NOT NULL COMMENT '学号', `department` varchar(100) DEFAULT NULL COMMENT '院系', `grade` varchar(20) DEFAULT NULL COMMENT '年级', `class_name` varchar(50) DEFAULT NULL COMMENT '班级', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`), UNIQUE KEY `uk_user_id` (`user_id`), CONSTRAINT `fk_student_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; -- 教师信息表 CREATE TABLE `teacher` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '关联用户ID', `teacher_no` varchar(20) NOT NULL COMMENT '工号', `title` varchar(50) DEFAULT NULL COMMENT '职称', PRIMARY KEY (`id`), UNIQUE KEY `uk_teacher_no` (`teacher_no`), UNIQUE KEY `uk_user_id` (`user_id`), CONSTRAINT `fk_teacher_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师信息表'; -- 课程表 CREATE TABLE `course` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `course_code` varchar(50) NOT NULL COMMENT '课程代码', `course_name` varchar(200) NOT NULL COMMENT '课程名称', `credit` decimal(3,1) NOT NULL COMMENT '学分', `capacity` int(11) NOT NULL COMMENT '课程容量', `selected_count` int(11) DEFAULT '0' COMMENT '已选人数(冗余字段,便于快速查询)', `teacher_id` bigint(20) DEFAULT NULL COMMENT '任课教师ID', `class_time` varchar(200) DEFAULT NULL COMMENT '上课时间,如“周一 1-2节,周三 3-4节”', `location` varchar(100) DEFAULT NULL COMMENT '上课地点', `description` text COMMENT '课程描述', `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-关闭,1-开放选课', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_course_code` (`course_code`), KEY `idx_teacher_id` (`teacher_id`), CONSTRAINT `fk_course_teacher` FOREIGN KEY (`teacher_id`) REFERENCES `teacher` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 选课记录表(核心事务表) CREATE TABLE `selection_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_id` bigint(20) NOT NULL COMMENT '学生ID', `course_id` bigint(20) NOT NULL COMMENT '课程ID', `selection_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', `status` varchar(20) DEFAULT 'SUCCESS' COMMENT '状态:SUCCESS-成功,DROPPED-已退选', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_course` (`student_id`, `course_id`) COMMENT '同一学生不能重复选同一门课', KEY `idx_course_id` (`course_id`), KEY `idx_selection_time` (`selection_time`), CONSTRAINT `fk_record_student` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`), CONSTRAINT `fk_record_course` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';设计要点与踩坑提醒:
- 密码存储:绝对不要明文存储密码!
sys_user表中的password字段应存储经过BCrypt等强哈希算法加密后的字符串。Spring Security提供了现成的BCryptPasswordEncoder。 - 冗余字段
selected_count:在course表中添加这个字段是典型的“空间换时间”优化。每次有选课/退课成功时,通过事务同步更新这个计数。这样在查询课程列表时,无需关联selection_record表进行COUNT聚合查询,极大提升了列表查询性能,尤其是在高并发场景下。 - 唯一索引
uk_student_course:这个约束保证了数据库层面的数据一致性,防止程序逻辑漏洞导致同一学生重复选择同一门课程。 - 外键约束:虽然有些大型互联网应用为了性能会放弃外键,由程序保证一致性。但对于选课系统这类对数据一致性要求极高的业务系统,在开发初期使用外键约束是明智的,它能有效避免产生“脏数据”(如引用了不存在的学生或课程)。
- 时间字段:使用
datetime类型,并设置合适的默认值(CURRENT_TIMESTAMP),便于追踪操作时间。
4. 后端API实现:业务逻辑与并发安全
数据库设计好后,我们开始构建后端的“大脑”——业务逻辑层和API接口。这里我将重点讲解几个最核心、也最容易出问题的接口实现。
4.1 用户认证与授权:使用Spring Security + JWT
安全是系统的生命线。我们采用无状态的JWT(JSON Web Token)方案,替代传统的Session,更适合前后端分离架构。
4.1.1 登录接口实现
- 自定义UserDetailsService:实现
loadUserByUsername方法,根据用户名从数据库查询用户信息(包含角色、权限),并封装成Spring Security识别的UserDetails对象。 - 密码校验:在登录逻辑中,使用
BCryptPasswordEncoder.matches(rawPassword, encodedPassword)来校验密码。 - 生成JWT:校验成功后,使用JJWT等库生成一个Token。Token的Payload中应包含用户ID、用户名、角色等必要信息,但切勿存放敏感信息如密码。同时设置一个合理的过期时间(如2小时)。
- 返回Token:将生成的JWT Token返回给前端。前端后续的每次请求,都需要在HTTP Header的
Authorization字段中携带这个Token(格式:Bearer <token>)。
4.1.2 配置Spring Security过滤器链我们需要自定义一个JwtAuthenticationFilter,将其添加到Spring Security的过滤器链中。这个过滤器的职责是:
- 从请求头中提取Token。
- 验证Token的签名和有效期。
- 如果Token有效,则根据Token中的信息构造一个
Authentication对象,并存入SecurityContextHolder,这样后续的Controller中就能通过@AuthenticationPrincipal注解获取当前用户信息。
同时,我们需要配置哪些API路径需要认证,哪些不需要(如登录接口、公开的课程列表查询接口)。这可以通过HttpSecurity配置来实现。
4.1.3 基于角色的权限控制在Controller的方法上,我们可以使用@PreAuthorize(“hasRole(‘STUDENT’)”)或@PreAuthorize(“hasAuthority(‘course:select’)”)这样的注解进行细粒度控制。例如,选课接口只能被具有STUDENT角色的用户访问。
4.2 核心业务接口:选课与退课
这是系统最核心、并发最高的部分。实现时需要考虑原子性和一致性。
4.2.1 选课接口 (POST /api/student/courses/{courseId}/select)这个接口的逻辑远比看起来复杂,必须在一个数据库事务中完成,并且要处理各种边界条件。
@Service @Transactional(rollbackFor = Exception.class) // 声明式事务管理 public class CourseSelectionService { @Autowired private CourseMapper courseMapper; @Autowired private SelectionRecordMapper recordMapper; public SelectionResult selectCourse(Long studentId, Long courseId) { // 1. 检查课程是否存在且状态为开放 Course course = courseMapper.selectByIdForUpdate(courseId); // 注意:使用SELECT ... FOR UPDATE 行级锁 if (course == null) { throw new BusinessException("课程不存在"); } if (!course.isOpen()) { throw new BusinessException("该课程未开放选课"); } // 2. 检查是否已选过该课程 SelectionRecord existingRecord = recordMapper.findByStudentAndCourse(studentId, courseId); if (existingRecord != null && !existingRecord.isDropped()) { throw new BusinessException("您已选择该课程"); } // 3. 检查课程容量是否已满 if (course.getSelectedCount() >= course.getCapacity()) { throw new BusinessException("课程容量已满"); } // 4. (可选但重要) 检查时间冲突。这里需要解析course.getClassTime(),并与该学生已选课程的时间进行比较。 // List<Course> myCourses = getSelectedCourses(studentId); // if (hasTimeConflict(course, myCourses)) { // throw new BusinessException("与已选课程时间冲突"); // } // 5. 创建选课记录 SelectionRecord record = new SelectionRecord(); record.setStudentId(studentId); record.setCourseId(courseId); record.setStatus(SelectionStatus.SUCCESS); recordMapper.insert(record); // 6. 更新课程已选人数(原子操作) int updatedRows = courseMapper.incrementSelectedCount(courseId); if (updatedRows == 0) { // 理论上不会发生,因为前面用了FOR UPDATE锁,但为安全起见 throw new ConcurrentSelectionException("选课失败,请重试"); } // 7. 返回成功结果 return new SelectionResult(true, "选课成功", record.getId()); } }关键点与踩坑经验:
SELECT ... FOR UPDATE:这是防止“超卖”的关键。在高并发下,多个线程可能同时检查到课程容量未满。如果不加锁,它们都会通过检查,然后都去插入选课记录和更新人数,导致实际选课人数超过容量。FOR UPDATE会对这条课程记录加行级排他锁,确保同一时间只有一个事务能处理这门课的选课逻辑。注意:这要求数据库引擎支持行级锁(如InnoDB),且事务隔离级别至少为READ COMMITTED。- 更新已选人数:使用
incrementSelectedCount这样的原子操作(UPDATE course SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < capacity),而不是先查询再更新。这提供了第二道防线。 - 时间冲突检查:这是一个业务复杂度很高的功能。简单的字符串比对(如
”周一 1-2节“)很难覆盖所有冲突情况(如单双周上课、连续多节等)。在初期MVP版本,可以考虑暂不实现,或采用简单的字符串匹配给出警告。更完善的方案需要设计独立的Schedule时间片模型。 - 异常处理:所有可能失败的地方(课程不存在、已选、已满、时间冲突)都要提前检查并抛出明确的业务异常,通过全局异常处理器(
@ControllerAdvice)转换成友好的错误信息返回给前端。
4.2.2 退课接口 (POST /api/student/courses/{courseId}/drop)退课逻辑相对简单,但也需要事务保证数据一致性:将选课记录状态标记为DROPPED,并原子减少课程的selected_count。同样,需要考虑退课是否应在特定时间范围内允许等业务规则。
4.3 课程查询与筛选接口
这是前端页面数据的主要来源。性能优化是关键。
4.3.1 课程列表接口 (GET /api/courses)这个接口通常需要支持分页、排序和复杂的多条件筛选(如按课程名称模糊查询、按教师查询、按学分范围、按上课时间等)。
@RestController @RequestMapping("/api/courses") public class CourseController { @GetMapping public PageResult<CourseVO> getCourses( @RequestParam(required = false) String keyword, @RequestParam(required = false) Long teacherId, @RequestParam(required = false) BigDecimal minCredit, @RequestParam(required = false) BigDecimal maxCredit, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { // 构建查询条件对象 CourseQuery query = new CourseQuery(); query.setKeyword(keyword); query.setTeacherId(teacherId); query.setMinCredit(minCredit); query.setMaxCredit(maxCredit); // 使用PageHelper进行物理分页 PageHelper.startPage(pageNum, pageSize); List<Course> courseList = courseMapper.selectByCondition(query); PageInfo<Course> pageInfo = new PageInfo<>(courseList); // 转换为前端需要的VO对象,可能包含是否已选等额外信息 List<CourseVO> voList = convertToVOList(courseList, currentUserId); return new PageResult<>(pageInfo.getTotal(), voList); } }性能优化点:
- 数据库索引:确保
course表上在常用的查询字段(如course_name,teacher_id,credit,status)上建立了合适的索引。对于keyword模糊查询(LIKE ‘%关键词%’),前导通配符会导致索引失效,如果数据量大,需要考虑使用全文索引(如Elasticsearch)。 - VO对象转换:不要在循环中查询数据库。例如,
convertToVOList方法中,如果需要判断当前学生是否已选某门课,应该先批量查询出该学生所有的选课记录ID集合(List<Long> selectedCourseIds),然后在内存中进行contains判断,避免N+1查询问题。 - 缓存考虑:对于变化不频繁的课程基本信息,可以考虑使用Redis进行缓存,减轻数据库压力。
5. 前端工程与关键页面实现
后端API准备好后,前端的工作就是如何优雅地消费这些API,并构建出直观易用的界面。
5.1 项目初始化与架构搭建
使用Vite初始化项目后,一个清晰的项目结构是高效开发的基础。
src/ ├── api/ # 所有API请求封装 │ ├── auth.js # 登录、注销接口 │ ├── course.js # 课程相关接口 │ └── index.js # 统一导出 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── Layout/ # 布局组件 │ ├── CourseCard.vue # 课程卡片组件 │ └── ... ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 │ ├── auth.js # 用户认证状态 │ └── course.js # 课程相关状态(可选) ├── views/ # 页面组件 │ ├── Login.vue │ ├── student/ # 学生相关页面 │ │ ├── Dashboard.vue │ │ ├── CourseList.vue # 课程列表/选课页 │ │ └── MyTimetable.vue # 我的课表 │ └── ... └── App.vue关键配置:
- Axios拦截器:在
api/index.js中配置axios实例,统一设置baseURL、超时时间,并添加请求/响应拦截器。请求拦截器用于在每次请求头中自动添加JWT Token;响应拦截器用于统一处理错误(如401未授权时跳转到登录页)。 - 路由守卫:在
router/index.js中,使用router.beforeEach实现全局前置守卫。检查目标路由是否需要认证,以及当前用户的角色是否有权限访问,实现页面级的权限控制。
5.2 核心页面:学生选课页的实现
这是学生交互最频繁的页面,需要良好的用户体验和健壮的错误处理。
5.2.1 页面布局与数据获取
<template> <div class="course-selection-page"> <!-- 筛选条件区域 --> <el-card class="filter-card"> <el-form :inline="true" :model="queryParams"> <el-form-item label="课程名称"> <el-input v-model="queryParams.keyword" placeholder="输入课程名或代码" clearable @clear="handleQuery"/> </el-form-item> <el-form-item label="任课教师"> <el-select v-model="queryParams.teacherId" placeholder="请选择" clearable @change="handleQuery"> <el-option v-for="teacher in teacherList" :key="teacher.id" :label="teacher.realName" :value="teacher.id"/> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">查询</el-button> <el-button @click="resetQuery">重置</el-button> </el-form-item> </el-form> </el-card> <!-- 课程列表区域 --> <el-card class="list-card"> <div v-if="loading">加载中...</div> <div v-else-if="courseList.length === 0">暂无课程</div> <el-row :gutter="20" v-else> <el-col :span="8" v-for="course in courseList" :key="course.id"> <CourseCard :course="course" @select="handleSelectCourse(course)" @drop="handleDropCourse(course)"/> </el-col> </el-row> <!-- 分页组件 --> <el-pagination class="pagination" @current-change="handlePageChange" :current-page="queryParams.pageNum" :page-size="queryParams.pageSize" layout="total, prev, pager, next, jumper" :total="total"> </el-pagination> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getCourseList, selectCourse, dropCourse } from '@/api/course' import { getTeacherList } from '@/api/teacher' import CourseCard from '@/components/CourseCard.vue' import { ElMessage, ElMessageBox } from 'element-plus' const loading = ref(false) const courseList = ref([]) const teacherList = ref([]) const total = ref(0) const queryParams = ref({ keyword: '', teacherId: null, pageNum: 1, pageSize: 9 }) // 获取教师列表(用于筛选下拉框) const fetchTeachers = async () => { try { const res = await getTeacherList() teacherList.value = res.data } catch (error) { console.error('获取教师列表失败:', error) } } // 获取课程列表 const fetchCourses = async () => { loading.value = true try { const res = await getCourseList(queryParams.value) courseList.value = res.data.list total.value = res.data.total } catch (error) { ElMessage.error('获取课程列表失败:' + error.message) } finally { loading.value = false } } // 选课操作 const handleSelectCourse = async (course) => { if (course.selected) { ElMessage.warning('您已选择该课程') return } if (course.selectedCount >= course.capacity) { ElMessage.warning('课程容量已满') return } try { await ElMessageBox.confirm(`确定要选择《${course.courseName}》吗?`, '提示', { type: 'warning' }) await selectCourse(course.id) ElMessage.success('选课成功!') // 选课成功后,刷新列表或局部更新该课程状态 fetchCourses() // 简单处理:刷新整个列表。更优做法是更新本地courseList中该课程的状态。 } catch (error) { if (error !== 'cancel') { // 用户点击了取消 ElMessage.error('选课失败:' + error.response?.data?.message || error.message) } } } // 初始化 onMounted(() => { fetchTeachers() fetchCourses() }) </script>5.2.2 交互细节与用户体验优化
- 按钮防抖与加载状态:在
handleSelectCourse函数中,点击选课按钮后,应立即将按钮置为禁用或加载状态,防止用户快速重复点击导致重复提交。可以使用一个loading变量或给按钮添加loading属性。 - 乐观更新:为了获得更快的响应,可以在调用选课API的同时,先更新本地
courseList中该课程的状态(selected: true,selectedCount + 1)。如果API调用失败,再回滚本地状态并提示错误。这能极大提升用户感知速度。 - 错误提示友好化:后端返回的错误信息应清晰明确(如“课程容量已满”、“时间冲突”),前端直接展示。对于网络错误或未知错误,应给出通用提示并记录日志。
5.3 课表展示页的实现
课表页需要将课程的时间、地点信息可视化。这里有一个挑战:如何将”周一 1-2节,周三 3-4节“这样的字符串,渲染成格子状的课表?
思路:
- 数据转换:写一个工具函数
parseSchedule,将课程的时间字符串解析成一个结构化的数组,例如:
其中// 输入: "周一 1-2节,周三 3-4节" // 输出: [{ dayOfWeek: 1, startSlot: 1, duration: 2 }, { dayOfWeek: 3, startSlot: 3, duration: 2 }]dayOfWeek用数字1-7表示周一到周日,startSlot表示第几节课开始,duration表示持续几节。 - UI渲染:使用一个二维网格来渲染。横轴是星期几(周一至周五),纵轴是节次(1-12节)。遍历学生已选的课程列表,根据解析出的时间数据,计算每个课程在网格中占据的起始位置和跨度(
rowspan),然后用绝对定位或CSS Grid布局将其放入对应的格子中。 - 使用现成库:如果觉得手动实现复杂,可以考虑使用一些日历或时间表组件库,如
vue-calendar-component,但可能需要根据课表格式进行定制。
6. 部署上线与性能调优考量
当系统开发测试完毕,准备投入使用时,部署和性能是最后一道关卡。
6.1 后端SpringBoot应用部署
- 打包:使用Maven或Gradle的打包命令,生成可执行的JAR文件。
生成的JAR文件在mvn clean package -DskipTeststarget目录下,包含了所有依赖和嵌入式Tomcat。 - 服务器环境:准备一台Linux服务器(如CentOS或Ubuntu),安装Java运行环境(JDK 8或11)。
- 运行:将JAR包上传至服务器,使用
nohup命令在后台运行,并将日志输出到文件。nohup java -Xms512m -Xmx1024m -jar course-selection-system.jar --spring.profiles.active=prod > app.log 2>&1 &-Xms和-Xmx设置了JVM堆内存的初始大小和最大大小,根据服务器内存调整。--spring.profiles.active=prod指定使用application-prod.yml配置文件,其中应配置生产环境的数据库地址、Redis地址、日志级别等。
- 使用反向代理:通常不会直接让用户访问8080端口。使用Nginx作为反向代理,将域名(如
api.yourschool.com)的请求转发到本地的8080端口。Nginx还可以处理静态文件、配置SSL证书(HTTPS)、做负载均衡等。server { listen 80; server_name api.yourschool.com; # 重定向到HTTPS(如果配置了SSL) # return 301 https://$server_name$request_uri; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
6.2 前端Vue应用部署
- 构建:运行构建命令,生成静态文件。
生成的文件在npm run builddist目录下。 - 部署:将
dist目录下的所有文件,上传到Web服务器(如Nginx)的某个目录下(例如/usr/share/nginx/html/course)。 - Nginx配置:配置一个Server块来服务这些静态文件,并处理前端路由(History模式)的Fallback。
server { listen 80; server_name course.yourschool.com; root /usr/share/nginx/html/course; index index.html; location / { try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } }
6.3 性能与高并发优化思路
选课系统在特定时段(如选课系统开放的前几分钟)可能会面临极高的并发请求。以下是一些优化方向:
数据库层面:
- 读写分离:将查询(如课程列表)导向从库,写操作(选课、退课)在主库进行。
- 连接池优化:合理配置HikariCP等连接池参数(最大连接数、最小空闲连接等),避免连接耗尽。
- 慢查询监控:开启MySQL慢查询日志,定期分析并优化耗时长的SQL。
应用层面:
- 缓存:使用Redis缓存热点数据,如课程基本信息、学生已选课程ID列表。对于选课结果,也可以考虑将“课程剩余名额”这种高频变化的数据放入Redis,利用Redis的原子操作(
DECR)来快速判断,但最终一致性仍需落库。 - 异步处理:对于一些非实时强一致的操作,如选课成功后的短信/邮件通知、选课日志记录,可以放入消息队列(如RabbitMQ、RocketMQ)异步处理,快速释放请求线程。
- 限流与降级:在网关层(如Spring Cloud Gateway)或应用层(使用Resilience4j、Sentinel)对核心选课接口进行限流,防止系统被突发流量打垮。当系统压力过大时,可以暂时降级非核心功能(如课程详情中的教师介绍、学生评价等)。
- 缓存:使用Redis缓存热点数据,如课程基本信息、学生已选课程ID列表。对于选课结果,也可以考虑将“课程剩余名额”这种高频变化的数据放入Redis,利用Redis的原子操作(
前端层面:
- CDN加速:将静态资源(JS、CSS、图片)部署到CDN,加速用户访问。
- 接口防抖:对搜索框输入等频繁触发API请求的操作,在前端做防抖处理,减少不必要的请求。
- 虚拟列表:如果课程列表数据量极大(成千上万条),使用虚拟列表技术(如
vue-virtual-scroller)只渲染可视区域内的DOM元素,避免浏览器卡顿。
构建一个完整的网上选课系统,是一个典型的全栈开发实践,它串联起了需求分析、数据库设计、后端业务逻辑、前端交互、系统部署和性能优化的完整链条。每一个环节都有其技术深度和值得推敲的细节。从最初的纸质表格到如今的在线系统,技术的价值在于切实地解决现实问题,提升效率与体验。在实现过程中,最深的体会是:技术方案没有绝对的好坏,只有是否适合当下的场景和团队。在项目初期,优先保证核心流程的稳定和正确,比追求技术的“时髦”和架构的“完美”更重要。例如,在时间冲突检测这个复杂功能上,如果初期业务规则不明确,完全可以先采用简单的字符串匹配给出提示,或者甚至先不做,待业务跑通、需求明确后再进行迭代优化。先让系统跑起来,再让它跑得更快、更稳,这或许是大多数项目成功的务实路径。
本文还有配套的精品资源,点击获取