
简介实验室排课系统是一套基于JSP技术开发的完整课程设计与毕业设计项目面向计算机专业学生和需要Web项目实战练习的学习者用于解决实验室资源排课与课程安排的管理问题。压缩包22.18MB共1062个文件核心包含java与jsp源码、js/css前端资源、png/jpg/gif等界面素材、sql数据文档以及论文、开发文档等说明材料。项目附有论文、开发文档、数据文档和源码四类内容从需求分析、系统设计到编码实现均有覆盖源码经过本地编译与严格调试可运行性有保障。已有27人学习下载。通过阅读和分析本项目可以理解JSP与Java代码的配合方式掌握数据库存取、分页、表单处理等动态网页开发要点也可为毕业设计或期末大作业提供可直接参考的完整方案。1. 实验室排课系统 JSP课设/毕设里最容易被低估的完整闭环拿到“实验室排课系统jsp.zip”这个包第一反应别急着觉得 JSP 技术栈老旧它恰恰是期末大作业最稳妥的选题。一个能跑通的排课系统意味着你同时交代了 JSP 指令、Servlet 请求转发、JDBC 数据库操作、Session 会话管理和权限拦截这五项硬指标。很多同学花大量时间纠结要不要换成 SSM 或者 Spring Boot反而把核心的排课逻辑草草写完答辩时被问两句就露馅。这套资源的价值在于它把课设必须展示的完整闭环——登录、排课申请、冲突检测、课表渲染——用最朴素的技术栈串起来拿来改一改就能当毕业设计交也适合临近期末需要快速落地项目的开发者参考架构。真正难啃的其实不是算法而是环境配置和参数细节下文按一个排课系统从搭建到排课落地的顺序拆开讲。2. 系统骨架ServletDAO 三层划分与登录拦截的完整链路2.1 为什么选 JSPServlet 而不是一套框架打天下课设和毕业设计评审最关注的不是框架多新而是“这个请求是怎么从浏览器走到数据库再走回来的”。JSPServlet 方案把这条链路完全暴露给你反而更容易讲清楚。常见的做法是三层结构JSP 页面负责展示Servlet 负责接收请求和控制跳转DAO 类负责 JDBC 数据访问。实体 Bean 只做属性传递不在 JSP 里直接写 SQL。这样分的好处是后面如果你想把持久层换成 MyBatis只需要替换 DAO 实现Servlet 和页面基本不动。我一般会在 Eclipse 或 IDEA 里先建一个 Dynamic Web Project包结构大致如下com.lab.entity放 User、Lab、Schedule 等实体com.lab.servlet放 LoginServlet、ScheduleServletcom.lab.dao放用户查询、排课查询和写入com.lab.filter放登录拦截过滤器。WebContent 下再按角色拆目录避免老师和管理员页面混在一起。《实验室排课系统jsp.zip》里的目录结构也符合这个套路拿到手先扫一眼包名就能确认分层是否完整。2.2 登录与会话管理的最小实现排课系统必须区分教师、管理员两种身份。最简单的做法是登录成功后把用户对象塞进 Session再写一个 Filter 拦截未登录请求。下面这段是 LoginServlet 里最关键的逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, user); session.setMaxInactiveInterval(30 * 60); if (admin.equals(user.getRole())) { response.sendRedirect(admin/index.jsp); } else { response.sendRedirect(teacher/index.jsp); } } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }这段代码有两个细节值得注意。setMaxInactiveInterval(30 * 60)是把 Session 超时时间设成 30 分钟避免学生挂在浏览器上半天不掉线影响下一个人登录。sendRedirect和forward的选择也有讲究登录成功后用重定向防止刷新页面时重复提交表单登录失败用转发把 error 信息带回登录页显示。如果发现排课页面刷新一次就重复插入一条记录多半是登录成功这段写了 forward 而不是 redirect。2.3 数据库表从用户表到排课记录的最小设计排课系统的表不用设计得很复杂四张表足够覆盖课设要求用户表、实验室表、排课记录表、学期周次表。下面是核心字段设计表名字段类型说明t_userid, username, password, roleint, varchar, varchar, varcharrole 取值 teacher / admint_labid, lab_name, capacity, locationint, varchar, int, varchar实验室名称、容量、位置t_termid, term_name, start_date, total_weeksint, varchar, date, int学期名称、起始日期、总周数t_scheduleid, course_name, teacher_id, lab_id, week_day, time_slot, weeks, statusint, varchar, int, int, varchar, varchar, varchar, int排课主表排课表里的time_slot建议直接存字符串比如1-2表示第一二节课3-4表示第三四节课比存数字编号更直观。weeks字段存类似1-16或者1,3,5,7这样的周次描述方便后面做跨周排课。这里最坑的是很多同学会把 weeks 设计成 int导致单周、双周需求一进来就要改表结构。如果这个包里的t_schedule已经用了 varchar说明作者把这一点考虑进去了是加分项。DAO 层连接数据库常用的做法是写一个 JdbcUtil 工具类静态代码块加载驱动提供一个 getConnection 方法。Java 8 以下用Class.forName(com.mysql.jdbc.Driver)MySQL 8 以上的驱动类名要改成com.mysql.cj.jdbc.Driver同时连接串里必须加useSSLfalse和serverTimezoneAsia/Shanghai否则会报时区错误。这个问题会在避坑章节重点展开。3. 排课核心逻辑冲突检测、数据库设计边界与参数调优3.1 冲突检测的核心抽象同一教室同一时段不能被占用两次排课系统的主流程是教师选择实验室、选择周次和节次系统检查这个实验室在目标时段是否已被占用没被占用就写入排课记录被占用了则提示冲突。冲突检测可以抽象成一句 SQL也可以拆成 Java 判断再加 SQL 落库。我建议用后者因为纯 SQL 写法参数一多容易出逻辑漏洞而且答辩时老师会问“你怎么判断冲突的”你如果能讲清楚两层校验比丢出一条 SQL 更显功底。先从排课记录里查出目标实验室在目标节次的所有记录再在 Java 里和周次字符串做交集判断。这是第一层校验。第二层是写入前给排课表加唯一索引数据库兜底防止两个请求同时通过第一层校验导致数据错乱。3.2 DAO 层查询与周次交集判断的落地实现按“实验室 星期 节次”查已有排课的方法如下public ListSchedule findConflictSchedules(int labId, String weekDay, String timeSlot) { ListSchedule list new ArrayList(); String sql SELECT * FROM t_schedule WHERE lab_id ? AND week_day ? AND time_slot ? AND status 1; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, labId); ps.setString(2, weekDay); ps.setString(3, timeSlot); ResultSet rs ps.executeQuery(); while (rs.next()) { Schedule s new Schedule(); s.setId(rs.getInt(id)); s.setWeeks(rs.getString(weeks)); s.setCourseName(rs.getString(course_name)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; }这里用status 1过滤已删除记录是软删除的做法。之后在 Servlet 里对查出来的结果做周次交集判断public static boolean isWeekRangeOverlap(String existingWeeks, String newWeeks) { SetInteger existingSet parseWeeksToSet(existingWeeks); SetInteger newSet parseWeeksToSet(newWeeks); existingSet.retainAll(newSet); return !existingSet.isEmpty(); } private static SetInteger parseWeeksToSet(String weeks) { SetInteger set new HashSet(); // 支持 1-16 和 1,3,5,7 两种写法 if (weeks.contains(-)) { String[] parts weeks.split(-); int start Integer.parseInt(parts[0].trim()); int end Integer.parseInt(parts[1].trim()); for (int i start; i end; i) { set.add(i); } } else { String[] weekArr weeks.split(,); for (String w : weekArr) { set.add(Integer.parseInt(w.trim())); } } return set; }parseWeeksToSet这个方法的输入格式很关键。如果前端表单提交的是周次多选组件可能提交成1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16这时contains(-)判断走 else 分支逐周拆分。如果提交的是1-16则走区间展开。两种格式都能处理比只支持一种格式健壮得多。retainAll是 Java 集合提供的交集操作调用后existingSet只剩下与newSet重叠的周次非空就说明撞车。3.3 数据库唯一索引的边界与双保险策略Java 层校验只能防止正常操作路径下的冲突防不住并发。两个教师同时提交同一实验室同一时段两个请求都通过了查询都认为没有冲突然后先后插入结果就是数据表里出现两条重复排课。解决方式是建唯一索引ALTER TABLE t_schedule ADD UNIQUE INDEX uk_lab_time (lab_id, week_day, time_slot, weeks);这个索引有一个明显边界weeks是字符串1-16和1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16表达的是同一组周次但字符串不同唯一索引判断不出来依然会插入重复数据。所以唯一索引适合作为硬兜底但不能完全替代周次交集判断。我在这类项目里的习惯是第一层 Java 做严格交集判断第二层索引做同字符串防重复第三层在插入前用SELECT ... FOR UPDATE锁住目标实验室记录。如果课设没有并发演示需求前两层足够第三层讲了可以加分。SELECT ... FOR UPDATE要把查询和插入放在同一个事务里才能生效否则锁自动释放等于白写。4. 课表可视化JSP 表格渲染与数据拼装思路4.1 把离散排课记录转成二维表格数据排课表里存的是一条一条独立的记录星期几、第几节、哪个教室、哪门课。展示给用户看的时候要转成传统课表的样子行是节次列是星期一到星期五表格里填课程名称和实验室名。这里的关键是先在 Java 端构造一个二维数据结构而不是把记录丢给 JSP 让页面去拼命 for 循环拼接。常见的做法是用MapString, ListSchedulekey 是周一_1-2这样的组合value 是该时段的所有排课记录。如果同一个时段有多条不同教室的排课value 就是一个 List页面上可以纵向罗列。另一种做法是直接构建Schedule[5][6]这样的二维数组第一维是星期几第二维是节次数组元素可能包含多门课。MapString, ListSchedule scheduleMap new HashMap(); ListSchedule allSchedules scheduleDao.findAllWithTeacherName(); for (Schedule s : allSchedules) { String key s.getWeekDay() _ s.getTimeSlot(); if (!scheduleMap.containsKey(key)) { scheduleMap.put(key, new ArrayList()); } scheduleMap.get(key).add(s); } request.setAttribute(scheduleMap, scheduleMap);findAllWithTeacherName需要连表查教师姓名SQL 大概长这样SELECT s.id, s.course_name, s.week_day, s.time_slot, s.weeks, s.lab_id, l.lab_name, u.username AS teacher_name FROM t_schedule s JOIN t_lab l ON s.lab_id l.id JOIN t_user u ON s.teacher_id u.id WHERE s.status 1注意 select 里直接把teacher_name作为别名返回省掉后续再查一次用户的麻烦。在 DAO 里把这个结果封装成 Schedule 对象额外加一个teacherName属性这个属性不属于数据库表字段只是为了展示。实际项目里遇到这种“实体类字段和表字段不一致”的情况不用慌张Java 实体类里的属性可以比表字段多只要数据库映射时不强制对应就行。4.2 JSP 页面按行遍历输出课表拿到scheduleMap之后JSP 页面里做一个双层循环即可。外层循环节次内层循环星期几然后去 map 里拿 key 对应的排课列表遍历输出。table border1 cellpadding6 cellspacing0 styleborder-collapse: collapse; tr th节次/th th周一/th th周二/th th周三/th th周四/th th周五/th /tr c:forEach varslot items${timeSlots} tr td${slot.name}/td c:forEach varday items${weekDays} td c:forEach varcourse items${scheduleMap[day.name _ slot.code]} div${course.courseName}/div div${course.labName} - ${course.teacherName}/div /c:forEach /td /c:forEach /tr /c:forEach /table${scheduleMap[day.name _ slot.code]}这种写法要求scheduleMap的 key 和页面拼接规则完全一致。这个点是最容易翻车的页面写的是周一Java 端 key 拼的是星期一结果表格全是空白。建议 Java 端和 JSP 里统一用1、2这种数字代码做 key显示名称单独再建一个 Map 或数组来对应这样不会出现中文对齐问题。timeSlots和weekDays可以从 Servlet 里通过request.setAttribute传过去也可以在 JSP 里用 JSTL 的c:forEach配合数组定义。课程设计里不强制用 EL 和 JSTL但如果页面里全是% %脚本片段答辩时印象分会打折改成 JSTL 标签后页面干净很多也更好维护。4.3 软删除与状态字段的隐藏作用排课模块最容易忽略的是撤销功能。教师排错课了要么删除记录要么修改周次。如果做物理删除DELETE FROM t_schedule WHERE id ?数据就没了后续想统计“本周实际排课数”或者做历史追溯就无从下手。我做这类系统时会在排课表里留一个status字段1表示有效0表示撤销或删除。查询时默认只查status 1的记录撤销时执行UPDATE t_schedule SET status 0 WHERE id ?。这个细节在原资源里如果已经实现那说明代码设计得比较完整如果没有实现建议拿到包后优先补上原因很简单演示的时候点一次撤销按钮记录还在表里但不再出现在课表上这个“软删除”概念可以直接写进论文的创新点小节。5. 排课系统避坑部署、编码、驱动的三个高频崩溃点5.1 现象Tomcat 端口被占用导致启动失败课设演示前几分钟启动 Tomcat控制台直接报Port 8080 was already in use页面完全打不开。这几乎每个学期都能见到原因是之前启动的 Tomcat 没有正常关闭或者电脑上装了其他 Web 服务占了 8080。解决方式有两个。一是找到占用端口的进程并结束netstat -ano | findstr :8080 taskkill /F /PID 进程号二是改 Tomcat 端口。到conf/server.xml里找到Connector port8080把 8080 改成 8081 或 9090。改端口要注意同时改访问路径因为浏览器访问地址要从http://localhost:8080/项目名变成http://localhost:9090/项目名。如果项目里配置了外部链接比如某些 JSP 里写死了跳转地址这些硬编码也要同步修改。5.2 现象控制台和页面中文全部变成乱码页面中文乱码最常见的现象是数据库里存的是中文页面显示成????或者实验室。这个问题的根源是两端编码不一致。首先确认 JSP 页面头部有pageEncodingUTF-8其次确认响应设置了编码。在 Servlet 里要在写任何输出之前调用response.setContentType(text/html;charsetUTF-8);然后在 JSP 的表单提交那边如果用的是doPost必须加上request.setCharacterEncoding(UTF-8);放在doPost方法第一行。如果做的是 Filter也可以写一个EncodingFilter拦截所有请求统一设置这样每个 Servlet 就不用重复写了。数据库连接串那边也别忘了加useUnicodetruecharacterEncodingUTF-8否则数据库读写端还是 ISO-8859-1 编码。5.3 现象连接 MySQL 报Could not create connection to database server这个报错是驱动版本和数据库版本不匹配导致的。项目里拷了老旧的mysql-connector-java-5.1.x.jar本机数据库却是 MySQL 8.0 以上驱动加载后协议对不上。解决办法是把WEB-INF/lib下的旧 jar 删掉换成 8.0 以上版本的驱动同时把驱动类名改成com.mysql.cj.jdbc.Driver。改动位置主要是 JdbcUtil 里的连接串private static final String URL jdbc:mysql://localhost:3306/lab_schedule ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } }serverTimezoneAsia/Shanghai这个参数在 MySQL 8.0 下几乎必须加上不加会报 CST 时区错误。连接串里的参数顺序不敏感但要注意不该有空格。如果数据库密码本身包含特殊字符比如、URL 里要转义否则解析会出错这个我踩过不止一次。5.4 现象改了 JSP 代码刷新页面还是不生效Tomcat 对 JSP 修改后的重编译通常比较敏感但是如果你修改的是 XML 配置文件、Java 类或者 web.xml不重启 Tomcat 往往不生效。JSP 修改后不生效的另一种情况是浏览器缓存特别是课设答辩现场用了教室的公共电脑。处理办法在 IDE 里执行Project - Clean然后再Restart服务器。如果是手动部署到 Tomcat 的webapps目录要把项目文件和 Tomcatwork目录下的缓存都删掉再重启。work目录存放的是 JSP 翻译后的 Java 源文件和 class 文件不清理的话可能出现页面内容改了但显示旧页面的情况。5.5 现象排课点击提交后瞬间列表还是空的表单提交了数据库表t_schedule里也有记录但页面上查不到。大概率是查询时没有加status 1条件或者插入时status默认为 0。如果建表语句里status字段没有设置默认值代码里插入时又没显式注入数据库会以 NULL 存储查询条件status 1自然匹配不到。解决方式是在插入排课记录时显式指定状态String sql INSERT INTO t_schedule (course_name, teacher_id, lab_id, week_day, time_slot, weeks, status) VALUES (?, ?, ?, ?, ?, ?, 1);同时检查排课表里的字段如果存在status默认值约束建议改成DEFAULT 1。遇到这种“数据在但界面空”时先用 Navicat 或命令行查一遍原始数据能省下不少排查时间。6. 进阶改造用位图实现“单双周排课”与并发防重前面的排课系统只解决了“按周次区间排课”的问题但课程设计里经常有老师提出“单周排课”和“双周排课”的需求也就是第 1、3、5 周上这门课第 2、4、6 周上另一门课。如果只靠weeks字符串做交集判断也能实现但维护起来很累。更优雅的改造是把周次字段替换成位图也就是用一个整数或字符串的二进制位表示 1 到 16 周的排课状态。位图的规则是第几周就用二进制第几位为 1 来表示排课。比如第 1、3、5 周排课二进制就是0000000000010101从低位到高位依次是第 1、2、3…周转换成十进制是 21。两个排课是否冲突只需要把两张位图做按位与运算结果不为 0 就说明有重叠。public static boolean checkBitmapConflict(long existingBitmap, long newBitmap) { return (existingBitmap newBitmap) ! 0; } public static long weeksToBitmap(ListInteger weeks) { long bitmap 0L; for (int week : weeks) { // 第 1 周对应位图第 0 位第 16 周对应位图第 15 位 if (week 1 week 16) { bitmap | (1L (week - 1)); } } return bitmap; }改造后t_schedule表里新增字段week_bitmap bigint原来的weeks字符串可以保留也可以慢慢迁移过去。判断冲突时findConflictSchedules查出同实验室同时段的记录后直接把返回的weekBitmap与新请求的weekBitmap做运算即可。这个方案还有个附带好处存储上 1~16 周只用一个 bigint比起变长字符串更紧凑。如果要在并发场景下彻底防重位图判断也要放在事务里加上锁。写一个方法专门处理“同一个lab_id week_day time_slot的并发插入”public synchronized boolean createScheduleWithLock(Schedule schedule) { ListSchedule conflicts scheduleDao.findConflictSchedules( schedule.getLabId(), schedule.getWeekDay(), schedule.getTimeSlot()); for (Schedule s : conflicts) { if (checkBitmapConflict(s.getWeekBitmap(), schedule.getWeekBitmap())) { return false; } } return scheduleDao.insertSchedule(schedule) 0; }synchronized在单台 Tomcat 部署下够用如果你想讲得更深可以把findConflictSchedules的 SQL 改成SELECT * FROM t_schedule WHERE lab_id ? AND week_day ? AND time_slot ? FOR UPDATE把查询和插入包在同一个事务里。这样并发请求到达数据库时后到的那个会等待前一个事务提交才继续从而保证不会同时插入两条冲突数据。做完这个位图改造后排课模块能轻松支持“1-8 周单周”“9-16 周双周”之类的复杂排课需求而页面上的周次选择器也只需要改成一组复选框或一个支持勾选周次的控件后端解析到ListInteger后直接调weeksToBitmap就能入库。从那以后我每次做排课类项目周次字段一律用位图或 JSON 数组结构不再用1-16这种字符串硬解析省掉了很多跨周判断的边界问题。这套改造花不了多少时间但对答辩和实用性提升都很直接希望帮到你。本文还有配套的精品资源点击获取