ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue+MySQL课表管理系统实战:排课冲突检测与前后端分离

SpringBoot+Vue+MySQL课表管理系统实战:排课冲突检测与前后端分离 课表管理系统听着不复杂真做起来坑不少。尤其是高校场景多校区、多教学楼、几千门课程、单双周、实训周、教师冲突、教室容量……任何一个点都能让排课逻辑直接崩掉。这篇文章会从一个可直接运行的 SpringBoot 后端 Vue 前端 MySQL 课表管理系统源码入手把从数据库建模到前后端联调、从环境准备到打包部署的完整链路拆开讲清楚。项目本身的业务背景是西安工商学院的实训课表但核心代码结构是完全通用的换个学校名、改几条配置就能落到自己的教务场景里。适合正在做毕业设计、课程设计或者想上手一套完整前后端分离项目的同学参考。我拆过不少类似的管理系统课表这类业务最大的特点就是看着简单、细节极多。排课冲突、周次判断、节次规则、前端展示矩阵每一项都有专门的玩法。所以这篇不打算只贴代码而是把每个关键环节的设计理由和踩坑经历一起说出来让你照着能跑、跑起来能懂、懂了能改。1. 项目整体设计与架构思路1.1 为什么选 SpringBoot Vue MySQL 这套组合先回答一个最容易被问的问题市面上做管理系统的方式那么多为什么这个项目选了这套技术栈SpringBoot内置 Tomcat无需单独配服务器默认集成 Spring MVC、数据校验、事务管理写接口时省掉大量 XML 配置。相比传统的 SSM 框架至少少写三分之一的样板代码非常适合周期短的实训项目。Vue组件化开发让课表这种复杂页面拆得很干净路由、状态管理都有成熟方案。配合 Element UI/Element Plus后台管理的表格、表单、弹窗几乎不用自己造轮子。MySQL课表数据是强结构化数据——教师、班级、课程、教室、时间段都有明确关系用关系型数据库再合适不过。查询、统计、事务支持都稳。另外从项目价值角度看这套技术栈是目前国内中小型管理系统的主流组合。做完这个项目简历上SpringBoot Vue 前后端分离的经验是扎实的面试官问起来你也有真实案例可以讲这种可复用性是 JSP、Servlet 老项目给不了的。注意如果你的毕业设计要求必须有分布式/微服务那这套单体架构可能不够看。但大多数本科毕设和课程设计单体 前后端分离完全够用把核心业务做深比堆技术更划算。1.2 功能模块与角色权限划分课表管理系统的用户角色一般分三类管理员、教师、学生。这个项目的权限设计也围绕这三个角色展开。管理员维护基础数据班级、教师、课程、教室核心功能是排课管理——给某个班级的某门课程分配教师、教室、时间。教师查看自己的个人课表按周次切换查看部分系统还会加调课申请功能。学生查看本班课表通常只读不需要任何写操作。这三个角色决定了后端接口的权限控制策略。项目里用的是 JWT 登录 拦截器校验的方式在 JWT 的 payload 里存放用户 ID、角色信息后端拦截器统一判断接口路径和角色是否匹配。我见过很多管理系统把权限判断散落在每个 Controller 里比如if(user.getRole()!1) return error这样改起来特别痛苦。这个项目的做法是把权限判断收敛到拦截器或注解层面管理员接口统一放在/admin/**路径下教师接口放在/teacher/**下拦截器里一条规则就能管住整个模块的访问代码可维护性好很多。1.3 前后端分离下的接口通信约定前后端分离项目最怕各写各的接口返回格式不统一前端拿到数据还要一层层拆。这个项目在接口设计上做了两件很关键的事统一返回结构。后端所有接口都返回ResultT对象结构是{ code, message, data }。前端只认这一个格式成功还是失败看code具体数据看data。这样前端 Axios 的响应拦截器可以集中处理错误提示不用每个页面都写 try-catch。统一异常处理。后端用RestControllerAdvice做了全局异常捕获业务异常、参数校验异常、未知异常分别返回不同的code和message。这样前端接收到的错误信息是能直接展示给用户的而不是一坨堆栈信息。这些设计属于前期多花半小时后期省一下午的典型。我见过不少项目接口字段一会儿是data一会儿是rows前端同事只能一路?.到底维护起来极其痛苦。2. 数据库设计一张表一个坑2.1 核心表结构与字段设计课表管理系统的数据库表一般包括用户表、学生表、教师表、班级表、课程表、教室表、排课主表。这里重点讲最容易出问题的排课主表course_schedule。排课主表的核心字段我按实际经验整理一下字段含义设计说明course_id课程 ID关联课程表teacher_id教师 ID关联教师表class_id班级 ID关联班级表classroom_id教室 ID关联教室表week_start起始周如第 1 周week_end结束周如第 16 周week_type周次类型0全部1单周2双周day_of_week星期几1-7对应周一到周日start_section开始节次如第 1 节end_section结束节次如第 2 节很多同学第一次设计时容易把星期几和第几节合并成一个字段比如存周一第1-2节这种字符串。前端显示倒是方便了但后端做冲突检测、按条件查询时必须去解析字符串难受得一批。规范做法就是把星期、节次、周次拆成独立字段存数字而不是中文显示格式化交给前端。注意不同学校的节次划分不一样有的学校一天 12 节有的只有 10 节。用start_section和end_section两个字段表示连堂课能兼容第 1-2 节第 3-4 节这类不同长度的排课需求。2.2 周次数据模型区间 单双周还是位图课程不是每学期每周都上的这就有个经典问题怎么存这门课上第 1-16 周的单周方案一用week_startweek_endweek_type三个字段。week_type为 0 表示全程、1 表示单周、2 表示双周。这种方案存储直观查询也好写缺点是表达不了第 1-4 周、第 9-12 周这种分段周次。方案二用位图或字符串存实际周次比如字段weeks存1,2,3,4,9,10,11,12。表达能力最强任意周次组合都能存但存储冗余查询时要用FIND_IN_SET之类的函数性能一般。这个项目采用的是方案一因为实训类课表的排课规律相对固定95% 以上都是连续若干周或者连续若干周里的单/双周。方案一实现简单查询效率高而且对单双周这种最常见的模式支持得非常好。如果你的业务有复杂的周次组合再考虑方案二也不迟。判断单双周的工具方法也不难比如判断第 n 周是否为单周直接n % 2 1即可但要注意不同学期第 1 周是周几单双周的起始基准需要和学校校历对齐最好在系统参数表里维护一个semester_start_date避免硬编码。2.3 索引设计查询效率和冲突校验的地基排课主表一旦数据量上来一个学期几千条排课记录很正常查询和冲突校验就会变慢。这个项目在索引设计上做了几个关键动作对teacher_id、class_id、classroom_id分别建普通索引。因为最频繁的业务就是查某个教师的课表查某个班级的课表查某个教室的使用情况。对day_of_week、start_section建联合索引。冲突检测时典型的查询条件是某天第几节带上这个索引能明显提速。不要对week_type建索引。这个字段区分度太低只有 0、1、2 三个值建了索引 MySQL 也不会走纯属浪费空间。另外一个数据一致性细节外键要不要建很多企业开发规范其实不推荐物理外键而是通过应用层保证逻辑关联。这个项目也走了这个路线——表结构上只保留逻辑关联字段不建物理外键约束。原因是高校数据的变更操作比较频繁物理外键会导致改一个班级名称要连带检查一堆关联表事务开销大而且排课系统本身就是高约束业务应用层的冲突检测已经足够严格。3. 后端核心逻辑与接口实现3.1 排课冲突检测业务校验的一次完整思考排课系统的核心校验就是三条同一教师不能同时上两门课、同一班级不能同时上两门课、同一教室不能同时被两个班级用。不加这个校验连点两次提交就能造出一个老师同时在两个教室上课的灵异事件。这个项目的冲突检测是在 Service 层实现的核心思路是插入一条排课记录前先把可能冲突的字段拼成查询条件查一下有没有重叠记录。举个例子校验教师冲突时的查询逻辑大概是// 伪代码实际用 MyBatis-Plus 的 LambdaQueryWrapper Integer count scheduleMapper.selectCount( new LambdaQueryWrapperCourseSchedule() .eq(CourseSchedule::getTeacherId, req.getTeacherId()) .eq(CourseSchedule::getDayOfWeek, req.getDayOfWeek()) .apply(start_section {0} AND end_section {1}, req.getEndSection(), req.getStartSection()) .apply(week_start {0} AND week_end {1}, req.getWeekEnd(), req.getWeekStart()) ); if (count 0) { throw new BizException(该教师该时间段已有排课); }这里有个关键点判断时间段是否冲突不是start_section完全相等而是区间重叠判断。新排课是第 3-4 节已有排课是第 1-4 节那两个只是比较起止节次相等是发现不了的必须用existing.start new.end AND existing.end new.start这种区间交叉判断。同理周次区间也是区间重叠判断。为什么不把冲突校验放在数据库层因为week_type的存在让问题变得复杂——第 1-16 周单周和第 1-16 周双周从区间上看完全重叠但实际并不会冲突。这种逻辑用数据库唯一约束是表达不了的所以必须在应用层做业务校验。实际做的时候可以写一个私有的checkConflict方法把教师、班级、教室三个维度都查一遍即使三个维度互相独立也都走同一条查重逻辑。注意查重和插入要放在同一个事务里避免并发情况下两条插入同时通过校验。3.2 周次工具类单双周与周数判断的正确姿势排课表展示时前端会传一个当前周参数比如第 8 周。后端需要判断这周有没有这门课这就要用到一个周次判断工具类。这个类的实现并不复杂但有几个边界条件容易踩坑。第一个坑是单双周的判断。有的同学直接写week_number % 2 1判断单周但没考虑到学期第 1 周到底是单周还是双周。如果校历规定第 1 周是双周那整个单双周都要取反。这个项目的做法是把第 1 周是单周作为默认规则写在配置文件里如果学校规则不同改一个配置项就行不用动代码。第二个坑是区间判断的边界。判断第 n 周是否在week_start到week_end之间很多人写的是return currentWeek weekStart currentWeek weekEnd;前半部分没问题容易漏的是week_type的配合判断。正确逻辑是先判断是否在区间内再根据week_type判断奇偶public boolean isCourseInWeek(CourseSchedule schedule, int currentWeek) { if (currentWeek schedule.getWeekStart() || currentWeek schedule.getWeekEnd()) { return false; } if (schedule.getWeekType() 0) { return true; } // 单周1、3、5...双周2、4、6... int parity currentWeek % 2; return schedule.getWeekType() 1 ? parity 1 : parity 0; }这类工具方法建议写成纯静态方法不依赖 Spring 容器方便单元测试。我见过有人把周次判断逻辑直接写在 Controller 里一页代码上百行测试还贼难写后面改需求差点崩溃。3.3 JWT 登录认证与权限拦截这个项目的登录认证用的 JWTJSON Web Token整体流程不复杂用户提交账号密码后端校验通过后生成一个带签名的 Token 返回给前端前端后续请求在请求头携带Authorization: Bearer token后端拦截器解析 Token 确认用户身份。关于 Token 生成项目里用的io.jsonwebtoken:jjwt库核心代码大概是String token Jwts.builder() .setSubject(userId.toString()) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) // 7天过期 .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这里有几个细节值得说说密码存储不要用明文。项目里用的是 BCrypt 加密比 MD5 靠谱得多。MD5 撞库太容易虽然实训项目无所谓但养成好习惯很重要。Spring Security 的BCryptPasswordEncoder或者 Hutool 的BCrypt工具都可以。Token 过期时间设 7 天比较合适。太短了用户老要重新登录太长了安全性下降。毕业设计场景 7 天刚好。拦截器注册注意排除登录接口和静态资源路径不然前端打包的 HTML、CSS、JS 也会被拦。这个坑很经典前端部署到 SpringBoot 后访问页面 401排查半天发现是拦截器没有放行静态资源。权限拦截的落地方式我前面提到过用路径规则 角色判断一句话说明拦截器从 JWT 里取出role结合request.getRequestURI()判断是否以/admin开头但角色不是管理员如果是就返回 403。代码量不大但安全性考量要完整。3.4 课表查询的 SQL 优化与 N1 问题课表查询最常见的操作是根据教师/班级查整周课表。如果直接遍历课表记录逐条查课程和教师名称就会出现 N1 查询问题——查一次课表要额外执行几十次关联查询数据量一大页面就卡。这个项目的优化思路是一次JOIN查出所有关联字段。比如查某教师第 8 周的课表核心 SQL 类似SELECT cs.id, cs.day_of_week, cs.start_section, cs.end_section, cs.week_start, cs.week_end, cs.week_type, c.course_name, c.course_color, t.teacher_name, cl.class_name, cr.classroom_name FROM course_schedule cs LEFT JOIN course c ON cs.course_id c.id LEFT JOIN teacher t ON cs.teacher_id t.id LEFT JOIN class_info cl ON cs.class_id cl.id LEFT JOIN classroom cr ON cs.classroom_id cr.id WHERE cs.teacher_id #{teacherId} AND #{currentWeek} BETWEEN cs.week_start AND cs.week_end ORDER BY cs.day_of_week, cs.start_section注意#{currentWeek} BETWEEN cs.week_start AND cs.week_end这种写法把周次过滤下推到 SQL 层而不是先把全量数据查出来再到 Java 里判断数据量大时性能差距非常明显。另外week_type的过滤建议在 Java 内存里做因为单双周的判断逻辑涉及取模和校历配置SQL 里写MOD(#{currentWeek}, 2) 1这种硬编码会让 SQL 变得很不灵活。4. 前端 Vue 实现与交互细节4.1 页面结构与路由设计前端的页面结构采用经典的后台管理布局顶部导航栏 左侧菜单 右侧内容区。用 Vue Router 做路由管理路由表按角色划分了三组/admin开头班级管理、教师管理、课程管理、教室管理、排课管理/teacher开头我的课表、调课申请/student开头班级课表路由设计上有两个实践要点值得展开。第一是路由守卫。在router.beforeEach里做登录状态和角色校验没登录一律跳转/login。这一点很多同学会忽略只做了后端拦截前端页面直接输入 URL 也能访问体验很割裂。第二是是否用动态路由。这个项目用的是静态路由表 菜单权限过滤而不是动态路由注册。原因是三个角色的菜单差异不大用静态路由表配合meta.roles字段做菜单显隐已经足够。动态路由适合不同角色看到的菜单差异巨大的大后台会引入路由刷新丢失、权限递归匹配等一堆问题实训项目不建议上。4.2 课表组件动态表格渲染与插槽用法课表展示是整个前端最核心的页面。数据模型长这样dayOfWeek从 1 到 7startSection从 1 到 N展示时需要把数据库的扁平记录转换成二维矩阵横轴是星期一到星期日纵轴是节次。实现思路是用一个计算属性遍历课表数据并填充到二维数组tableData[section][day]const tableData computed(() { const rows []; for (let s 1; s maxSection; s) { const row { section: s, cells: {} }; for (let d 1; d 7; d) { row.cells[d] scheduleList.value.filter( item item.dayOfWeek d s item.startSection s item.endSection ); } rows.push(row); } return rows; });这里有两个容易翻车的地方。第一是跨节次的课程合并。一门课占了第 1-2 节在表格里应该是一个跨两行的单元格而不是两行都显示。Element Plus 的el-table可以用span-method做合并但逻辑写起来比较绕。实际项目里很多同学直接用 CSS Grid 或 flex 布局自己画网格自由度更高合并和悬停效果都好做。第二是插槽slot的合理使用。课表单元格里除了课程名往往还要展示教师、教室信息有时候还要根据课程类型显示不同背景色。用el-table的话定义多个具名插槽会非常灵活。这个项目里使用了 Element Plus 表格的#default插槽自定义单元格内容并在单元格里嵌入了课程标签组件算是把 Vue 插槽的优势用到位了。4.3 Axios 封装与跨域处理前端所有请求通过一个统一封装的 Axios 实例发出。这个封装做的事情包括设置baseURL统一为/api这样后端的 Controller 路径就不需要到处写绝对地址。请求拦截器从 localStorage 取出 Token加到Authorization头。响应拦截器统一处理后端返回如果code不是 0直接弹出 ElMessage 提示如果是 401跳转登录页。开发环境下的跨域配置用的是 Vite 的代理。在vite.config.js里写export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端开发服务器把/api开头的请求转发到后端8080端口浏览器看到的请求是同源的就不会有跨域报错。生产环境更简单——前端打包后的静态文件直接放在 SpringBoot 的resources/static目录下前后端同源部署跨域问题自动消失。注意如果没配代理直接在前端代码里写axios.get(http://localhost:8080/api/xxx)开发时也能跑但每次改后端地址都要动代码打包上线还得改成相对路径非常麻烦。规范的做法是一开始就用/api相对路径 代理。5. 环境配置与项目启动全流程5.1 版本选型JDK、SpringBoot、Node、MySQL 怎么搭这个阶段是照着项目跑起来的关键版本选错了后面全是泪。结合这个项目和常见的热搜问题我按实际经验给一套稳妥的版本组合组件推荐版本说明JDK1.8 或 11兼容性最好排错资料最多SpringBoot2.5.x - 2.7.x注意 SpringBoot 3.x 要求 JDK 17项目依赖如果用的还是老版本 MyBatis-Plus很容易类路径报错Node.js14.x - 18.xVue 3 Vite 2 在这个区间都很稳MySQL5.7 或 8.05.7 资历老更稳8.0 功能强但要额外注意时区问题这里特别强调一下 SpringBoot 版本的坑。如果你用的 SpringBoot 3.x很多第三方组件MyBatis-Plus 老版本、一些 starter会报ClassNotFound或者自动配置失效网上教程大多是旧版本的照着配各种对不上。实训项目别追新能跑通才是第一优先级。用 2.7.x JDK 8/11 是绝对不会出大问题的组合。MySQL 的安装配置也经常被人问。Windows 下装 8.0 时如果报Unable to load authentication plugin caching_sha2_password说明驱动版本太旧不支持 8.0 的新认证方式解决方案要么换 8.0 对应版本的 MySQL Connector/J要么在创建用户时指定mysql_native_password认证。这类问题属于环境层面的经典坑遇到不要慌一顿查改就完事。5.2 后端启动步骤与配置修改拿到项目源码后后端的启动步骤并不复杂按顺序来导入项目到 IDEA选择 Maven 方式导入等待依赖下载完成。在 MySQL 里创建数据库比如course_schedule_db设置字符集为utf8mb4。然后执行项目里自带的.sql脚本把表结构和初始数据导入。修改application.yml里的数据库连接配置spring: datasource: url: jdbc:mysql://localhost:3306/course_schedule_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/Shanghai这一项非常关键不加它 MySQL 8.0 连接会报时间差异常。运行启动类看到Started ... in 3.5 seconds之类的日志说明启动成功。常见的启动报错我在后面第 6 节统一整理。这里先提两个最容易遇到的问题一是8080端口被占用改application.yml里的server.port二是数据库密码或者 URL 写错控制台会直接报连接失败。5.3 前端启动与打包部署前端运行流程是熟悉的npm install然后npm run serve。但依赖安装这块有个非常普遍的问题npm install报错或安装慢。解决方案是换镜像源比如用npmmirror.com的镜像npm config set registry https://registry.npmmirror.comNode 版本太高也会出现依赖不兼容的问题。比如 Node 20 装 Vite 2 的项目可能会有警告甚至安装失败。遇到这种情况建议用nvmNode Version Manager切换到项目要求的 Node 版本这是踩过最多次的坑。前端开发测试没问题后进入打包阶段npm run build打包完成后dist目录下就是纯静态文件。把这个目录下的内容复制到 SpringBoot 项目的src/main/resources/static目录下重新打包 SpringBoot 应用一个前后端集成好的可执行 jar 就出来了。访问http://localhost:8080就是完整的系统这也回答了vue打包放进springboot中这类经常被搜的问题。注意如果前端路由用的是 history 模式直接访问/admin或/teacher这类深层路径时SpringBoot 会返回 404因为后端没有对应的 Controller。解决方案有两种要么把 Vue Router 改成 hash 模式URL 带#要么在 SpringBoot 里写一个转发规则把非/api开头的路径都转发到首页。实训项目图省事建议直接用 hash 模式。5.4 Excel 导入导出排课的加分项排课数据手工一条条录入一个管理员得录大半天。所以大部分可用的课表系统会带 Excel 导入导出功能这也是实训答辩时老师比较关注的完整度。Excel 处理的常用库是 Apache POI 或 EasyExcel。推荐 EasyExcel内存占用低API 简单对实训项目来说写代码的量少很多。导入的核心思路是前端上传文件到后端后端用 EasyExcel 监听器逐行读取每读一行就做一次业务校验教师是否存在、时间是否冲突、节次是否合法校验失败记录行号和原因最后汇总返回错误报告。// 伪代码 EasyExcel.read(file.getInputStream(), ScheduleExcelDTO.class, new ScheduleExcelListener(scheduleService)).sheet().doRead();这个功能介绍的人不多但在实际业务里非常关键。如果你做毕设加上这个功能至少能在答辩时多讲三分钟属于性价比很高的投入。6. 常见问题与排查技巧实录这一节我把实际运行这套项目时最常见的坑整理成速查表基本都是社区里被反复问的问题按问题–原因–解决的思路列清楚。问题现象原因分析解决办法后端启动报Access denied for user rootlocalhost数据库密码错误或账号无权限检查 application.yml 的账号密码用命令行mysql -u root -p测试同一个账号能否连接连接 MySQL 报时区异常没有配置 serverTimezoneURL 加serverTimezoneAsia/Shanghai或执行SET GLOBAL time_zone 08:00报Port 8080 was already in use8080 端口被占用换server.port或netstat -ano查占用进程后结束前端npm install卡住或报 ERESOLVE依赖源较慢或版本冲突切换 npmmirror 镜像源Node 版本过高时用 nvm 切到 16.x前端请求接口返回 404跨域代理没配或后端路径不对检查 vite.config.js 的 proxy确认后端 Controller 的 RequestMapping 是否是/api开头登录成功后刷新页面就退出Token 没有持久化只存在内存里把 Token 存到 localStorage 或 sessionStorage路由守卫从存储里读取部署后直接访问深层路由 404Vue history 模式与 SpringBoot 无对应路由改为 hash 模式或后端加一个非 API 路径转发到 index.html打包后界面正常但接口 404前端静态文件和 API 请求路径冲突确认后端接口都在/api下且请求不会匹配到静态资源运行报Failed to configure a DataSource数据库连接配置没生效确认 application.yml 里的数据源配置是否被正确扫描SpringBootApplication默认扫描当前包及子包除了表格里的这些还有几个实操经验值得单独说一说。第一个经验排查前后端联调问题时先打开浏览器开发者工具的 Network 面板看请求状态。如果请求根本没发出去就是前端问题如果请求发出了但响应 4xx/5xx就看后端日志。很多同学一上来就百度其实 90% 的问题看一眼请求和响应就能定位。联调阶段一定要学会看 Network 和 Console这个习惯能省下一大半 debug 时间。第二个经验数据库脚本导入时如果 SQL 文件里写死了数据库名先在 MySQL 里手动创建同名数据库再导入。另外字符集统一用utf8mb4不然遇到课程名称里的特殊字符比如《数据结构》的引号、生僻字会报错或乱码。第三个经验SpringBoot 版本太高导致的启动报错尽量不要硬解。我见过有人为了用最新版 SpringBoot 3.2把项目里的 MyBatis-Plus、Spring Security、Redis 缓存全部升级到新版本结果每个组件都有兼容问题调了一周。后来回退到 SpringBoot 2.7全部正常。实训项目的目标是把业务跑通不是做版本升级实验稳才是第一位。写在最后的一些个人体会课表管理系统这类项目最难的不是某个技术难点而是数据模型和业务边界是否想清楚了。我第一次做类似系统时把排课校验放在了前端后端只管插入结果管理员快速连点两次提交数据库里就出现了两条完全冲突的排课记录。后来把所有校验收拢到后端 Service 层并加上事务才算真正能用。如果你在做类似的项目我的建议是先把排课冲突检测 周次判断 课表展示这三个核心环节吃透这三个是整个课表系统的骨架。Excel 导入、调课审批、消息通知这些功能都属于加分项时间不够可以放到第二期再做。骨架稳了功能丰富度只是时间问题。另外一个小技巧数据库里可以给课程表预留一个color字段前端渲染时用不同颜色区分不同课程视觉上比纯文字清晰太多这个细节经常被实训老师当成亮点看待。源码本身能直接跑通但真正跑通之后建议你动手改一改。比如把固定的教室容量校验改成动态的或者把单双周规则改成可配置的校历模式每一次改动都会让你对这个系统的理解更深一层。把一个项目吃透比草草跑五个项目有用得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进