简介这是一套面向高校计算机专业学生与Java Web初学者的学生宿舍管理系统完整项目资料围绕住宿信息管理、宿舍与床位分配、日常行为记录等核心业务展开可用于课程设计、毕业设计或Java Web入门实战。压缩包共661个文件约77.19MB以400个js、46个css等前端资源为主配合35个java源码、40个class编译文件、8个xml与8个html页面以及2个sql脚本、部署文档、docx说明和5个mp4辅导视频覆盖从数据库设计、DAO数据访问、MVC分层到Servlet与JSP交互的完整链路。项目提供MySQL 5与MySQL 8两套适配版本并附权限控制、异常处理、资源分配算法等实现思路便于读者对照源码理解业务逻辑、按文档完成环境搭建与部署也可借助视频复盘关键功能开发过程。目前已有1615人学习下载适合需要一套可运行、可拆解、可二次开发的Java Web综合案例的读者参考。1. 从一份 Java 课程设计压缩包说起宿舍管理系统到底要解决什么很多计算机专业的学生在期末会拿到一个名为“基于java的学生宿舍管理系统设计与实现”的课程设计任务配套的压缩包里通常包含源代码、数据库脚本、部署文档和辅导视频。这个标题背后对应的真实场景非常具体一栋宿舍楼里住着几百上千号人管理员需要登记入住、调换寝室、记录晚归、统计空床位还要在毕业季批量处理退宿。用 Excel 手工维护这些信息不出两周就会乱套——同一个床位被分给两个人、学生换寝室后旧记录没删、报修单和住宿记录对不上。这套系统的核心价值就是把“人—寝室—床位—事件”四类数据用关系型数据库管起来再通过 Java Web 层提供增删改查和统计报表。它适合正在做课程设计的学生、需要快速搭建小型管理系统的初级开发者以及想拿一个完整项目练手 Java 基础、JDBC、Servlet 和前端交互的入门者。理解了这个定位后面讨论技术选型和落地步骤才不会跑偏。2. 技术选型与数据库设计为什么用 Java MySQL 而不是别的2.1 后端为什么锁定 Java Servlet JDBC 这套组合课程设计场景下技术栈的选择标准不是“先进”而是“可控、可调试、依赖少”。常见做法是用 Java Servlet 接收前端请求JDBC 直接操作 MySQL不引入 Spring 全家桶。原因很实际Servlet 的请求响应模型足够简单出问题时堆栈信息直接指向具体行号JDBC 虽然写起来啰嗦但每一条 SQL 都握在自己手里方便对照数据库里的真实数据排查。如果换成 MyBatis 或 JPA多一层映射配置对新手来说反而增加了“明明代码没错但数据就是不对”的玄学概率。我一般会建议把项目分成四层实体类Student、Room、Bed、Record、DAO 层每个实体一个 DAO封装增删改查、Service 层处理业务规则比如“一个床位只能有一个在住学生”、Servlet 层接收参数、调用 Service、转发 JSP。这样分层的好处是数据库字段变了只需要改实体和 DAO业务规则变了只动 Service页面调整只碰 JSP。// 以 BedDAO 为例演示 JDBC 查询空闲床位 public ListBed findAvailableBeds(int roomId) throws SQLException { String sql SELECT bed_id, room_id, status FROM bed WHERE room_id ? AND status FREE; ListBed list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); // 参数1寝室ID try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Bed bed new Bed(); bed.setBedId(rs.getInt(bed_id)); bed.setRoomId(rs.getInt(room_id)); bed.setStatus(rs.getString(status)); list.add(bed); } } } return list; }这段代码的关键点有三个用PreparedStatement而不是Statement是为了防止 SQL 注入参数用?占位再setInt填入try-with-resources保证 Connection、PreparedStatement、ResultSet 自动关闭避免连接泄漏status FREE这个条件把“空闲床位”的判定逻辑放在 SQL 里而不是查出来再用 Java 过滤减少数据传输量。参数roomId由上层 Servlet 从请求中解析通常对应前端传过来的寝室编号。2.2 数据库表结构怎么定五张核心表与三个容易忽略的字段宿舍管理系统的数据库设计有一个经典陷阱把“学生”和“住宿记录”混在一张表里。一旦学生换寝室历史记录就丢了。正确做法是拆成五张表student学号、姓名、性别、班级、联系方式、room寝室号、楼层、床位数、性别限制、bed床位号、所属寝室、状态、checkin_record入住记录学生ID、床位ID、入住时间、退宿时间、repair_order报修单寝室ID、报修内容、状态、提交时间。三个容易忽略但必须加的字段第一student表加status字段在读/毕业/休学毕业季批量退宿时只改状态不删数据第二checkin_record加is_active布尔字段表示这条记录是否当前有效换寝室时把旧记录置为 false 再插新记录第三bed表的status用枚举值FREE / OCCUPIED / MAINTENANCE而不是用 0/1 表示后期加“维修中”状态时不用改代码逻辑。-- 建表语句节选床位表与入住记录表 CREATE TABLE bed ( bed_id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, bed_no VARCHAR(10) NOT NULL, status ENUM(FREE,OCCUPIED,MAINTENANCE) DEFAULT FREE, FOREIGN KEY (room_id) REFERENCES room(room_id) ); CREATE TABLE checkin_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, bed_id INT NOT NULL, checkin_time DATETIME DEFAULT CURRENT_TIMESTAMP, checkout_time DATETIME, is_active TINYINT(1) DEFAULT 1, FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (bed_id) REFERENCES bed(bed_id) );建表时注意FOREIGN KEY约束要加否则删了学生记录但入住记录还指着不存在的 ID查询时会出现空指针。is_active用TINYINT(1)而不是BOOLEAN是因为 MySQL 里 BOOLEAN 本质就是 TINYINT显式写出来更清楚。checkin_time设默认值CURRENT_TIMESTAMP插入记录时不用手动传时间减少参数错误。2.3 部署文档里最容易漏掉的环境配置步骤压缩包里的部署文档通常只写“导入数据库、改数据库连接、启动 Tomcat”但实际跑起来经常卡在环境上。我一般会按这个顺序检查先确认 JDK 版本和 Tomcat 版本匹配Tomcat 9 配 JDK 8Tomcat 10 配 JDK 11混用会报UnsupportedClassVersionError再确认 MySQL 驱动 jar 包放在WEB-INF/lib下而不是只加在 IDE 的 classpath 里最后检查数据库连接 URL 里的时区和编码参数。# 导入数据库脚本的典型命令 mysql -u root -p -e CREATE DATABASE dorm_db DEFAULT CHARSET utf8mb4; mysql -u root -p dorm_db dorm_db.sql # 检查 Tomcat 日志确认启动无异常 tail -f $CATALINA_HOME/logs/catalina.outDEFAULT CHARSET utf8mb4是为了支持中文姓名和特殊符号用utf8在某些 MySQL 版本下存 emoji 会报错。导入脚本时如果报Foreign key constraint fails说明建表顺序不对先建student和room再建bed最后建checkin_record。启动后看catalina.out如果出现Communications link failure八成是数据库没启动或者连接 URL 里的端口写错了。3. 核心功能实现从入住登记到退宿统计的完整链路3.1 入住登记一个 Servlet 如何同时处理床位占用和记录插入入住登记看起来只是“填个表”但背后要保证两件事同时成功床位状态从 FREE 改成 OCCUPIED同时往checkin_record插一条is_active 1的记录。如果只改了一个数据就不一致。常见做法是在 Service 层用一个方法包住这两个操作并且加上事务控制。// CheckInService.java 核心逻辑 public boolean checkIn(int studentId, int bedId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步检查床位是否空闲 BedDAO bedDAO new BedDAO(); Bed bed bedDAO.findById(conn, bedId); if (bed null || !FREE.equals(bed.getStatus())) { conn.rollback(); return false; } // 第二步占用床位 bedDAO.updateStatus(conn, bedId, OCCUPIED); // 第三步插入入住记录 CheckinRecordDAO recordDAO new CheckinRecordDAO(); recordDAO.insert(conn, studentId, bedId); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这里的关键是conn.setAutoCommit(false)之后所有 DAO 方法必须共用同一个 Connection 对象不能各自去DBUtil.getConnection()。很多新手在这里翻车BedDAO 里自己开了一个连接CheckinRecordDAO 里又开了一个事务根本管不到两个连接。参数studentId和bedId由前端表单提交Servlet 里用request.getParameter拿到后转成 int转换失败要返回错误提示而不是抛异常给用户看。3.2 调换寝室为什么不能直接 UPDATE 床位号调换寝室是宿舍管理里最容易被写错的功能。直觉做法是找到学生的入住记录把bed_id改成新床位。但这样做的后果是旧床位没有释放新床位可能已经被别人占了而且历史记录被覆盖查不到学生曾经住过哪里。正确流程分四步先把旧记录的is_active置为 0 并填上checkout_time再把旧床位状态改回 FREE然后检查新床位是否 FREE最后插入一条新的入住记录并占用新床位。这四步同样要放在一个事务里。-- 调换寝室的事务 SQL 序列在 Service 层按顺序执行 UPDATE checkin_record SET is_active 0, checkout_time NOW() WHERE student_id ? AND is_active 1; UPDATE bed SET status FREE WHERE bed_id (SELECT bed_id FROM checkin_record WHERE student_id ? AND is_active 0 ORDER BY record_id DESC LIMIT 1); -- 检查新床位空闲后执行 UPDATE bed SET status OCCUPIED WHERE bed_id ? AND status FREE; INSERT INTO checkin_record (student_id, bed_id, is_active) VALUES (?, ?, 1);注意第二条 SQL 里用子查询找旧床位实际项目中更稳妥的做法是在 Java 里先查出旧记录的bed_id再传参避免子查询返回多行。ORDER BY record_id DESC LIMIT 1是为了拿到最近一条被置为无效的记录防止历史数据干扰。如果UPDATE bed SET status OCCUPIED影响行数为 0说明新床位不是 FREE事务要回滚并提示“目标床位已被占用”。3.3 退宿统计按楼层和班级生成报表的 SQL 与分页处理管理员最常用的功能是“看当前还有多少空床位”和“按班级统计在住人数”。这两个查询都不复杂但数据量大了之后分页没做好会拖慢页面。空床位统计直接SELECT room_id, COUNT(*) FROM bed WHERE status FREE GROUP BY room_id就行。按班级统计在住人数需要 join 三张表。-- 按班级统计当前在住人数只统计 is_active 1 的记录 SELECT s.class_name, COUNT(*) AS resident_count FROM student s JOIN checkin_record cr ON s.student_id cr.student_id WHERE cr.is_active 1 GROUP BY s.class_name ORDER BY resident_count DESC; -- 分页查询入住记录每页 20 条 SELECT cr.record_id, s.name, r.room_no, b.bed_no, cr.checkin_time FROM checkin_record cr JOIN student s ON cr.student_id s.student_id JOIN bed b ON cr.bed_id b.bed_id JOIN room r ON b.room_id r.room_id WHERE cr.is_active 1 ORDER BY cr.checkin_time DESC LIMIT 20 OFFSET ?;OFFSET的参数是(页码 - 1) * 每页条数在 Servlet 里根据request.getParameter(page)计算。如果数据量超过几万条LIMIT OFFSET会越翻越慢这时候可以改成基于record_id的游标分页WHERE record_id ? ORDER BY record_id DESC LIMIT 20把上一页最后一条的 ID 传进来。课程设计的数据量通常不大用 OFFSET 足够但知道这个边界在哪面试被问到分页优化时能答上来。4. 避坑与排查部署和运行阶段最常见的五个问题4.1 现象页面报 500日志显示 ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 驱动 jar 包没有放到WEB-INF/lib目录下或者放的是新版本驱动但代码里写的还是旧版类名。MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver5.x 才是com.mysql.jdbc.Driver。解决确认 jar 包在WEB-INF/lib下然后检查DBUtil里Class.forName的字符串是否和驱动版本匹配。如果用的是 Maven检查mysql-connector-java的 scope 是不是provided那样打包时不会带进去。4.2 现象中文姓名存进数据库变成问号原因数据库连接 URL 没有指定编码或者建库时字符集不是 utf8mb4。解决连接 URL 加上?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时确认数据库和表的字符集。如果已经存了乱码数据需要先改表字符集再重新导入。4.3 现象Tomcat 启动正常但访问页面 404原因web.xml里的url-pattern和实际访问路径不一致或者 Servlet 注解WebServlet(/checkIn)里的路径写错。解决先看 Tomcat 启动日志里有没有Servlet [xxx] marked as unavailable再看浏览器地址栏的路径是否和注解一致。常见错误是项目部署路径带了版本号比如/dorm_system_war_exploded/而代码里写的是绝对路径/checkIn。4.4 现象入住登记时提示“床位已被占用”但数据库里明明是 FREE原因事务没有正确提交或者查询床位状态时用的连接和更新时不是同一个。解决在 Service 层打印每次conn.getAutoCommit()的值确认事务开启后没有中途被重置。另一个可能是前端重复提交用户点了两次按钮第一次已经占用了床位第二次自然失败。可以在前端加一个提交后禁用按钮的逻辑。4.5 现象辅导视频里的代码能跑自己照着敲就报错原因视频里的环境是特定版本组合比如 JDK 8 Tomcat 8 MySQL 5.7而你本地是 JDK 17 Tomcat 10 MySQL 8.0。解决不要盲目追新课程设计项目用 JDK 8 Tomcat 9 MySQL 5.7 或 8.0 是最稳的组合。如果已经装了高版本可以用 SDKMAN 或手动切换 JAVA_HOME。另外注意视频里可能省略了 import 语句和 jar 包导入步骤这些恰恰是新手最容易卡住的地方。5. 进阶技巧用一条 SQL 查出“从未住过宿舍的学生”和批量退宿的稳妥做法课程设计做到最后答辩老师常会问两个进阶问题怎么找出“从来没有入住记录的学生”以及毕业季怎么批量退宿而不丢历史数据。第一个问题用LEFT JOIN加IS NULL就能解决比用NOT IN更高效因为NOT IN在子查询结果有 NULL 时会返回空集是个经典陷阱。-- 查出从未住过宿舍的学生LEFT JOIN IS NULL SELECT s.student_id, s.name, s.class_name FROM student s LEFT JOIN checkin_record cr ON s.student_id cr.student_id WHERE cr.record_id IS NULL; -- 批量退宿只改状态不删记录 UPDATE checkin_record SET is_active 0, checkout_time NOW() WHERE is_active 1 AND student_id IN ( SELECT student_id FROM student WHERE status GRADUATED ); UPDATE bed SET status FREE WHERE bed_id IN ( SELECT bed_id FROM checkin_record WHERE is_active 0 AND checkout_time CURDATE() );第一条 SQL 里cr.record_id IS NULL是判断“没有匹配到入住记录”的标准写法换成cr.student_id IS NULL效果一样但用主键更直观。第二条批量退宿分两步先失效入住记录再释放床位。注意第二个UPDATE里的子查询条件用了checkout_time CURDATE()只释放今天退宿的床位避免把历史退宿的床位重复释放。如果直接UPDATE bed SET status FREE WHERE bed_id IN (SELECT bed_id FROM checkin_record WHERE is_active 0)会把所有曾经退宿的床位都刷一遍虽然结果一样但影响行数虚高日志不好看。验证批量退宿是否成功不要只看页面提示直接查数据库SELECT COUNT(*) FROM checkin_record WHERE is_active 1应该等于当前实际在住人数SELECT COUNT(*) FROM bed WHERE status OCCUPIED应该和上一条相等。如果两个数对不上说明有记录的is_active和床位的status不同步需要写一个修复脚本按checkin_record为准去校正bed表。我自己的习惯是每次改完和住宿状态相关的代码先跑一遍这两个 COUNT 查询数字一致才继续往下做页面。这个习惯帮我省掉了无数次“页面显示空床位但实际有人住”的后悔药。希望帮到你。本文还有配套的精品资源点击获取
