搞毕业设计或者课程设计的同学,应该对课表管理系统这个题目不陌生。每年都有大量学生选它,但真正能把SpringBoot+Vue这套技术栈跑通、把课表排课逻辑讲清楚的项目,其实并不算多。我前阵子正好完整过了一遍“西安工商学院课表管理系统”这个项目——基于SpringBoot+Vue的源码,后端用MyBatis操作MySQL,前端是Vue全家桶,整体结构清爽,非常适合拿来作为Java全栈入门到进阶的练手项目。这篇文章我不打算给你贴一堆源码就完事,而是把这套系统的设计思路、数据库表结构、后端关键接口、前端组件实现、部署流程以及我实际踩过的坑,全部掰开揉碎讲清楚。
1. 项目背景与整体设计思路拆解
1.1 课表管理系统到底要解决什么问题
很多人觉得课表管理系统就是“把课程信息往页面上一摆”,这个理解太浅了。高校课表管理的真实痛点在于几个矛盾的叠加:
一是数据维度多。一门课涉及班级、教师、教室、时间(星期几、第几节)、周次(单周双周还是全周),这些维度交叉起来,数据量不大但关系复杂。
二是查询场景杂。学生要看“我这周有什么课”,老师要查“我周几在哪个教室上课”,教务处要维护排课数据,这三种角色的视角完全不同。如果系统不做角色区分,所有逻辑都揉在一起,后期维护会非常痛苦。
三是排课冲突检测难。同一时间,一个教室不能被两门课占用,一个老师不能同时在两个教室上课,一个班级也不能拆到两个地方。这几条规则看起来简单,但落到SQL和接口逻辑里,很容易漏掉边界情况。
这套基于SpringBoot+Vue的课表管理系统,核心就是围绕以上需求做角色划分:管理员负责基础数据维护,教师端可以查看和录入个人课表,学生端按班级/个人课表查询。后端提供RESTful接口,前端按角色渲染不同页面,整体是一个典型的后台管理+信息查询型Web应用。
1.2 为什么选SpringBoot + Vue这套黄金组合
这个选择其实是“市场主流”和“学习成本”之间权衡的结果。如果你自己搭项目,可能会纠结用SSH还是SSM,或者前端用JSP还是Thymeleaf。但放到2025年的视角,SpringBoot + Vue几乎已经成了Java Web项目的工业标准配置。
后端用SpringBoot的原因很直接:它省掉了Spring MVC时代大量的XML配置,内嵌Tomcat,一个java -jar就能跑起来,对于课表管理这种中小型项目来说,开发效率比传统SSM高太多。更关键的是,SpringBoot的自动配置机制让MyBatis接入变得异常简单,引入依赖、写Mapper接口、加@MapperScan,三步搞定。
前端用Vue而不是JSP,核心区别在于前后端彻底分离。JSP时代的页面渲染靠服务端拼接HTML,每次点按钮都要刷新页面,体验很差。Vue负责前端路由和组件渲染,通过axios异步调后端接口拿JSON数据,用户在页面上点击“查询课表”时,浏览器不会整页刷新,只是局部更新数据区域,这种体验是传统模板引擎给不了的。
另外还有个很实在的考虑:Vue的生态太成熟了。Element UI的表格、弹窗、表单组件直接拿来改样式就能用,一个课表管理系统里的课程表展示、班级管理、教师管理、教室管理这几个核心页面,用Element UI能省下大量前端时间。
从项目结构上看,这套系统的方案选型还有一个隐性优势:分层足够清晰。Controller层只负责接收请求和返回结果,Service层专注业务逻辑,Mapper层只做数据库操作。这种三板斧结构虽然代码量会多一点点,但排查问题的难度会低很多——哪一层出了问题,直接看那一层的日志就能定位。
2. 数据库设计与表结构解析
2.1 核心表结构:五张表搞定课表核心逻辑
课表管理系统听起来功能不少,但落到数据库层面,核心就五张表。我画一下实际的建表思路,大家对照这个结构去看源码会轻松很多。这里我用MySQL 5.7+的InnoDB引擎为例,字符集统一用utf8mb4,避免中文乱码和emoji存储问题。
第一张是admin管理员表,字段不多,id、username、password、real_name、role、create_time。密码存储这里必须提醒一句:用BCrypt加密,不要用MD5。MD5加盐倒也不是完全不能做,但Spring Security自带的BCryptPasswordEncoder几行代码就接上了,安全性和实现成本都比MD5加盐更优。
第二张是student学生表,核心字段包含id、student_no(学号)、name、gender、class_id、grade_year。这里class_id要关联到班级表,而不是直接存一个字符串班名,否则后续统计“某班有哪些学生”需要写LIKE模糊匹配,性能很差。
第三张是teacher教师表,字段相对简单,id、teacher_no、name、title(职称)、department_id、phone。
第四张是course课程表,这块需要重点设计。字段包括id、course_name、course_code、credit(学分)、course_type(必修还是选修)、total_hours(总学时)。学分和总学时这类信息对于课表渲染没什么用,但对于教务管理场景的扩展非常关键,源码里保留这些字段是有道理的。
第五张是schedule课表核心表,这张表的设计直接决定了业务逻辑的复杂度。字段大概是:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| course_id | bigint | 关联课程表 |
| teacher_id | bigint | 关联教师表 |
| class_id | bigint | 关联班级表 |
| week_day | tinyint | 星期几,1-7 |
| section_start | tinyint | 开始节次 |
| section_end | tinyint | 结束节次 |
| week_start | tinyint | 起始周 |
| week_end | tinyint | 结束周 |
| week_type | tinyint | 0全周 1单周 2双周 |
| classroom | varchar | 教室地点 |
| semester | varchar | 学期标识 |
这张schedule表是整个系统数据层面的灵魂。week_day决定星期,section_start和section_end决定第几节到第几节,week_start和week_end决定这个课程从第几周上到第几周,week_type则用于处理单双周这种高校特有的排课规则。很多初版课表系统不考虑单双周,结果后面排课的时候发现有的课程奇数周在A教室、偶数周要在B教室,只能再改表结构,非常被动。
2.2 数据库设计的权衡与避坑
数据库设计这块,我基于源码和实际落地经验给你几个比较实用的建议。
先说说为什么不搞冗余字段。有人会想,既然查询课表总是要显示课程名、教师名、班级名,那直接把course_name、teacher_name、class_name冗余到schedule表里不就行了吗?省得JOIN了。这个做法在小项目里确实能提速,但有一个隐患:一旦课程名称改了或者教师换校区了,你所有冗余字段都要同步更新,漏一条数据课表就是错的。课表管理系统的数据特点是“读多写少”,用JOIN查询的成本完全可控,完全没必要牺牲一致性来换那点性能。
再说说索引设计。schedule表最频繁的查询条件是class_id + semester,其次是teacher_id + semester。设计索引时应该建联合索引,而不是单独给每个字段建索引。比如idx_class_semester(class_id, semester),它能同时覆盖按班级查课表的场景,还能通过最左前缀原则在单独查class_id时生效。看过不少项目源码,索引这块经常被忽略,数据少的时候没感觉,数据到几千条之后查询速度下降特别明显。
还有时间字段的坑。create_time、update_time字段建议统一用datetime类型,并且让MySQL自己维护,不要在前端或者Service层手动set。做法是在建表语句里加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,这样插入和更新时完全不用管时间字段,数据可靠性更高,还不容易因为前后端时区不同导致时间错乱。
CREATE TABLE `schedule` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `course_id` bigint(20) NOT NULL COMMENT '课程ID', `teacher_id` bigint(20) NOT NULL COMMENT '教师ID', `class_id` bigint(20) NOT NULL COMMENT '班级ID', `week_day` tinyint(4) NOT NULL COMMENT '星期几 1-7', `section_start` tinyint(4) NOT NULL COMMENT '开始节次', `section_end` tinyint(4) NOT NULL COMMENT '结束节次', `week_start` tinyint(4) NOT NULL DEFAULT '1' COMMENT '起始周', `week_end` tinyint(4) NOT NULL DEFAULT '20' COMMENT '结束周', `week_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '周类型 0全周 1单周 2双周', `classroom` varchar(100) DEFAULT NULL COMMENT '上课教室', `semester` varchar(20) NOT NULL COMMENT '学期标识,如2024-2025-2', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_class_semester` (`class_id`, `semester`), KEY `idx_teacher_semester` (`teacher_id`, `semester`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课表信息表';3. 后端核心实现:SpringBoot + MyBatis的综合实战
3.1 项目分层结构与核心依赖配置
这套后端项目的包结构很规范,基本是以下这些包:controller放接口路由,service放业务逻辑,mapper放MyBatis的数据库操作接口,entity放实体类,common放统一返回结果和异常处理,config放配置类。
依赖方面,pom.xml里保留了几个关键的依赖:spring-boot-starter-web就不用多说了,mybatis-spring-boot-starter负责整合MyBatis,mysql-connector-java负责连接MySQL,lombok用来简化实体类代码(减少Getter/Setter的编写),还有hutool或fastjson这类工具库处理JSON。这里我建议用hutool,它的工具类覆盖比较全,课表管理里涉及日期处理、字符串转换的场景,它都能减少不少代码量。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>对应的application.yml配置如下。注意MyBatis的mapper-locations路径要和实际资源目录对应,如果配错了,启动的时候会报Invalid bound statement (not found)错误。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_schedule?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.courseschedule.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl其中map-underscore-to-camel-case这个配置特别重要。你数据库字段是week_day,实体类是weekDay,如果不开启驼峰映射,查询结果里weekDay就是null,前端拿不到数据还会以为是接口报错,实际是这里没配。
3.2 Mapper层设计与SQL解析
MyBatis的用法有两种,一种是注解写SQL,一种是XML写SQL。课表管理这种查询条件比较灵活的项目,我更推荐用XML方式。原因很简单:注解方式虽然写起来快,但SQL一复杂,字符串拼在注解里可读性和可维护性都会变差,而且没法实现动态SQL那种“条件可选”的效果。
可以看看课表查询的核心Mapper实现。比如按条件查询课表,前端传过来可能带班级ID、带学期、带教师ID,也可能不带某个条件,这时候就需要MyBatis的动态SQL来处理。
<select id="selectScheduleList" resultType="com.example.courseschedule.entity.ScheduleVO"> SELECT s.*, c.course_name, t.name as teacher_name, cl.name as class_name FROM schedule s LEFT JOIN course c ON s.course_id = c.id LEFT JOIN teacher t ON s.teacher_id = t.id LEFT JOIN class cl ON s.class_id = cl.id <where> <if test="classId != null"> AND s.class_id = #{classId} </if> <if test="teacherId != null"> AND s.teacher_id = #{teacherId} </if> <if test="semester != null and semester != ''"> AND s.semester = #{semester} </if> </where> ORDER BY s.week_day, s.section_start </select>这段SQL用 标签包了一层,里面每个 都会自动判断参数是否为空,为空就跳过这个条件。相比手动拼SQL,这种方式既安全又能应对课表管理里查询条件不固定的场景。LEFT JOIN而不是INNER JOIN,是为了保证即使关联的教师、班级信息缺失,课程记录本身也能查出来,前端不至于整条数据消失。
还有查询学生个人课表的场景。核心思路是:前端传入studentId,后端先查出该学生所属的classId,再用classId去schedule表查课表数据。这种两步查询可以放在Service层做,也可以在Mapper层写一个嵌套子查询直接一步完成。子查询的方式效率更高,但可读性略差,如下:
<select id="selectStudentSchedule" resultType="..."> SELECT s.*, c.course_name, t.name as teacher_name FROM schedule s LEFT JOIN course c ON s.course_id = c.id LEFT JOIN teacher t ON s.teacher_id = t.id WHERE s.class_id = ( SELECT class_id FROM student WHERE id = #{studentId} ) AND s.semester = #{semester} </select>这种嵌套子查询在数据量不大的情况下完全够用。本质上就是“先用学生表找到班级,再用班级找课表”的两步操作,只是并到了一条SQL里,能省一次网络往返。这类小优化,在课表管理这种需要频繁按学号查课表的场景里,体感差距是实在的。
3.3 冲突检测的核心业务逻辑
课表系统里最容易被低估的就是冲突检测。管理员排课的时候,如果同一时间、同一教室已经有别的课,系统必须给出提示。源码里的做法很标准:在插入schedule记录之前,先做一次重叠判断。判断的条件是:时间上有交集(星期相同、节次区间重叠)、资源上有交集(教室相同、或者教师相同学时)。
关键SQL可以这么写:
<select id="countConflict" resultType="int"> SELECT COUNT(*) FROM schedule WHERE semester = #{semester} AND week_day = #{weekDay} AND classroom = #{classroom} AND NOT (section_end < #{sectionStart} OR section_start > #{sectionEnd}) <if test="weekType != null"> AND (week_type = 0 OR week_type = #{weekType}) </if> </select>这里的核心是区间重叠判断,我用的是“NOT(结束节次小于新开始节次 OR 开始节次大于新结束节次)”这个逻辑,等价于两个时间段有交集。这个写法在写课表系统的排课功能时通用性很强,也不只适用于教室冲突,把classroom字段换成teacher_id,就是教师冲突检测,换成class_id,就是班级冲突检测。
比较隐蔽的是单双周的处理。比如A课程单周上周一的1-2节,B课程双周也是周一1-2节,时间上看起来重叠,但实际上并不冲突。如果冲突检测SQL不考虑week_type,上课就会误报。处理方式是:如果两条课表的week_type都是单周或都是双周,确实冲突;但如果一个是单周一个是双周,即使时间重叠也能共存。上面SQL里AND(week_type = 0 OR week_type = #{weekType})这个条件就是用来过滤这个场景的。
4. 前端核心实现:Vue组件化开发与联调细节
4.1 Vue项目结构与路由设计
前端部分基于Vue 2 + Element UI + axios + Vue Router的经典组合。之所以用Vue 2而不是Vue 3,主要原因是Element UI对Vue 2的支持最成熟,网上资料多,遇到问题很快能找到解决方案。对课表管理这种以表格和表单为主的中后台项目,Vue 2 + Element UI完全够用,而且对新手更友好,没有Composition API那套概念负担。
src目录下的结构大概是这样:views目录放页面级组件,比如Login.vue、AdminDashboard.vue、TeacherSchedule.vue、StudentSchedule.vue,router目录放路由配置,api目录放axios请求封装。页面级组件和通用业务组件的拆分逻辑要清晰:像我这种DataTable在多个页面复用的场景,抽出来做成一个子组件就很合理;如果只是某个页面独有的逻辑,就不要强行封装,否则过度设计反而增加维护成本。
axios请求封装这块,课表系统里有一个通用做法:把后端返回格式统一成{ code, message, data },axios的响应拦截器里判断code是否为200,不是200就弹出错误提示。这样Controller层只需要返回业务数据,不用关心错误提示的UI逻辑,前端也只需要在拦截器里统一处理错误。源码里是这样接的:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { this.$message.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default service4.2 课表展示组件的实现思路
课表展示是前端功能里最关键的模块,核心难点在于要把数据库中一条条的schedule记录转换成二维表格矩阵。行的维度是星期一到星期日,列的维度是第1节到第8节,每个单元格放课程名称、教师和教室。
这里不建议直接用Element UI的el-table来渲染,因为课程通常跨多个节次(比如1-2节连上),需要合并单元格。更靠谱的方案是用CSS Grid或Table布局自己渲染。实现思路是把schedule数据转换成二维数组matrix[weekDay][section],先初始化一个7x8的空白矩阵,然后遍历课表数据填充进去。跨节次的课程用rowspan属性合并行。
// 将后端返回的课表数据转换为7x8矩阵 buildScheduleMatrix(scheduleList) { const matrix = [] for (let day = 1; day <= 7; day++) { matrix[day] = [] for (let section = 1; section <= 8; section++) { matrix[day][section] = null } } scheduleList.forEach(item => { for (let section = item.sectionStart; section <= item.sectionEnd; section++) { matrix[item.weekDay][section] = item } }) return matrix }这里还有一个小细节:跨周次的显示。比如某门课是单周一三五有课,周的维度在二维表格里不好直接呈现。实际项目里的做法是,在课程卡片的tooltip里展示周次信息,比如悬停单元格会显示“1-16周 单周”,或者在单元格里用更小的字体展示周次范围。这种信息层级的设计,在课表系统里比在一个格子里塞满所有文字要舒服得多。
4.3 前后端联调的关键细节
前后端联调阶段有几个问题非常高频,我一个个说。
第一个是跨域问题。前端开发服务器跑在localhost:8081(Vue CLI默认端口,或者3000),后端跑在localhost:8080,浏览器会拦截跨域请求。最简单的处理方案是让后端允许跨域,写一个WebMvcConfigurer配置类,重写addCorsMappings方法,放行所有路径。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8081") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }第二个是时间格式问题。MySQL里的datetime类型通过MyBatis查出来是java.util.Date或者LocalDateTime,Jackson序列化成JSON时默认可能是时间戳格式,前端拿到后显示成一大段数字。解决方案是在application.yml里全局配置Jackson的日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8第三个是请求参数命名问题。Java后端字段用驼峰命名(classId、studentNo),前端传参时要保持完全一致。如果使用FormData或者query string传参,粗细大小写写错了后端接收不到值。这个排查起来比较烦躁,建议大家在axios里把所有参数名统一用小驼峰,后端实体类字段也统一小驼峰,从源头上杜绝这个问题。
5. 实操部署全流程:从本地跑起来到服务器发布
5.1 本地环境准备清单
先把环境列清楚,这一步省不了,Environment出问题后期排查成本反而更高。
JDK要用8或11。SpringBoot 2.x版本对JDK 8的支持最稳定,很多课程设计项目用的都是JDK 8。如果你本地装的是JDK 17,也不是不能跑,但需要把SpringBoot版本升级到2.5+以上,依赖方面会有一点兼容性问题,建议直接用JDK 8减少麻烦。
MySQL推荐使用5.7或8.0。5.7的用户量大、教程多,8.0的窗口函数更强、性能更好,但要注意连接驱动需要mysql-connector-java 8.0+,并且URL里要加serverTimezone=Asia/Shanghai,不然插入数据时会报时间相关的错误。
Node.js用来跑Vue开发环境,推荐装14.x或16.x的LTS版本,npm包管理器会随着Node一起装上。这里提醒一下,不要追求最新版Node,Vue 2的相关依赖对Node版本兼容性有限制,太新反而跑不起来。
最后是开发工具,后端用IntelliJ IDEA(社区版免费就够用),前端用VS Code。IDEA要装Lombok插件,不然实体类里的@Data注解不会生效,编译时直接报找不到getter/setter方法。
5.2 项目启动步骤与验证方法
启动步骤不复杂,但顺序错了容易迷糊。我按实际操作顺序给你列一下:
第一步,创建数据库。打开MySQL命令行或者Navicat,新建一个course_schedule数据库,编码选utf8mb4,然后把项目里自带的course_schedule.sql文件导入。导入完可以先验证一下,执行show tables,能看到admin、student、teacher、course、schedule这五个表就算成功。这里尤其强调一下导入操作,如果SQL脚本里有中文字段值,navicat导入时编码不对会直接乱码,一定要把连接编码也设为utf8mb4。
第二步,改数据库配置。打开后端项目的application.yml,修改spring.datasource里的username和password。很多新手在这里会漏掉时区参数,如果MySQL是8.0版本,URL里少了serverTimezone=Asia/Shanghai,启动时会直接抛异常。
第三步,启动后端。在项目根目录执行mvn spring-boot:run,或者在IDEA里直接运行主启动类。看到Spring Boot的启动日志出现“Started Application in x.xxx seconds”就代表启动成功。这里再教你一个小技巧,启动成功后可以直接在浏览器访问http://localhost:8080/api/ping,如果返回{ code: 200 }就说明接口通了,不用急着进前端判断。
第四步,启动前端。进入frontend目录,执行npm install安装依赖,第一次装会花几分钟,如果装的时候出现node-sass报错,多半是Node版本和依赖不兼容,升级或者降级Node的LTS版本可以解决。依赖装完后执行npm run serve启动开发服务器,默认端口是8081。浏览器打开localhost:8081,能看到登录页就说明前端起来了。
第五步,用测试账号登录。管理员账号一般是admin/admin123,学生账号可以看student表里的测试数据。登录后先用管理员账号添加一条课程信息,再排一条课表,验证一下前端页面能否正常展示课表矩阵。到这一步,整个本地环境就算真正跑通了。
5.3 打包发布到服务器的核心操作
本地跑通之后,如果想把项目发布到服务器上,需要注意前后端是独立的两个项目。
前端打包:在frontend目录执行npm run build,生成dist目录。这里有一个特别容易踩的坑,Vue项目默认的静态资源路径是绝对路径/,部署到服务器的子目录下会白屏。解决办法是修改vue.config.js里的publicPath,设为'./',这样构建出来的文件就是相对路径,放到Nginx任意目录下都能直接访问。
后端打包:在项目根目录执行mvn clean package -DskipTests,生成target目录下的jar包。把这个jar包上传到服务器,执行nohup java -jar course-schedule.jar > app.log 2>&1 &,就可以后台运行了。默认端口8080,如果要修改端口,记得在application.yml里改完再打包。
服务器上建议用Nginx做反代,Nginx监听80端口,把/api开头的请求转发到本机的8080端口,其他请求交给前端静态文件。这个配置很基础,但实用性极高:
server { listen 80; server_name yourdomain.com; root /var/www/course-schedule/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }注意try_files这行,Vue Router如果使用History模式,刷新页面时路由地址会直接打到Nginx,必须通过try_files把未命中的路径都指向index.html,否则一刷新就404。如果你不想处理这个情况,也可以在Vue Router里用hash模式(URL里带#),但体验略差。
6. 常见问题排查与性能优化心得
6.1 启动阶段的经典报错排查实录
把这段时间我和学生一起调试时遇到过的问题整理成一份速查表,基本覆盖了这类项目99%的报错场景:
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
| Invalid bound statement (not found) | Mapper接口与XML的namespace或id不对应 | 检查Mapper接口的全限定类名与XML中的namespace完全一致 |
| 数据库连接失败,Access denied for user | 数据库账号密码错误 | 检查application.yml里的username和password |
| Failed to configure a DataSource | 没有配置数据源或数据源配置被扫到了其他环境 | 确认application.yml里spring.datasource配置完整 |
| 前端页面空白,控制台报404 | 静态资源路径配置错误 | 修改publicPath为'./',重新构建 |
| 端口被占用 | 8080或8081被其他进程占用 | 用netstat -ano |
| npm install卡住不动 | npm官方源访问慢 | 设置registry为https://registry.npmmirror.com |
这里单独展开两个比较隐蔽的问题。第一个是MyBatis的Mapper扫描问题。启动类上必须加@MapperScan注解,或者每个Mapper接口上加@Mapper注解,否则MyBatis根本找不到这些接口,启动数据库操作时会报Bean找不到的错。很多人项目跑着跑着突然Mapper失效了,往往就是加新包时忘了扫描路径。
第二个是MySQL 8.0的驱动配置。老项目里用的驱动类是com.mysql.jdbc.Driver,MySQL 8.0以后必须换成com.mysql.cj.jdbc.Driver,只改pom依赖不换驱动类,启动时大概率会直接报ClassNotFoundException。
6.2 性能优化:课表系统的三大提速方案
课表管理系统数据量不算大,但还是有几个值得优化的点。我从源码和工程实践的角度挑三个最有价值的说。
第一个是查询接口的响应时间优化。课表查询接口最怕的是前端在页面里发起N个请求,比如学生登录后需要同时查个人信息、查学期列表、查课表数据,三个请求串行发送很慢。正确的姿势是在后端做一个聚合接口,一次请求返回studentDTO和scheduleDTO列表,前端一个接口搞定。HTTP请求次数少了,整个页面加载的体感速度会明显提升。这种聚合接口在SpringBoot里实现很简单,Service层把结果塞进一个Map,或者定义一个聚合DTO就能搞定。
第二个是MyBatis一级缓存和二级缓存的合理使用。课表数据的特点是读多写少,非常适合使用MyBatis的二级缓存。一级缓存是SqlSession级别的,默认开启,但SpringBoot整合MyBatis后每次Mapper调用可能是不同SqlSession,所以一级缓存基本形同虚设。二级缓存是Mapper级别的,配置方式是在Mapper的XML里加 标签,并且要求实体类实现Serializable接口。要注意的是,一旦加入了二级缓存,课程信息更新后缓存不会自动清理,需要调用clearCache()或者在Mapper里配置flushCache="true"。课表管理项目数据变动不频繁,开二级缓存对查询性能的提升还是很可观的。
第三个是SQL层面避免SELECT *。源码里的Mapper查询基本都是明确列出字段的,这样做的好处一方面是减少网络传输数据量,更重要的是避免因为表结构变化导致的隐性问题。比如schedule表将来加了remark字段但没更新Mapper,前端DTO里根本没有这个字段,如果用了SELECT *,MyBatis的自动映射阶段可能因为字段对不上而报错。明确指定字段的习惯,在后期维护中能少踩很多莫名其妙的坑。
6.3 从课表管理系统延伸出去:这套架构还能做什么
聊点实际经验。课表管理系统虽然听起来只是一个毕业设计级的项目,但把它的架构吃透之后,延伸出其他系统是非常快的。因为它的核心模式——“基础数据管理 + 角色化查询 + 二维表格展示”——本质上覆盖了很多信息管理类系统的共性需求。
同一套SpringBoot + MyBatis + MySQL + Vue的代码骨架,把schedule表换成会议室预约记录,weekDay和section换成日期段和时段,就变成“会议室预约管理系统”;把student表换成会员表,把课表记录换成预约记录,就变成了“健身房管理系统的预约子模块”;把course换成项目,schedule换成排期,就能做成一个极简“项目管理排期工具”。
所以大家在啃这套源码的时候,不要只是把它当作业去糊弄,而是去理解它的分层思想、查询模型和前端组件化思路。这些东西才是真正能复用下去的资产。
我也建议动手改一改源码。比如试着给课表系统加一个“教室课表”维度,在查询条件里加一个room参数,前端新增一个按教室维度查看的视图。做完这个小改动,你对这套系统的理解会上一整个台阶。课表管理系统的繁琐之处就在于维度交叉,把它理顺了,你再去写任何信息管理类系统都会顺手很多。
我自己做这套项目复盘时最深的体会是:课表管理系统最复杂的不是任何单点技术,而是“字段之间的关联关系”和“边界情况的处理”。单周还是双周、跨节次还是单节次、学期跨年还是同年,这些奇葩情况真实存在,代码里如果不去处理,上线之后就会有学生反映课表不对。做这类系统,先把业务规则理解透,再谈技术方案,顺序反了,后面改动起来是真折腾。