【原创唯一】基于SpringBoot+Vue3的学生选课管理系统 课程设计/大作业/期末作业(源码+MySQL数据库+实验报告+PPT+远程部署)
作者介绍:专注于计算机课设、毕设辅导,本人开发,原创代码,一题一稿,绝不撞题,坚持原创,个人创作,非工作室,源码全网唯一。
🍅文末获取联系🍅
✅ 原创唯一:个人原创开发,独立设计数据库与业务逻辑,拒绝工作室代码改造
✅ 技术主流:SpringBoot + Vue 前后端分离,MySQL,Echarts数据统计,可本地运行
✅ 配套资料:源码 + 数据库 + 实验报告/论文 + 答辩 PPT+部署演示+远程调试+问题解答
学生选课管理系统摘要
随着高校信息化建设的推进,传统人工或分散的选课方式已难以满足师生对效率与准确性的要求。本文设计并实现了一套基于 B/S 架构的高校选课管理系统,采用 Spring Boot 3 + MyBatis-Plus 构建后端服务,Vue 3 + Element Plus 构建前端界面,MySQL 作为数据存储。系统面向管理员、教师、在校生三类角色,实现了课程维护、开课审核、选课退课、个人课表、公告发布及数据统计等功能。管理员、教师、学生分表存储,保证角色数据独立;后端按 JWT 鉴权与角色权限控制接口访问;选课模块支持人数上限校验与时间冲突检测。经功能测试,系统运行稳定,满足课程设计预期目标。
技术栈: Spring Boot 3 + MyBatis-Plus + MySQL + Vue 3 + Element Plus + ECharts
数据库表:6张

技术范围:SpringBoot、Vue、数据可视化、小程序、HLMT、Nodejs、uni-app、MySQL数据库、ElementUi等设计与开发。
适用范围:软件工程、软件技术、数据库课程设计、计算机科学与技术、数据库系统原理、JavaWeb开发、JavaEE、Java、Web应用开发、动态网页设计的课程设计、课设、大作业、课程实验、期末作业
实验报告参考内容
实验报告可供大家参考使用
功能需求
(1)认证模块:支持管理员、教师、学生分身份登录;学生可注册;登录后 JWT 令牌维持会话;支持个人资料修改与密码变更。
(2)课程模块:管理员可直接创建开放课程;教师提交开课申请后由管理员审核;课程包含编号、名称、学分、教室、上课时间(星期与节次)、选课上限等字段。
(3)选课模块:学生仅可选状态为 OPEN 的课程;校验是否重复选课、是否超过人数上限、是否与已选课程时间冲突;支持退课并回退已选人数。
(4)用户管理:管理员对教师表、学生表分别进行增删改查及启用/禁用操作。
(5)公告模块:管理员发布/编辑/下架公告;教师与学生可浏览已发布公告。
(6)统计模块:工作台展示课程、用户、选课等汇总数据及漏斗图、环形图、折线图、雷达图。


|
角色 |
主要职责 |
|
管理员 |
维护课程信息、限制选课人数、审核开课申请、管理教师与学生、发布公告 |
|
教师 |
提交授课课程申请、查看我的课程、查看选课名单、浏览系统公告 |
|
在校生 |
浏览可选课程、选课/退课、查看个人课表、浏览系统公告、注册账号 |
学生端功能



教师端功能


管理员端功能




数据库设计
系统数据库设计为:

数据库名称为 0vuez_xuanke。三类用户采用独立表 admins、teachers、students。主要数据表如下:
|
表名 |
说明 |
主要字段 |
|
admins |
管理员 |
username, password, real_name, enabled |
|
teachers |
教师 |
username, password, employee_no, enabled |
|
students |
学生 |
username, password, student_no, enabled |
|
announcements |
公告 |
title, content, admin_id, status |
|
courses |
课程 |
course_code, name, teacher_id, max_students, status |
|
enrollments |
选课记录 |
student_id, course_id, status |
1 admins(管理员表)
admins 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,管理员唯一标识 |
|
2 |
username |
varchar(50) |
否 |
无 |
UNIQUE |
登录用户名,全局唯一 |
|
3 |
password |
varchar(100) |
否 |
无 |
— |
BCrypt 加密后的登录密码 |
|
4 |
real_name |
varchar(50) |
是 |
NULL |
— |
管理员真实姓名 |
|
5 |
phone |
varchar(20) |
是 |
NULL |
— |
联系电话 |
|
6 |
enabled |
tinyint(4) |
否 |
1 |
— |
账号启用状态:1 启用,0 禁用 |
|
7 |
created_at |
datetime |
是 |
NULL |
— |
账号创建时间 |
表 4.1 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
username |
唯一索引 |
username |
登录名不重复 |
4.2 teachers(教师表)
teachers 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,教师唯一标识 |
|
2 |
username |
varchar(50) |
否 |
无 |
UNIQUE |
登录用户名,全局唯一 |
|
3 |
password |
varchar(100) |
否 |
无 |
— |
BCrypt 加密后的登录密码 |
|
4 |
real_name |
varchar(50) |
是 |
NULL |
— |
教师姓名,用于课程展示 |
|
5 |
phone |
varchar(20) |
是 |
NULL |
— |
联系电话 |
|
6 |
employee_no |
varchar(30) |
是 |
NULL |
— |
工号 |
|
7 |
enabled |
tinyint(4) |
否 |
1 |
— |
账号启用状态:1 启用,0 禁用 |
|
8 |
created_at |
datetime |
是 |
NULL |
— |
账号创建时间 |
表 4.2 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
username |
唯一索引 |
username |
登录名不重复 |
4.3 students(学生表)
students 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,学生唯一标识 |
|
2 |
username |
varchar(50) |
否 |
无 |
UNIQUE |
登录用户名,全局唯一 |
|
3 |
password |
varchar(100) |
否 |
无 |
— |
BCrypt 加密后的登录密码 |
|
4 |
real_name |
varchar(50) |
是 |
NULL |
— |
学生姓名 |
|
5 |
phone |
varchar(20) |
是 |
NULL |
— |
手机号码 |
|
6 |
student_no |
varchar(30) |
是 |
NULL |
— |
学号,注册时填写 |
|
7 |
enabled |
tinyint(4) |
否 |
1 |
— |
账号启用状态:1 启用,0 禁用 |
|
8 |
created_at |
datetime |
是 |
NULL |
— |
账号创建时间 |
表 4.3 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
username |
唯一索引 |
username |
登录名不重复 |
4.4 courses(课程表)
courses 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,课程唯一标识 |
|
2 |
course_code |
varchar(30) |
否 |
无 |
UNIQUE |
课程编号,如 CS101 |
|
3 |
name |
varchar(200) |
否 |
无 |
— |
课程名称 |
|
4 |
credit |
decimal(3,1) |
是 |
0.0 |
— |
学分,如 3.0 |
|
5 |
classroom |
varchar(100) |
是 |
NULL |
— |
上课教室 |
|
6 |
week_day |
int(11) |
否 |
无 |
— |
星期几:1-7 表示周一至周日 |
|
7 |
start_section |
int(11) |
否 |
无 |
— |
开始节次 |
|
8 |
end_section |
int(11) |
否 |
无 |
— |
结束节次 |
|
9 |
teacher_id |
bigint(20) |
是 |
NULL |
INDEX, FK |
授课教师 ID,关联 teachers.id |
|
10 |
max_students |
int(11) |
否 |
50 |
— |
选课人数上限 |
|
11 |
enrolled_count |
int(11) |
否 |
0 |
— |
当前已选人数,选课时递增、退课时递减 |
|
12 |
semester |
varchar(30) |
是 |
2024-2025-1 |
— |
开课学期 |
|
13 |
status |
varchar(20) |
否 |
PENDING |
— |
课程状态:PENDING 待审、OPEN 开放、REJECTED 驳回 |
|
14 |
reject_reason |
varchar(200) |
是 |
NULL |
— |
管理员驳回开课申请时的原因 |
|
15 |
description |
varchar(500) |
是 |
NULL |
— |
课程简介 |
|
16 |
created_at |
datetime |
是 |
NULL |
— |
记录创建时间 |
|
17 |
updated_at |
datetime |
是 |
NULL |
— |
记录最后更新时间 |
表 4.4 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
course_code |
唯一索引 |
course_code |
课程编号不重复 |
|
teacher_id |
普通索引 |
teacher_id |
按教师查课程 |
表 4.4 外键约束
|
外键名 |
本表字段 |
引用表 |
引用字段 |
删除规则 |
更新规则 |
|
courses_ibfk_1 |
teacher_id |
teachers |
id |
RESTRICT |
RESTRICT |
4.5 enrollments(选课记录表)
enrollments 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,选课记录唯一标识 |
|
2 |
student_id |
bigint(20) |
否 |
无 |
INDEX, FK |
学生 ID,关联 students.id |
|
3 |
course_id |
bigint(20) |
否 |
无 |
INDEX, FK |
课程 ID,关联 courses.id |
|
4 |
status |
varchar(20) |
否 |
ENROLLED |
— |
选课状态:ENROLLED 已选(有效记录) |
|
5 |
created_at |
datetime |
是 |
NULL |
— |
选课时间 |
表 4.5 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
uk_student_course |
唯一索引 |
student_id, course_id |
同一学生不能重复选同一门课 |
|
course_id |
普通索引 |
course_id |
按课程查选课名单 |
表 4.5 外键约束
|
外键名 |
本表字段 |
引用表 |
引用字段 |
删除规则 |
更新规则 |
|
enrollments_ibfk_1 |
student_id |
students |
id |
RESTRICT |
RESTRICT |
|
enrollments_ibfk_2 |
course_id |
courses |
id |
RESTRICT |
RESTRICT |
4.6 announcements(系统公告表)
announcements 字段结构
|
序号 |
字段名 |
数据类型 |
允许空 |
默认值 |
约束/索引 |
字段说明 |
|
1 |
id |
bigint(20) |
否 |
自增 |
PRIMARY KEY |
主键,公告唯一标识 |
|
2 |
title |
varchar(200) |
否 |
无 |
— |
公告标题 |
|
3 |
content |
text |
否 |
无 |
— |
公告正文内容 |
|
4 |
admin_id |
bigint(20) |
是 |
NULL |
INDEX, FK |
发布管理员 ID,关联 admins.id |
|
5 |
status |
tinyint(4) |
否 |
1 |
— |
发布状态:1 已发布,0 已下架 |
|
6 |
created_at |
datetime |
是 |
NULL |
— |
发布时间 |
|
7 |
updated_at |
datetime |
是 |
NULL |
— |
最后更新时间 |
表 4.6 索引设计
|
索引名 |
索引类型 |
字段 |
说明 |
|
PRIMARY |
主键 |
id |
聚簇索引 |
|
admin_id |
普通索引 |
admin_id |
按管理员查公告 |
表 4.6 外键约束
|
外键名 |
本表字段 |
引用表 |
引用字段 |
删除规则 |
更新规则 |
|
announcements_ibfk_1 |
admin_id |
admins |
id |
RESTRICT |
RESTRICT |
系统架构

Controller及Service层核心代码写法:
// 选课记录分页列表接口
@GetMapping
@RequireRole({UserRole.ADMIN, UserRole.TEACHER, UserRole.STUDENT})
public ApiResponse<PageResult<Enrollment>> list(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long course_id,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return ApiResponse.ok(enrollmentService.list(keyword, course_id, page, size));
}
// 学生个人课表接口
@GetMapping("/timetable")
@RequireRole(UserRole.STUDENT)
public ApiResponse<List<Map<String, Object>>> timetable() {
return ApiResponse.ok(enrollmentService.timetable());
}
// 选课接口
@PostMapping
@RequireRole(UserRole.STUDENT)
public ApiResponse<Enrollment> enroll(@Valid @RequestBody EnrollmentDTO dto) {
return ApiResponse.ok("选课成功", enrollmentService.enroll(dto));
}
// 退课接口
@PutMapping("/{id}/drop")
@RequireRole({UserRole.STUDENT, UserRole.ADMIN})
public ApiResponse<Void> drop(@PathVariable Long id) {
enrollmentService.drop(id);
return ApiResponse.ok("退课成功", null);
}
@Service
@RequiredArgsConstructor
public class EnrollmentService extends ServiceImpl<EnrollmentMapper, Enrollment> {
private final CourseMapper courseMapper;
private final StudentMapper studentMapper;
private final RelationFillHelper relationFillHelper;
public PageResult<Enrollment> list(String keyword, Long course_id, int page, int size) {
var query = lambdaQuery().eq(Enrollment::getStatus, "ENROLLED");
if (AuthContext.isStudent()) {
query.eq(Enrollment::getStudent_id, AuthContext.getUserId());
} else if (AuthContext.isTeacher()) {
List<Long> course_ids = courseMapper.selectList(
Wrappers.<Course>lambdaQuery().eq(Course::getTeacher_id, AuthContext.getUserId()))
.stream().map(Course::getId).toList();
if (course_ids.isEmpty()) {
return new PageResult<>(List.of(), 0, page, size);
}
if (course_id != null) {
if (!course_ids.contains(course_id)) {
throw new RuntimeException("无权查看该课程选课名单");
}
query.eq(Enrollment::getCourse_id, course_id);
} else {
query.in(Enrollment::getCourse_id, course_ids);
}
} else if (course_id != null) {
query.eq(Enrollment::getCourse_id, course_id);
}
if (StringUtils.hasText(keyword)) {
List<Long> student_ids = studentMapper.selectList(
Wrappers.<Student>lambdaQuery()
.like(Student::getUsername, keyword)
.or().like(Student::getReal_name, keyword)
.or().like(Student::getStudent_no, keyword))
.stream().map(Student::getId).toList();
if (student_ids.isEmpty()) {
return new PageResult<>(List.of(), 0, page, size);
}
query.in(Enrollment::getStudent_id, student_ids);
}
Page<Enrollment> result = query.orderByDesc(Enrollment::getId).page(new Page<>(page, size));
relationFillHelper.fillEnrollments(result.getRecords());
return PageResult.of(result);
}
@Transactional
public Enrollment enroll(EnrollmentDTO dto) {
if (!AuthContext.isStudent()) {
throw new RuntimeException("仅学生可以选课");
}
Long student_id = AuthContext.getUserId();
Course course = courseMapper.selectById(dto.getCourse_id());
if (course == null) {
throw new RuntimeException("课程不存在");
}
if (!"OPEN".equals(course.getStatus())) {
throw new RuntimeException("该课程未开放选课");
}
if (course.getEnrolled_count() >= course.getMax_students()) {
throw new RuntimeException("选课人数已满");
}
Enrollment existed = lambdaQuery()
.eq(Enrollment::getStudent_id, student_id)
.eq(Enrollment::getCourse_id, course.getId())
.one();
if (existed != null) {
if ("ENROLLED".equals(existed.getStatus())) {
throw new RuntimeException("您已选过该课程");
}
removeById(existed.getId());
}
checkScheduleConflict(student_id, course);
Enrollment enrollment = new Enrollment();
enrollment.setStudent_id(student_id);
enrollment.setCourse_id(course.getId());
enrollment.setStatus("ENROLLED");
save(enrollment);
course.setEnrolled_count(course.getEnrolled_count() + 1);
courseMapper.updateById(course);
relationFillHelper.fillEnrollment(enrollment);
return enrollment;
}
@Transactional
public void drop(Long id) {
Enrollment enrollment = getById(id);
if (enrollment == null) {
throw new RuntimeException("选课记录不存在");
}
if (!AuthContext.isStudent() && !AuthContext.isAdmin()) {
throw new RuntimeException("无权退课");
}
if (AuthContext.isStudent() && !AuthContext.getUserId().equals(enrollment.getStudent_id())) {
throw new RuntimeException("只能退选自己的课程");
}
if (!"ENROLLED".equals(enrollment.getStatus())) {
throw new RuntimeException("该记录已退课");
}
Course course = courseMapper.selectById(enrollment.getCourse_id());
if (course != null && course.getEnrolled_count() > 0) {
course.setEnrolled_count(course.getEnrolled_count() - 1);
courseMapper.updateById(course);
}
removeById(id);
}
public List<Map<String, Object>> timetable() {
if (!AuthContext.isStudent()) {
throw new RuntimeException("仅学生可查看个人课表");
}
List<Enrollment> list = lambdaQuery()
.eq(Enrollment::getStudent_id, AuthContext.getUserId())
.eq(Enrollment::getStatus, "ENROLLED")
.list();
relationFillHelper.fillEnrollments(list);
List<Map<String, Object>> items = new ArrayList<>();
for (Enrollment e : list) {
Course c = e.getCourse();
if (c == null) {
continue;
}
Map<String, Object> item = new HashMap<>();
item.put("course_id", c.getId());
item.put("course_code", c.getCourse_code());
item.put("name", c.getName());
item.put("classroom", c.getClassroom());
item.put("week_day", c.getWeek_day());
item.put("start_section", c.getStart_section());
item.put("end_section", c.getEnd_section());
item.put("teacher_name", c.getTeacher() != null ? c.getTeacher().getReal_name() : null);
item.put("credit", c.getCredit());
items.add(item);
}
return items;
}
private void checkScheduleConflict(Long student_id, Course target) {
List<Enrollment> enrolled = lambdaQuery()
.eq(Enrollment::getStudent_id, student_id)
.eq(Enrollment::getStatus, "ENROLLED")
.list();
for (Enrollment e : enrolled) {
Course c = courseMapper.selectById(e.getCourse_id());
if (c == null || !c.getWeek_day().equals(target.getWeek_day())) {
continue;
}
if (target.getStart_section() <= c.getEnd_section() && target.getEnd_section() >= c.getStart_section()) {
throw new RuntimeException("与已选课程「" + c.getName() + "」时间冲突");
}
}
}
}
博主本身从事软件开发、有丰富的编程能力和水平,累积给上千名同学进行辅导,论文纯手写查重低于10%,全都顺利通过答辩!
擅长:功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文降重、长期答辩答疑辅导、腾讯会议一对一专业讲解辅导答辩、模拟答辩演练、和理解代码逻辑思路等。
更多个人原创作品👇🏻
获取联系
项目功能完整,可在本地运行,并可远程调试,确保运行顺利!
查看👇🏻👇🏻获取联系方式👇🏻👇🏻
更多推荐

所有评论(0)