
简介这份zip压缩包面向计算机专业毕业生和Java全栈学习者提供一套基于SpringBoot与Vue.js的选课/排课管理系统毕业设计材料包含完整源码、SQL脚本、项目文档和演示PPT可用于课程管理类课题的参考、复现与二次开发。系统围绕用户与权限、课程信息、学生选课、教师排课及选课数据统计等模块展开后端采用SpringBoot构建RESTful接口并连接MySQL前端以Vue组件化方式实现交互适合用来理解前后端分离项目的接口设计、数据库表结构与业务逻辑组织。压缩包约17.55MB文件总数与类型明细上游暂未提供从资源说明可知主要包含源码、数据库脚本、文档和PPT等材料。目前已有561人学习文档可帮助梳理架构设计与技术选型PPT便于答辩展示源码则适合对照调试和扩展功能。1. 选课与排课系统到底难在哪从一次课程冲突说起一套 springbootvue 的选课与排课管理系统真正难的不是把页面画出来而是让“选课不超卖、排课不撞车”这两件事在数据库里站得住。很多 Java 毕业设计 做到后期功能列表很长演示时一选课就出现余量负数一排课就出现教师同一节上两门课答辩老师追问一句“并发怎么处理”就卡住。这份源码、SQL 脚本、文档和 PPT 围绕的正是这条主线学生选课、教师查课表、教务排课与审核三类角色共用一套时间片与容量规则。适合正在做 Java 选课、SpringBoot 排课 方向毕业设计的人也适合需要一套能改造成教务工具的项目参考。2. 需求与数据模型六张核心表把选课、排课、课表串起来选课与排课系统最容易犯的错是一上来就画十几个页面结果课程、教学班、排课记录各存一套时间到了课表展示环节对不上。比较稳妥的做法是先收边界只做“基础数据维护—排课—选课—课表”这一条闭环把冲突检测和容量控制做扎实再考虑调课申请、消息通知这类扩展。数据模型一旦定错后面补多少代码都像在填坑这也是我见过最多的血泪经验。2.1 角色与用例拆解学生、教师、教务三种视角学生端的核心用例只有四个登录、浏览可选教学班、选课/退课、查看个人课表。教师端更轻查看自己的排课、查看选课名单、提交调课申请。教务端承担基础数据维护和规则裁决课程库、教师库、教室库、行政班、学期时间片以及教学班开设、排课、选课开关、名单导出。三端共用同一套教学班数据但看到的字段不同学生看到余量和教师教师看到上课时间和教室教务看到容量、已选人数和冲突状态。这里要提前定一条原则课程和教学班必须分开。课程是“数据结构”这门课本身教学班是“2024-2025-1 学期某教师面向某行政班容量 60 人”的一次开设。很多 Java 毕业设计 把课程、教师、上课时间塞进一张表导致同一门课换一个教师就要复制整行排课冲突查询也变成字符串比较。拆开之后选课选的是教学班排课排的也是教学班课表按教学班聚合三端的数据口径自然一致。用例边界还要明确“谁可以改什么”。学生不能改选课记录的状态只能新增或删除自己的记录教师不能直接改排课只能提交申请教务才能写排课表和教学班容量。权限控制放在后端接口层前端菜单只负责隐藏入口不能把“学生看不到按钮”当成权限。这个边界定清楚后面的接口和表结构才不会互相打架。2.2 核心表结构与字段取舍课程、教学班、时间片、教室六张业务核心表分别是课程、教学班、排课记录、选课记录、教室、时间片。身份相关的用户、学生、教师表单独放不跟业务表混在一起。下面三张表是排课链路的骨架字段取舍直接影响冲突检测怎么写。-- 课程主表只放课程本身属性不绑定教师和学期 create table course ( id bigint primary key auto_increment, course_code varchar(32) not null comment 课程编号, course_name varchar(128) not null comment 课程名称, credit decimal(3,1) not null default 0 comment 学分, hours int not null default 0 comment 总学时, course_type tinyint not null default 1 comment 1必修 2选修, unique key uk_course_code (course_code) ) engineInnoDB default charsetutf8mb4 comment课程主表; -- 教学班一门课在某学期由某教师面向某班级开设 create table teaching_class ( id bigint primary key auto_increment, course_id bigint not null, teacher_id bigint not null, class_id bigint not null comment 行政班, semester varchar(20) not null comment 如2024-2025-1, capacity int not null default 0 comment 容量, selected_count int not null default 0 comment 已选人数, status tinyint not null default 1 comment 1开放 0关闭, unique key uk_class_semester (course_id, teacher_id, class_id, semester) ) engineInnoDB default charsetutf8mb4 comment教学班; -- 排课记录一条记录对应一个教学班的一次上课安排 create table course_schedule ( id bigint primary key auto_increment, teaching_class_id bigint not null, teacher_id bigint not null comment 冗余便于冲突查询, class_id bigint not null comment 冗余便于行政班冲突查询, classroom_id bigint not null, day_of_week tinyint not null comment 1-7, start_section tinyint not null comment 1-12, end_section tinyint not null, week_start tinyint not null, week_end tinyint not null, key idx_conflict (day_of_week, start_section, end_section), key idx_teacher (teacher_id, day_of_week), key idx_room (classroom_id, day_of_week) ) engineInnoDB default charsetutf8mb4 comment排课记录;排课记录里冗余了teacher_id和class_id原因是冲突查询要同时按教师、教室、行政班三个维度过滤每次 join 教学班和课程表会让 SQL 变长索引也不好走。冗余字段在排课写入时一次性填好后续教师或班级变更属于低频操作由教务端统一修正。day_of_week用 1 到 7start_section和end_section用 1 到 12不要存“周一第 1-2 节”这种字符串否则范围重叠判断只能靠拆分或正则查一次冲突要扫全表。时间片和教室单独建字典表。时间片表约束每天可选节次比如上午 1 到 4 节、下午 5 到 8 节、晚上 9 到 12 节教室表存教学楼、房间号、容量和类型。排课记录不直接存时间片 id而是拆成星期、开始节次、结束节次、起始周、结束周范围查询更直接。如果学校节次经常变再在时间片表里维护一份展示用的节次名称排课记录仍然保留拆开的字段。2.3 选课容量与排课约束怎么落到表里选课记录表的关键不是字段多而是约束放在哪里。很多项目在 Service 里先select count(*)再判断是否小于容量最后插入单线程演示没问题两个浏览器同时点就会超卖。正确做法是把“容量未满”做成一条带条件的更新把“不能重复选”做成唯一索引数据库层面兜底。create table enrollment ( id bigint primary key auto_increment, student_id bigint not null, teaching_class_id bigint not null, status tinyint not null default 1 comment 1已选 0已退, create_time datetime not null default current_timestamp, unique key uk_student_class (student_id, teaching_class_id), key idx_class (teaching_class_id) ) engineInnoDB default charsetutf8mb4 comment选课记录;uk_student_class保证同一学生同一教学班只有一条记录。退课如果保留状态字段唯一索引会让“退课后重新选”插入失败所以通常的做法是退课直接物理删除选课记录另建一张选课日志表记录退课动作或者选课时先查旧记录存在则把status改回 1并同步扣减容量。两种做法都能跑毕业设计里更推荐物理删除加日志表逻辑简单答辩时也容易讲清楚。教学班表的selected_count是冗余计数用一条原子更新维护不要每次选课都去count(enrollment)。冗余计数会带来一个经典问题退课时忘记减回去导致余量越用越少。解决办法是把选课和退课都收在同一个 Service 里退课先删记录再执行selected_count selected_count - 1 where id? and selected_count 0两条语句放在同一个事务里。排课约束则落在course_schedule的三个索引上教师、教室、班级分别建索引冲突查询用范围重叠条件后面第 3 章会展开。3. 后端实现SpringBoot 分层、鉴权与两个关键算法后端部分不需要堆太多框架SpringBoot 加 MyBatis-Plus 足够把选课和排课讲清楚。真正要花心思的是三件事接口边界怎么切、登录态怎么传、选课和排课的并发与冲突怎么在数据库里收口。下面按工程分层、鉴权、选课、排课四个点展开代码只留关键片段参数和失败路径会写清楚。3.1 工程分层与接口边界Controller 不写业务Service 不碰 Web 对象包结构建议按职责分不要按页面分。controller只做参数接收和结果返回service放业务规则mapper只做 SQLentity对应表dto放入参vo放出参config放拦截器和跨域common放统一返回和异常。这样分的好处是排课冲突检测可以单独测不用启动整个 Web 容器。接口命名按资源走/api/course、/api/teaching-class、/api/schedule、/api/enrollment。选课接口是POST /api/enrollment退课是DELETE /api/enrollment/{id}学生课表是GET /api/enrollment/timetable?semester...。统一返回结构用code、msg、data三个字段业务异常抛BizException全局异常处理器转成code500或自定义错误码前端只判断code不要把 HTTP 状态码和业务码混在一起。Controller 里不要出现HttpServletRequest取用户 id 后到处传登录拦截器把userId和role放进请求属性Controller 用RequestAttribute取。Service 方法参数只保留业务字段比如enroll(Long studentId, Long classId)不要把HttpServletRequest传进 Service。这样选课逻辑在单元测试里可以直接调用排课冲突检测也能脱离登录态跑用例。3.2 登录鉴权与角色路由JWT 拦截器的落地方式登录成功后签发 JWTpayload 里放userId和role不要放密码和敏感字段。前端把 token 放在Authorization: Bearer xxx请求头后端拦截器统一解析白名单只放登录和验证码接口。角色校验可以再加一层注解比如RequireRole(admin)拦截器解析到方法上的注解后比对角色避免每个 Controller 手写 if。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行否则跨域时浏览器拿不到响应 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { writeJson(response, 401, 未登录); return false; } Claims claims; try { claims JwtUtil.parse(token.substring(7)); } catch (Exception e) { writeJson(response, 401, 登录已过期); return false; } // 用户信息放进请求属性Controller 不再重复解析 token request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role, String.class)); return true; } }JwtUtil.parse负责验签和过期判断writeJson统一输出 JSON避免拦截器返回空响应导致前端报跨域错误。白名单在WebMvcConfig里用excludePathPatterns(/api/auth/**)配置不要在图省事在拦截器里写一堆if判断 URL。密码存 BCrypt 哈希登录时用matches比对不要明文存库也不要在日志里打印密码。角色路由分两层前端按角色过滤菜单后端按角色校验接口。学生不能调教务的排课接口教师不能调选课接口这些规则写在拦截器或方法注解里。前端隐藏菜单只是体验不能当作权限。答辩时老师很爱问“我把前端代码改了是不是就能排课”答案必须是否定的因为后端接口还有角色校验。3.3 选课并发控制容量扣减、唯一索引与 Redis 预校验选课接口的核心是两条约束教学班余量不能小于 0同一学生不能重复选同一教学班。前者用带条件的原子更新后者用唯一索引。顺序通常是先扣减容量再插入选课记录插入失败抛异常事务回滚扣减自动恢复。这样比“先查余量再扣减”少一次查询也避免了检查与更新之间的窗口。Transactional(rollbackFor Exception.class) public void enroll(Long studentId, Long classId) { // 1. 原子扣减只有未满且开放时才加 1返回受影响行数 int rows teachingClassMapper.increaseSelected(classId); if (rows 0) { throw new BizException(余量不足或教学班未开放); } // 2. 写选课记录唯一索引 uk_student_class 兜底重复选课 try { enrollmentMapper.insert(new Enrollment(studentId, classId)); } catch (DuplicateKeyException e) { // 事务回滚上面扣减的容量会自动恢复 throw new BizException(已经选过这门课); } }对应的 Mapper 更新语句只有一条条件里必须带status 1和selected_count capacityupdate idincreaseSelected update teaching_class set selected_count selected_count 1 where id #{classId} and status 1 and selected_count lt; capacity /updaterows 0有两种可能教学班关闭或余量不足前端提示可以合并成“该教学班不可选”日志里再区分。DuplicateKeyException说明唯一索引拦住了重复选课事务回滚后容量恢复不会出现“没选上但余量少了”的情况。这里的事务隔离级别用默认的REPEATABLE READ就够update会对教学班行加锁并发请求排队执行最后只有一个能扣减成功。选课高峰时数据库行锁会成为瓶颈可以加一层 Redis 预校验把每个教学班的余量预热到 Redis用 Lua 脚本判断并自增返回 0 直接提示满员返回 1 再走数据库。Redis 只做前置拦截最终以数据库的原子扣减为准。Redis 计数可能因为事务回滚和缓存过期出现偏差选课结束后按数据库selected_count校正一次不要把它当成唯一数据源。-- KEYS[1] 教学班余量 keyARGV[1] 容量 local cap tonumber(ARGV[1]) local current tonumber(redis.call(get, KEYS[1]) or 0) if current cap then return 0 end redis.call(incr, KEYS[1]) return 1Lua 脚本的cap从教学班表读取后传入key 的过期时间覆盖整个选课窗口。返回 0 时前端直接提示“已满”不要再打数据库返回 1 时继续走enroll方法数据库仍可能因为余量变化返回失败前端按最终结果刷新列表。这套组合在毕业设计里足够讲清楚“为什么要两层”也不会把项目复杂度拉得过高。3.4 排课冲突检测时间片、教室、教师三维校验排课冲突的本质是范围重叠同一学期、同一天两个排课记录的节次区间有交集并且周次区间也有交集同时教师、教室、行政班三个维度中任意一个相同就算冲突。SQL 用start_section 新结束节次 and end_section 新开始节次判断节次重叠周次同理。下面这条查询一次把三个维度都查出来返回第一条冲突记录即可。select id, teaching_class_id from course_schedule where day_of_week #{dayOfWeek} and start_section #{endSection} and end_section #{startSection} and week_start #{weekEnd} and week_end #{weekStart} and (teacher_id #{teacherId} or classroom_id #{classroomId} or class_id #{classId}) limit 1参数说明dayOfWeek取 1 到 7startSection和endSection是新课的节次区间weekStart和weekEnd是周次区间teacherId、classroomId、classId从教学班和排课表单里带过来。查询命中后不要只返回“冲突”最好再查一次冲突记录的类型告诉教务是教师冲突、教室冲突还是班级冲突前端才能高亮对应行。排课保存时先查冲突再插入批量排课放在同一个事务里任意一条冲突整批回滚避免出现“排了一半”的中间状态。排课是低频操作通常由教务端单线程触发但也不能完全不管并发。两个教务同时排同一间教室先查后插仍然可能都通过。稳妥的做法是在事务里对教师和教室加行锁比如select id from classroom where id? for update再执行冲突查询和插入。锁的粒度按教师和教室分别加不要锁全表。排课接口还要校验教学班是否存在、教室容量是否足够、周次是否在学期范围内这些校验放在冲突查询之前失败信息越具体前端越容易定位。4. 前端实现Vue 课表渲染、角色菜单与选课交互前端不只是把接口数据铺到页面上选课和排课两个页面的交互细节会直接影响演示效果。课表要按星期和节次画成二维网格选课页要把余量、冲突提示和按钮状态做清楚排课页要允许调整但把最终裁决留给后端。下面四个点按路由、课表、选课、排课的顺序展开。4.1 路由与菜单按角色动态生成登录后拿到角色前端按角色过滤路由表菜单只展示当前角色能访问的页面。路由守卫里做一次兜底目标路由不在当前角色允许列表里直接跳到 403 页面。注意前端路由只负责展示后端接口仍要校验角色否则改一下 localStorage 就能进教务页面。const roleRoutes { student: [/course/select, /course/timetable], teacher: [/teacher/schedule, /teacher/apply], admin: [/base/course, /base/classroom, /schedule/manage, /course/audit] } router.beforeEach((to, from, next) { const role localStorage.getItem(role) const allowed roleRoutes[role] || [] if (to.meta.public || allowed.includes(to.path)) { next() } else { next(/403) } })roleRoutes只列一级路径子路由通过meta.role再校验一次。菜单数据可以从后端返回也可以前端按同一份配置生成毕业设计里前端配置更直观答辩时打开代码就能看到角色和页面的对应关系。token 过期时在响应拦截器里统一跳登录页不要在每個请求里单独判断。4.2 课表组件二维网格怎么从课程列表映射出来课表的数据源是排课记录列表每条记录包含dayOfWeek、startSection、endSection、weekStart、weekEnd、课程名、教师、教室。渲染前先按当前周过滤再把每条记录拆成“星期-节次”的映射。模板用 CSS Grid 画 7 列 12 行每个单元格判断是否有课有课且是起始节次时渲染一个跨行单元格。const sections Array.from({ length: 12 }, (_, i) i 1) const weekDays [周一, 周二, 周三, 周四, 周五, 周六, 周日] const grid computed(() { const map {} schedules.value.forEach(item { for (let s item.startSection; s item.endSection; s) { // 数据层节次从 1 开始渲染数组下标从 0 开始这里统一减 1 map[${item.dayOfWeek}-${s}] { ...item, rowSpan: item.endSection - item.startSection 1, isFirst: s item.startSection } } }) return map })模板里用grid[${day}-${section}]取单元格只有isFirst为 true 时才渲染内容并设置rowSpan。颜色按courseId取模选一组预设色同一门课在不同周次颜色一致答辩演示时更容易看清。跨周课程要按当前周过滤不能把 1 到 16 周的课全部铺在同一张表上否则课表会重复。节次表头固定 1 到 12星期表头固定周一到周日数据层不要出现 0 或 8 这种越界值后端插入时就要校验。4.3 选课页面的交互细节余量、冲突提示与结果反馈选课列表每行展示课程名、教师、上课时间、教室、容量、已选、余量。余量为 0 时按钮置灰已选时按钮变成“退课”这两个状态只是体验服务端仍要再校验。点击选课按钮后加 loading防止连点失败后根据后端返回的msg区分“已满”“已选”“时间冲突”并刷新当前列表避免本地余量和数据库不一致。async function handleEnroll(row) { if (row.selectedCount row.capacity) return if (row.selected) return const { data } await enrollApi(row.id) if (data.code 200) { row.selected true row.selectedCount 1 ElMessage.success(选课成功) } else { ElMessage.warning(data.msg) await loadList() } }enrollApi里统一处理 token 和错误码loadList重新拉取当前页数据。退课成功后同样刷新列表不要只把按钮改回去。选课页还可以加一个“已选课程”Tab按学期查询自己的选课记录和课表页共用同一份数据。时间冲突提示可以在前端先做一次粗判拿已选课程的星期和节次区间比对命中就提示“与已选课程冲突”但最终仍以服务端为准前端判断只是减少无效请求。4.4 排课端的拖拽与批量调整前端只做展示后端做裁判排课页通常是一个可拖拽的周网格教务把教学班拖到某个星期和节次上前端更新临时排课列表点击保存时整批提交。拖拽过程中不要实时写库否则拖错一次就产生一条脏数据。保存接口接收一个数组后端逐条校验冲突返回每条的结果前端根据结果把失败的条目标红成功的保留。批量接口的返回结构建议用successCount、failCount、failListfailList里带教学班 id、冲突类型和冲突描述。前端不要看到code200就认为全部成功要判断failCount。排课保存放在一个事务里时任意一条失败整批回滚前端需要把整个列表恢复成保存前的状态如果业务允许部分成功就不要用一个大事务改成逐条事务并在返回里说明。毕业设计里更推荐整批事务逻辑简单答辩时也容易讲“要么全排要么全不排”。拖拽组件只负责视觉位置星期和节次从放置的单元格里取不要从鼠标坐标反算。放置后校验教学班是否已经排过、教室容量是否匹配明显不合法的直接在前端拦下减少后端压力。但教师冲突、教室冲突、班级冲突仍然交给后端 SQL 判断前端不做最终裁决。5. 避坑与排查选课排课项目里最容易翻车的五件事选课排课项目的 bug 有个特点单线程点一遍都能过一到演示就出问题。下面五条是这类项目里最常见的翻车现场每条按“现象—原因—解决”写照着排查能省不少时间。5.1 时间片不统一导致“看起来没冲突”现象排课页面提示冲突但数据库里查不到冲突记录或者两门课明明撞了系统却允许保存。原因通常是两套时间表示并存排课表存day_of_week和节次教学班表却存“周一第 1-2 节”字符串冲突查询只查了其中一套另一套没参与比较。还有的项目把周次存成“1-16周”字符串范围重叠判断只能靠like边界条件一多就漏。解决全链路统一成数值字段。排课记录只存day_of_week、start_section、end_section、week_start、week_end展示层再拼成“周一第 1-2 节1-16 周”。教学班如果也需要展示上课时间从排课记录聚合不要另存一份字符串。导入历史数据时写一个转换脚本把“周一第 1-2 节”拆成数值后再入库转换完抽查几条边界数据比如“周日第 11-12 节”和“第 16 周”。5.2 并发选课超卖先查余量再扣减的顺序错了现象教学班容量设为 1两个学生同时点选课两个人都显示成功selected_count变成 2。原因是在 Service 里先select余量判断小于容量再update加 1两个请求都通过了判断更新时没有条件限制。这是典型的 check-then-act单线程测不出来一并发就现形。解决把判断放进update的where里用受影响行数判断是否成功。update teaching_class set selected_count selected_count 1 where id? and status1 and selected_count capacity返回 0 就抛业务异常。再加唯一索引uk_student_class防止同一学生重复选。事务里先扣减再插入插入失败回滚容量自动恢复。测试时用两个浏览器或无头脚本同时请求看数据库最终记录数和selected_count是否一致。5.3 排课冲突只查教师不查教室现象教师时间不冲突两门课却排进了同一间教室或者同一个行政班在同一节被排了两门课。原因是冲突查询只加了teacher_id条件漏了classroom_id和class_id。有的项目虽然查了教室但只查“教室 id 相同”没有判断节次区间重叠相邻节次也被误判。解决冲突 SQL 用or把教师、教室、班级三个维度串起来节次和周次都用范围重叠条件。命中后不要只返回“冲突”再查一次冲突记录告诉用户是教师冲突、教室冲突还是班级冲突。保存前把三个维度分别查一遍任何一条命中就拒绝批量保存放在事务里避免排一半。测试用例至少覆盖同教师同节次、同教室同节次、同班级同节次、节次部分重叠、周次部分重叠。5.4 前端课表渲染错位从 0 开始还是从 1 开始现象周一第一节课显示在周日或者所有课整体错开一格。原因是数据层节次从 1 开始前端数组下标从 0 开始渲染时直接拿section当下标用。还有的项目星期用 0 到 6后端用 1 到 7两边没对齐周日变成周一。解决约定数据层星期 1 到 7、节次 1 到 12前端渲染时统一减 1 再映射到数组或网格。课表组件的grid用“星期-节次”字符串做 key模板里按 key 取不要用二维数组下标硬算。写一个最小用例只排一条周一第 1-2 节的课看它是否落在第一列第一行再排一条周日第 11-12 节看是否落在最后一列最后一行。两个边界过了中间基本不会错。5.5 SQL 脚本导入失败与字符集问题现象导入 SQL 脚本时报错或者导入成功但中文全是乱码日期默认值报错本文还有配套的精品资源点击获取