
做了这么多年开发课表管理系统这种“看起来简单、做起来全是细节”的项目我前后至少经手过三个版本。这次给西安工商学院做的这套基于 SpringBoot Vue MySQL MyBatis 的课表管理系统算是我个人比较满意的一个完整方案。今天把整个设计与实现过程从头到尾捋一遍包括数据库怎么建、排课冲突怎么处理、前端课表怎么渲染、前后端联调踩了哪些坑全部交代清楚。做毕设、做课设、或者想入门这类管理系统的朋友这份实战记录应该能帮你省掉不少弯路。选这套技术栈不是因为什么“流行”而是这套组合在高校类管理系统中确实最稳、最不折腾。SpringBoot 负责后端接口Vue 负责前端交互MySQL 存数据MyBatis 做持久层每个环节的生态都成熟到不能再成熟出了问题随便一搜就有答案。课表系统本质上就是一个典型的 CRUD 加业务约束的系统用这套栈来开发效率和稳定性都照顾到了。1. 整体设计思路与需求拆解1.1 课表管理系统的真实需求是什么先说需求。很多人一听到“课表管理系统”第一反应就是“不就是增删改查吗”。真做起来你会发现课表系统的难点根本不在 CRUD 本身而在三个地方排课冲突检测、多维度的课表查询、权限隔离。西安工商学院的场景下使用角色主要有三类教务管理员、教师、学生。教务管理员负责维护教学计划、排课、调课、安排教室和教师教师需要查看自己的周课表可能还需要申请调课学生只需要查询班级课表知道自己什么时间在哪上课就行。从数据维度看一张课表至少要表达清楚五件事谁教师、教谁班级/学生、在哪教室、什么时间周次星期节次、上什么课课程。五件事拆开看都很简单但合在一起就是一张多维表格加上“一个教室同一时间不能上两门课、一个老师同一时间不能出现在两个教室、一个班级同一时间不能上两门课”这三条硬约束逻辑复杂度一下子就上来了。所以我做这套系统时没有急着写代码而是先把排课的业务约束列成清单然后再倒推数据库设计和接口设计。这也是我特别想强调的一点——做管理系统业务建模永远比技术选型更值得花时间。1.2 技术选型为什么是 SpringBoot Vue MySQL MyBatis这个选型组合核心思路就四个字稳定、好招人。后端用 SpringBoot看中的是它整合生态的能力和环境隔离。SpringBoot 的自动配置能把第三方库的集成成本降到最低集成 MySQL、MyBatis、Redis、JWT 这些组件基本上就是几行配置的事。而且内嵌 Tomcat部署就是一个 jar 包跑起来这对高校机房或者个人服务器来说非常友好。持久层选 MyBatis 而不是 JPA原因也很实际。课表系统的查询条件非常灵活——按学期查、按教师查、按班级查、按星期查条件经常动态组合。MyBatis 的动态 SQL 在这种场景下写起来非常顺手而且 SQL 可控性强遇到性能瓶颈可以精确优化某一条 SQL。JPA 虽然省代码但复杂查询反而别扭。前端选 Vue理由就一个组件化开发适合这类后台管理界面。课表渲染、排课表格、弹窗表单、筛选面板全部可以拆成独立组件团队协作和后期维护都会轻松很多。加上 Vue 生态里的 Element UI 组件库表格、表单、日期选择器这些后台高频组件开箱即用开发效率直接翻倍。数据库选 MySQL 不用多解释高校场景数据量根本到不了需要上 PostgreSQL 或商业数据库的程度MySQL 的稳定性和易用性足够了。2. 数据库设计与核心表结构2.1 建表思路先定约束再定表我建表有个习惯——先列业务规则再设计表结构。课表系统的核心业务规则是每一学期有一份独立的课表不能跨学期混排排课单位是“教学班”一个教学班由一个教师、一个课程、一个选修班级集合构成教室是有限资源排课必须校验教室占用教师课表、学生课表、教室课表都是排课结果的不同查询视角基于这些规则我把表拆成了两组基础信息表和排课核心表。基础信息表用户表、角色表、课程表、教师表、学生表、班级表、教室表、学期表。排课核心表排课表课程安排表、选课关系表。这里我把选课关系单独拆出来是有原因的。一个班级的多个学生同时上一门课如果课程安排表直接关联学生数据冗余会非常严重。所以课程安排表关联的是教学班或者班级而选课关系表负责记录“哪个学生属于哪个教学班”这样课程安排表保持干净学生维度的课表也能直接查出来。2.2 关键表结构细节学期表是整个课表系统的时间基准。每条记录包含学期编号、名称、起始周日期、周数、当前状态是否当前学期。课程表相对简单课程编号、名称、学时、学分、课程类型。排课表是整套系统的核心字段设计直接影响后续查询和冲突检测的复杂度。我最终的设计是这样的主键安排排课编号关联字段学期ID、教学班ID、课程ID、教师ID、教室ID时间字段起始周、结束周、周次类型单周/双周/每周、星期几、开始节次、结束节次辅助字段备注、创建时间、更新时间时间字段这里我特别说明一下“周次类型”。现实中很多课程不是每周都上有的课单周上、有的课双周上如果用一条记录表达就会出错。我在表里加了一个“周次类型”字段值范围是“每周、单周、双周、自定义周次”自定义周次时用逗号分隔的周数列表字符串存。这样既兼容绝大多数排课场景又不会把表结构搞得太复杂。班级表、教师表、学生表就是常规设计注意一个点学生表和班级表是关联关系教师表和用户表是关联关系不要用冗余字段把关联做成拷贝否则后续改名字、调部门的时候会到处不一致。2.3 关于 MyBatis 的映射与设计约束数据库设计阶段容易忽略一件事——外键约束。我在建表语句里不强加物理外键而是在 MyBatis 映射和 Service 层做逻辑校验。原因很简单物理外键在高并发写入时会有性能损耗而且排课系统里经常做批量导入、批量调整外键约束会阻碍这些操作的灵活性。但不建外键不代表不管理关系。我在代码里通过关联查询保证数据引用完整性比如删除教室之前先查排课表是否有引用有引用就阻止删除并返回提示。这种“逻辑外键”的方式在管理类系统里很常见也足够可靠。给一个核心排课表的建表 SQL 片段作为参考CREATE TABLE course_schedule ( schedule_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 排课编号, semester_id bigint(20) NOT NULL COMMENT 学期ID, teaching_class_id bigint(20) NOT NULL COMMENT 教学班ID, course_id bigint(20) NOT NULL COMMENT 课程ID, teacher_id bigint(20) NOT NULL COMMENT 教师ID, classroom_id bigint(20) NOT NULL COMMENT 教室ID, start_week int(4) DEFAULT NULL COMMENT 起始周, end_week int(4) DEFAULT NULL COMMENT 结束周, week_type varchar(20) DEFAULT ALL COMMENT 周次类型: ALL-每周, ODD-单周, EVEN-双周, CUSTOM-自定义, custom_weeks varchar(100) DEFAULT NULL COMMENT 自定义周次, 逗号分隔, weekday int(1) NOT NULL COMMENT 星期: 1-7, start_section int(2) NOT NULL COMMENT 开始节次, end_section int(2) NOT NULL COMMENT 结束节次, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (schedule_id), KEY idx_semester_teacher (semester_id, teacher_id), KEY idx_semester_weekday (semester_id, weekday) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT排课表;索引设计上我针对高频查询建了复合索引学期教师、学期星期。这两个组合基本覆盖了“老师查课表”和“按星期排课”两个核心场景。教室维度也值得加一个学期教室的索引因为教务排课时要反复校验教室占用。3. 后端实现SpringBoot MyBatis 核心代码设计3.1 工程结构按功能包而不是按层包后端代码结构我用的是按功能模块分包而不是传统的 controller/service/mapper 三层平铺。对比一下你就会发现按功能分包的好处传统分包是 controller 包里面装所有接口service 包里面装所有业务逻辑。项目一大了找代码全靠滚动。按功能分包则是管理、课表、基础信息各是一个包每个包内自带 controller、service、mapper高内聚低耦合改一个功能时文件全在一个目录下。实际结构大概是这样的com.xagong.dashboard ├── config // 配置类跨域、拦截器、MyBatis配置 ├── common // 公共类统一返回体、异常处理、工具类 ├── module │ ├── auth // 登录认证 │ ├── user // 用户管理 │ ├── course // 课程管理 │ ├── schedule // 排课管理核心模块 │ └── baseinfo // 教室、班级、学期等基础信息 └── XagongApplication.java这种结构看起来简单但对管理系统来说非常好用尤其是多人协作时每个人负责一个 module互相不纠缠。3.2 登录认证与权限控制课表系统有三个角色接口必须做权限区分。我用 JWT 做无状态认证在 SpringBoot 里通过拦截器实现 Token 校验。原理不复杂用户登录成功后后端用 secret 生成一个 Token 返回给前端前端在每个请求的 Header 里带上Authorization: Bearer token后端拦截器解析 Token拿到用户ID和角色放入请求上下文接口内部再根据角色做数据权限控制。这里有个细节值得注意JWT 里不要放敏感信息。我见过有人把密码直接放进 JWT这是很不安全的做法。JWT 本身只是 Base64 编码不是加密里面放用户ID、角色、过期时间就够了其他信息需要时再查数据库。权限控制的重点在于普通用户和管理员看到的数据范围完全不同。学生登录后只能查自己所属班级的课表教师登录后只能看自己的课和授课班级信息管理员才有排课权。这些数据权限我放在 Service 层处理而不是拦截器层。拦截器只负责认证Service 层做数据过滤职责更清晰。3.3 排课冲突检测核心业务逻辑排课接口是整套系统里业务逻辑最重的一个。我的做法是新增排课和修改排课走同一个校验方法校验通过后才落库。冲突检测分三类教室冲突同一学期、同一周的同一时间段不能有两个教学班占用同一教室。查询条件就是教室ID相同、星期相同、节次时间有交集、周次有交集、排除自身排课ID。节次时间判断其实是一个区间重叠判断两个时间段[a_start, a_end]和[b_start, b_end]冲突的条件是a_start b_end AND a_end b_start。这个判断很多人第一次会写错写成a_start b_start || a_end b_end只覆盖了部分重叠情况。教师冲突同一教师在同一时间不能出现在两门课上。判断逻辑和教室冲突完全一样只是查询维度换成教师ID。班级冲突属于同一教学班或存在选课关联时同一班级不能同一时间上两门课。这里要注意课程表里的教学班可能由多个班级组成所以班级冲突检测要关联教学班和班级的关系。周次交集判断是另一块容易出错的地方。两个排课周次如果都是“每周”一定有交集如果是单周和双周没有交集如果是自定义周次和每周需要解析周次数组做集合交集判断。我写了一个工具方法public static boolean hasWeekOverlap(String weekTypeA, String customWeeksA, int startWeekA, int endWeekA, String weekTypeB, String customWeeksB, int startWeekB, int endWeekB) { SetInteger weeksA expandWeeks(weekTypeA, customWeeksA, startWeekA, endWeekA); SetInteger weeksB expandWeeks(weekTypeB, customWeeksB, startWeekB, endWeekB); if (weeksA.isEmpty() || weeksB.isEmpty()) { return false; } weeksA.retainAll(weeksB); return !weeksA.isEmpty(); }expandWeeks方法把各种周次类型统一展开成周数集合然后再做交集判断。展开逻辑每周就返回[startWeek, endWeek]所有周数单周就返回其中的奇数周双周返回偶数周自定义就直接解析customWeeks字符串。冲突检测做完之后还有一个细节检测动作要加事务。因为冲突检测和新增排课是两个操作如果检测完之后、落库之前有其他请求插入了一条冲突排课就会出现“检测通过但写入了冲突数据”的竞态条件。我把检测和插入放到同一个Transactional方法里配合排课表上的唯一约束兜底基本就稳了。排课接口的核心流程大概是这样校验学期状态只能给当前学期排课校验课程、教师、教室、教学班是否存在且状态正常解析节次区间、周次类型统一转成标准化对象执行三类冲突检测插入排课记录返回排课结果ID3.4 MyBatis 动态 SQL 的实战写法MyBatis 动态 SQL 在我这个系统里使用频率最高的场景是“多条件组合查询”和“批量更新”。比如排课查询接口调用方可能只传学期ID也可能传学期ID教师ID星期几还可能加教室ID做过滤。这种查询你不可能为每个组合写一条 SQL所以动态 SQL 是刚需。核心写法是用where标签配合if条件。以排课查询为例select idselectScheduleList resultTypemap SELECT cs.schedule_id, cs.weekday, cs.start_section, cs.end_section, cs.start_week, cs.end_week, cs.week_type, cs.custom_weeks, ct.course_name, t.teacher_name, room.classroom_name, tc.class_name FROM course_schedule cs LEFT JOIN course ct ON cs.course_id ct.course_id LEFT JOIN teacher t ON cs.teacher_id t.teacher_id LEFT JOIN classroom room ON cs.classroom_id room.classroom_id LEFT JOIN teaching_class_info tci ON cs.teaching_class_id tci.id LEFT JOIN class_info tc ON tci.main_class_id tc.class_id where if testsemesterId ! null AND cs.semester_id #{semesterId} /if if testteacherId ! null AND cs.teacher_id #{teacherId} /if if testweekday ! null AND cs.weekday #{weekday} /if if testclassroomId ! null AND cs.classroom_id #{classroomId} /if if testclassId ! null AND EXISTS ( SELECT 1 FROM teaching_class_member tcm WHERE tcm.teaching_class_id cs.teaching_class_id AND tcm.class_id #{classId} ) /if /where ORDER BY cs.weekday, cs.start_section /select用where时 MyBatis 会自动干掉第一个多余的AND不需要人为写WHERE 11这种丑陋写法。这也是动态 SQL 里最基本的注意点。批量插入排课场景也会用到动态 SQL用foreach标签。比如管理员一次排十节课如果循环单条插入就是十次数据库往返性能差而且事务不好统一管理。用批量插入一次搞定insert idbatchInsertSchedule INSERT INTO course_schedule ( semester_id, teaching_class_id, course_id, teacher_id, classroom_id, start_week, end_week, week_type, custom_weeks, weekday, start_section, end_section ) VALUES foreach collectionlist itemitem separator, (#{item.semesterId}, #{item.teachingClassId}, #{item.courseId}, #{item.teacherId}, #{item.classroomId}, #{item.startWeek}, #{item.endWeek}, #{item.weekType}, #{item.customWeeks}, #{item.weekday}, #{item.startSection}, #{item.endSection}) /foreach /insert注意 MySQL 的连接串上要加rewriteBatchedStatementstrue否则批量插入不会真正走批量提交性能提升不明显。3.5 Service 层的事务与异常处理事务管理我全部用注解Transactional没有手写事务管理代码。默认情况下 Spring 的声明式事务只在 Runtime 异常时回滚所以我的 Service 层约定是业务校验失败直接抛自定义业务异常而不是返回错误码让调用方再判断。这样做的好处是事务边界清晰哪里出了错就自动回滚不会出现“插入一半但返回错误”的脏数据。比如冲突检测到教室被占用了直接throw new BizException(教室在周三第3-4节已被占用)。这个异常被全局异常处理器接住统一返回给前端前端弹出提示。事务因为异常自动回滚检测和插入两条步骤保持原子性。另外还有一个经验值得分享不要在 controller 里写业务逻辑。我见过不少项目把参数校验、状态判断、甚至冲突检测都写在 controller 里controller 变得臃肿且难以测试。我的做法是 controller 只做参数接收和调用 service一行scheduleService.addSchedule(req)搞定service 内部做全部校验和组装。后期加单元测试也好写重构也好改。4. 前端实现Vue 页面设计与课表渲染4.1 Vue 工程结构与页面规划前端我用 Vue 2 Element UI 开发工程结构按页面和组件划分。管理员端和学生端/教师端其实可以复用大量组件只是入口菜单不同。页面规划登录页面账号密码登录登录成功存 Token 到本地存储课表展示页核心页面支持按周视图展示区分教师课表、学生课表、教室课表排课管理页管理员专用提供排课表单、冲突提示、调课功能基础信息管理页课程、教师、教室、班级的增删改查学期管理页维护学期信息和当前学期状态路由用 Vue Router 的异步路由加载登录的时候根据角色动态生成菜单也顺便做了权限控制。访问未授权页面时会跳转到 404 提示页。4.2 课表周视图渲染CSS Grid 布局课表展示是整个前端最核心的部分。我用 CSS Grid 来绘制周课表7列周一至周六南工商这里偶尔还会有周日的补课安排纵向是节次行。数据结构上后端返回的是一个排课数组每个元素包含 courseName、teacherName、classroomName、startSection、endSection、weekday 等字段。前端拿到数组后需要按 weekday 分组再按节次排序生成一个二维映射。渲染时最关键的点是跨行处理。一门课可能占用两节课甚至三节课所以在 Grid 布局里要让这个课程单元格跨多行。我的做法是先做一个空白课表结构然后把排课记录填入对应格子通过grid-row-start和grid-row-end控制单元格跨行。// 生成课表映射的核心逻辑 buildGridMap(scheduleList, totalSections) { const map {}; for (let i 1; i 7; i) { map[i] new Array(totalSections 1).fill(null); } scheduleList.forEach(item { const row item.weekday; for (let section item.startSection; section item.endSection; section) { // 记录这门课覆盖的节次 if (!map[row][section]) { map[row][section] item; } } }); return map; }然后模板里遍历生成 Grid 单元格遇到一门课的起始节次时渲染课程块非起始节次的位置渲染空白占位。这么做有个视觉上的好处——课表上的课程块颜色可以根据课程类型区分用计算属性动态设背景色一目了然。实际用下来老师和学生反馈都说比原来的纯文字课表舒服多了。4.3 排课表单与冲突反馈排课管理页是管理员使用最频繁的页面。表单字段包括课程、教师、教室、教学班、学期、周次类型、起始周、结束周、星期、节次。提交时前端先做一次基础校验比如结束节次不能小于开始节次、起始周不能大于结束周。然后请求后端接口后端做冲突检测如果冲突会在返回体中带上冲突原因。我在前端做了一层优化体验的预提示排课表单选择教师后联动查出该教师的已有课表在提交之前就能看到时间占用情况。实现逻辑是监听教师选择事件调用查询接口把当前教师的排课渲染到一个小型课表弹窗里。这样做虽然多了一次请求但极大减少了提交后才发现冲突的情况。这里要给一个忠告不要试图在前端把冲突检测逻辑做完整。前端预提示只是体验优化真正的业务校验必须放在后端。你永远无法预料所有前端输入路径就算前端校验再全绕过前端直接调接口的情况依然存在。后端校验是安全底线前端校验是用户体验两者的定位要想清楚。4.4 前后端联调与跨域问题开发阶段前后端分离跑在两个端口上前端 8080后端 8088必然遇到跨域。我用 SpringBoot 统一配置了 CORSConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意这里有个细节addAllowedOriginPattern(*)和allowCredentials(true)在 Spring 5.3 之后可以共存。如果用的是旧版本 Spring写addAllowedOrigin(*)加allowCredentials(true)会被浏览器拒绝报“credential is not supported if wildcard is used”错误。这种问题排查起来特别费时间最好一开始就注意配置版本兼容。生产环境部署时我更推荐另一种方式前端 Vue 打包后直接把静态资源放进 SpringBoot 的src/main/resources/static目录由 SpringBoot 统一提供页面和接口。这样的话根本不存在跨域问题一个端口搞定所有。后面打包部署我再展开讲。5. 常见问题与排查实录5.1 中文乱码问题这套系统第一批测试就遇到了中文乱码原因包括数据库表格字符集不是 utf8mb4插入的中文变成了问号MySQL 连接串上没有设置字符集解决方式就是两处都改数据库层面把表字符集改为utf8mb4连接串加上characterEncodingutf8。另外前端 HTML 和后端 Response 都统一使用 UTF-8SpringBoot 默认就是 UTF-8一般不用额外配置。如果你用的 MySQL 版本较老要注意驱动连接串里serverTimezoneAsia/Shanghai也加上否则时间会差 8 小时。5.2 前端打包后访问提示 NotFound 错误Vue 默认的 history 路由模式下打包部署到 SpringBoot 静态目录后直接访问根路径正常但刷新某个子路径页面时会报 404。原因是前端路由是浏览器端处理的刷新时浏览器直接向服务器请求该路径而服务器上并没有对应的物理文件。解决办法有两种一种是用/路径分配器把未知路径全部转发到首页。SpringBoot 里实现一个简单的 ControllerController public class PageController { RequestMapping(value {path:^(?!api|static|.*\\..*).*$}) public String forward() { return forward:/index.html; } }另一种更省事把前端路由改成 hash 模式URL 带#刷新永远请求的是根路径不存在 404 问题。但 hash 模式的 URL 不美观对 SEO 不友好。由于这里是课表系统基本都是登录后才使用不见得需要 SEO所以用 hash 模式其实更稳。我自己的做法是 history 模式加转发看着干净不过如果你不想折腾直接 hash 模式也没毛病。5.3 MyBatis 查询结果驼峰映射失效数据库字段名用的是下划线风格比如schedule_id、teacher_nameJava 实体类是驼峰风格scheduleId、teacherName。如果没配置驼峰映射查询出来的字段全是 null。一开始我在 SQL 里手写别名AS scheduleId解决了一部分但每个查询都写别名太累。后来直接在 application.yml 里开启了全局配置mybatis: configuration: map-underscore-to-camel-case: true配完这个MyBatis 自动把下划线字段名映射到驼峰属性所有查询一次性解决。这个配置建议在项目初期就加上否则越到后面手写别名越痛苦。5.4 排课并发提交导致的冲突数据开发阶段测试时发现两个管理员同时提交“同一教室同一时间排课”的请求偶尔会出现两条冲突记录同时入库。原因之前提过冲突检测和插入不在同一个事务和锁的范围内。我最终的解决方式是三重保障检测和插入放进同一个Transactional方法排课表增加一个唯一索引字段组合是semester_id classroom_id weekday start_section end_section数据库层面直接拒绝重复教室占用业务层对DuplicateKeyException做了特殊捕获转成友好提示“该教室此时段已被排课请刷新后重试”加了唯一索引之后哪怕有几个并发请求同时通过业务校验数据库也会兜底拒绝重复数据。这大概是分布式锁之外的次优解法但对单机部署的高校系统来说完全够用。5.5 课表数据导入慢的问题有时候教务从 Excel 导入大量排课数据几百条记录一条一条插入耗时明显。我发现这是因为 MySQL JDBC 默认不会批量执行单条插入语句即使你用了PreparedStatement如果没有开启 rewrite 批量参数仍然是逐条网络往返。在连接串里加上rewriteBatchedStatementstrue后批量插入耗时从几十秒降到一两秒效果非常明显。这也是batchInsert场景里最常见的性能优化点建议所有 MyBatis 批量插入项目都检查一下这个参数。6. 部署上线配置与后续扩展建议6.1 后端打包与部署SpringBoot 打包我用的 Maven 插件clean package生成 fat jar服务器上直接java -jar xagong-course-system.jar --spring.profiles.activeprod启动。环境区分我在resources目录下放了三个配置文件application.yml公共配置application-dev.yml本地开发配置application-prod.yml生产环境配置生产环境的数据库密码不要写在配置文件里明文保存用环境变量注入。比如spring: datasource: username: ${DB_USERNAME} password: ${DB_PASSWORD}服务器上用 systemd 做进程守护开机自启崩溃自动重启[Unit] DescriptionXagong Course System Afternetwork.target [Service] ExecStart/usr/bin/java -jar /opt/xagong/course-system.jar --spring.profiles.activeprod Restartalways Userappuser [Install] WantedBymulti-user.target这个配置不用 Nginx 也能直接把服务跑起来只是静态资源和接口共用一个端口。如果前端是单独的 Nginx 托管那 Nginx 配置里加一个反向代理/api开头请求转发到后端 8088其余请求走前端静态文件。两种方案我都实际跑过高校内部访问量不大单端口部署最省事。6.2 后续功能扩展空间做的过程中积累了一些可扩展的思路如果后面接续做课表系统可以从这几个方向延展对接学校的统一身份认证把密码校验替换成 CAS 或 OAuth2 对接不需要重建用户表移动端适配Vue 代码打包成 H5 版本做一套响应式布局学生用手机直接看课表消息通知调课成功后在系统内生成通知甚至可以对接邮件或短信调课审批流把调课从管理员直接改课表改成教师发起申请、管理员审批后生效的流程这些扩展都不需要动表结构的大框架基于现有的用户、排课、学期等基础体系就能逐步加功能。我个人在整套系统做完之后最大的体会是课表管理系统这类项目真正考验人的不是哪个技术点特别难而是有没有把业务约束拆清楚再对应到合适的代码结构和数据库设计上。SpringBoot 和 Vue 只是工具你对排课场景理解得越深写出来的系统就越顺手。如果你正在做类似的系统建议先花时间把业务规则和核心流程理顺再动手写代码这条路比我踩过的坑要顺畅得多。