每年一到毕业设计季,后台私信里问得最多的就是“SpringBoot+Vue+MySQL课表系统怎么做”。这个西安工商学院的课表管理平台,我看着像是一套标准的全栈毕设项目:Java后端负责接口和业务逻辑,Vue前端做页面交互,MySQL存课表数据,外加源码、数据库脚本、论文和部署文档。今天不扯虚的,我把这类项目从选型、建库、写接口、做前端到打包部署的完整链路拆一遍,把我踩过的坑和能直接抄的代码结论都给你。适合正在做毕设的计科软工同学,以及想通过一个真实业务场景入门全栈开发的初学者。
1. 项目整体设计与技术选型思路
1.1 为什么课表管理系统是毕业设计的“安全牌”
每年选题季,大家一窝蜂涌向电商系统、图书管理系统,课表管理系统反而是一个被低估的方向。我当初选它,核心看中三点。
第一,业务边界足够清晰。排课、查课、调课、选课,这些流程在高校里存在很多年,需求是现成的,不需要自己凭空臆造业务规则。三种角色——老师排课、学生查课、管理员统一维护,天然对应三种权限,权限模型做起来一点不显得刻意。
第二,数据模型有典型性。课程、教师、教室、时间段这几个实体之间存在多对多关系:一个教师能教多门课,一门课也可以由多个教师带;一个教室在不同时间段被不同课程占用。把这几张表设计明白,既要处理联合唯一约束,又要做时间冲突检测,这些都是数据库课上的核心知识点,论文里也有的写。
第三,答辩演示效果好。课表天然适合表格化展示,前端做周视图、列表视图都顺理成章。答辩现场打开系统,一屏看全一周的课,比那种需要不停点按钮才能看到核心数据的系统直观太多。
1.2 三大技术栈的分工与逻辑
SpringBoot、Vue、MySQL这个组合,放在当前毕业设计的语境里,依然是最稳的选择。原因很现实:参考代码多、社区问答充足、答辩老师熟悉这套技术栈。
- SpringBoot管后端接口。它是Java生态里最“无感”的框架,内置Tomcat,服务直接跑起来,不用像老SSH框架那样折腾一堆XML配置。对毕设来说,它“约定大于配置”的特性可以让你把精力放在业务代码上,而不是环境搭建上。
- Vue管前端页面。Vue 2生态成熟、踩坑答案多;Vue 3的组合式API更现代、写起来更清爽。如果没人强制,我建议Vue 3 + Element Plus,毕竟新项目没必要抱着旧版本不放。
- MySQL管数据存储。单机部署、社区版免费、Navicat可视化操作,对教务系统这种并发量极低的场景完全够用。别被“MySQL不适合生产环境”的论调带偏,那是大型互联网公司的语境,跟毕设项目不搭边。
三个技术栈各司其职:MySQL存储数据,SpringBoot读数据、算逻辑,Vue把数据渲染成用户看得懂的界面。数据从数据库到浏览器这条链路,跑通一遍,全栈开发的核心能力就有了。
2. 数据库设计与核心表结构
2.1 实体关系拆解与建模思路
课表管理系统的核心实体,掰开揉碎就是五个:学生、教师、课程、教室、时间段。
这五个里面最容易想当然的是“时间段”。我见过很多第一版设计直接在课程表里存一个“周一第1-2节”的字符串字段,看起来简单,后面做冲突检测、调课、跨周查询的时候,字符串解析会让你痛不欲生。更合理的做法是把星期几、开始节次、结束节次、起始周、结束周拆成独立字段,让查询能用上索引,也让冲突检测变成纯数字逻辑。
我推荐一组核心表结构:
- sys_user:用户表,统一存登录账号,用type字段区分学生、教师、管理员
- teacher_info:教师信息表,关联sys_user
- student_info:学生信息表,关联sys_user、班级
- course_info:课程信息表,存课程名、学分、学时
- classroom_info:教室信息表,存教学楼、容纳人数
- course_schedule:课表主表,关联课程、教师、教室、时间,全系统最核心的一张表
2.2 课表主表的详细设计方案
course_schedule这张表的设计决定整个系统的成败。建议结构如下:
CREATE TABLE `course_schedule` ( `id` bigint NOT NULL AUTO_INCREMENT, `course_id` bigint NOT NULL COMMENT '课程ID', `teacher_id` bigint NOT NULL COMMENT '教师ID', `classroom_id` bigint NOT NULL COMMENT '教室ID', `class_id` bigint DEFAULT NULL COMMENT '班级ID', `day_of_week` tinyint NOT NULL COMMENT '星期几,1-7', `start_section` tinyint NOT NULL COMMENT '开始节次,如1、3、5', `end_section` tinyint NOT NULL COMMENT '结束节次,如2、4、6', `week_start` tinyint DEFAULT 1 COMMENT '起始周', `week_end` tinyint DEFAULT 20 COMMENT '结束周', `semester` varchar(20) NOT NULL COMMENT '学期,如2024-2025-1', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_classroom_time` (`classroom_id`, `day_of_week`, `start_section`, `week_start`, `week_end`, `semester`), KEY `idx_course` (`course_id`), KEY `idx_teacher` (`teacher_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课表主表';这里有个关键细节:唯一索引。教室在某天某节被占用后,再来一条冲突记录,数据库层面直接报错拒绝。比你在Java代码里先查一遍再插入可靠得多,因为代码查完到插入之间永远有微小的窗口期,并发情况下两个请求可能同时通过检查。答辩时老师问“你怎么保证同一教室不排课冲突”,你回答“数据库唯一索引兜底,业务层还有前置检测”,比只说“我在代码里判断了”专业太多。
周次的处理同样重要。课表不是每门课都上满整学期,有的课是第1-10周,有的是第11-18周。拆出week_start和week_end,方便做时间范围重叠判断。两门课是否冲突的判断条件是t1.week_start <= t2.week_end AND t1.week_end >= t2.week_start,这个区间交叉判断在后面会反复用到。
2.3 初始化数据与密码加密的坑
数据库建好后,需要往sys_user里插入管理员账号才能登录。这里有个经典坑:密码加密方式。如果项目集成了Spring Security或Sa-Token,密码不能明文存。我用的是BCryptPasswordEncoder,插入管理员的初始密码必须是BCrypt加密后的字符串,不然怎么登录都报密码错误。
INSERT INTO sys_user (username, password, type, status) VALUES ('admin', '$2a$10$7JB720yuzVS83UjWHz2T6umVvG67o3eH1pM4YyN9iV0F1z2NxX2aO', 3, 1);注意这个加密串是我随手写的,实际使用时应该用项目自己的加密器生成。我看见太多同学直接抄网上的密码串,跟自己的加密器对不上,来回折腾一下午。正确做法是写一段临时测试类,用项目自己的BCryptPasswordEncoder生成加密串,打印出来再粘到SQL里。
3. 后端SpringBoot接口开发与关键逻辑
3.1 版本选择与项目结构
SpringBoot版本选择,每年坑一批新手。网上教程多是2.x的写法,你要是图新鲜装了3.x,会发现javax.servlet变成jakarta.servlet,很多老代码直接编译不过。我切过的实际教训是:毕设用什么版本,取决于参考源码用什么版本,而不是官网推荐什么版本。
参考源码是SpringBoot 2.7.x,JDK就用1.8或11;参考源码是3.x,JDK必须17以上。版本不匹配,项目都跑不起来。另一个连带问题是MySQL驱动:2.x默认用com.mysql.cj.jdbc.Driver,3.x里驱动坐标和Maven依赖都换了。别做那个“版本太高、全家升天”的冤种。
标准项目结构建议这样分:
src/main/java/com/xagcd/ktmsys/ ├── controller/ # 接口层,只做参数接收和结果返回 ├── service/ # 业务逻辑层,核心判断都在这里 ├── mapper/ # MyBatis数据访问层 ├── entity/ # 实体类 ├── config/ # 配置类,如跨域、拦截器 └── common/ # 统一返回结果、异常处理分层不是装样子,答辩被问“项目怎么组织代码”时,你要能一句话说清每层职责。Controller只接参数、返回结果,业务逻辑全在Service,数据访问只在Mapper。这样耦合度低,改代码也舒服。
3.2 登录鉴权与三种角色权限实现
毕设级别的系统,登录鉴权不用硬上Spring Security全家桶,太重,配置复杂,答辩还容易被追问细节。更务实的选择是JWT + 拦截器。
流程是:用户提交用户名密码,后端校验通过后用JWT工具类生成token,token里带上用户id和角色type,返回前端。前端把token存到localStorage,每次请求在axios拦截器里加Authorization: Bearer xxx。后端写一个拦截器,拦截除登录接口外的所有请求,从token解析用户信息放到ThreadLocal,供后续业务取用。
角色控制用很朴素的方式:接口方法里判断当前用户type,不是管理员就直接抛异常。三个角色三种视图,前端根据登录返回的role字段路由到不同页面。这个体量下不用搞复杂的RBAC权限模型,简单的角色判断反而清晰好维护。
3.3 课表查询核心SQL与冲突检测
查询课表是系统关键路径。学生查课表、管理员按班查、教师查自己教的课,本质是对course_schedule的多表关联查询,只是过滤条件不同。
<select id="getScheduleByStudent" resultType="map"> SELECT cs.day_of_week, cs.start_section, cs.end_section, cs.week_start, cs.week_end, ci.course_name, ci.course_type, ti.teacher_name, cr.classroom_name, ci.credit FROM course_schedule cs JOIN course_info ci ON cs.course_id = ci.id JOIN teacher_info ti ON cs.teacher_id = ti.id JOIN classroom_info cr ON cs.classroom_id = cr.id JOIN student_info si ON si.class_id = cs.class_id WHERE si.id = #{studentId} AND cs.semester = #{semester} ORDER BY cs.day_of_week, cs.start_section </select>核心思路:学生课表通过class_id关联班级课表。这符合高校排课的现实逻辑:学生属于班级,班级课表就是学生课表,数据库不需要存每个学生的冗余课表记录。
另一条关键SQL是冲突检测:
SELECT COUNT(*) FROM course_schedule WHERE classroom_id = #{classroomId} AND semester = #{semester} AND day_of_week = #{dayOfWeek} AND #{newStart} <= end_section AND #{newEnd} >= start_section AND week_start <= #{newWeekEnd} AND week_end >= #{newWeekStart}注意这个区间交叉判断的写法。新手容易写成start_section <= #{newStart} AND end_section >= #{newEnd},这只能判断包含关系,没法判断交叉关系。比如已有的课是3-4节,你要排1-4节,旧写法不会报冲突,但实际两门课在3-4节重叠了。正确的交叉判断是:新区间开始 <= 旧区间结束 且 新区间结束 >= 旧区间开始。这个细节,答辩和面试都可能问到。
4. 前端Vue页面开发与环境配置
4.1 开发环境搭建避坑指南
Vue项目开发环境,看着简单,每一步都有坑。
Node.js版本先选对。Vue 2老项目推荐Node 14-16,Vue 3推荐Node 16以上。别看到官网有Node 20就无脑下载,老项目npm install直接报错。装好Node后,先把npm源换成国内镜像,npm config set registry https://registry.npmmirror.com,不然装依赖能等到睡着。
创建项目,Vue 3用npm create vite@latest,Vue 2用vue create project-name。很多同学分不清这两个,照着教程创建完发现路由写法对不上。毕设我更推荐Vite,启动快、热更新舒服,网上的Vue 3教程也基本默认Vite。
依赖安装永远会报错,最常见的是node-sass安装失败,这玩意要下载二进制文件,网络差一点就挂。解决办法:用sass包替代node-sass,语法基本一致。另一个高频报错是npm ERR! code ERESOLVE,依赖树冲突,npm 7+严格解析导致,加--legacy-peer-deps参数就能绕过。
4.2 前端路由与页面骨架
前端页面围绕三种角色设计:
- 管理员:登录后进入后台管理,包含课程管理、教师管理、学生管理、排课管理、教室管理
- 教师:查看我的课表、个人信息
- 学生:查看我的课表、按周切换
Vue Router配置时用路由懒加载,首屏加载速度快一些:
const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/admin/layout', component: () => import('@/layout/AdminLayout.vue'), meta: { role: 3 }, children: [ { path: 'schedule', component: () => import('@/views/admin/ScheduleManage.vue') } ] } ]路由守卫必须加,每次跳转前读取localStorage里的token,没有就重定向到登录页。容易被忽略的细节:守卫里除了判断token,还要校验当前用户角色是否匹配路由的meta.role,不匹配就跳到自己的角色首页。不然学生手动改路由地址就能打开管理页面,答辩现场被老师试出来就尴尬了。
4.3 课表组件渲染实践
课表展示最靠谱的方案是用Element Plus的el-table,动态生成列。周一至周五作为列,节次作为行,每个单元格根据是否排课填充课程信息。
// 伪代码示意 const tableData = Array.from({ length: 10 }, (_, i) => ({ section: `第${i + 1}节`, detail: buildCellContent(i) }))buildCellContent函数遍历后端返回的课表数据,根据dayOfWeek和startSection定位到对应行列。这里有个实践心得:后端返回数组结构时,前端转成二维映射会更高效。我封装了一个Map,key用day_startSection,value是课程对象,渲染时间复杂度大幅降低,代码也更清晰。
样式细节提醒一下:课表单元格里课程名较长,加上教室和教师信息,可能需要换行,应让单元格高度自适应,内容超长省略显示,鼠标悬浮用tooltip展示完整信息。答辩时老师看到这种细致处理,印象分会好很多。
5. 部署流程与常见问题排查
5.1 本地联调:前后端接口打通的两种方式
后端启动在8080端口,前端Vite开发服务器默认5173端口,浏览器访问前端页面再向后端发请求,必然触发跨域问题。解决办法有两种。
方案一,后端加全局CORS配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }方案二,前端在vite.config.js里配置代理:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }我推荐代理方式,因为它更接近生产环境:部署时用Nginx做反向代理,前端统一访问/api路径,Nginx转发给后端,跟开发时的代理模式完全一致。后端CORS方案本地开发省事,但生产环境还得额外配Nginx,等于维护两套逻辑。
5.2 打包与服务器部署全流程
后端打包:
mvn clean package -DskipTeststarget目录会生成jar包,服务器上执行java -jar xagcd-ktmsys.jar就能跑。如果服务器同时有多个Java项目或端口占用,用nohup java -jar xxx.jar > app.log 2>&1 &后台运行,日志输出到app.log方便排查。
前端打包:
npm run build生成dist目录,全是静态文件,上传到服务器某个目录,由Nginx托管。Nginx配置两个关键块:
server { listen 80; server_name your-domain.com; location / { root /opt/app/ktmsys/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这行是前端history路由模式的关键配置,没有它刷新非首页路由会报404。后端接口统一加/api前缀,Nginx按前缀转发,省去前后端地址匹配的扯皮。
数据库迁移:把本地SQL文件在服务器MySQL执行一遍,改application.yml里的数据库连接地址和密码。建议单独建一个application-prod.yml,通过启动参数--spring.profiles.active=prod选择。别把生产环境密码写在默认配置里,答辩老师看到数据库密码是123456,嘴上不说,心里已经扣分。
5.3 高频报错速查表
整理一份毕设全流程最常见的报错和处理方案,都是踩过的坑:
| 报错现象 | 根本原因 | 解决办法 |
|---|---|---|
| Failed to configure a DataSource | 启动类扫描到MyBatis但没配数据源 | 检查application.yml的spring.datasource配置,确认MySQL已启动 |
| Access denied for user 'root'@'localhost' | MySQL密码错误或root仅限localhost访问 | 核对密码,或给root授予远程访问权限 |
| java.sql.SQLNonTransientConnectionException | MySQL版本与驱动不兼容 | SpringBoot 2.7用mysql-connector-j 8.0.x,3.x用新版驱动 |
| npm install卡住或报错 | 网络源问题或Node版本过高 | 换npm镜像,降Node版本,node-sass换sass |
| npm run build报内存溢出 | Node默认堆内存不够 | 设置NODE_OPTIONS="--max-old-space-size=4096" |
| 页面刷新404 | 前端history路由未配置try_files | Nginx加try_files $uri $uri/ /index.html; |
| Duplicate key报错 | 同一时间同一教室排了多门课,唯一索引触发 | 业务层做冲突检测,数据库唯一索引兜底 |
| npm ERR! code ERESOLVE | 依赖树冲突,npm 7+严格解析 | 加--legacy-peer-deps参数,或降低npm版本 |
排课系统的业务层一定要做前置冲突检测,不然数据库唯一索引报出来的英文错误用户看不懂,体验会很差。
5.4 论文与部署文档的写作要点
毕设的产出不只有跑的代码,论文和部署文档同样关键。部署文档我建议按“环境准备、数据库初始化、后端启动、前端构建、Nginx配置、常见问题”六个章节写,每步附截图,操作路径写具体。老师拿到文档能复现,缺环节也能自己排查。
论文部分,课表管理系统的结构一般是:绪论、需求分析、系统设计(架构图+功能模块+数据库设计)、系统实现、测试。数据库设计这一章多放ER图和表结构说明,这是课表系统的亮点。测试部分设计完整的测试用例,特别是冲突检测用例:同一教室、同一时间段、不同课程,系统应拒绝添加。这类用例写进论文,答辩老师一眼能看出你是真做了系统性测试。
前阵子有人在群里问“毕设做完了能学到啥”。说实话,我在做这套课表系统的过程中,被SpringBoot版本坑过、被跨域折腾到怀疑人生、被Node依赖安装折磨到深夜。但回头看,这些恰恰是项目最值钱的部分——你现在踩的每个坑,都会变成答辩时的从容和面试时的底气。课表管理系统能一直火不是没道理,它麻雀虽小五脏俱全,从数据库建模、接口设计、前端交互到部署上线,完整覆盖了软件生命周期。如果你正好也在做相似项目,记住一句话:代码跑不通先别怀疑自己,去检查版本号。很多问题,真的只是版本号的问题。