☰
可运行Java宿舍管理系统源码拆解:核心链路、部署排错与二次开发
2026/11/6 5:30:54 网站建设 项目流程

简介:这套基于Java的学生宿舍管理系统源码包,面向计算机专业毕业设计、课程设计及Java Web入门进阶学习者,帮助读者快速搭建一个具备多角色权限的宿舍管理演示项目。系统前端采用Bootstrap构建管理界面,后端基于SpringBoot框架与MySQL数据库,划分管理员、宿管、学生三类角色:管理员可维护学生、楼宇、宿舍、入住、宿管及管理员信息,并支持各类数据的增删改查与Excel导入导出;宿管侧重学生、宿舍与入住管理;学生端提供学生及宿舍信息查看。包内共661个文件,以400个js、46个css、35个java、若干class与xml配置为主,另有sql脚本、html页面和说明文档,压缩包约72.37MB,并提供MySQL 5与MySQL 8两套可运行项目。目前已有469人学习下载。读者可据此理解SpringBoot分层结构、权限登录流程、Bootstrap后台页面组织及数据导入导出实现,也可作为毕业设计原型进行二次开发与功能扩展。

1. 宿舍管理系统:一个能直接跑起来的 Java 毕设项目长什么样

每年毕业季,后台总有人问同一个问题:有没有一个结构别太花哨、能在一周内跑起来、答辩时讲得清楚的 Java 项目。宿舍管理系统就是这类需求里命中率最高的选题之一,而这份基于 Java 的学生宿舍管理系统源码,恰好是那种「拉下来配好数据库就能登录」的工程。它解决的场景很具体:学生入住、调宿、退宿,管理员分配房间、统计床位、登记报修,辅导员查在寝情况。技术栈走的是 Java 后端加关系型数据库的经典路线,没有堆微服务,也没上容器编排,对新手友好,对熟手也留了改造空间。这篇笔记就按「它是什么、目录长什么样、核心代码怎么转、怎么跑起来、哪里会翻车、二次开发往哪改」的顺序拆一遍,尽量把血泪经验提前说,省得你在环境上卡两天。

2. 技术选型与目录结构:从一次登录请求看整体骨架

2.1 为什么是这套经典组合而不是时髦方案

这类管理系统型项目的技术选型逻辑很简单:数据关系明确(学生、楼栋、房间、床位、报修单),并发压力低,读多写少,核心诉求是增删改查跑通加权限隔离。所以常见的做法是 Servlet/JSP 或者 Spring Boot 二选一,前者偏教学、后者偏工程。这份源码走的是 Java Web 主线,持久层用 JDBC 或轻量 ORM,前端是 JSP 加少量 JS,数据库 MySQL,构建工具 Maven,部署到 Tomcat。选这套的理由得说清楚,不然改起来会迷路:

  • 分层清晰:Controller/Service/DAO 三层各管一段,改业务只动 Service,改 SQL 只动 DAO,适合二次开发时快速定位。
  • 依赖少:没有注册中心、没有消息队列,一个 war 包丢进 Tomcat 就能起,本地调试链路短。
  • 可讲性:答辩场景下,面试官或老师问「你的请求怎么走的」,一条链路能从浏览器讲到数据库再讲回来,比讲微服务熔断好答得多。

如果你接手的是 Spring Boot 版本,结构会变成controller / service / mapper / entity,差异主要在依赖注入和配置方式,业务逻辑的组织方式是一致的。下面按通用 Java Web 结构讲,遇到 Boot 版本自行对应。

2.2 目录结构:每个包负责什么

一个合格的宿舍管理系统目录大致长这样,先建立地图感,后面看代码才不会觉得哪段都是孤立的:

dormitory-management/ ├── src/main/java/com/example/dorm/ │ ├── controller/ # 接收请求,参数校验,返回视图或 JSON │ │ ├── LoginController.java │ │ ├── StudentController.java │ │ ├── RoomController.java │ │ └── RepairController.java │ ├── service/ # 业务规则:床位是否已满、能否调宿 │ │ ├── StudentService.java │ │ ├── RoomService.java │ │ └── RepairService.java │ ├── dao/ # SQL 与数据库交互,一个表一个 DAO │ │ ├── StudentDao.java │ │ ├── RoomDao.java │ │ └── BaseDao.java # 连接获取、关闭、通用查询封装 │ ├── entity/ # 与表结构一一对应的 POJO │ │ ├── Student.java │ │ ├── Room.java │ │ └── Repair.java │ ├── filter/ # 登录态校验、编码过滤 │ │ └── AuthFilter.java │ └── util/ # 数据库连接、分页、日期工具 │ └── DBUtil.java ├── src/main/webapp/ │ ├── WEB-INF/views/ # JSP 页面,按模块分子目录 │ ├── static/ # css、js、图片 │ └── WEB-INF/web.xml # Servlet 映射、过滤器配置 ├── src/main/resources/ │ └── db.properties # 数据库连接参数 ├── sql/ │ └── dorm.sql # 建库建表加初始数据 └── pom.xml # 依赖与打包配置

这张图里有三个地方值得注意。BaseDao是所有 DAO 的父类,连接管理和通用查询写在这里,避免每个 DAO 重复写Connection那套样板代码。filter包承担登录拦截,没登录访问任何管理页都会被弹回登录页,这是权限隔离的第一道门。sql/dorm.sql是能不能跑起来的关键,很多下载的源码跑不起来,八成是这份脚本没执行或者执行了但字符集不对。

提示:拿到源码第一件事不是打开 IDE,是先看sql目录和db.properties,这两处决定了你后面 80% 的时间是花在业务上还是环境上。

2.3 表结构设计:五个核心表撑起全部业务

关系型系统的表设计直接决定代码复杂度。这套系统的核心表不多,但关系要绕清楚:

表名关键字段说明
studentid, sno, name, gender, class_name, room_id, status学生基本信息,room_id 关联房间,status 标记在住/退宿
roomid, building_id, room_no, capacity, occupied房间,capacity 总床位,occupied 已住人数
buildingid, name, floors, manager楼栋,宿管归属
repairid, student_id, room_id, content, status, create_time报修单,status 走待处理/处理中/已完成
adminid, username, password, role管理员账号,role 区分超级管理员和普通宿管

设计上最容易出问题的是room.occupied这个冗余字段。它和student表里room_id的计数是重复的,好处是查空床位不用每次count(*),坏处是调宿、退宿时必须同步更新,漏一处就会出现「显示满员但实际有空床」的玄学。看到这个字段,就要在脑子里给它挂个警示牌,改业务时优先检查同步逻辑。

3. 核心模块拆解:一个入住请求从进入到写库的完整链路

3.1 登录与权限拦截:filter 里那道门

先看入口。所有管理端页面都要经过AuthFilter,它的职责单一:判断 session 里有没有登录用户,没有就重定向到登录页。这是最朴素也最有效的权限控制,比在每个 Controller 里判断一遍优雅得多。

public class AuthFilter implements Filter { // 白名单:登录页、静态资源不需要登录态 private static final Set<String> WHITE_LIST = Set.of("/login", "/static", "/css", "/js"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 白名单直接放行,否则登录页自己也会被拦 for (String prefix : WHITE_LIST) { if (uri.startsWith(request.getContextPath() + prefix)) { chain.doFilter(req, resp); return; } } Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录统一回登录页,避免用户看到 404 或裸数据 response.sendRedirect(request.getContextPath() + "/login"); return; } chain.doFilter(req, resp); } }

这段代码的关键在WHITE_LIST和contextPath的处理。contextPath是应用部署路径,如果项目不是部署在根路径下(比如http://host/dorm/),直接拿uri和/login比会永远不匹配,导致登录页也被拦截,形成死循环——这是新手最常踩的一个坑。另外loginUser这个 key 必须和登录成功时session.setAttribute的 key 完全一致,大小写都别错。

3.2 学生入住:业务校验写在 Service 而不是 Controller

入住是这个系统里最典型的写操作,涉及三张表的联动:更新学生room_id和status、房间occupied加一、可能还要写一条操作日志。这类逻辑必须收在 Service 层,Controller 只负责取参数和返回结果。

public class StudentService { private final StudentDao studentDao = new StudentDao(); private final RoomDao roomDao = new RoomDao(); /** * 学生入住 * @param studentId 学生主键 * @param roomId 目标房间主键 * @return 操作结果提示语 */ public String checkIn(Long studentId, Long roomId) { Student student = studentDao.findById(studentId); if (student == null) { return "学生不存在"; } // 已入住的不允许重复分配,防止 occupied 被加两次 if ("IN".equals(student.getStatus())) { return "该学生已在住,请先办理退宿"; } Room room = roomDao.findById(roomId); if (room == null) { return "房间不存在"; } // 床位已满直接拒绝,这里用 occupied 冗余字段快速判断 if (room.getOccupied() >= room.getCapacity()) { return "该房间床位已满"; } // 两表更新放在同一事务里,任一失败整体回滚 boolean ok = roomDao.increaseOccupied(roomId) && studentDao.bindRoom(studentId, roomId); if (!ok) { throw new RuntimeException("入住失败,事务回滚"); } return "入住成功"; } }

逐段说明:findById先做存在性检查,避免空指针;status判断是防重复入住的闸门,没有它occupied会被重复累加,这是最隐蔽的数据错误;床位判断用occupied >= capacity而不是等号,防止历史脏数据导致occupied已经超过容量时仍然放行;最后的&&短路结合事务,保证两表一致。

注意:increaseOccupied内部如果是update room set occupied = occupied + 1,就天然带并发保护;如果写成「先查再 set」,并发下会丢更新。看到这种写法要第一时间改掉。

3.3 条件分页查询:学生列表怎么不拖垮页面

列表页是使用频率最高的功能,学生一多就必须分页,还要支持按楼栋、班级、状态组合筛选。实现上分两步:先查总数,再查当前页数据,SQL 用limit控制偏移。

public class StudentDao extends BaseDao { // 动态拼条件的思路:用 StringBuilder 收集 where 片段,用参数列表收集占位符值 public List<Student> queryPage(String buildingName, String className, String status, int page, int pageSize) { StringBuilder sql = new StringBuilder( "select s.*, r.room_no from student s left join room r on s.room_id = r.id where 1=1"); List<Object> params = new ArrayList<>(); if (buildingName != null && !buildingName.isEmpty()) { sql.append(" and r.building_id = (select id from building where name = ?)"); params.add(buildingName); } if (className != null && !className.isEmpty()) { sql.append(" and s.class_name = ?"); params.add(className); } if (status != null && !status.isEmpty()) { sql.append(" and s.status = ?"); params.add(status); } // limit 的两个参数是偏移量和条数,不能直接拼 page 和 pageSize sql.append(" order by s.id desc limit ?, ?"); params.add((page - 1) * pageSize); params.add(pageSize); return queryList(sql.toString(), Student.class, params.toArray()); } }

参数说明:page从 1 开始计,pageSize建议 10~20,(page - 1) * pageSize就是偏移量。where 1=1是拼接动态条件的惯用写法,避免判断第一个条件要不要加and。left join room是为了在列表里直接显示房间号,省一次查询。queryList用反射把结果集映射成实体,模型类字段名必须和列别名一致,select s.*出来的列名要和Student属性对得上,对不上就返回 null,表现为「列表有行但字段全空」——这种黑匣子问题调起来很烦,记住先打印结果集元数据。

3.4 报修单状态流转:一个字段把业务串起来

报修模块的代码量不大,但状态流转是业务完整性的体现:学生提交(待处理)→ 宿管接单(处理中)→ 维修完成(已完成)。状态更新要做合法性校验,不能随意跳转。

public class RepairService { // 合法流转表:key 是当前状态,value 是允许的下一状态 private static final Map<String, Set<String>> FLOW = Map.of( "PENDING", Set.of("PROCESSING"), "PROCESSING", Set.of("DONE"), "DONE", Set.of() ); public String updateStatus(Long repairId, String targetStatus) { Repair repair = repairDao.findById(repairId); if (repair == null) { return "报修单不存在"; } Set<String> allowed = FLOW.getOrDefault(repair.getStatus(), Set.of()); if (!allowed.contains(targetStatus)) { return "非法的状态变更:" + repair.getStatus() + " -> " + targetStatus; } repairDao.updateStatus(repairId, targetStatus); return "状态已更新"; } }

这段代码的用意是把状态机显式化。很多同类项目直接update repair set status = ?,前端传什么就写什么,结果出现「已完成」被改回「待处理」的脏数据。用一张FLOW表把规则钉死,改需求时只动这一处,业务代码不用散落判断。

4. 跑起来:环境准备、导库与启动排错

4.1 环境清单与版本匹配

跑这类项目,版本对不上是最耗时的坑。下面这套组合是踩过几轮后比较稳的:

组件建议版本说明
JDK8 或 11源码若用了var或List.of,需要 11+
Maven3.6+负责拉依赖,注意配国内镜像
MySQL5.7 或 8.08.0 注意驱动包和时区参数
Tomcat8.5 或 9不要用 10,包名从javax换成jakarta会直接报错
IDEIntelliJ IDEA社区版即可,配合 Smart Tomcat 插件调试

MySQL 8.0 连接串必须带时区和 SSL 参数,否则启动时报时区错误:

# db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/dorm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=your_password

参数逐个解释:useUnicode和characterEncoding决定中文不乱码;serverTimezone解决 8.0 驱动的时区异常;useSSL=false关掉本地不必要的加密握手;allowPublicKeyRetrieval=true避免 8.0 默认认证方式下的连接拒绝。四个少一个都可能起不来,这套组合建议原样抄。

4.2 导库与数据初始化

拿到sql/dorm.sql后,别在图形化工具里点开复制粘贴执行,容易因为编码问题丢中文。走命令行更稳:

# 先建库并指定字符集,再导入表结构和数据 mysql -uroot -p -e "CREATE DATABASE dorm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p dorm < sql/dorm.sql # 导入后核对表和数据量,确认没导空 mysql -uroot -p -e "use dorm; show tables; select count(*) from student;"

第一句里的utf8mb4不要改成utf8,后者在部分版本下只支持三字节,名字里带生僻字或特殊符号会插入失败。第二句用重定向导入,比交互式执行快且不出编码岔子。第三句是验证,show tables应该看到前面说的五张核心表,count(*)应该有初始数据,如果两张表都空,说明脚本里只有结构没有insert,需要看脚本末尾有没有初始账号。

4.3 启动与第一次登录

IDEA 里导入 Maven 项目,等依赖下载完,配置 Tomcat 运行。这里有个高频操作顺序:先确认pom.xml里的打包方式是 war,再配置 Artifact 为war exploded,用 exploded 模式改 JSP 不用重启,调试效率高一截。

启动成功后访问http://localhost:8080/dorm/login,用sql脚本里的初始管理员账号登录。如果登录页能出但提示密码错误,先去数据库里看admin表的password字段是明文还是密文:明文就直接对照,密文则要确认登录逻辑用的是 MD5 还是 BCrypt,不匹配永远登不进去。这一步别猜,直接看LoginController里的比对代码。

5. 避坑排查:五条真实会卡住你的问题

5.1 中文乱码:从页面到数据库的整条链路都要管

现象:学生姓名在列表里显示成问号或方块,提交表单后存进库是乱码。

原因:乱码不是单点问题,请求编码、响应编码、数据库字符集、连接串四处任何一处不对都会出。常见的是 POST 请求体默认编码不是 UTF-8,且 JSP 页面没声明pageEncoding。

解决:按链路逐段设。JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;加一个编码过滤器或在web.xml里配CharacterEncodingFilter设encoding=UTF-8;连接串带上characterEncoding=utf8;建库用utf8mb4。四处对齐之后乱码基本绝迹。

5.2 数据库连接池耗尽:连接关了但没真关

现象:用一阵子后报Too many connections或请求一直卡在获取连接。

原因:BaseDao里getConnection拿到的连接,在finally里只close了ResultSet和Statement,漏了Connection,或者异常分支直接return跳过了关闭逻辑。

解决:用 try-with-resources 改写,让编译器保证关闭;或者引入 Druid/HikariCP 连接池统一管理。手工关连接这种写法在演示时没问题,一旦多人同时用就暴露,建议一开始就换池化方案,省得后面查连接泄漏查到怀疑人生。

5.3 调宿后床位数对不上:冗余字段没同步

现象:房间显示已满,但实际点进去只有两个人;或者反过来,明明住满了还能继续分配。

原因:room.occupied是冗余计数,入住时加了、退宿时忘了减,或者调宿时旧房间没减、新房间加了。业务分支一多就漏。

解决:把所有影响occupied的操作收口到RoomDao一个方法里,比如syncOccupied(roomId)内部按student表实时count回写,业务层只调这一个方法。牺牲一点性能换一致性,在这个数据量级完全划算。

5.4 分页参数被篡改:page 传负数直接 SQL 报错

现象:手动改 URL 把page=0或page=-1,后端抛异常或查出全表。

原因:(page - 1) * pageSize算出负偏移,limit -10, 10在 MySQL 里直接语法错误;pageSize不限制的话传个 100000 会把整表拉进内存。

解决:在 Controller 入口做参数兜底,page小于 1 归 1,pageSize夹在 1~100 之间。这类边界校验不写不影响正常使用,但一旦被触发就是 500,属于典型的「上线才炸」问题。

5.5 静态资源 404:过滤器把 css 也拦了

现象:登录页样式全丢,控制台一堆静态资源 404,但接口本身正常。

原因:AuthFilter的url-pattern配成了/*,把/static/**、/css/**也纳入拦截范围,未登录时被重定向到登录页,浏览器拿到的响应体是 HTML 而非 CSS。

解决:白名单里补齐静态资源前缀(前面AuthFilter代码里已经这么做了),或者把url-pattern收窄到只拦业务路径。改完记得清浏览器缓存,不然你会以为是代码没生效,白白多调半小时。

6. 二次开发:从能跑到好用的几条改造路径

项目跑通只是起点,真正体现功力的是怎么在它上面加东西。按投入产出比排,我一般会先动这三处。

第一处,密码存储换成加盐哈希。初始脚本里的管理员密码多半是明文或裸 MD5,MD5 已经可以被彩虹表反查。改造点集中在两处:注册/改密时用BCrypt.hashpw(raw, BCrypt.gensalt())存库,登录时用BCrypt.checkpw(raw, stored)比对。注意存量数据要写个一次性迁移脚本,或者登录时判断密文格式做平滑升级。代码量不超过二十行,但答辩时是加分项。

第二处,把报修状态流转扩展成带时间的工单。现在只有状态字段,加两个时间戳字段accept_time、finish_time,再在 Service 的状态变更处顺手写入当前时间,就能算出平均处理时长。前端加一列「处理耗时」,统计页做个按楼栋分组的平均时长排行,业务价值立刻不一样。改造只需要动repair表、RepairService和两个 JSP,边界清楚。

第三处,加一层操作日志。入住、退宿、调宿这类改变数据的操作,建议记一条operation_log:谁、什么时候、对哪个学生做了什么。实现上不用上 AOP 那么重,在 Service 的关键方法末尾插一条 insert 即可。好处是出了问题能回溯,比如床位对不上时,能查到是哪次操作漏了同步。

改造方向涉及文件难度收益
密码加盐哈希LoginController、AdminDao、迁移脚本低安全性明显提升
工单时间统计repair 表、RepairService、统计页中业务完整度提升
操作日志新增表、Service 各处中可追溯,便于排错
批量导入学生StudentController、POI 依赖中高真实场景刚需

改造后的验证方法别偷懒:每改一处,至少走一遍「新增→查询→修改→删除」的完整闭环,并顺手看一眼数据库里的冗余字段对不对。我见过太多人改完接口就以为完事,结果occupied早就飘了。

说个自己的教训。早些年接手这类系统,我习惯先跑起来再读代码,图快。后来在一个调宿功能上翻了车:页面显示成功,数据库里旧房间的床位数没减,学生的room_id却已经改了,等于凭空多出一张床位。那次排查花了整整一下午,最后发现是 Service 里少了一行同步调用。从那以后我每次动涉及冗余字段的逻辑,都强制先把「哪些表会被改、改完谁负责核对」写在纸上再动手。希望这些能帮到你,少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询