
1. 项目整体定位与设计思路这些年校园信息化系统一直有稳定的需求尤其课表管理这个场景几乎每所高校都要用。拿到这个“基于SpringBootVue的西安工商学院课表管理系统”项目时我第一反应是这活儿看着简单真做起来比想象中麻烦。排课逻辑、权限隔离、课表冲突检测、周次判断任何一个环节没考虑到位上线都会被师生骂。先说结论这套系统采用SpringBoot做后端、Vue做前端、MySQL存数据、MyBatis操作数据库本质是一个经典的前后端分离项目。它解决的核心问题是把教务处排课、教师调课、学生查课表这些流程线上化替代传统的Excel排课和纸质课表张贴。1.1 为什么选这套技术栈而不是SSM或JSP不少刚入门的朋友会问学校课表系统这种CRUD为主的项目用传统的JSPServlet不也能做确实能但维护成本完全是两个量级。SpringBoot最大的价值在于自动化配置它把SpringMVC、Tomcat、数据源等一堆东西集成好开发者只需要关注业务代码。对比传统SSM框架动辄要写一大堆XML配置SpringBoot的起步依赖和自动配置让整个项目变得清爽很多。前端为什么选Vue而不是JSP模板引擎或者Thymeleaf课表管理系统有个典型交互场景学生按班级、按教师、按教室查看课表页面需要频繁切换筛选条件并且局部刷新数据。用JSP每次都要整页刷新体验特别差。Vue的双向数据绑定和组件化开发配合Axios异步请求能让课表数据刷新变成秒级响应这在实际使用中感知非常明显。MySQL和MyBatis这对组合属于务实之选。MySQL对高校这种中小并发量的场景完全够用部署简单、运维成本低学生和教师几千人同时查询课表基本没有压力。MyBatis则让SQL控制回归程序员手里——课表查询往往涉及多表关联、动态条件拼接比如按教师编号查课、按班级查课、按教室查课SQL条件各不相同MyBatis的动态SQL正好能优雅地处理这个问题。1.2 系统角色与核心业务流程梳理这个课表管理系统设计了三类角色学生、教师、管理员。别小看角色划分它直接影响后续所有功能设计和数据库表结构。管理员的核心工作是排课和调度。具体来说管理员维护基础数据班级、教师、教室、课程然后把课程分配到固定的时间片和教室。排完课之后还要处理调课申请教师提交调课请求管理员审核通过后系统自动更新对应时间段的课表数据。教师角色的功能相对聚焦查询自己名下的课表查看所带班级的课程安排提交调课申请。学生角色最简单登录后看自己班级的课表也可以通过教师或教室维度做交叉查询。这个流程拆清楚之后数据库表结构就很好设计了。班级表、教师表、学生表、课程表是基础的静态数据课表表是核心动态数据还需要一张调课申请表来处理调课流程。角色和权限我建议直接用三条用户表加一个角色字段的方式没必要引入Spring Security那套重框架课表系统这种内部系统JWT登录加拦截器校验角色就够了。提示权限设计别过度设计。很多人在这种课表项目上非要上Spring Security OAuth2结果光配置就花了两天实际用到的功能不过是一个登录拦截和按钮级别权限。用JWT加自定义拦截器五十行代码就能搞定维护起来反而更轻松。2. 数据库设计的核心细节与建表实操数据库设计是整个课表系统的地基地基打歪了后面写SQL、写业务逻辑、做前端展示会处处别扭。我在这类校园系统上的经验是先花大力气把表和字段设计完整后面能省一半的调试时间。2.1 核心表结构与字段设计思路先看最基础的四张数据表。班级表比较简单字段包括班级编号、班级名称、所属学院、入学年份。教师表涉及教师编号、姓名、职称、所属学院、手机号。课程表需要注意的是一般要区分课程编码和课程名称课程编码作为业务主键负责在系统内部逻辑里被引用。学生表则关联班级表通过班级编号找到学生归属。最核心的是课表总表t_schedule。这张表的每条记录应当对应一个班级在某个时间段、某个教室上某门课程。它至少需要这些字段字段类型说明schedule_idint主键自增class_idint关联班级表course_idint关联课程表teacher_idint关联教师表classroom_idint关联教室表week_daytinyint星期几1-7start_sectiontinyint开始节次第几节课开始end_sectiontinyint结束节次week_starttinyint起始周week_endtinyint结束周week_typechar全周/单周/双周用1表示全周2表示单周3表示双周semestervarchar学期比如2025-2026-1节次这个字段很多第一次做课表系统的朋友容易忽略。高校一天通常有12-14节课安排在不同时间段比如上午第一二节是同一门课第三四节是另一门课。你不能只存一个“上课时间”得通过start_section和end_section来确定课在哪几个节次前端渲染的时候再把节次映射成具体时间展示。调课申请表t_course_change的字段设计也要提前想好包括申请编号、原课表ID、目标节次、目标教室、调课原因、申请状态、申请时间、审核时间。这里有个关键点调课申请不能只存修改后的结果必须保留原课表信息和目标调整信息这样才能做到管理员审核后精确更新而不是盲目覆盖数据。2.2 建表SQL实操与索引优化我直接贴一份核心建表SQL在MySQL 8.0上测试通过。需要注意表名和字段名都用反引号包起来避免和MySQL保留字冲突。CREATE TABLE t_schedule ( schedule_id int NOT NULL AUTO_INCREMENT COMMENT 排课ID, class_id int NOT NULL COMMENT 班级ID, course_id int NOT NULL COMMENT 课程ID, teacher_id int NOT NULL COMMENT 教师ID, classroom_id int NOT NULL COMMENT 教室ID, week_day tinyint NOT NULL COMMENT 星期几 1-7, start_section tinyint NOT NULL COMMENT 开始节次, end_section tinyint NOT NULL COMMENT 结束节次, week_start tinyint NOT NULL DEFAULT 1 COMMENT 起始周, week_end tinyint NOT NULL DEFAULT 18 COMMENT 结束周, week_type char(1) NOT NULL DEFAULT 1 COMMENT 1全周 2单周 3双周, semester varchar(20) NOT NULL COMMENT 学期, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (schedule_id), KEY idx_teacher_week (teacher_id, week_day, start_section), KEY idx_class_week (class_id, week_day, start_section), KEY idx_classroom_week (classroom_id, week_day, start_section) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课表信息表;索引设计是这张表的重中之重。课表系统查询频率最高的是三个维度按班级查课表、按教师查课表、按教室查课表因此一定要为这三个维度分别建立联合索引。这里有个容易被忽略的细节索引字段的顺序有讲究如果你经常用teacher_id week_day start_section查询索引就按这个顺序建。多条件查询时索引最左前缀规则决定了字段顺序必须匹配查询条件否则索引就失效了。另外课表冲突检测也需要依赖这些索引。管理员在排课的时候系统要检查同一个教师、同一个时间段是否已安排课程通过idx_teacher_week索引一条SQL就能立刻查出冲突记录性能完全没问题。2.3 课表排课的逻辑约束与冲突检测排课是整个系统业务复杂度最高的地方核心逻辑分三层第一层是教师冲突检测。一个教师在同一个时间段星期几节次只能上一个班的课。现实里还有单双周的情况同一时间段单周上A班的课、双周上B班的课是允许的所以冲突检测SQL需要把周次类型这个条件考虑进去。第二层是教室冲突检测。教室资源是有限的排课不能出现两个班同时占用一个教室的情况。这个检测直接比对classroom_id、week_day、start_section这三个字段如果存在复用记录系统直接提示教室冲突。第三层是班级冲突检测。同一个班级在同一时间段只能上一门课这个检测和教师检测逻辑类似但重点在class_id字段上。我实际写过的冲突检测方式是在Service层编写一个checkConflict方法接收待排课的课表对象在数据库里分别执行三类查询只要有一个查询返回了记录就抛出业务异常提示管理员调整排课。这种方法实现简单也符合课表系统的管理流程——排课本来就不是自动化智能排课而是管理员人工选择时间段后做合法性校验没必要引入复杂的算法。注意节次重叠问题一定要算清楚。比如一门课上1-2节另一门课上2-3节start_section和end_section构成区间不能只看start_section是否相等。我建议冲突判断条件写成原记录的start_section小于新记录的end_section且原记录的end_section大于新记录的start_section这样才能准确覆盖区间重叠的情况。3. 后端SpringBoot核心环节实现后端工程搭建本身不难难点在于把分层架构写清楚、把数据库操作封装得当、把接口设计得让前端好用。这里详细拆解我完成这个项目时编写的核心环节。3.1 项目结构规划与依赖配置项目使用Maven构建SpringBoot版本选2.7.x这个版本稳定且兼容性佳。太高版本比如3.x会有Jakarta命名空间变化对新手反而容易踩坑。核心依赖包括spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、lombok、jjwt用于JWT生成和校验。这里MyBatis选择mybatis-plus还是原生MyBatis取决于项目的CRUD复杂程度——课表系统的SQL关联查询比较多用原生MyBatis配XML更直观而且面试答辩时讲原生MyBatis能展示更多技术细节。工程包结构建议按照分层模式拆分让代码可维护性更好。具体是controller包接收前端请求并返回JSON数据service包处理业务逻辑和事务mapper包定义数据库操作方法接口entity包放置对应数据库表的实体类还有dto包放前端交互的数据传输对象。另外还需要配置类包和工具类包。我习惯把统一返回结构放在common包下。MyBatis的XML文件放在resources/mapper目录下application.yml里做对应路径配置。这里有个值得注意的经验实体类的属性命名和表字段命名要遵循驼峰和蛇形相互转换的规则。比如数据库的classroom_id对应实体类的classroomIdMyBatis配置mapUnderscoreToCamelCase为true后就能自动映射省去大量写resultMap的时间。记住一级缓存默认开启查询课表这种高频操作要合理利用缓存但涉及更新后的缓存刷新要处理干净否则很容易出现查出来是旧数据的情况。3.2 JWT登录认证与角色权限控制用户表t_user包含user_id、username、password、role、user_ref_id这几个字段其中user_ref_id用于关联具体的教师表或学生表ID这样登录后可以很自然地获取用户的业务信息。密码存储不用明文采用MD5加盐处理虽然强度不算最高但校园内部系统的场景下已经足够也便于答辩时讲解原理。登录接口的逻辑是接收username和password按用户名查出用户记录比对密码比对通过后生成JWT令牌返回前端。JWT里封装userId、username、role三个信息设置24小时有效期。前端把token存到localStorage每次请求时在Authorization请求头中携带。后端用一个拦截器统一拦截非登录接口的请求。拦截器里解析token校验签名和有效期然后将用户信息放入ThreadLocal或Request域中后续Service层和Controller层都能取到当前操作人的角色和ID。角色权限控制则很简单——每个接口标注需要的角色拦截器解析出用户角色后做判断不匹配就返回403。这个方案比Spring Security轻量得多。实际项目中我做权限校验的完整代码差不多100行包含了token生成、拦截器、注解定义、权限判断并且把登录状态校验和角色校验合在一起前端只需在后端返回401时跳转登录页403时提示无权限使用体验很干净。3.3 课表查询接口的多条件动态SQL课表系统的查询接口是整个系统里变化最丰富的接口前端可能需要按学期加班级查课表按教师名称查课程安排按星期几查空闲教室每个查询条件组合都不同。MyBatis的动态SQL此时就能发挥最大价值。XML里使用where标签配合多个if标签实现条件自动拼接而且MyBatis会智能处理where关键词和多余的and这样前端传过来的参数只会有传递了的那些被拼接进SQL中查询条件变化就能灵活适配。可以看一下按维度查询课表的核心XML片段select idselectScheduleList resultTypecom.example.entity.ScheduleVO SELECT s.schedule_id, s.week_day, s.start_section, s.end_section, s.week_start, s.week_end, s.week_type, c.course_name, c.course_code, t.teacher_name, cl.class_name, r.classroom_name FROM t_schedule s LEFT JOIN t_course c ON s.course_id c.course_id LEFT JOIN t_teacher t ON s.teacher_id t.teacher_id LEFT JOIN t_class cl ON s.class_id cl.class_id LEFT JOIN t_classroom r ON s.classroom_id r.classroom_id where if testclassId ! null AND s.class_id #{classId} /if if testteacherId ! null AND s.teacher_id #{teacherId} /if if testclassroomId ! null AND s.classroom_id #{classroomId} /if if testsemester ! null and semester ! AND s.semester #{semester} /if if testweekDay ! null AND s.week_day #{weekDay} /if /where ORDER BY s.week_day, s.start_section /select把课表信息连表查出来之后还需要做周次判断这一步要在Service层处理。由于存在单周、双周的区分前端在渲染完整学期的课表时如果只按真实排课记录来填格子就会出现有些周没课的情况。我采用的做法是查询出排课记录后在Service层根据weekType生成整个学期的周次明细列表返回给前端由前端做负责渲染这样前端逻辑简单后端又保证了数据完整性。这个接口是课表系统里最核心的查询也是学生和教师使用最频繁的接口。我在测试中发现一个值得注意的细节LEFT JOIN连接了五张表数据量在一万条以内时响应很快但如果学期数据积累到几个学期再叠加未来排课不加索引的全表扫描就会变得明显变慢。所以在测试阶段就要用EXPLAIN查看执行计划确认走了联合索引比如idx_class_week这种索引就能有效提速。3.4 调课审核的事务处理调课功能始终是课表系统里用户最关心的功能之一但实现细节很容易被忽略。教师提交调课申请后系统只是保存一条申请记录还不动课表主表。管理员审批通过时才会真正修改t_schedule里的数据。这个流程一定要用事务来保证数据一致性。管理员点击通过的操作包含两步更新原课表记录的时间段或教室同时更新调课申请的状态为已通过。如果第二步失败而第一步成功数据库里就会出现课表改了但申请状态还是待审核的窘况。通过Transactional注解让这个方法整体处于事务管理范围内任何一步抛出RuntimeExceptionJDBC就会自动回滚整个事务保证要么全部成功、要么全部失败。Service层的实现大概如此Transactional(rollbackFor Exception.class) public void approveChange(Integer changeId) { CourseChange change courseChangeMapper.findById(changeId); if (change null || !0.equals(change.getStatus())) { throw new BusinessException(申请不存在或已被处理); } // 先更新课表主表 int updated scheduleMapper.updateScheduleByChange(change); if (updated 0) { throw new BusinessException(原课表信息不存在或已变更); } // 再更新申请状态 courseChangeMapper.updateStatus(changeId, 1); }值得注意的是原课表行锁并发问题两个管理员同时审批同一个申请必须保证只有一个人成功。这里一个简单的update影响行数判断就能起到乐观锁的效果即updateScheduleByChange方法在更新时带上原课表ID和当前状态作为条件如果影响行数异常就会触发回滚。不过这个项目的使用场景下管理员审批并发概率不大所以上述方案已经足够稳定。4. Vue前端核心页面与交互实现前端用Vue 2.6配Element UI来搭建这个组合在国内校园系统里用得非常多组件齐全、文档丰富。Vue 3虽然已经成熟但考虑到Element Plus兼容性和旧项目维护Vue 2在这个技术栈下反而更稳妥。核心页面包括登录页、管理员排课管理页、教师课表页、学生课表页。4.1 前端路由设计与Axios请求封装前端路由需要用Vue Router配合动态路由配置来做权限隔离根据不同角色展示不同的菜单和页面。这里有一个开发效率上的关键点路由配置中通过meta字段标记该路由可访问的角色列表在路由守卫里判断当前用户角色逐层匹配。这种方式比后端返回菜单再动态生成路由要简单直接适合课表系统这种功能固定的内部系统。Axios请求封装可以用统一的拦截器机制处理三类逻辑为每个请求自动添加Authorization头携带JWT token响应拦截器识别HTTP状态码401自动跳转登录页统一处理业务错误码弹信息提示。封装完成后页面组件里的请求代码就变得非常简洁业务逻辑更清晰。4.2 课表网格渲染的实现方案课表展示是整个前端最核心的交互。用户期待看到的是传统课表的样式——横轴是星期一到星期日纵轴是节次每个单元格展示课程信息。我用Element UI的el-table来实现这个网格布局。**横轴方向设置七个固定的列代表周一到周日纵轴方向根据一天最大节次比如12节生成对应的行数据。**每行的数据源是一个包含七格的对象。后端返回的课表记录需要在前端转换成这种按节次归类的结构核心转换思路是遍历排课记录根据week_day和start_section定位到对应的行列位置再把课程名称、任课教师、教室、起止周等字段填入gridData。buildGridData(scheduleList) { const maxSection 12 const grid [] for (let i 1; i maxSection; i) { const row { section: i, day1: null, day2: null, day3: null, day4: null, day5: null, day6: null, day7: null } scheduleList.forEach(item { if (item.startSection i item.endSection i) { row[day item.weekDay] item } }) grid.push(row) } return grid }这种方法最直观但有一个需要控制好的情形同一门课跨多个节次比如1-2节连上代码逻辑会同时往第1行和第2行的对应位置填入相同的课程信息视觉上就会显得重复。比较理想的处理是让跨节次的课程只显示在起始节次上并用rowspan做单元格合并。但el-table的单元格合并逻辑需要计算span-method写起来比较绕。我最终采用了折中方案跨节次课程在起始节次的位置渲染完整信息同时在该行的其他位置留空白。这个方法实现复杂度低信息也能完整传达。如果你对视觉有更高要求可以把span-method计算为课程持续的节次数做一个类似合并单元格的效果。需要注意的是前端转换的时候如果操作的是数组对象必须在数据更新后重新创建新数组触发Vue视图刷新否则会出现课表切换班级后页面数据不变这种看起来像“卡死”的问题。4.3 管理员排课表单与冲突提示交互管理员的排课页面采用对话框加表单的方式表单包含班级选择、课程选择、教师选择、教室选择、星期选择、节次范围、起止周、单双周类型。这些选择器都需要异步加载数据Element UI的远程搜索功能可以用上输入关键字后请求后端接口返回匹配的班级列表、教师列表等。提交排课信息的流程是前端将表单数据组装成JSON对象通过POST请求提交到后端后端先执行冲突检测发现冲突后抛出业务异常消息给前端提示否则保存成功。冲突提示需要显眼不能让管理员毫无感知地认为排课已经成功。我在代码里执行了这样的处理逻辑提交成功后Message显示成功提示如果后端返回冲突错误则把具体冲突原因全局展示提示管理员调整。教室选择器建议只展示当前时间段空闲的教室虽然做不到实时的强约束但能在排课前就避免一大半冲突。我为此专门写了一个空闲教室查询接口传入星期、节次范围、起止周后排除掉该时间段已被占用的教室剩下的返回前端供选择。这个功能虽然只花了一下午开发但上线后管理员好评度非常高很大程度上减少了排课操作的沟通成本。5. SpringBoot与Vue联调部署要点前后端分离项目的联调阶段容易出现一堆环境导致的问题。这些常见问题如果不提前处理新手很容易在这个阶段卡好几天。5.1 跨域问题与开发环境代理配置前端在8080端口启动Vue的开发服务器后端在8080端口运行SpringBoot这之间会产生跨域问题。后端最容易的方案是在SpringBoot配置类里加一个CorsFilter过滤器允许前端开发地址跨域访问同时要处理预检请求OPTIONS。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意一个细节如果将allowCredentials设置为true前端携带Cookie跨域时有严格要求但这里我们只用token做认证所以允许范围可以放开。如果后端配置不生效可以检查过滤器链路中是否有其他拦截器提前返回了响应。还有一个情况也值得注意如果Nginx拦截了请求部分网关配置会直接丢弃OPTIONS请求导致CORS预检阶段就失败这个层面要在部署时排查。5.2 前端打包放入SpringBoot的两种方式项目最终交付有两种常见的部署方式。第一种是纯前后端分离部署后端SpringBoot跑8080端口前端打包后的dist目录丢到Nginx里配置反向代理将/api前缀转发到后端。这种方式适合服务器资源充足的场景Nginx还能顺手做负载均衡和HTTPS证书配置。第二种是“前端融入后端”把Vue打包后的静态资源直接放到SpringBoot的src/main/resources/static目录下并额外配置路由支持history模式。这样做出来的产品就是一个可执行的Jar包直接部署到装有Java环境的服务器上就能访问对校园内部运维人员非常友好。如果使用history模式路由后端还需要配置一个转发规则除了真实接口路径之外所有非接口的请求都转发到index.html否则用户刷新某个前端页面时会出现404。Controller public class PageController { GetMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }要注意接口路径与页面路径的区分比如/api开头的请求不能走这个转发或者让页面请求路径和接口路径在规则上就能区分开否则会干扰后端API的访问。5.3 生产环境MySQL连接参数优化生产环境部署时MySQL的连接配置不能再使用开发阶段的默认参数。我遇到过一些生产事故都是由连接超时导致的比如MySQL默认的wait_timeout值是8小时长时间没有请求的数据库连接会被服务端断开而应用连接池中如果还持着旧连接使用时就会抛出连接异常能排查半天。在application-prod.yml里我通常会设置以下连接参数spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: xxxx hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000timezone参数值得单独提一下。MySQL 8.0之后连接URL如果不指定serverTimezone在非中国时区的服务器上部署时时间字段的读写会出现8小时的偏移。这个坑很大但也好排查一般看到数据库里的create_time和当前系统时间不一致基本就是这个原因。处理完之后在JDBC连接参数中固定配置好时区可以有效规避这个问题。6. 开发过程中的典型问题与排查经验这部分整理一下在开发这两个项目环节中最常见的问题以及对应的处理思路和排查路径这些经验是正规文档里通常不会写明但实际开发最需要的。6.1 MyBatis常见问题速查**问题一查询结果字段全是null。**多数情况是实体类属性名与表字段映射不上。先检查application.yml里是否配置了map-underscore-to-camel-case: true如果实体属性是studentId、表字段是student_id配置得当就能自动映射。如果开启了还是null重点检查SQL别名的写法看返回的类型是否指定成了resultType而不是resultMap。**问题二动态SQL语句一直报语法错误。**排查方式是先开启MyBatis的SQL日志输出在application.yml里设置mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台就会打印最终执行的SQL。把SQL复制到数据库工具里单独执行比对问题这样通常能很快定位。**问题三循环依赖或Mapper注入为空。**Service层注入Mapper时使用了Autowired但启动报找不到Bean可以检查Mapper接口上有没有加Mapper注解或者在启动类上有没有配置MapperScan。这个检查操作值得做完再启动否则排查起来非常耗时。6.2 Vue与后端联调典型问题**问题一请求能到后端但前端Control台报跨域。**后端CORS配置如果已经生效看前端请求路径是否携带了完整域名和端口。特别要注意前端axios请求的baseURL指的是完整的后端访问地址比如http://localhost:8080/api如果你的Vue代理配置和后端CORS配置叠在一起用各自的规则要配合好否则会造成双重处理反而失效。**问题二课表刷新后视图不变。**出现这个场景时通常是在buildGridData方法中直接修改了原数组对象。Vue 2的响应式系统对数组索引的赋值操作感知较弱用this.$set(grid[rowIndex], dayweekDay, item)方式赋值或者处理好整个数组的重新赋值视图就能正常更新。**问题三打包之后首屏白屏。**这个现象通常是由静态资源路径的问题引起。因为前后端融合部署时资源路径需要为相对路径处理在vue.config.js中设置publicPath: ./打包后的index.html里引用的js和css路径就会从绝对的/js变为相对的./js适配jar包部署的情况。6.3 排课冲突检测漏掉单双周的坑这个问题是我在这类项目中踩过比较典型的坑必须单独拿出来说。最早版本的冲突检测SQL只比对teacher_id、week_day、start_section、end_section完全忽略week_type和起止周的存在。当时测试发现教师甲周一上午1-2节在1-18周全周上数据库结构导论同时期又被排了同一时段的单周课程冲突检测居然通过了课表数据直接乱掉。修正后的检测逻辑是两个排课记录冲突需要满足三个条件同时成立。星期几相同、节次区间重叠、周次有交集。周次有交集这个条件需要判断全周与全周看起止周范围是否重叠全周与单周看单周所在的奇偶周是否在起止范围内单周和双周如果起止范围有重叠且奇偶相同同样有交集。这个判断写起来比想象中琐碎我把它提炼成了一个公共方法用循环遍历待排课程涉及的每一个实际周次和已有课程的实际周次做集合交集判断。尽管方法实现不算精细但正确性有保障。这个案例说明了一个问题课表系统的业务语义比CRUD复杂得多单纯堆数据库查询和接口调用不出问题但业务规则理不清就会出麻烦。7. 已经实现的功能之外还能再补充什么最后分享几个开发这套课表系统过程中的额外思考这些点我在跟别人讨论时也经常聊到。如果你已经把这套课表管理系统写完了可以试着给它增加一个批量导入导出功能。学校每学期的排课数据量很大在界面上一条条新增显然不现实更好的方案是提供Excel模板管理员按模板填写好表格后批量导入系统逐行校验冲突关系有问题的那行单独标记出来。导出则是一键把当前学期的课表导出为Excel格式学生打印出来贴在书桌前这类辅助功能虽然不起眼但比重做UI调整更能提升真实使用体验。从技术角度你可以在这个系统的基础上学习Redis缓存。课表查询是典型的读多写少场景课表数据一旦排好一学期内基本不变化但学生老师每天都在查。如果在课表查询接口上缓存一层Redis数据设置key的有效期当调课审核通过后主动删除或更新缓存查询性能会有明显提升。这个思路也能让系统技术含量高一块是一个值得花时间补充的扩展点。我在开发这类校园系统时始终保留一个体会技术栈选得再流行最终还是要落到实际使用者的场景里。你在决定前后端方案时多考虑老师怎么排课、学生怎么查课、管理员怎么维护数据在这些具体环节上多花功夫系统做出来才真正有人愿意用。这套课表系统我做了两轮迭代第一轮基本按教科书模板走技术是顺的但使用反馈一般第二轮加入空闲教室查询、调课审批完整流程、批量导入导出这些贴近真实排课场景的细节之后整体的好评分才真正上去。做项目尤其是校园类信息系统用心贴近使用场景永远比单纯花哨的技术重要一些。