ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3课表管理系统:从数据库到部署的全栈实战

SpringBoot+Vue3课表管理系统:从数据库到部署的全栈实战 1. 项目整体设计与技术选型思路1.1 这个项目到底在解决什么问题课表管理系统在Java Web项目里几乎是绕不开的经典场景。无论你是做毕业设计、期末课程设计还是刚学完SpringBoot和Vue3想找个完整项目练手这个题目都会出现在你面前。这套源码解决的痛点很明确传统的课表排布手工用Excel管理到了学期中调整课程、临时调课、多班级冲突检查时极易出错。做成管理系统后管理员维护基础数据学生或教师按条件查询课表课程冲突校验交给程序一套流程下来效率提升非常明显。适合谁来参考如果你是刚学完SpringBoot基础、Vue3刚上手的初级开发者这个项目可以作为第一个完整的全栈项目来研究如果你正在准备毕业设计这套代码可以直接作为骨架在其上扩展权限模块、公告模块如果你是培训机构的讲师也可以拆解它作为教学案例。核心价值在于一个典型的前后端分离管理系统覆盖了从数据库设计、后端CRUD、接口开发到前端页面渲染的全链路。1.2 技术栈为什么这么选SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这套组合在目前确实是主流且稳妥的。先说SpringBoot2。很多人纠结现在SpringBoot3都出了为什么要用2。实际开发中SpringBoot2.7.x仍然是存量项目最大的版本群体教程多、踩坑记录全、各类中间件兼容性成熟。而SpringBoot3强制要求JDK17如果你本地还是JDK8或JDK11直接上3会有很多环境层面的磕绊。这个项目选2是求稳不是落后。Vue3这边目前已经成了绝对主流组合式API、响应式代理、Fragment支持这些特性让开发体验比Vue2好太多。搭配Element Plus组件库后台管理系统的表格、表单、弹窗、消息提示基本就是拖拽式开发。MyBatis-Plus的价值我用一句话总结它把单表CRUD的代码量压缩了至少80%。内置的BaseMapper直接提供了插入、删除、修改、分页查询等常用方法你不需要手写XML和SQL只需在Service层做业务逻辑组装。对于课表这种以单表查询为主、复杂报表为辅的系统选MyBatis-Plus非常合适。MySQL8.0的话值得说的是身份认证插件和时区设置。8.0默认用了caching_sha2_password插件老版本驱动会连不上这个问题不少人踩过后面我会专门讲。这套技术栈还有个隐藏优势岗位需求量大。目前中小企业Java后端招聘里SpringBootMyBatis-Plus几乎是标配Vue3前端更是必备技能。做完这个项目简历上的项目经验就有了一个能写清楚全流程的内容。2. 数据库设计与核心表结构2.1 基础表设计课程、教师、班级、学期数据库设计是这类管理系统最见功夫的地方也直接决定了后期代码好不好写。课表管理系统我建议至少拆成这几张表课程表course、教师表teacher、班级表class_info、学期表semester、教室表classroom以及关联用的排课表course_schedule。每张表的字段设计都有讲究。课程表除了课程名称、学时、学分这些基础字段最好加一个course_type字段区分必修选修——很多实际需求里导出Excel报表时都会按这个维度统计。教师表不用搞太复杂工号、姓名、职称、所属院系最多再加一个联系电话。班级表要注意的是班级名称不能只用字符串存个“软件2101班”就完事建议拆成grade年级和class_no班号两个字段查询和排序都方便。教室表则要记录容量和是否多媒体教室排课时用来校验教室冲突。学期表容易被忽略但它很重要。课表和学期是强关联的每个学期的课程安排独立查询时先选学期再加载课表。没有这张表历史课表查询和学期切换功能就无从实现。2.2 排课关系表与字段设计思路排课表course_schedule是整个系统的核心它的字段设计决定了系统能支持的排课复杂度。我先列一下这个表的字段方案字段名类型设计说明idbigint主键雪花ID或自增ID均可semester_idbigint关联学期表course_idbigint关联课程表teacher_idbigint关联教师表class_idbigint关联班级表classroom_idbigint关联教室表weekdaytinyint星期几1~7start_sectiontinyint开始节次如第1节end_sectiontinyint结束节次如第2节week_starttinyint起始周week_endtinyint结束周week_typetinyint周类型0表示每周1表示单周2表示双周这里最核心的设计是把“周几”“第几节”“第几周到第几周”拆成了独立字段而不是直接存一个“周一第1-2节全周”。这么设计的原因很简单排课冲突检测时需要按weekdaystart_sectionend_section去查同一时间段的已有排课独立字段可以用一条简单的SQL完成冲突判断。如果把这些信息拼成一个字符串字段查询时就要做字符串解析性能和逻辑复杂度都会翻倍。week_type字段是容易被新手忽略的点。很多学校有单双周课程的概念比如有些课只在单周上。如果不在表结构里预留这个字段后面做排课逻辑时会非常痛苦要改表结构不说还会影响前端展示的逻辑。2.3 索引与数据初始化注意事项排课表的索引设计我的建议是建一个组合索引(semester_id, weekday, start_section, end_section)。这是课表查询和冲突检测使用频率最高的查询条件。再配合(class_id, semester_id)做班级课表查询、(teacher_id, semester_id)做教师课表查询。这里需要注意不要每个字段都单独建一个索引MySQL8.0虽然支持索引合并但从执行计划来看组合索引的效率和空间占用都更优。数据初始化方面建议在SQL脚本里预置一份基础数据。比如一个学期、几个班级、几十门课、几个教师的示例数据方便项目跑起来后立刻能看到效果也方便演示。这个细节能让拿到源码的人直接进入业务逻辑而不是先花时间造数据。不要只给空表结构那样体验真的差很多。3. 后端核心实现SpringBoot2 MyBatis-Plus3.1 工程结构的关键约定后端工程结构建议采用标准的Maven多模块或单模块分包这套源码采用的是单模块分包结构清晰src/main/java ├── controller ├── service ├── mapper ├── entity ├── config └── commoncontroller层只做参数接收和结果封装直接调service接口service层处理业务逻辑比如排课冲突校验、课表查询组装mapper层继承MyBatis-Plus的BaseMapper基本不用写SQL。pom.xml里的依赖需要特别注意几个核心依赖版本要写对dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.3.1/version /dependency有一个我踩过的坑MySQL驱动包的groupId在老版本里是mysql:mysql-connector-java但8.0.31之后推荐用com.mysql:mysql-connector-j两者坐标不一样。用SpringBoot2.7.x内置的依赖管理可以直接省略version标签让Boot统一管理可避免版本冲突。3.2 MyBatis-Plus通用CRUD与分页配置MyBatis-Plus让单表CRUD变得非常轻松。entity类加注解映射表名mapper继承BaseMapper后增删改查方法全部内置。以课程实体为例核心代码是这样的Data TableName(course) public class Course { TableId(type IdType.ASSIGN_ID) private Long id; private String courseName; private Integer credit; private Integer courseType; private Integer totalHours; }TableName指定表名TableId配置主键策略IdType.ASSIGN_ID使用雪花算法生成分布式ID。在单体应用里也可以直接用IdType.AUTO依赖数据库自增看项目规模和个人习惯。Service层直接继承ServiceImpl也是MyBatis-Plus的经典玩法Service public class CourseServiceImpl extends ServiceImplCourseMapper, Course implements CourseService { }这一行代码就拥有了最基本的CRUD能力。但有两点必须提醒第一业务逻辑复杂的方法不要偷懒直接在Controller里用Mapper还是要放在Service中封装事务和校验第二多表关联查询MyBatis-Plus没法直接做到需要手写SQL或用VO拼接课表详情这种就要在Service层自行组装。分页插件配置也要提前弄好不然分页查询会不生效。MyBatis-Plus的分页需要显式注入分页插件配置类如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个拦截器不配置的话调用Page对象做分页查询时会查出全部数据非常坑。很多新手项目跑起来数据量小看不出问题数据一多就暴露了。3.3 课表查询接口与业务逻辑细节课表查询是系统的核心接口它要返回的不是排课表原始记录而是前端可直接渲染的周课表数据结构。我的实现思路是先根据semester_id和查询条件班级或教师查出该学期全部排课记录然后在内存中组装成一个二维结构int[7][12]这样的星期与节次矩阵每个格子放课程信息对象。这个方案的好处是前端拿到的数据是固定结构的渲染时不必再做二次数据转换。而且因为一个学期一个班级的课表最多几十条记录全量查询到内存中组装完全不存在性能问题没必要做复杂的SQL连表查询再分页。Service层的核心代码逻辑可以这样写public ScheduleVO getScheduleByClazz(Long semesterId, Long classId) { ListCourseSchedule scheduleList scheduleMapper.selectList( new LambdaQueryWrapperCourseSchedule() .eq(CourseSchedule::getSemesterId, semesterId) .eq(CourseSchedule::getClassId, classId) ); ScheduleVO vo new ScheduleVO(); // 初始化7行12列二维数组 for (int day 1; day 7; day) { for (int section 1; section 12; section) { // 根据weekday和节次匹配课程填充课时信息 } } return vo; }实际排课时还会遇到跨周课程的问题。比如某门课是第2周到第16周每周三第3-4节而周数是单双周交替的。这里有三种处理方式如果week_type为0表示全周期固定为1表示单周上为2表示双周上。前端渲染时看到week_type字段直接用简单判断决定是否显示这节课。事务处理方面排课接口必须加事务。一次排课操作可能同时插入多条排课记录比如同一门课同时给两个班级上课中间任何一条插入失败都要整体回滚。用Transactional注解在Service方法上然后让Controller只负责接收参数这个习惯要养成。4. 前端核心实现Vue3 Element Plus4.1 Vue3工程搭建与组合式API的使用习惯前端部分使用Vite作为构建工具创建工程命令很简单npm create vitelatest course-frontend -- --template vue选Vite而不是Webpack的原因很朴素——启动快热更新快。Vite开发阶段按需编译一个中大型管理系统的开发体验比Webpack好太多了。构建工具这块2026年开发Vue3项目基本默认Vite没有太多纠结的必要。项目目录我习惯按下述方式组织src ├── api // 接口请求封装 ├── views // 页面组件 ├── components // 通用组件 ├── router // 路由配置 ├── store // 状态管理Pinia ├── utils // 工具函数 └── layout // 布局组件Vue3的组合式API这里有一个核心习惯建议把课程管理、排课管理、课表查询这些业务拆成独立的useCourse、useSchedule等组合式函数。比如排课页面里课程表单的可见性、课程列表数据、加载状态都可以放进一个setup函数里统一管理。组合式API和选项式API的区别这里用一句话说透选项式API把同一份数据的代码拆散在data、methods、computed里而组合式API允许你按业务维度聚合代码。实际维护时你会发现排课相关的逻辑集中在一个函数块里阅读和修改体验完全不同。4.2 课表网格组件设计与渲染逻辑课表展示是前端最核心的部分。我的做法是做一个ScheduleGrid组件用CSS Grid布局生成7列周一~周日乘若干行节次的网格。表格表头是星期一到星期日左侧第一列是节次序号中间的单元格在对应的星期和节次位置渲染课程卡片。课程卡片上显示课程名、教师、教室和起止周。模板结构大概是这样的div classschedule-grid div classschedule-header v-forday in dayNames :keyday{{ day }}/div div classschedule-section v-forsection in maxSection :keysection {{ section }} /div div v-forcell in cells :keycell.id classschedule-cell div v-ifcell.course classcourse-card div{{ cell.course.courseName }}/div div{{ cell.course.teacherName }}/div div{{ cell.course.classroomName }}/div /div /div /div这里要特别说明课程卡片跨节次的处理。第1-2节连续两节课的情况普通表格布局会出现两个格子各显示一次课程信息非常丑。解决方案有两种。第一种把每个时间段的数据合并用CSS Grid的网格区域实现单元格合并——代码复杂度较高。第二种更实用每个格子只放一次课程信息但课程卡片的背景色让视觉上看起来是连续的。如果追求真正的合并效果建议计算每门课占据的网格行列然后动态生成grid-row样式。移动端适配问题上课表系统是桌面优先的手机浏览器展示时7列表格会非常拥挤。一个妥协方案是显示横向滚动条虽然不是最优体验但保证了数据可见性。真正要适配移动端的话需要改成按天切换的视图布局后端接口不用变前端做两套渲染逻辑就行。4.3 前后端联调与代理配置前后端分离开发最难的不是代码而是联调。接口请求这块我习惯在src/api目录下按模块建文件比如schedule.js统一封装axios实例import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) export function getScheduleByClazz(semesterId, classId) { return request.get(/schedule/clazz, { params: { semesterId, classId } }) }开发阶段最关键的配置是Vite的代理否则前端请求/api/schedule时会直接404。要在vite.config.js加上export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这个配置的意思是前端发的/api开头的请求会被转发到后端8080端口同时把/api前缀去掉。不配置的话每个接口都要写全路径部署到线上还得额外配Nginx转发规则非常麻烦。还有个经常遇到的问题——跨域。后端如果开启了CORS配置前端不配代理也能请求通。但我的建议是开发阶段统一走Vite代理后端不要开CORS这样能避免很多奇奇怪怪的问题。线上部署时用Nginx做静态文件服务和反向代理天然解决跨域问题。5. 常见问题与排查技巧实录5.1 MySQL8.0连接时区与驱动问题MySQL8.0安装完成后最容易出的问题就是应用启动时报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这是时区问题。MySQL8.0默认时区设置的比较怪连接串需要在末尾加上时区参数。正确的JDBC连接串配置spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai是必须加的。allowPublicKeyRetrievaltrue也是8.0连接时常见的参数不加的话某些版本驱动会提示requires allowPublicKeyRetrieval。另外useSSLfalse建议保留本地开发环境下SSL验证意义不大还能避免证书相关报错。另外说一个和驱动版本相关的问题。MySQL8.0.33之前的驱动可能不支持MySQL8.4甚至更高版本的认证协议。如果服务器用的是MySQL8.4建议驱动版本放在8.0.33以上同时确认数据库用户的认证方式。如果遇到Public Key Retrieval is not allowed就是缺少allowPublicKeyRetrieval参数。5.2 MyBatis-Plus的更新空值问题这个坑几乎每个用MyBatis-Plus的人都会遇到。默认情况下使用updateById方法时实体中为null的字段不会更新到数据库。这在大部分场景下是合理的但有时候需要主动把某个字段置空。解决办法有两种。一种是在实体字段上加TableField(updateStrategy FieldStrategy.IGNORED)让这个字段更新时忽略空值检查。另一种是自己在Service层构造UpdateWrapper用.set方法显式设置字段值LambdaUpdateWrapperCourseSchedule wrapper new LambdaUpdateWrapper(); wrapper.eq(CourseSchedule::getId, scheduleId) .set(CourseSchedule::getClassroomId, null);这个方法更灵活不用动实体类配置推荐使用。另一个相关的问题是字段自动填充。创建时间、更新时间这类字段如果表结构里有而实体类里没配置插入数据时会报错或者插入脏数据。建议在实体类使用TableField(fill FieldFill.INSERT)配合MetaObjectHandler实现自动填充这个配置一次到位后续所有表都能复用。5.3 前端渲染与接口调试的几个坑课表页面渲染空白优先检查接口数据结构。表格组件渲染依赖最终返回的二维数组如果接口返回的字段名大小写不一致前端拿不到数据也不会报错只是页面上什么都没有。调试技巧是把接口返回的数据先打印在控制台和后端约定的字段名对一遍。Vue3中有一个很隐蔽但常见的坑使用reactive定义数组并直接用索引赋值界面可能不更新。比如const scheduleData reactive([]) scheduleData[0] someData // 界面不更新这是因为Vue3的reactive对数组的索引访问和length修改做了额外处理直接赋值索引理论上应该触发更新但在某些情况下就是不行。稳妥方案是用ref定义数组赋值时用.value整体替换或者用splice方法。这也是我建议课表组件里的cells数组使用ref而非reactive的原因。数据回显的问题也遇到过几次。比如编辑课程信息弹窗打开后表单初始值要回显。如果用reactive定义整个表单对象直接赋值某个字段不会触发校验器更新。正确做法是打开弹窗时重建表单对象或者用Object.assign整体替换const editForm ref({}) const openEditDialog (row) { editForm.value { ...row } dialogVisible.value true }这种写法每次打开弹窗都是新对象引用响应式系统能正常追踪变化。5.4 排课冲突校验的实战心得排课冲突校验是这个系统里业务逻辑最密集的地方。我的校验逻辑分三层。第一层时间段冲突。查询同一个教室、同一周几、相同节次范围内是否有排课。SQL的where条件类似WHERE semester_id #{semesterId} AND classroom_id #{classroomId} AND weekday #{weekday} AND start_section #{endSection} AND end_section #{startSection}这个区间重叠判断的SQL是个通用姿势记住start_section endSection AND end_section startSection即可任何时间冲突检测都能套用。第二层教师冲突。同一个教师不能在同一时间出现在两个教室校验逻辑跟教室冲突类似换一下teacher_id条件即可。第三层班级冲突。这个场景是同一门课给两个班同时上教室里坐了两个班的学生——排课表示例里课时记录的class_id只有一个所以需要额外约定。我的做法是在排课表里加入多个classId以逗号分隔的方式维护或者拆一张排课与班级的关联子表。考虑到课程管理系统大多数场景是一对一用逗号分隔是偷懒但实用的方案。你要是想做得更完善用关联子表是正解。排课主表存课程、老师、教室、时间段关联表存排课ID和班级ID的映射关系。查询班级课表时从关联表反向查出排课记录即可。代码多写一个Mapper但数据的规范性和扩展性会好很多。6. Docker部署MySQL8.0与环境搭建6.1 本地开发环境怎么快速跑起来如果本地没装MySQL8.0最省事的方案是用Docker直接起一个实例。用Docker跑MySQL8.0的好处是环境隔离不污染本机而且振作起来快。docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEcourse_system \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0这个命令参数逐一说一下-p 3306:3306把容器的3306端口映射到本机MYSQL_ROOT_PASSWORD设置root密码MYSQL_DATABASE自动创建数据库-v把数据持久化到宿主机目录这样容器删了数据还在。推荐端口用3306还是自定义如果本机有MySQL占用建议改映射端口如13306:3306避免冲突。启动后验证容器状态docker ps | grep mysql8 docker exec -it mysql8 mysql -uroot -p如果容器起不来最可能的原因是端口被占用或卷目录权限问题。查看日志用docker logs mysql8大概率能看到真正的报错信息。6.2 项目部署的完整流程项目开发完成后部署是另一个话题。前后端分离项目的部署方案推荐用Nginx同时托管前端静态文件和后端反向代理。前端执行npm run build生成dist目录后端执行mvn clean package生成jar包。然后简单配置Nginxserver { listen 80; server_name your-domain.com; location / { root /var/www/course-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files配置是SPA路由的关键——因为Vue3路由用的history模式直接刷新页面时Nginx要把请求回退到index.html否则会出现404。这个部署细节非常值得注意上当的人极多。jar包的启动命令记得加上JVM参数nohup java -jar -Xms512m -Xmx1024m -Dfile.encodingUTF-8 course-system.jar app.log 21 不加-Dfile.encodingUTF-8的话Linux服务器上偶尔会出现中文乱码问题因为系统的默认字符集会跟程序的编码处理冲突。6.3 文档与二次开发建议源码附带文档这块我的习惯是至少包含三份数据库初始化脚本、接口文档、部署说明。数据库脚本要保证在一键执行后能还原完整结构加基础数据接口文档用Swagger生成的JSON格式或Markdown格式都可以部署说明要把上面提到的时区、编码这些坑提前写清楚。二次开发方向上比较有代表性的扩展点有三个。第一加上SpringSecurity或Sa-Token做登录鉴权和角色管理把课表管理从完全公开变成有权限控制第二增加Excel导入导出功能用EasyExcel实现课程批量导入和课表导出实用性会明显增强第三加一个公告或通知模块排课调整后能将变动推送给相关用户。我个人在实际操作中的体会是这类系统做完一遍最大的收获并不是那些CRUD代码写得有多熟练而是你对“先设计表、再设计接口、最后设计页面”这条主线的理解。很多新手拿到需求后第一件事就是打开IDE写代码结果越写越乱。课表管理系统最大的价值就是让你在改动不大、技术栈齐全的范围内体会一次完整的业务闭环梳理过程。最后再分享一个小技巧。排课冲突校验这类业务逻辑你可以在数据库表里先不加唯一约束而是在Service层做校验后抛出异常。这样在业务提示上会友好得多。数据库约束是兜底的但用户绝不应该在界面上看到一条原生SQL报错。这个原则在任何管理系统里都成立。
返回列表