ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+MyBatis+MySQL课表管理系统完整实现与部署指南

SpringBoot+Vue3+MyBatis+MySQL课表管理系统完整实现与部署指南 前阵子一个学弟找我帮忙收拾一套课表管理系统他的需求说得很直白老师要求后端必须用SpringBoot前端用Vue数据库用MySQL还要能按周次切换课表、按班级/教师/教室三种维度查询。我看了一眼他下载的所谓“完整源码”数据库脚本少了两张表前端页面路由一堆报错跑起来连登录都给404。最后没办法我带他把这套系统从头理了一遍重新整理成一套干净的前后端分离架构。今天我就把这套基于SpringBootVue3MyBatisMySQL的课表管理系统完整实现思路拆给大家从数据库设计到后端接口再到Vue3课表组件渲染和实际部署都挑重点说。正在做课设、毕设或者想练手前后端分离项目的同学可以直接照着改少走几个月的弯路。1. 项目概述与整体设计思路1.1 课表管理系统的真实需求是什么很多同学一看到“课表管理系统”这六个字就以为只是把每周课程显示在表格里。真正动手做之后才发现课表系统的核心不是“显示”而是“排课约束”。西安工商学院这样的场景下实际使用角色有三种管理员负责维护基础数据和排课教师查看自己的授课安排学生查看班级课表。基础需求可以拆成四块基础信息管理班级、教师、学生、课程、教室的新增、修改、删除、分页查询。排课管理管理员选择班级、课程、教师、教室、起止周次、星期几、第几节生成一条排课记录。课表查询学生按班级查课表教师按教师ID查课表管理员可以按教室查课表所有查询都支持按周次切换比如只看第8周。登录认证不同角色登录后看到不同的功能菜单和数据范围权限设计不复杂但必须有。容易被忽略的非功能需求也很关键排课不能冲突意思是同一时间、同一教室不能同时排两门课同一个班级同一时间也不能排两门课同一个教师同一时间也不能跨教室上课。这些约束光靠前端校验不够数据库和后端接口都必须兜底。后面我会重点讲这块。1.2 为什么选择前后端分离 SpringBoot Vue3 MyBatis先说结论这套组合不是最新潮的但绝对是课程设计和中小型管理系统里最稳的。前后端分离的好处是后端只提供JSON接口前端独立开发、独立部署本地开发时用Vite代理转发请求生产环境用Nginx托管静态页面职责非常清晰。后端用SpringBoot理由很简单内嵌Tomcat、自动配置、依赖管理省心。不需要像传统SSH项目那样写一堆web.xml和Spring配置文件一个Application类直接启动。版本选择上我建议如果本地JDK是8就用SpringBoot 2.7.18如果是JDK17或更高可以用SpringBoot 3.2.x。网上很多人说“SpringBoot版本太高报错”多半是JDK没匹配上。MyBatis在这个项目里很合适因为课表查询往往要带很多动态条件——按班级查、按教师查、按教室查、按周过滤直接用JDBC拼接SQL很容易出错MyBatis的动态SQLif和where可以把这些条件组合写得很干净。再加上数据库字段习惯用下划线命名MyBatis开启驼峰映射后查出来的结果能直接对应实体类。前端选Vue3主要看重组合式API和响应式数据。课表本质是一个二维矩阵Vue3的ref和reactive在处理动态渲染表格、点击单元格弹窗这类交互时非常顺手。组件库用Element Plus表格、表单、弹窗、消息提示都现成做后台管理系统效率极高。1.3 项目目录结构与模块划分后端项目名我这里叫timetable-backend标准的Maven结构timetable-backend ├── src/main/java/com/example/timetable │ ├── controller // 接口层AuthController, CourseController, ScheduleController... │ ├── service // 业务逻辑ScheduleService, UserService... │ ├── mapper // MyBatis接口ScheduleMapper, CourseMapper... │ ├── entity // 数据库实体 │ ├── dto // 请求/响应对象 │ ├── common // 统一返回结果、全局异常处理、JWT工具等 │ └── config // 拦截器、跨域配置 └── src/main/resources ├── mapper // MyBatis XML文件 └── application.yml前端项目叫timetable-frontendVite默认结构的基础上加了timetable-frontend/src ├── api // axios 请求封装 ├── assets // 静态资源 ├── components // 课表组件、课程弹窗等 ├── router // 路由配置 ├── store // Pinia 状态管理 ├── views // 登录页、课表页、管理页 ├── utils // 工具函数 └── App.vue模块划分清晰的好处是后面你想加“调课申请”“教室空闲查询”这类扩展点不需要动太多老代码。2. 数据库设计课表系统的地基2.1 课表数据建模的关键难点课表系统最核心的难点是把“课程安排”这个多维信息在关系型数据库里表达清楚。一条排课记录至少涉及班级、课程、教师、教室、星期几、节次、起止周、单双周这8个维度。很多毕设源码喜欢把所有字段堆到一张course表里这样写起来最省事但后果是课程基础信息和具体排课混在一起想查“第8周3-4节机械楼101有没有课”要写一堆模糊条件性能差而且极容易排重。我推荐的做法是拆两层课程基础表和排课安排表。课程表只存课程名称、学分、总学时这类不变信息排课表存“哪个班级、哪门课、哪个老师、哪个教室、什么时间上”一条记录就是一次排课。这样做的一个直观好处是一门课可以拆成多条排课记录比如周一1-2节一次、周三3-4节一次学分和总学时依然记在课程表里数据结构非常干净。2.2 核心表结构设计下面是精简版的表清单实际做课设可以再加字段。表名用途关键字段tb_user登录账号user_id, username, password, role, real_nametb_class班级class_id, class_name, gradetb_student学生student_id, user_id, class_id, nametb_teacher教师teacher_id, user_id, name, titletb_course课程course_id, course_name, credit, total_hours, teacher_idtb_classroom教室classroom_id, room_name, capacity, buildingtb_schedule排课记录schedule_id, class_id, course_id, teacher_id, classroom_id, week_start, week_end, week_type, day_of_week, section_start, section_end排课表字段的命名直接对应前端课表组件的数据结构这点很关键。week_start和week_end表示这门课从第几周上到第几周比如第1周第16周week_type用1表示全周2表示单周3表示双周避免单双周交替的课排不出来day_of_week存1到7section_start和section_end表示第几节到第几节比如“3-4节”。建表SQL里值得注意的地方是字符集和排序规则我统一用utf8mb4和utf8mb4_general_ci千万别用latin1否则中文姓名和课程名插入时会直接乱码。MySQL 5.7和8.0都支持这个字符集。2.3 索引设计与排课防冲突索引不要贪多根据查询场景来。课表查询的WHERE条件通常是class_id day_of_week section_start week_start这个组合最需要索引教师查课表走teacher_id教室查询走classroom_id。我的建议是至少给tb_schedule加三个单列索引idx_class_id、idx_teacher_id、idx_classroom_id如果数据量不大几千条这三个索引足够用。排课防冲突是重点。最稳妥的兜底办法是在tb_schedule上建一个联合唯一索引但问题是“同一教室同一时间”和“同一班级同一时间”是两组不同维度无法用一个唯一索引同时覆盖。实际操作中我是这样处理的数据库给tb_schedule加主键自增不做复杂唯一约束而是在Service层提供排课校验方法判断新增的排课记录是否和已有记录“时间重叠”。比如同样教室、同样星期、同样节次范围如果两条记录在周次上存在交集且单双周规则也重叠就拒绝保存。这种判断逻辑用Java写很直白就是在插入前先SELECT一次。并发量很低的教务系统场景这个方案完全够用。3. 后端核心实现SpringBoot MyBatis3.1 项目初始化和关键依赖项目用Spring Initializr生成就好不要自己手写pom。不同版本的依赖名称有区别用SpringBoot 2.7.x时MyBatis Starter依赖是mybatis-spring-boot-starter如果是SpringBoot 3.x建议用mybatis-spring-boot-starter的3.x版本。MySQL驱动在SpringBoot 2.7里用mysql-connector-java在SpringBoot 3里改成了com.mysql:mysql-connector-j这个细节常年坑人。另外建议加上lombok实体类不用写一堆getter/setter代码清爽很多。spring-boot-starter-validation做参数校验也建议带上。application.yml里重点配置几块spring: datasource: url: jdbc:mysql://localhost:3306/timetable?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.timetable.entity configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai这个参数必填很多本地能跑、部署到服务器后时间差8小时的问题就是这里漏了。map-underscore-to-camel-case开启后course_name这种字段能自动映射到courseName省掉一大堆resultMap。3.2 登录认证与权限拦截课表系统不推荐引入SpringSecurity全家桶太重了。我用的方案是JWT 拦截器。登录接口接收用户名密码校验通过后生成一个JWT字符串返回前端前端存在localStorage里每次请求在Authorization头带上。后端写一个拦截器放行/api/auth/login其他/api/**请求都校验Token解析失败直接返回401。Token里我放了userId、role和realName三个信息。这样后端接口里需要用到当前用户时直接从Token里取不用每次查数据库。拦截器里校验角色的地方要注意比如管理员才能调用排课保存接口学生只能查自己的课表。这个角色判断用HandlerInterceptor的preHandle写就行代码量不大但能挡住90%的越权调用。密码存储不要用明文。我建议用BCryptPasswordEncoderSpring Security框架里抽出来的这个工具类可以单独用不依赖整套Security。前端传来明文密码后端用BCrypt加密后存库。注意如果是从网上下的源码经常看到密码直接明文存库必须改掉。3.3 课表查询接口MyBatis动态SQL课表系统的灵魂接口是getTimetable它同时支持三种查询条件按班级查、按教师查、按教室查并且支持按周数过滤。接口参数我用一个DTO接收public class TimetableQueryDTO { private Integer classId; private Integer teacherId; private Integer classroomId; private Integer week; // 要查询的周数如8 }Controller层先做参数校验至少传入一个查询维度然后调用Service查询。Mapper接口方法定义如下ListScheduleVO selectTimetable(Param(query) TimetableQueryDTO query);MyBatis XML里的核心是动态SQL多表关联加条件拼装select idselectTimetable resultTypecom.example.timetable.dto.ScheduleVO SELECT s.schedule_id, s.class_id, cl.class_name, s.course_id, c.course_name, s.teacher_id, t.name AS teacher_name, s.classroom_id, cr.room_name, s.week_start, s.week_end, s.week_type, s.day_of_week, s.section_start, s.section_end FROM tb_schedule s LEFT JOIN tb_class cl ON s.class_id cl.class_id LEFT JOIN tb_course c ON s.course_id c.course_id LEFT JOIN tb_teacher t ON s.teacher_id t.teacher_id LEFT JOIN tb_classroom cr ON s.classroom_id cr.classroom_id where if testquery.classId ! null AND s.class_id #{query.classId} /if if testquery.teacherId ! null AND s.teacher_id #{query.teacherId} /if if testquery.classroomId ! null AND s.classroom_id #{query.classroomId} /if /where ORDER BY s.day_of_week, s.section_start /select注意周数过滤不在SQL里做而是查出来后在内存中过滤。为什么这么做因为一条排课记录的week_start和week_end是一个区间可能跨好几周加上单双周规则直接写SQL判断稍不留神就把边界情况漏了。数据量小、性能影响几乎可忽略内存过滤逻辑更直观// 判断某条排课记录在指定周是否上课 public boolean isScheduleInWeek(ScheduleVO schedule, int week) { if (week schedule.getWeekStart() || week schedule.getWeekEnd()) { return false; } if (schedule.getWeekType() 1) { return true; } // 单周week 为奇数双周week 为偶数 if (schedule.getWeekType() 2) { return week % 2 1; } if (schedule.getWeekType() 3) { return week % 2 0; } return false; }这样一周没课的格子就不会显示任何课程前端渲染也就不会出现条数错位的问题。3.4 排课保存事务和冲突校验管理员提交排课时前端传过来的是一个排课对象包含班级、课程、教师、教室、周次范围、单双周、星期、节次。Service层要做三件事参数合法性检查节次是否1到12之间星期是否1到7周次是否结束大于开始。冲突检查查库判断这个班级、这个教室、这个教师在相同星期和节次范围内是否已有排课。插入记录通过后用事务包裹保证冲突检查和插入是原子操作。冲突检查方法我单独抽出来调用Mapper的一个查询方法查询参数包括班级ID、教室ID、教师ID、星期、节次、周次范围。全部分别查一次只要任何一个结果集非空就拒绝。有人会问为什么不写一个超级SQL一次查清实际上分开查代码更易读课设场景下性能也不是瓶颈。事务只需要在Service方法上加Transactional插入失败自动回滚不会留下半条脏数据。4. Vue3 前端实现把课表画出来4.1 Vite Vue3 工程搭建前端工程用Vite创建最省事npm create vitelatest timetable-frontend -- --template vue cd timetable-frontend npm install npm install vue-router4 pinia element-plus axiosVue3项目建议一定要用script setup语法课表页面的逻辑都写在setup函数式代码块里比Options API的data/methods分散写法清晰很多。Element Plus按需引入还是全量引入课表管理系统这类后台项目我建议全量引入省心。开发模式下无所谓生产构建时会自动tree-shake一部分但核心组件一般都会用到。前端路由我分了三个页面登录页/login课表主页/timetable管理后台/admin。管理后台里面用子路由区分班级管理、课程管理、排课管理。4.2 课表组件渲染方案课表渲染是整个前端的重头戏。我的思路是先把后端返回的排课记录做一次扁平化处理转成一个以“星期几-节次”为key的Map结构。比如一条周一3-4节的记录如果本周有效就存到map[3-1]、map[4-1]两个key下面key是节次值和星期值的组合每个key对应一个数组。这个转换在组件里用computed操作不污染原始数据。模板部分我用Element Plus的el-table来展示。因为课表是一个行列固定、内容可变的二维矩阵用el-table比手写table方便得多。行是节次列是星期一到星期日。某一行的某个单元格就显示这个节次对应的全部课程块每个课程块用el-tag加不同颜色区分课程名称。为了让视觉更接近真实课表我给课程块加了背景色和鼠标悬停效果点击后弹窗显示课程详情。核心渲染代码思路类似这样el-table :datasections border el-table-column label节次 width80 template #default{ row }{{ row.label }}/template /el-table-column el-table-column v-forday in 7 :keyday :label周 weekNames[day] template #default{ row } div v-foritem in getItems(day, row.value) :keyitem.scheduleId clickshowCourseDetail(item) {{ item.courseName }}br small{{ item.teacherName }}/smallbr small{{ item.roomName }}/small /div /template /el-table-column /el-tablegetItems(day, section)返回的是前面那个Map里对应的数组。节次常量我定义在一个独立的utils/constants.js里包括每节课的时间段说明比如上午1-2节是8:00-9:40。4.3 Axios封装与跨域处理前端必须统一封装axios请求不要在每个组件里乱写fetch。我在src/api/request.js里创建axios实例设置baseURL为/api请求拦截器里把JWT Token塞进请求头响应拦截器统一处理401跳登录、500弹错误提示。开发环境会遇到最经典的跨域问题。Vite通过代理解决配置vite.config.jsserver: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端开发服务器在5173端口请求/api/schedule/list时自动转发到8080后端浏览器不会出现跨域报错。后端的跨域配置也保留一份主要给非浏览器环境或者部署后特殊场景兜底使用。实际部署时生产环境的请求路径由Nginx转发也不存在跨域问题。4.4 课程详情弹窗与周次切换课表页顶部放一个周次选择器默认显示当前周。切换周次时重新调用查询接口拿到数据后重新生成课表Map表格区域自动响应刷新。这一步对初学者来说容易踩坑的是直接把后端返回的所有排课记录都渲染出来没有先做“本周是否生效”的过滤。我在组件里加了currentWeek的ref所有数据转换都基于它切周时数据同步更新。点击课程块弹窗我用Element Plus的el-dialog显示课程名、教师、教室、上课时间、起止周、单双周信息。同时弹窗里提供“这周有课”的标识方便学生查看。管理员在弹窗里还可以看到编辑和删除按钮这个按钮根据用户角色动态渲染思路也很简单role admin才显示。5. 打包部署从本地到服务器5.1 后端打包运行后端打包命令是老生常谈mvn clean package -DskipTests java -jar target/timetable-backend-0.0.1-SNAPSHOT.jar这里有一个细节打包前一定要检查application.yml里的数据库地址、账号密码是不是生产环境的很多系统本地能跑部署到服务器突然启动失败九成是数据库连不上。如果用的是云服务器还要注意安全组和防火墙是否放行8080端口。MySQL服务器端如果部署在单独的机器上记得给应用账号授权远程访问不然从Java应用连接时会被拒绝。还有MySQL 8.0默认认证插件是caching_sha2_password老版本MySQL驱动连不上报错关键字是Public Key Retrieval is not allowed解决办法是在JDBC URL上加allowPublicKeyRetrievaltrue或者把驱动版本升到8.0.33以上。5.2 前端构建部署到Nginx前端构建npm run build生成dist目录后把里面的文件上传到服务器的Nginx静态目录。Nginx配置里最关键的是一段try_files否则切换路由后刷新页面会404server { listen 80; server_name your-server-ip; root /usr/share/nginx/html/timetable; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; } }try_files $uri $uri/ /index.html这行是Vue Router使用history模式时的标准配置刷新/admin页面时Nginx不会去找真实的admin.html而是把请求回退到index.html交给前端路由处理。如果不加刷新就404这是Vue3项目部署最常见的毛病。5.3 另一种做法把前端打进SpringBoot有同学不想单独部署Nginx问能不能把前端dist目录直接放进SpringBoot的src/main/resources/static目录下。理论上可以但有一个大坑Vue Router如果用history模式后端必须处理未知路径的转发。简单处理方式是前端使用哈希路由createWebHashHistory()地址栏里会带#虽然不够美观但省事。如果你坚持用history模式需要在SpringBoot里配置一个转发规则把非/api开头的所有路径转发到index.html操作成本不大但对新手来说很容易漏配。我个人的建议是如果服务器资源充足还是用Nginx部署前端SpringBoot只做API服务。前后端分离项目就该这么玩后续扩展微信小程序或者App端时后端接口可以直接复用不用再动前端生产包。6. 常见问题与避坑实录6.1 前端请求后端总是404或跨域报错这大概是我被问过最多的问题。404要先分清是前端路由的404还是接口404。如果浏览器Network里能看到请求已经发到后端状态码是404那就是后端没有这个接口路径检查RequestMapping的类路径和方法路径是不是多了一个/。如果是前端请求根本没发出去控制台报跨域错误那先看Vite代理配置是否生效确认路径前缀是不是/api。我在排查时习惯先直接浏览器访问http://localhost:8080/api/schedule/list如果这个地址能返回JSON问题就出在前端代理或配置上。6.2 MyBatis查询结果某些字段为null最常见的原因是数据库列名和下划线转驼峰没生效。检查两项map-underscore-to-camel-case是否设为true以及实体类字段名是否真的和驼峰对应。比如数据库字段teacher_id实体类属性名一定要是teacherId漏掉一个字母就会导致结果里有null。还有一种情况是使用LEFT JOIN时被关联表的字段正好是数据库保留字比如order、desc查询时直接报语法错误处理办法是给字段加反引号或者干脆改字段名。6.3 排课保存后查不到怀疑缓存作怪MyBatis的一级缓存是SqlSession级别的默认开启二级缓存默认关闭。如果在同一个SqlSession里先查再插再查会命中缓存导致查不到刚插入的数据。但SpringBoot中每次Mapper操作都从连接池获取新连接实际上很难触发一级缓存问题。我在这个项目里故意关闭了二级缓存毕竟排课数据更新频繁用本地缓存容易引发“课表改了前端还是旧数据”的幻觉。如果你确实要用缓存记得CacheNamespace配置好并保证每次排课变更后执行flushCache。6.4 时间字段和星期几显示错乱这个问题几乎都是时区导致的。MySQL连接URL里的serverTimezone和系统时区不一致或者服务器的系统时区是UTC就会看到时间差8小时。课表系统里虽然主要存星期几和节次但如果你加了创建时间字段插入后查询会发现时间差这时统一把serverTimezone设为Asia/Shanghai别偷懒。还有通过day_of_week存数字前端映射时注意JS里getDay()返回的是0到60代表星期天这个映射关系写错课表会整体错位一格。6.5 打包部署后页面白屏页面白屏先看浏览器控制台如果报错是Failed to fetch dynamically imported module说明前端资源的路径不对。Vite默认构建出来的资源路径是绝对路径/assets/...如果你的前端部署在子路径下比如/timetable/资源就加载不到。解决办法是在vite.config.js里设置base: /timetable/然后重新构建。还有可能是服务器静态目录权限不对Nginx的user配置没有读文件权限404或者403都会导致白屏。6.6 MySQL安装和字符集问题很多同学在Windows上安装MySQL后启动时总报Cant connect to MySQL server先检查服务是否启动再检查3306端口是否被占用。课表系统对MySQL版本不挑5.7或8.0都行但5.7的默认字符集可能是latin1建库时一定要显式指定CREATE DATABASE timetable DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果表已经建好了再补一道转换语句ALTER TABLE tb_course CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;不然插入中文课程名后查出来是一堆问号十有八九是字符集问题。7. 写在最后的经验与建议课表管理系统这套源码实现到能跑通前后端其实只完成了60%剩下的40%在数据初始化和边界场景。我陪学弟调完这个项目后最大的感触是很多同学一上来就急着写代码结果排课数据不知道怎么造单双周的课也没设计好演示的时候老师随便输一个班级号页面一片空白体验很差。建议你们拿到任何课表系统源码后第一件事不是跑起来而是先把数据库脚本看明白自己往里造两周的完整课表数据包括单周课、双周课、跨周课、同教室不同时间段课全部造全了再启动项目这样演示时才有底气。如果想让这个项目在答辩时更出彩可以往后端加一个“班级课表导出Excel”功能前端一个按钮后端用EasyExcel把当前周课表导出来这属于典型的加分项。另外还可以把排课冲突的校验结果用可视化的形式反馈给管理员比如前端页面上直接标红冲突格子这个改动不复杂但很能体现你对业务的理解深度。我实际开发中还踩过一个很有意思的坑前端课表组件一次性渲染整个学期的课表数据结果有四十多周每个格子都要过滤判断页面明显卡顿。后来改成只渲染当前周切周时重新组装数据流畅很多。这种性能和体验上的细节往往比接口写得多更能让老师眼前一亮。课表系统没有很复杂的高级算法它的价值在于把多维度数据、权限和交互细节处理得足够稳你可以在此基础上放心加功能也可以拿它作为前后端分离项目练手的起点。
返回列表