简介:一套完整的 JavaWeb 学生宿舍管理系统设计与实现资料包,面向计算机相关专业毕业设计、课程实训及 JavaWeb 初学者。资源将程序源码、毕业论文和数据库整合在一起,覆盖从系统分析、总体设计、详细设计到系统实现与测试的完整流程;论文目录包含摘要、绪论、相关技术介绍、数据库设计、系统实现与测试等章节,可帮助读者理解 SSM、JSP、MySQL 等技术在宿舍管理场景中的落地方式。压缩包共 1070 个文件,约 73.72MB,主要包含 java/class 后端源码、vue/js 前端页面、xml/yml 配置、sql 数据库脚本、jar 依赖以及 doc/docx 论文文档,jpg/png 图片可用于界面参考,目录结构清晰,便于按模块学习。已有 23430 人学习下载。资料内实现了登录注册、学生管理、房间信息、来访登记、物品报修、日志功能等核心模块,并附部署说明,适合作为毕业设计参考或 JavaWeb 项目实践素材。
1. 一个宿舍管理系统,最值钱的是表结构
换个角度看“javaweb学生宿舍管理系统”,它其实是一个固定矛盾:课设代码到处都有,能讲清楚“为什么这样建表”的资料反而不多。绝大多数人卡住的地方不是 Servlet 写不出来,而是宿舍分配这种看似简单的业务,落到数据库里就不知道怎么表达“一间宿舍住几个人、谁在住、退宿之后记录怎么留”。
这个系统典型的技术组合是 JSP + Servlet + JDBC 或者 JSP + Servlet + MyBatis,数据库用 MySQL。它的核心收益在于:把学生、宿舍、入住、退宿、报修这五件事用事务和关联约束串起来,是一个完整的 JavaWeb 课程设计和数据库课程设计双料场景。适合正在做课设、毕业设计,或者想用一份结构清晰的代码去准备项目答辩的开发者。项目里包含程序、论文和 SQL 文件,正好对应了从表设计到论文绘图的一条完整链路。本篇顺着这条链路,直接拆解这套系统从建表到跑通的全部关键点。
2. 数据库设计先行:宿舍管理系统的表结构该怎么拆
2.1 为什么先定 5 张表,而不是一张大表
学生宿舍管理系统常见的错误做法是把所有字段塞进一张 student 表,配宿舍号、床位号、入住时间。这个设计在报告里写起来简单,但“退宿再入住”时历史数据被覆盖,报修记录又没办法按宿舍关联——一次数据结构答辩就会被追问到哑口无言。常规做法是拆成 5 张核心业务表:学生表、宿舍表、入住记录表、报修表、管理员表。有些项目还会加一栋楼宇表,但课设场景下 5 张表已经能覆盖完整的业务闭环。
拆表的核心依据是处理“多对多”关系。一个学生多次入住不同宿舍,一个宿舍在不同时间段住过不同的学生,这两者之间不是简单的外键能表达的,必须引入入住记录表作为关联实体。这一点在论文的 E-R 图里也是重点:学生与宿舍通过“入住”这个联系形成多对多,入住记录本身又携带入住时间和退宿时间属性。讲解时用“选课系统里学生和课程之间要选课表”来类比,答辩老师一听就懂。
2.2 建库建表 SQL:字段类型和索引的取舍
CREATE DATABASE IF NOT EXISTS dormitory DEFAULT CHARSET utf8mb4; USE dormitory; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(32) ) ENGINE=InnoDB; CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT, building_no VARCHAR(16) NOT NULL COMMENT '楼栋号,如5栋', room_no VARCHAR(16) NOT NULL, bed_count INT NOT NULL DEFAULT 4 COMMENT '床位总数', UNIQUE KEY uk_building_room (building_no, room_no) ) ENGINE=InnoDB; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(32) NOT NULL, gender CHAR(1) NOT NULL COMMENT 'M/F', phone VARCHAR(20), dorm_id INT, bed_no INT COMMENT '当前床位号,1-4', CONSTRAINT fk_stu_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINE=InnoDB; CREATE TABLE check_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, dorm_id INT NOT NULL, bed_no INT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE, status TINYINT NOT NULL DEFAULT 1 COMMENT '1在住 0已退宿', CONSTRAINT fk_check_stu FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_check_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINE=InnoDB; CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, dorm_id INT NOT NULL, student_id INT, content VARCHAR(255) NOT NULL COMMENT '报修内容', report_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已完成', handler VARCHAR(32) COMMENT '处理人', CONSTRAINT fk_repair_dorm FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINE=InnoDB;这里的关键参数说明:
utf8mb4不是可选项。MySQL 8 默认就是 utf8mb4,JavaWeb 项目里用户输入中文和表情符号,latin1 或 utf8 都可能出现乱码,建库时就锁死字符集能省掉大量编码坑。UNIQUE KEY uk_building_room (building_no, room_no)是联合唯一约束。它的意义是防止同一栋楼出现两个相同房号,这是数据完整性的第一道防线。student表直接冗余一个bed_no字段,看起来违反了第三范式,但有实际理由:查询“谁住在这个宿舍”是最频繁的操作,冗余字段避免每次 JOIN 入住记录表。在论文里可以主动提这个冗余设计,体现对读性能的考虑。check_record.status用 TINYINT 而不是字符串,状态值由代码层控制。这样可以避免“住”和“在住”这种同一含义不同写法造成的数据混乱。
2.3 宿舍分配逻辑:触发器还是代码事务
宿舍分配是一个典型的多步操作:查询空床位 → 写入入住记录 → 更新学生表宿舍字段 → 检查是否满员。常见实现是在 Service 层用事务包住这几步,而不是用触发器。原因很简单:触发器会让业务逻辑分散在数据库端,课设代码里不好展示分层结构,排错也麻烦。
一个面试常问的问题:宿舍满员后怎么办?代码里要做两层校验,第一层查 dorm 表的 bed_count 和当前在住人数,第二层在插入 check_record 前查同一宿舍 status=1 的记录数。注意这里要加行锁或者使用SELECT ... FOR UPDATE,否则并发请求可能同时通过校验。课设项目并发量低,不需要真正引入分布式锁,但这一步逻辑在答辩时能主动说出来,相当于给自己的设计加分。
3. 从 IDEA 创建 JavaWeb 项目到跑通 JDBC 连接池
3.1 用 IDEA 2026 创建项目时的骨架选择
新版本 IDEA 创建 JavaWeb 项目和旧版本有区别:不再推荐直接创建 Web Application 空项目,而是建议先创建 Maven 项目,再手动补 web 目录结构。用 IDEA 2026 创建 JavaWeb 项目的标准路径是:新建项目时选择 Maven Archetype 里的maven-archetype-webapp,注意这个骨架生成的项目默认没有 java 源码目录,需要手动在src/main下新建java文件夹并标记为 Sources Root。
pom.xml里需要引入的最少依赖是:Servlet API、JSP API、MySQL 驱动、数据库连接池。注意 Servlet 和 JSP 的依赖范围要设为provided,因为 Tomcat 本身就带了这两个包,如果设为默认的 compile 范围,部署到 Tomcat 时可能出现类冲突。
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> </dependency>Druid 在这个场景下是最稳妥的连接池选择。它比 DBCP 和 C3P0 的优势在于自带监控页面,配置StatFilter后可以通过http://localhost:8080/druid/index.html查看 SQL 执行次数和耗时,这个功能在项目答辩演示时非常实用——可以现场展示“每次查询宿舍列表实际执行了哪些 SQL”,比口头解释数据流向有说服力得多。
3.2 配置 Druid 连接池的参数
@WebServlet("/dorm/list") public class DormListServlet extends HttpServlet { private DruidDataSource dataSource; @Override public void init() { dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/dormitory?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxActive(10); dataSource.setMinIdle(2); dataSource.setMaxWait(60000); } }参数说明:
initialSize(5)是启动时预创建的连接数。课设项目一般不需要预热太多,5 个足够。maxActive(10)是最大活跃连接数。课设场景 Tomcat 单机部署,10 已经是上限——调太大反而浪费内存。maxWait(60000)单位是毫秒,超过 60 秒拿不到连接就抛异常。做演示时如果看到wait millis 60000, active 10的报错,说明连接泄漏了——大概率是某处用了连接没 close。- JDBC URL 里的
serverTimezone=Asia/Shanghai必不可少,MySQL 8 驱动对时区敏感,不加这个参数运行时会报时区异常。
3.3 IDEA 里配置 Tomcat 运行 JavaWeb 项目的关键步骤
IDEA 运行 JavaWeb 项目配置的难点不在 Tomcat 本身,而在 Artifact 的部署方式。正确做法是:在 Run/Debug Configurations 里新增 Tomcat Server Local,然后 Deployment 页签添加 Artifact,Application context 设置为/dormitory。这里有一个常见的坑——如果不配置 Artifact,直接运行 Tomcat 会提示 404,因为根本没有把项目打包成 war 放入 Tomcat 的 webapps 目录。
还有一个开发者容易忽略的点:src/main/resources下的配置文件要确保被打进 target/classes,然后在代码里通过ClassLoader.getResourceAsStream("db.properties")读取。不要用FileInputStream去读绝对路径,否则换一台机器跑就直接崩。
4. 宿舍管理系统核心功能实现:登录、分配与报修
4.1 登录认证:用 Session 还是 JWT
学生宿舍管理系统的登录逻辑介于课设和真实项目之间。用纯 Session 是最常见做法,也足够用:用户登录成功后把管理员 ID 写入 Session,再通过 Filter 拦截未登录的请求。JWT 在这个场景里属于过度设计——没有移动端,没有跨域需求,引入令牌机制反而给答辩增加解释成本。选择 JWT 的唯一合理理由是论文里想写“前后端分离”,但这就改成了 Vue + 后端 API 项目,和 javaweb 这个核心定位就不符了。
登录密码存储要说清楚。常见做法是 MD5 加盐,虽然 SHA-256 更安全,但课设报告里写 MD5 也能过。重点是盐值不能是固定字符串,每个用户独立盐值或者直接用用户名做盐。代码里按如下顺序实现:先按用户名查管理员表,取到记录后把用户输入的密码拼接盐值做 MD5,和数据库里的密文比较。
String inputPwd = DigestUtils.md5Hex(password + username); String sql = "SELECT * FROM admin WHERE username=? AND password=?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, inputPwd); ResultSet rs = ps.executeQuery(); if (rs.next()) { request.getSession().setAttribute("adminId", rs.getInt("id")); response.sendRedirect("index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }DigestUtils.md5Hex来自 Apache Commons Codec,需要额外引入依赖。如果不想引入这个库,用 Java 原生的MessageDigest也完全可行,只是需要自己写 Hex 转码。注意这里没有用SELECT *后在前端判断,而是直接把密码比较放到 SQL 里,虽然结果一样,但存在一个安全隐患——SQL 注入。真实项目应该先按用户名查出整条记录,再在 Java 代码里比对密码,即使密码错误也不要让攻击者通过闭合引号绕过。这段代码作为课设可以跑通,但在论文的“安全设计”章节里,建议把改进版写出来,展示你知道这个风险。
4.2 宿舍分配的事务控制
宿舍分配是系统里最能体现“数据库”含量的一个功能。用 MyBatis 或者纯 JDBC 实现都可以,核心动作是一致的:开启事务 → 查询目标宿舍当前已住人数 → 判断是否有空床位 → 插入入住记录 → 更新学生表的宿舍字段 → 提交。任何一个步骤失败,全部回滚。纯 JDBC 的做法是conn.setAutoCommit(false),然后try-catch-finally里做 commit 和 rollback。
Mapper 接口里需要这样三个方法:
public interface DormMapper { // 查询宿舍当前在住人数 int countOccupied(@Param("dormId") int dormId); // 插入入住记录 int insertCheckRecord(CheckRecord record); // 更新学生所在宿舍 int updateStudentDorm(@Param("studentId") int studentId, @Param("dormId") int dormId, @Param("bedNo") int bedNo); }这三条 SQL 必须放在同一个事务里。具体的 XML 映射中,countOccupied对应的是SELECT COUNT(*) FROM check_record WHERE dorm_id = #{dormId} AND status = 1。这里常见的 bug 是忘了加status = 1条件,导致查出来的数据把已经退宿的历史记录也算进去——一个好端端的系统,退了几个人之后就报满员,问题根源就在这里。
逻辑上还要考虑一个边界情况:学生是“换宿舍”而不是“新入住”。换宿舍时,要先把旧入住记录的状态置为 0 并填上退宿日期,才能执行新入住。这个流程如果不在同一事务里,一旦中途出异常,学生既不在新宿舍也不在旧宿舍,数据就悬空了。
4.3 报修工单的增删改查与状态流转
报修模块是最容易展示数据库增删改查基本功的部分。常见做法是把报修做成一个状态机:待处理 → 处理中 → 已完成。查询列表默认只显示待处理,管理员可以点击“接单”修改状态为处理中,完成后填入处理人名字。
@WebServlet("/repair/update") public class RepairUpdateServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int repairId = Integer.parseInt(req.getParameter("id")); int status = Integer.parseInt(req.getParameter("status")); String handler = req.getParameter("handler"); String sql = "UPDATE repair SET status=?, handler=? WHERE id=?"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, status); ps.setString(2, handler); ps.setInt(3, repairId); ps.executeUpdate(); resp.sendRedirect("repair/list?status=0"); } catch (SQLException e) { e.printStackTrace(); resp.sendError(500); } } }代码逻辑说明:DbUtil.getConnection()从 Druid 连接池取连接,每个请求用完即还回连接池。状态值通过前端下拉框传入,这里有一个容易被忽略的安全问题——Integer.parseInt对非数字参数会直接抛 500 异常。课设阶段可以不做严格校验,但至少要捕获NumberFormatException并返回友好提示。
报修列表的分页查询是课程设计里必然会要求的功能。用 MySQL 的LIMIT关键字即可:
SELECT * FROM repair ORDER BY report_time DESC LIMIT #{offset}, #{pageSize};前端页码一般从 1 开始,而后端偏移量从 0 开始,所以offset = (currentPage - 1) * pageSize。总页数需要先执行一条SELECT COUNT(*)获取总数,再用(total + pageSize - 1) / pageSize计算,这个写法比Math.ceil更直接。分页参数建议用两个独立入参而不是拼在 URL 后面,方便后续改造成 MyBatis-PageHelper——在论文里写一句“本系统底层 SQL 兼容 PageHelper 分页插件”,能体现你有迁移意识。
4.4 宿舍可视化展示与床位状态计算
宿舍列表页面的经典实现是网格布局:一页显示所有宿舍,每个宿舍卡片上列出房号、已住人数/总床位数、每个床位的入住学生姓名。这个页面的数据查询涉及两张表,对应 SQL 如下:
SELECT d.id, d.building_no, d.room_no, d.bed_count, GROUP_CONCAT(CONCAT(s.name, '(', s.bed_no, ')') SEPARATOR '、') AS occupants FROM dorm d LEFT JOIN student s ON d.id = s.dorm_id GROUP BY d.id, d.building_no, d.room_no, d.bed_count;GROUP_CONCAT是 MySQL 特有的聚合函数,作用是把同一宿舍的所有学生名字拼成一个字符串。注意LEFT JOIN不能用INNER JOIN,否则空宿舍根本不会出现在结果里。前端拿到 occupants 字段后按宿舍显示就行,空缺床位用灰色占位符。
5. 从连表查询到 MySQL 慢 SQL 处理的三个优化习惯
5.1 联合索引和覆盖索引的取舍
宿舍列表的查询条件通常是“楼栋号 + 房号”,联合索引已经建好。但DormMapper.countOccupied这个高频查询的事务里还有一个隐患:check_record表上只有外键索引,WHERE dorm_id = ? AND status = 1这个条件,MySQL 会用 dorm_id 索引过滤出大量历史记录,再逐条回表查 status。如果入住记录有几千条,这个查询会随着数据量增长越来越慢。
针对 MySQL 数据库优化,可以新建一个复合索引:
CREATE INDEX idx_dorm_status ON check_record(dorm_id, status);dorm_id和status组成联合索引后,WHERE dorm_id=? AND status=1可以直接在索引里完成过滤,不需要回表。这里不需要加bed_no到索引里,因为查询里没有用 bed_no 做过滤条件,加进去只是浪费空间。用EXPLAIN SELECT COUNT(*) FROM check_record WHERE dorm_id=1 AND status=1能看到key从fk_check_dorm变成idx_dorm_status,这就是一个可以在答辩时展示的量化优化过程。
5.2 批量导入学生数据的 PreparedStatement 批处理
宿舍管理系统的初始化场景里,管理员往往需要一次性导入几百个学生。逐条插入不仅慢,还会频繁提交事务,影响连接池里其他请求。改用 JDBC 的addBatch批处理:
String sql = "INSERT INTO student(student_no, name, gender, phone) VALUES(?,?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (Student stu : studentList) { ps.setString(1, stu.getStudentNo()); ps.setString(2, stu.getName()); ps.setString(3, stu.getGender()); ps.setString(4, stu.getPhone()); ps.addBatch(); if (studentList.indexOf(stu) % 200 == 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); }批处理有个隐含的坑:使用批处理前必须setAutoCommit(false),否则每条 insert 依然单独提交,批处理只是“批量发送”,事务不会被合并。每 200 条执行一次executeBatch()是为了避免插入几千条时内存积压。如果导入中途报错,conn.rollback()会把整个批次回滚,不会留下半截数据。
5.3 用表结构验证反向检查论文中的 E-R 图
论文里的 E-R 图画完,反过来对照建表 SQL 是一个值得养成的习惯。学生表与宿舍表的联系是“入住”,它在 E-R 图里是多对多关系,对应到物理模型就是check_record关联表,同时携带两个外键。如果 E-R 图上画了一对多,代码里却用外键直接挂在 student 表上,那论文图表和实现就对不上。数据库课程设计答辩的常见扣分点恰恰是图表和数据字典对不上——用一条标准 SQL 验证两个库的表结构一致性,比人工翻文档可靠得多。
本文还有配套的精品资源,点击获取