简介:这份压缩包面向学习数据库应用课程设计的学生与Web课程大作业开发者,围绕教室管理系统给出可参考的完整项目素材,帮助理解从需求分析、E-R建模到关系表设计、前后端联调与查询优化的实践路径。包内共39个文件,约1.94MB,以8个JSP页面、CSS与JavaScript静态资源、Java源码、SQL Server数据库文件及docx说明文档为主,可据此梳理页面结构、业务逻辑与数据交互关系。已有2114人学习,说明该案例在课程设计与入门级Web数据库项目中具有一定参考价值。读者可从中获得教室管理系统的目录组织、功能模块划分、数据库表与日志文件、前端样式脚本等线索,用于对照自己的课程设计任务,理解MVC分层、用户认证与SQL注入防范、并发访问控制及测试调试等关键知识点,适合作为综合大作业的搭建参考与排错思路来源。
1. 从一次教室冲突说起:这套数据库课程设计要交付什么
每学期排课那几天,教务办公室最怕的不是课多,而是教室撞车。某高校的数据库应用课程设计里,教室管理系统几乎每年都会被选到,题目听起来简单,真做起来却容易在时间冲突、借用审批和统计报表上翻车。这份 web大作业 资源要解决的不是“能不能增删改查”,而是把教室、课程、排课、借用四条数据主线理清楚,让管理员能排课、教师能申请、学生能查空闲教室,最后还能用一份建表脚本和一套接口把流程跑通。适合正在做数据库课程设计、Web 大作业,或者需要交一份完整教室管理系统方案的人。读到后面你会发现,重点不在页面多漂亮,而在数据库约束和事务边界有没有兜住。
2. 技术选型与总体架构:为什么用 MySQL 加 Spring Boot 做教室管理系统
这套教室管理系统用 MySQL 做数据底座,后端 Spring Boot,前端 Vue 加 Element Plus,持久层用 MyBatis-Plus。这个组合不是因为它“流行”,而是因为课程设计的时间通常被压缩在几周内,数据库设计、接口联调、页面演示三件事必须同时推进,框架层越轻,越能把精力留给表结构、约束和事务。选型一旦偏重,最后很容易出现页面能点、数据对不上、答辩一问就露怯的情况。
2.1 技术栈取舍:数据库课设别把重点丢在框架上
数据库应用课程设计的评分点通常集中在实体关系是否合理、表结构是否规范、约束和索引有没有用对、业务 SQL 能不能跑通。前端页面是展示层,后端框架是组织层,真正决定系统是否站得住的是数据模型。用 Spring Boot 的好处是控制器、服务层、数据访问层边界清楚,写事务和统一异常处理不费劲;用 MyBatis-Plus 的好处是单表增删改查省代码,复杂查询仍然能落到手写 SQL,不会把数据库课设做成“框架配置课”。
| 方案 | 优点 | 风险 | 适用场景 |
|---|---|---|---|
| Spring Boot + MyBatis-Plus + Vue | 分层清楚,接口调试方便,复杂 SQL 可控 | 需要理解事务和前后端分离 | 课程设计、Web 大作业、需要演示完整流程 |
| Servlet + JSP | 上手直接,页面和数据放一起 | 页面逻辑容易和业务逻辑混在一起 | 课时少、只要求单机演示 |
| 纯前端 + 本地存储 | 演示快 | 没有数据库约束,答辩容易被问住 | 只做界面原型,不适合数据库课设 |
我一般会建议把技术栈控制在“能跑通业务”的程度,不追新,也不为了显得复杂而堆中间件。配置上保持最简:一个数据源、一个 MyBatis 映射路径、一个文件上传目录就够。下面这段配置是后端启动前的核心部分,数据库名、用户名和密码按本机环境改。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/classroom_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueserver.port是后端服务端口,前端开发时请求代理到这里;spring.datasource.url里的classroom_db是数据库名,建库脚本和表脚本要对应;map-underscore-to-camel-case让数据库的room_no自动映射成 Java 的roomNo,少了大量手写映射。密码不要照抄,按本机环境改;如果启动时报驱动类找不到,先检查依赖有没有引入 MySQL 驱动,而不是急着改业务代码。
2.2 四层结构与角色边界:管理员、教师、学生各自能做什么
系统按表现层、控制层、服务层、数据访问层四层组织。表现层只负责展示和表单校验,控制层收参数、做权限入口,服务层写业务规则和事务,数据访问层落 SQL。教室管理系统的业务规则并不复杂,但角色边界要提前定清楚,否则后面会出现教师能删教室、学生能审批借用这类权限漏洞。
| 角色 | 主要操作 | 数据范围 | 不能做的事 |
|---|---|---|---|
| 管理员 | 教室维护、课程维护、排课、审批、统计 | 全部教室和排课数据 | 不直接修改审批记录 |
| 教师 | 查看课表、提交教室借用申请、查看审批结果 | 本人课程和本人申请 | 不能排课、不能审批 |
| 学生 | 查询空闲教室、查看班级课表 | 公开教室和本班课表 | 不能提交借用、不能改数据 |
角色边界落在接口上,不落在页面上。页面隐藏按钮只是体验,后端仍然要按角色拦截。比如/api/classroom/delete只允许管理员,/api/borrow/apply只允许教师,/api/classroom/free登录后都可用。这样做的好处是答辩时即使前端页面被绕过,接口层仍然能兜住。
2.3 工程目录与配置:压缩包里的模块怎么对应
这套资源的工程按“数据库脚本、后端服务、前端页面、说明文档”四块组织。后端按控制器、服务、实体、映射分层,前端按页面和接口请求分层,数据库脚本单独放,方便先建库再启动。对照目录时不要一上来改包名,先把建表脚本跑通,再启动后端,最后起前端,顺序反了会在跨域和登录态上耗时间。
| 目录/模块 | 职责 | 落地要点 |
|---|---|---|
| db | 建库、建表、初始化数据 | 先执行建库,再执行建表,最后插初始化数据 |
| controller | 接口入口 | 每个接口只做参数接收和权限注解 |
| service | 业务规则 | 排课冲突、审批状态流转放在这里 |
| mapper | SQL 映射 | 复杂查询写 XML,单表用 MyBatis-Plus |
| web | 前端页面 | 登录、教室列表、排课、借用、统计五个主页面 |
总体架构上,浏览器请求先到前端路由,再走接口请求模块,后端控制层按角色做第一道拦截,服务层做第二道业务校验,数据库用主键、外键、唯一索引和事务做最后一道兜底。三层校验听起来多,实际写起来并不复杂,关键是别把校验全压在页面上。教室管理系统的很多问题不是“不会写”,而是“只写了一半”。
3. 数据库设计:从 ER 图到 12 张核心表的落地细节
数据库应用课程设计的重头戏在表结构。教室管理系统看着只有“教室”和“课程”,实际要把教学楼、班级、教师、时间片、排课、借用申请、审批记录都串起来。表设计得粗,后面冲突检测和统计查询就会变成补丁堆补丁;表设计得过度复杂,课程设计周期又撑不住。我的做法是先定四条主线,再把每条主线落到表,最后补约束和索引。
3.1 实体与关系梳理:教室、课程、排课、借用四条主线
第一条主线是空间:教学楼和教室是一对多,教室属于某个教学楼,教室里可以有容量、类型、设备说明。第二条主线是教学组织:班级、教师、课程,课程关联授课教师,班级关联所属院系或专业方向。第三条主线是排课:一条排课记录把课程、班级、教师、教室、学期、周次、星期、节次绑在一起,它是冲突检测的核心。第四条主线是借用:教师提交借用申请,管理员审批,审批通过后占用教室的某个时间段。
这四条主线的关系可以这样理解:教室是资源,课程和班级是需求,排课是正式占用,借用是临时占用。正式占用和临时占用都要参与冲突检测,否则会出现“课表上没课,但教室被借走”的尴尬。实体关系不要求画得多漂亮,但主键、外键、一对多、多对多的落点要清楚。用户和角色是多对多,通过用户角色表拆开;课程和班级如果存在一个课程多个班上课,可以用排课表承载班级,不必再建一张复杂的多对多表。
12 张核心表可以这样分配:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 登录用户 | id、username、password、role_id、status |
| sys_role | 角色 | id、role_code、role_name |
| sys_user_role | 用户角色关联 | user_id、role_id |
| building | 教学楼 | id、building_name |
| classroom | 教室 | id、building_id、room_no、capacity、room_type、status |
| class_group | 班级 | id、class_name、major_name |
| teacher | 教师 | id、user_id、teacher_no、teacher_name |
| course | 课程 | id、course_code、course_name、teacher_id、credit |
| time_slot | 时间片 | id、section_no、start_time、end_time |
| schedule | 排课 | id、course_id、class_id、teacher_id、classroom_id、semester、week_start、week_end、day_of_week、start_section、end_section |
| borrow_apply | 借用申请 | id、classroom_id、applicant_id、semester、week_no、day_of_week、start_section、end_section、reason、status |
| approval_record | 审批记录 | id、apply_id、approver_id、result、comment、created_at |
这张清单不是让你照抄字段名,而是让你知道每张表为什么存在。time_slot用来统一节次时间,前端展示“第 1-2 节”时不用在排课表里重复存开始时间和结束时间;approval_record单独存审批流水,避免把审批意见塞进借用申请表导致一条申请只能留一次意见。
3.2 表结构与约束:主键、外键、唯一索引和状态字段
建表时最容易省掉的是外键和唯一索引。课程设计里数据量不大,省掉外键也能跑,但一旦删教室、删课程,排课表里就会留下孤儿记录,统计查询时数字对不上。我的建议是核心业务表加外键,状态字段用统一枚举值,能加唯一索引的地方不要只靠代码判断。
-- 教室表:同一教学楼内教室编号唯一 CREATE TABLE classroom ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL, room_no VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 0, room_type VARCHAR(20) NOT NULL DEFAULT '普通教室', status TINYINT NOT NULL DEFAULT 1 COMMENT '1可用 0停用', remark VARCHAR(200), CONSTRAINT uk_room UNIQUE (building_id, room_no), CONSTRAINT fk_classroom_building FOREIGN KEY (building_id) REFERENCES building(id) ) COMMENT '教室信息'; -- 排课表:正式占用教室的教学安排 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, class_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, classroom_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL, week_start INT NOT NULL, week_end INT NOT NULL, day_of_week TINYINT NOT NULL COMMENT '1-7', start_section INT NOT NULL, end_section INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_schedule_course FOREIGN KEY (course_id) REFERENCES course(id), CONSTRAINT fk_schedule_class FOREIGN KEY (class_id) REFERENCES class_group(id), CONSTRAINT fk_schedule_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id), CONSTRAINT fk_schedule_room FOREIGN KEY (classroom_id) REFERENCES classroom(id), INDEX idx_schedule_room_time (classroom_id, semester, day_of_week, week_start, week_end) ) COMMENT '排课记录'; -- 借用申请表:临时占用教室,审批通过后才生效 CREATE TABLE borrow_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, classroom_id BIGINT NOT NULL, applicant_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL, week_no INT NOT NULL, day_of_week TINYINT NOT NULL, start_section INT NOT NULL, end_section INT NOT NULL, reason VARCHAR(200) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/APPROVED/REJECTED/CANCELED', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_borrow_room FOREIGN KEY (classroom_id) REFERENCES classroom(id), CONSTRAINT fk_borrow_user FOREIGN KEY (applicant_id) REFERENCES sys_user(id), INDEX idx_borrow_room_time (classroom_id, semester, week_no, day_of_week) ) COMMENT '教室借用申请';字段说明上,status用TINYINT还是VARCHAR取决于业务。教室状态只有启用和停用,用TINYINT足够;借用申请有多个状态,用VARCHAR更直观,答辩时也容易讲清楚。week_start、week_end表示课程持续的教学周范围,day_of_week表示星期几,start_section、end_section表示节次区间。排课冲突检测不是判断“节次相等”,而是判断两个区间是否重叠,所以字段要成对存起止值。
注意:外键约束在课程设计里不是负担,而是防止脏数据的最后一道门。如果答辩环境不允许外键,至少要在服务层补删除前检查,并把检查写进说明文档。
3.3 冲突检测 SQL:同一时间同一教室只能有一节课
冲突检测的核心是“同一教室、同一学期、同一天、周次重叠、节次重叠”。这四个条件缺一个都会漏判。下面这条 SQL 查排课表里的正式冲突,参数依次是教室 ID、学期、星期几、新排课的周次起止、新排课的节次起止。
SELECT COUNT(1) FROM schedule WHERE classroom_id = #{classroomId} AND semester = #{semester} AND day_of_week = #{dayOfWeek} AND week_start <= #{weekEnd} AND week_end >= #{weekStart} AND start_section < #{endSection} AND end_section > #{startSection};week_start <= 新周次结束和week_end >= 新周次开始表示两个周次区间有交集;start_section < 新节次结束和end_section > 新节次开始表示两个节次区间有交集。注意这里用的是小于和大于,不是小于等于,因为“第 1-2 节”和“第 2-3 节”在第 2 节上是重叠的,但如果新排课结束节次等于已有开始节次,说明首尾相接不重叠。很多同学在这里把条件写成start_section <= endSection AND end_section >= startSection,结果相邻节次也判冲突,演示时明明没撞车却排不进去。
借用申请也要参与检测,但只查已通过的申请。SQL 结构类似,把表换成borrow_apply,周次字段换成week_no,状态条件加status = 'APPROVED'。正式排课和借用申请是两张表,冲突检测要在同一个事务里查两次:先查排课表,再查已通过的借用表。两次查询之间如果有并发审批,仍然可能插进来,所以还要在服务层锁教室行,下面第 4 章会写。
3.4 初始化数据与测试数据:让演示不冷场
初始化数据不是为了好看,是为了让答辩现场能立刻演示冲突。至少准备三个教学楼、十个教室、五门课程、三个班级、两个教师账号、一个管理员账号,再准备一条已排课记录和一条已通过的借用记录。测试数据里要故意留一个“同一教室同一时间”的用例,演示时先查空闲教室,再提交冲突排课,页面弹出冲突提示,这比空库点增删改查有说服力。
INSERT INTO building (building_name) VALUES ('第一教学楼'), ('第二教学楼'); INSERT INTO classroom (building_id, room_no, capacity, room_type, status) VALUES (1, 'A101', 60, '普通教室', 1), (1, 'A102', 40, '普通教室', 1), (2, 'B201', 120, '多媒体教室', 1); INSERT INTO schedule (course_id, class_id, teacher_id, classroom_id, semester, week_start, week_end, day_of_week, start_section, end_section) VALUES (1, 1, 1, 1, '2025-2026-1', 1, 16, 1, 1, 2);semester用统一的学期字符串,前端下拉框和统计查询都按这个值过滤;week_start和week_end用 1 到 16 周,演示时不用真的等到某一周。初始化数据要和建表脚本分开,先建表再插数据,否则外键顺序会报错。测试数据里不要把时间片写死成“1-2 节”,要留出“3-4 节”“5-6 节”的教室空闲时间,学生查空闲教室时才有结果。
4. 核心模块实现:排课、借用审批、查询统计三条链路
模块实现要围绕业务链路写,不要按“增删改查”平铺。排课链路的核心是冲突检测和事务,借用审批链路的核心是状态机,统计链路的核心是聚合查询不重复计数。三条链路都跑通,教室管理系统才算完整。权限和登录放在最后,是因为它贯穿所有接口,但不应该抢业务代码的篇幅。
4.1 排课模块:时间片建模与事务内冲突校验
排课接口收的参数包括课程、班级、教师、教室、学期、周次起止、星期、节次起止。服务层先锁教室行,再查正式排课冲突和已通过借用冲突,最后插入排课记录。锁教室行的目的是防止两个管理员同时给同一教室排课,一个查完没冲突,另一个也查完没冲突,最后两条都插进去。课程设计的数据量不大,但这种并发场景在答辩时很容易被追问。
@Transactional(rollbackFor = Exception.class) public void createSchedule(ScheduleDTO dto) { // 锁定教室行,避免并发排课同时通过冲突检测 classroomMapper.selectByIdForUpdate(dto.getClassroomId()); int scheduleConflict = scheduleMapper.countConflict( dto.getClassroomId(), dto.getSemester(), dto.getDayOfWeek(), dto.getWeekStart(), dto.getWeekEnd(), dto.getStartSection(), dto.getEndSection() ); if (scheduleConflict > 0) { throw new BizException("该教室在所选周次和节次已有课程"); } int borrowConflict = borrowMapper.countApprovedConflict( dto.getClassroomId(), dto.getSemester(), dto.getWeekStart(), dto.getWeekEnd(), dto.getDayOfWeek(), dto.getStartSection(), dto.getEndSection() ); if (borrowConflict > 0) { throw new BizException("该教室在所选时间已有通过的借用申请"); } Schedule entity = new Schedule(); entity.setCourseId(dto.getCourseId()); entity.setClassId(dto.getClassId()); entity.setTeacherId(dto.getTeacherId()); entity.setClassroomId(dto.getClassroomId()); entity.setSemester(dto.getSemester()); entity.setWeekStart(dto.getWeekStart()); entity.setWeekEnd(dto.getWeekEnd()); entity.setDayOfWeek(dto.getDayOfWeek()); entity.setStartSection(dto.getStartSection()); entity.setEndSection(dto.getEndSection()); scheduleMapper.insert(entity); }@Transactional(rollbackFor = Exception.class)保证冲突提示抛出后不会留下半条数据;selectByIdForUpdate对应的 SQL 是SELECT * FROM classroom WHERE id = ? FOR UPDATE,它锁住教室行,让同一教室的排课请求排队执行。countConflict的参数就是第 3 章那条 SQL 的参数顺序,weekStart、weekEnd传新排课的周次范围,startSection、endSection传新排课的节次范围。异常里的提示要写清楚是“已有课程”还是“已有借用”,答辩时演示能直接看出系统拦的是哪类冲突。
前端提交时不要只传教室编号,要把学期、周次、星期、节次一起传,否则后端无法判断冲突。下面这段请求代码是排课表单提交的核心,form里已经做了必填校验,提交后按后端返回的code决定提示。
// 排课提交:后端返回 code=0 表示成功,其余按 message 提示 async function submitSchedule(form) { const res = await request.post('/api/schedule', { courseId: form.courseId, classId: form.classId, teacherId: form.teacherId, classroomId: form.classroomId, semester: form.semester, weekStart: form.weekRange[0], weekEnd: form.weekRange[1], dayOfWeek: form.dayOfWeek, startSection: form.sectionRange[0], endSection: form.sectionRange[1] }) if (res.code !== 0) { ElMessage.error(res.message || '排课失败') return false } ElMessage.success('排课成功') return true }weekRange和sectionRange是前端区间选择器的值,拆成起止两个字段传给后端;semester从当前学期下拉框取,不要写死。返回code的约定要在接口文档里写清楚,避免前端把业务失败当网络错误处理。
4.2 借用审批模块:申请、审批、释放的状态机
借用申请有四个状态:待审批、已通过、已驳回、已取消。教师提交申请后是待审批;管理员审批通过后变已通过,同时占用教室时间;驳回后变已驳回,不占用;教师取消待审批的申请变已取消。审批通过时不能只改状态,还要再查一次冲突,因为申请提交后可能已经有排课插进来。
| 当前状态 | 操作 | 下一状态 | 校验 |
|---|---|---|---|
| 无 | 教师提交 | PENDING | 教室可用、时间合法 |
| PENDING | 管理员通过 | APPROVED | 再次查排课和已通过借用冲突 |
| PENDING | 管理员驳回 | REJECTED | 必须填驳回原因 |
| PENDING | 教师取消 | CANCELED | 只能取消本人申请 |
| APPROVED | 管理员撤销 | CANCELED | 记录审批流水 |
审批接口要在事务里做三件事:锁申请行、锁教室行、查冲突并更新状态。锁申请行防止同一申请被重复审批,锁教室行防止审批通过时和排课插入撞车。审批记录表单独插入一条流水,result存APPROVED或REJECTED,comment存意见。不要直接把审批意见更新到申请表的reason字段,否则教师提交的理由会被覆盖。
4.3 查询统计模块:教室利用率和空闲教室查询
统计查询最容易出两个问题:重复计数和笛卡尔积。教室利用率可以按“已占用节次 / 可排课总节次”算,已占用节次来自排课表和已通过借用表,总节次按教室数乘以周数乘以每天节次数。如果直接多表 join 再COUNT,一个教室有多条排课时会被放大,算出来的利用率可能超过 100%。
-- 按教室统计已占用节次,排课和已通过借用分两条子查询再汇总 SELECT room_id, SUM(used_sections) AS used_sections FROM ( SELECT classroom_id AS room_id, SUM(end_section - start_section + 1) * (week_end - week_start + 1) AS used_sections FROM schedule WHERE semester = #{semester} GROUP BY classroom_id UNION ALL SELECT classroom_id AS room_id, SUM(end_section - start_section + 1) AS used_sections FROM borrow_apply WHERE semester = #{semester} AND status = 'APPROVED' GROUP BY classroom_id ) t GROUP BY room_id;子查询先按教室聚合,再在外层汇总,避免排课和借用两条明细直接 join 造成乘法放大。空闲教室查询用NOT EXISTS更稳:给定学期、星期、节次区间,排除排课表和已通过借用表中时间重叠的教室。NOT EXISTS的语义是“只要存在一条冲突就不选这个教室”,比左连接后判空更直观,也不容易漏条件。
4.4 权限与登录:基于角色的接口拦截
登录成功后后端返回用户信息和角色码,前端按角色渲染菜单,后端用拦截器按接口路径和角色码做校验。管理员可以访问/api/classroom/**、/api/schedule/**、/api/approval/**;教师可以访问/api/borrow/apply、/api/schedule/my;学生可以访问/api/classroom/free、/api/schedule/class。拦截器里只做粗粒度控制,细粒度数据范围在服务层做,比如教师只能查自己的借用申请。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String uri = request.getRequestURI(); String role = (String) request.getSession().getAttribute("roleCode"); if (role == null) { response.setStatus(401); return false; } if (uri.startsWith("/api/approval") && !"ADMIN".equals(role)) { response.setStatus(403); return false; } if (uri.startsWith("/api/borrow/apply") && !"TEACHER".equals(role)) { response.setStatus(403); return false; } return true; }这段拦截器只演示路径级权限,roleCode从会话里取,实际项目里可以换成令牌解析。401表示未登录,403表示已登录但无权限,前端要分开处理:401 跳登录页,403 提示“无权限操作”。不要把权限判断只写在前端菜单里,否则答辩时换一个接口工具就能绕过。
5. 避坑与常见问题排查:五个最容易翻车的点
教室管理系统的代码量不算大,翻车点却很集中。下面五条是我在课程设计里见过最多的问题,每一条都按“现象、原因、解决”拆开写。答辩前照着过一遍,能省掉不少现场调试时间。
5.1 时间字段用字符串存,冲突检测直接失效
现象是排课页面能提交,冲突检测偶尔拦得住、偶尔拦不住,换一个教室就失灵。原因通常是把start_section、end_section存成了字符串,比较时按字典序走,“10”会排在“2”前面,区间重叠判断自然错。还有一种是把周次存成“1-16周”这样的文本,SQL 里无法直接比较起止。解决方式是把周次和节次拆成整型起止字段,前端展示时再拼成文本。建表时用INT或TINYINT,冲突 SQL 用数值比较,不要用LIKE或字符串截取。已经建了字符串字段的,加字段迁移数据,别在 SQL 里套CAST硬撑,索引会用不上。
5.2 外键缺失导致删教室后排课成孤儿记录
现象是管理员删了一个停用教室,课表页面还能查出这个教室的排课,统计利用率时教室名为空。原因是排课表没有外键,或者外键没开级联检查,删除接口只删了教室表。解决方式有两个:核心表加外键,删除前检查引用;或者服务层做删除前校验,存在排课和借用时拒绝删除,提示“先处理关联记录”。课程设计里我更推荐后者加外键双保险,外键保证数据库层不出现孤儿记录,服务层给出可读提示。删除教室不要做物理删除,改成status = 0停用,历史排课还能保留。
5.3 审批并发重复占用,事务边界没包住
现象是两个管理员同时点通过,两条审批都成功,同一教室同一时间出现两条已通过借用。原因是审批服务方法没加事务,或者冲突查询在事务外,查的时候都没冲突,更新的时候都成功。解决方式是把“锁申请行、锁教室行、查冲突、更新状态、写审批记录”放在同一个@Transactional方法里,冲突查询用当前连接,不要另起线程。申请行锁用SELECT ... FOR UPDATE,教室行锁同理。事务方法不要吞异常,捕获后要重新抛出,否则回滚不了。
5.4 统计查询笛卡尔积,利用率算出来大于 100%
现象是教室利用率页面出现 120%、300% 这种数字,换一个学期又正常。原因是排课表和借用表直接 join,再对节次求和,一个教室有多条排课时每条借用记录都会被重复计算。解决方式是先分表聚合再汇总,或者用UNION ALL把两类占用转成同一结构后再按教室分组。统计 SQL 写完拿三条排课、两条借用手工算一遍,和页面结果对得上再继续。别迷信DISTINCT,它只能去重行,去不了求和放大。
5.5 演示环境数据太干净,答辩现场查不出冲突
现象是答辩时演示排课,随便选一个时间都成功,老师问“冲突怎么拦”时现场造数据,结果手忙脚乱。原因是初始化数据只有一条排课,空闲教室太多。解决方式是准备一组固定演示数据:第一教学楼 A101 周一第 1-2 节已有课,A101 第 3-4 节已有一条通过的借用,B201 周五第 5-6 节空闲。演示顺序固定为“查空闲教室、排冲突课、看冲突提示、审批借用、再排课再次冲突”。数据脚本和演示脚本放在说明文档里,换机器也能复现。血泪经验是,答辩现场最怕的不是功能少,而是演示路径临时想。
6. 验收与答辩:用一份演示脚本把方案讲清楚
验收前先按四项清单自查:数据库能一键建库建表并插入初始化数据;后端能启动且接口文档里的核心接口都能调通;前端五个主页面能走通登录、教室、排课、借用、统计;说明文档里有 ER 说明、表结构说明、演示账号和演示步骤。四项里任何一项靠现场记忆,答辩都会打折扣。下面这张表可以直接当验收清单用。
| 验收项 | 检查内容 | 通过标准 |
|---|---|---|
| 数据库 | 建库、建表、初始化数据 | 一条命令跑完,无外键报错 |
| 接口 | 登录、排课、借用、审批、统计 | 冲突用例能返回明确提示 |
| 页面 | 五个主页面 | 不同角色菜单不同,越权接口返回 403 |
| 文档 | ER 说明、表结构、演示步骤 | 照着文档能复现演示 |
答辩演示不要从登录页开始平铺,先讲业务背景,再直接进排课冲突用例。第一步展示 A101 已有排课,第二步提交同一教室同一时间的新排课,页面弹出冲突提示;第三步展示已通过借用占用的时间段,再排课再次被拦;第四步进审批页面通过一条新申请,回排课页面验证冲突;第五步打开统计页面,说明利用率的分母和分子怎么算。整个流程控制在五到七分钟,剩下的时间留给追问。
常见追问一般集中在三处:冲突检测为什么用区间重叠而不是相等判断;审批通过时为什么要再查一次冲突;统计 SQL 怎么避免重复计数。回答时落到字段和事务上,不要背概念。比如冲突检测就说“周次区间和节次区间分别判断交集,四个条件同时满足才算冲突”;审批再说“申请提交后可能有新排课插进来,所以通过时要再校验一次”。如果老师问为什么不用数据库唯一索引直接拦冲突,可以回答“区间重叠不是等值唯一,唯一索引管不住跨周次和跨节次的交集,所以用事务加查询锁”。
从那以后我每次带课程设计都强制先跑一遍冲突用例,再让学生演示审批并发,最后才看页面。顺序反过来,很容易被一个空库演示蒙过去。这套资源的建表脚本、后端接口和前端页面按第 2 章的目录对应,先跑脚本,再照着第 6 章的演示顺序走一遍,课程设计的基本盘就稳了。希望帮到你。
本文还有配套的精品资源,点击获取