
简介一份基于SSM框架的学生宿舍管理系统源码与数据库包面向计算机及相关专业毕业设计、课程设计、期末大作业场景也适合正在从零练习Java Web整合开发的初学者。项目经由导师指导并通过高分验收导入JDK与IDEA环境后即可运行调试。压缩包共591个文件约40.15MB包含Java源码、JSP页面、XML配置、JAR依赖包及SQL数据库脚本还附有项目说明文档与软件工具其中150个GIF动图可直观演示各功能模块的操作流程77个JAR库保障框架依赖完整SQL脚本让数据库一键初始化。业务模块覆盖管理员、宿舍分配、学生管理、班级管理、卫生检查与宿舍维修等典型场景controller类与页面分离便于跟随源码理解SSM整合开发流程也能看清controller、service、dao分层调用关系。目前已有541人学习适合直接作为毕设或课设的基础项目节省搭建与排错时间。1. 学生宿舍管理系统为什么值得自己写一遍拿到一个“基于SSM的学生宿舍管理系统源码数据库.zip”大多数人的第一反应是解压、导入、跑起来。但只做到这一步等于把最好的练手机会扔掉了。SSMSpring SpringMVC MyBatis在 java 后端开发的校园项目里依然是出现频率最高的组合之一很多 java 面试八股文里的 IOC、AOP、动态代理、数据库事务都能在这个项目里找到对应的代码位置。宿舍管理系统本身业务清晰学生、宿舍、入住、退宿、报修、卫生检查数据间关系不复杂但足够支撑一张 ER 图比图书管理多了“分配”这种带状态流转的逻辑适合用来理清数据库设计和三层架构。这篇文章不会假装我见过那个 zip 里的每一行代码但我会按大多数毕设项目的实际结构把前面提到的源码包应该长什么样、你拿到后怎么改、怎么讲清楚给面试官听完整走一遍。重点是如何从数据库表设计开始到 SpringMVC 接口再到 MyBatis 查询把一条入住流程串起来并解决运行时大概率会遇到的乱码、端口冲突、分页失效、事务不生效这些坑。2. 拆解 SSM 版本的宿舍管理系统模块划分与数据库设计2.1 先画清楚业务边界谁在用这个系统在动手看代码之前先把用户角色理清。常见的宿舍管理系统分为三种角色管理员、宿管员、学生。管理员维护楼栋和宿舍基础数据宿管员负责学生入住、退宿、报修处理、日常检查记录学生只能查看自己的宿舍信息、提交报修、查询水电明细。权限划分直接决定后续表结构和拦截器的写法。很多同学习惯上来就建十几张表我一般不建议。这个项目核心表五张就够用户表user、学生表student、宿舍楼表dormitory、宿舍表room和报修表repair。如果要做卫生检查或晚归记录再加两张表就好。表一多SSM 里的关联查询会变得很难维护尤其是 MyBatis 的 ResultMap 嵌套调试起来非常头疼。2.2 从 ER 图到建表 SQL关键字段和约束毕业设计文档要求画 ER 图其实数据库设计就是先定实体和关系。这里的关系是一个宿舍楼有多个宿舍一个宿舍有多个床位住宿人数上限一个学生只属于一个宿舍简化版一个学生可以提交多条报修记录。如图 2.1 的文字描述所示user 表id, username, password, role, statusstudent 表id, user_id, student_no, name, gender, phone, room_id, bed_no, create_timedormitory 表id, building_name, manager, phoneroom 表id, dormitory_id, room_no, floor, capacity, occupied_num, detailrepair 表id, student_id, room_id, content, repair_time, status, reply2.2.1 学生表和用户表为什么要分开我见过很多简化版把用户名密码直接写进学生表这样登录和业务逻辑会耦合。分开之后学生表里的 user_id 关联 user 表管理员和宿管员也可以共用 user 表只是 role 字段不同。这样做有个实际好处后续你写 Spring Security 或拦截器时只需要从 user 表查角色不用关心这个人到底是不是学生。建表 SQL 里要注意几点。room 表的 capacity 和 occupied_num 要设计成 int 类型occupied_num 默认 0。每次分配宿舍Java 业务层要同时在 student 表插入学生记录并把对应 room 的 occupied_num 加一这两步必须放在同一个事务里。很多版本直接把 occupied_num 用 count 查出来也可以但会在高并发时出现超卖床位的问题虽然毕设不需要扛并发但面试官可能会问。提前在代码里做好 select ... for update 或者乐观锁反而是一个加分项。2.2.2 给出可以直接执行的 MySQL 建表脚本CREATE DATABASE IF NOT EXISTS dormitory_system DEFAULT CHARACTER SET utf8mb4; USE dormitory_system; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT STUDENT, status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dormitory ( id INT NOT NULL AUTO_INCREMENT, building_name VARCHAR(50) NOT NULL, manager VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE room ( id INT NOT NULL AUTO_INCREMENT, dormitory_id INT NOT NULL, room_no VARCHAR(20) NOT NULL, floor INT NOT NULL, capacity INT NOT NULL, occupied_num INT NOT NULL DEFAULT 0, detail VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_dormitory_id (dormitory_id), CONSTRAINT fk_room_dormitory FOREIGN KEY (dormitory_id) REFERENCES dormitory (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里的外键约束在 MyBatis 关联查询时不会参与但能保证入库数据完整。我建议保留外键因为学生宿舍管理系统数据量小不用考虑外键对性能的影响。另外所有表都用 utf8mb4不是 utf8因为最新 MySQL 中 utf8 实际上是 utf8mb3遇到 emoji 或者特殊表情会报错。在数据库课程设计答辩时能说清楚这个编码差异比背概念有用得多。2.2.3 初始化数据把测试账号和宿舍数据预置好INSERT INTO user (username, password, role) VALUES (admin, MD5(123456), ADMIN), (house1, MD5(123456), MANAGER), (20230001, MD5(123456), STUDENT); INSERT INTO dormitory (building_name, manager, phone) VALUES (A栋, 张三, 13800001111), (B栋, 李四, 13800002222); INSERT INTO room (dormitory_id, room_no, floor, capacity, occupied_num, detail) VALUES (1, A101, 1, 4, 0, 四人间), (1, A102, 1, 4, 1, 四人间), (2, B201, 2, 6, 0, 六人间);密码用 MD5 虽然不安全但这是最老练的毕设风格。如果你在项目里引入了 MD5 工具类切记在注册功能里也要调用同一个加密方法否则会出现登录时密码对不上数据库的问题。我通常会在 user 表里加一个 salt 字段或者直接使用 MyBatis 的 typeHandler 对 password 做自动加密解密面试时提这个点会显得你懂安全存储。不过注意MD5 已经不是安全的加密算法如果答辩被问到可以回答说“用于演示生产环境需要 BCrypt 或 Spring Security”。当前 java 面试题库里不少八股文都会问到哈希加密和彩虹表这里正好是一次实践契机。3. 从数据库到 JavaSSM 三层架构下的核心功能实现3.1 Mapper 层写一个带条件查询的 MyBatis 映射随便打开一个 SSM 毕设项目的 mapper 目录你大概率会看到一堆 XML 文件每个文件对应一个实体类的增删改查。我的建议是不要把简单查询也塞在 XML 里。像selectById这种单表主键查询用注解Select写在 Mapper 接口上就够了复杂一点的多表联查、动态排序、模糊搜索再用 XML 写。这样既保留了 MyBatis 的优势又不至于让 XML 文件膨胀到几百行。下面这段代码是学生列表查询的 Mapper它接受一个查询条件对象StudentQuery里面包含姓名、学号、宿舍楼 id 三个可空字段public interface StudentMapper { ListStudent selectByCondition(Param(query) StudentQuery query); }对应 XMLselect idselectByCondition resultMapStudentResultMap SELECT s.id, s.user_id, s.student_no, s.name, s.phone, s.bed_no, r.id AS room_id, r.room_no, d.id AS dormitory_id, d.building_name FROM student s LEFT JOIN room r ON s.room_id r.id LEFT JOIN dormitory d ON r.dormitory_id d.id where if testquery.name ! null and query.name ! AND s.name LIKE CONCAT(%, #{query.name}, %) /if if testquery.studentNo ! null and query.studentNo ! AND s.student_no #{query.studentNo} /if if testquery.dormitoryId ! null AND d.id #{query.dormitoryId} /if /where ORDER BY s.id DESC /select逻辑说明where标签会自动去掉第一个多余的 ANDif是 MyBatis 的动态 SQL 核心所有查询条件都是可选的。CONCAT(%, #{query.name}, %)能防止 SQL 注入不能用字符串直接拼接。这里最常犯的错是把#{param}写成${param}前者会预编译成占位符后者是直接拼 SQL可以在 java 面试题里背一下这两个的区别实际项目里基本只用#{}。3.2 Service 层入住分配的业务规则和事务处理宿舍分配是业务逻辑最密集的地方。规则很简单宿舍未满员、学生未被分配过。但写起来要注意并发和事务。Service Transactional public class RoomAssignServiceImpl implements RoomAssignService { Autowired private StudentMapper studentMapper; Autowired private RoomMapper roomMapper; Override public boolean assign(String studentNo, Integer roomId, String bedNo) { Student student studentMapper.selectByStudentNo(studentNo); if (student null || student.getRoomId() ! null) { throw new BusinessException(学生不存在或已分配宿舍); } Room room roomMapper.selectByIdForUpdate(roomId); if (room null || room.getOccupiedNum() room.getCapacity()) { throw new BusinessException(宿舍已满或不存在); } // 修改房间已住人数 roomMapper.increaseOccupiedNum(roomId); // 给学生绑定宿舍和床位 studentMapper.updateRoomId(student.getId(), roomId, bedNo); return true; } }参数说明Transactional放在类上表示这个类的所有 public 方法都受事务控制。selectByIdForUpdate是查询宿舍时加了FOR UPDATE行级锁防止两个管理员同时操作同一边房间导致超住。increaseOccupiedNum的 SQL 用的是UPDATE room SET occupied_num occupied_num 1 WHERE id #{id}这是原子操作不需要先查后改。很多人刚开始写时会先查occupied_num然后在 Java 里加 1再 UPDATE 回去。这在单线程演示没问题但并发下会丢更新。如果你不想用FOR UPDATE也可以把 UPDATE 语句写成UPDATE room SET occupied_num occupied_num 1 WHERE id #{id} AND occupied_num capacity用乐观更新的思路。这个细节在面试时会非常亮眼因为大多数八股文只会说“事务隔离级别”而你能说出“在业务里怎么用锁”说明真的做过。3.3 Controller 层REST 接口和参数校验SSM 项目里 Controller 常见的坑是返回视图名字还是 JSON 数据。现在前后端不分离的毕设也有但更多是返回 JSON 给 Vue 或 jQuery 页面。用ResponseBody加Controller或者直接用RestController最稳妥。我在写宿舍管理系统时会统一返回一个 Result 对象Controller RequestMapping(/api/student) public class StudentController { Autowired private StudentService studentService; PostMapping(/assign) ResponseBody public Result assignRoom(RequestBody AssignForm form) { try { studentService.assign(form.getStudentNo(), form.getRoomId(), form.getBedNo()); return Result.success(分配成功); } catch (BusinessException e) { return Result.error(e.getMessage()); } } }这段代码的逻辑很直接。RequestMapping(/api/student)定义了类级别的访问前缀PostMapping(/assign)映射到 POST 请求。RequestBody会把前端传的 JSON 自动解析成 AssignForm 对象Form 类里的字段名必须和 JSON 里的 key 一致否则解析为 null。很多同学在这里踩坑前端传studentNoForm 类里写student_no结果一直拿不到值。SpringMVC 默认启用驼峰匹配但底层依赖 Jackson 的配置所以不要混用下划线和驼峰。3.3.1 使用拦截器统一做登录和角色校验这个项目的权限控制不需要 Spring Security一个 HandlerInterceptor 就能搞定。重写preHandle方法判断 session 中是否有登录用户以及当前请求的 URL 前缀和角色是否匹配。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return false; } // 只有管理员可以访问 /api/room 和 /api/user if (request.getRequestURI().contains(/api/room) || request.getRequestURI().contains(/api/user)) { User user (User) loginUser; if (!ADMIN.equals(user.getRole())) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } } return true; } }这个拦截器在 spring-mvc.xml 里注册需要配置拦截路径。通常写成mvc:interceptormvc:mapping path/api/**/bean class...//mvc:interceptor然后把登录接口/api/login排除掉。拦截器是 ssm 框架面试的高频考点只要你能说清楚“preHandle/postHandle/afterCompletion 的执行顺序”并指出它们分别在什么时候处理请求面试官基本就认可你真用过。4. 本地跑通源码包环境配置、导入和修改步骤4.1 拿到 zip 后的标准检查清单解压“源码数据库.zip”后第一件事不是双击 IDEA 导入而是先看目录结构。规范的项目至少包含src/main/java、src/main/resources、src/main/webapp以及一个dormitory_system.sql或一堆.sql文件。如果发现源码目录里只有.java文件没有 Maven 的pom.xml那说明这是一个 Eclipse 项目IDEA 导入时需要选择Import Project from external model并指定Eclipse或Maven。常见的构建产物列表如表 4.1 所示你可以根据文件是否缺失提前预判哪里会报错。文件/目录作用缺失后果pom.xmlMaven 依赖和打包配置IDEA 无法识别依赖jdbc.properties数据库连接参数启动找不到数据源mybatis-config.xmlMyBatis 全局配置无法扫描 mapperspring-mvc.xmlController 扫描和视图配置请求 404spring-mybatis.xml事务、SqlSessionFactory连接池无法启动web.xml前端控制器 DispatcherServlet 配置Tomcat 启动失败*.sql初始化数据登录无账号4.2 修改三个必须改的配置文件4.2.1 数据库连接jdbc.properties打开后在本地 MySQL 建库导数据然后修改连接地址。注意驱动的版本com.mysql.jdbc.Driver是 5.x 驱动com.mysql.cj.jdbc.Driver是 8.x 驱动如果你本机是 MySQL 8要改成后者并在 URL 后加参数jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/dormitory_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456serverTimezone不设置的话MySQL 8 会报 CST 和 UTC 时区错误。allowPublicKeyRetrievaltrue是很多新手首次连接 MySQL 8 时遇到的坑如果不加发送方会因无法获取公钥而抛出Public Key Retrieval is not allowed。这两个参数看起来不起眼却是整个项目能不能跑起来的关键。4.2.2 MyBatis 映射与实体类别名在spring-mybatis.xml中有这样一段bean classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.dorm.mapper/ /beanmapperLocations指定了 XML 映射文件的位置如果你把 XML 放在src/main/java目录下需要额外配置资源插件。最可靠的做法是放在src/main/resources/mapper/下。basePackage必须与你的 Mapper 接口所在包一致错一个字都会在启动时提示找不到 bean。建议核对一下代码包名大多数 zip 包里的包名是com.xxx.dorm或com.xxx.ssm不统一的话就全局替换。4.2.3 Maven 依赖版本统一打开 pom.xml重点看 spring、springmvc、mybatis 和 druid 的版本。SSM 是历史项目很多人还在用 Spring 4.x MyBatis 3.4配合 Tomcat 8 没问题。如果你本机安装的是 Tomcat 9 或 IDEA 自带的 Tomcat 9Spring 4 也能跑但 Java 版本不要超过 8。JDK 8 是兼容性最稳的JDK 11 以上运行 Spring 4 偶尔会出现IllegalAccessError。建议在 pom.xml 里把maven.compiler.source和maven.compiler.target固定为 1.8。有了 MyBatis 之后数据库增删改查操作都交给动态代理完成但仍然建议保留一个通用的 BaseMapper把分页和批量操作提前写好。例如使用 PageHelper 时在 Service 层调用PageHelper.startPage(pageNum, pageSize)后紧接着执行的一条 Mapper 查询会自动拼接LIMIT。注意 startPage 和查询之间不能有其他查询否则分页会作用到错误的 SQL 上这是使用 PageHelper 最常见的问题。4.3 启动时的三个典型排错方向4.3.1 端口被占用Tomcat 启动时报Port 8080 was already in use优先排查控制台是否残留了之前的进程。Windows 下用netstat -ano | findstr 8080找到 PID然后taskkill /PID 对应PID /F。如果你在 IDEA 中同时砸开过多个项目很容易遇到这个问题。也可以在 server.xml 里把启动端口改成 8090但改成非常用的端口会影响后续前端页面里写死的8080接口地址。4.3.2 中文乱码乱码有两个来源数据库连接 URL 里没有characterEncodingutf8以及 Tomcat 未设置请求编码。后者建议在 web.xml 中加入 CharacterEncodingFilterfilter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping这个过滤器是强制编码比你手动在 Controller 里写request.setCharacterEncoding要省心得多。在 ssm 框架中过滤器优先级高于拦截器因此能拦截到所有进入 DispatcherServlet 的请求。4.3.3 页面能打开但接口 404如果登录页正常但提交表单后 404一般是 SpringMVC 的DispatcherServlet没有捕获到带后缀的请求。检查 web.xml 中url-pattern是/还是*.do。如果前端 action 指向/api后端只处理*.do那永远匹配不上。统一改成/让 DispatcherServlet 拦截所有请求再去 spring-mvc.xml 里配置静态资源放行。静态资源放行是经常被问到的问题写法如下mvc:default-servlet-handler/ mvc:resources location/static/ mapping/static/**/这样 CSS、JS 和图片不会被前端控制器截胡页面样式才能正常加载。5. 让这个毕设项目更有说服力几个能直接用的进阶技巧写到这里项目已经能跑了但如果你想让代码在老师或面试官眼里更扎实一点下面这几个点不用大改却能显著提升项目的完整度。第一个技巧是增加一个操作日志表并用 Spring AOP 环绕通知自动记录。现在很多毕设的系统管理模块里都有一个“日志管理”但通常是手动在 Service 里调日志 service代码很脏。用 AOP 的话写一个注解Log(分配宿舍)再写一个切面拦截所有标注了该注解的方法在Around里把操作人、操作名、参数、执行耗时统一存库。代码量不大但能体现你对 Spring AOP 的理解这在 java 八股文里是高频考点。第二个技巧是把宿舍的学生人数增加统计接口。当前第 4 章提到occupied_num字段会在分配时更新。你可以写一个 DashboardController用一条聚合查询返回“每个楼栋入住率”SELECT d.building_name, COUNT(r.id) AS total_rooms, SUM(r.occupied_num) AS occupied_beds, SUM(r.capacity) AS total_beds, CONCAT(ROUND(SUM(r.occupied_num) / SUM(r.capacity) * 100, 2), %) AS occupancy_rate FROM dormitory d LEFT JOIN room r ON d.id r.dormitory_id GROUP BY d.id这段 SQL 在 Service 层返回一个 ListMap前端用 ECharts 画饼图。现在很多宿舍管理系统课程设计都要求可视化界面你提前把这个做进去等于一个功能同时考核了 SQL 聚合、级联联合查询和前端图表库。第三个技巧是验证事务是否真的生效。我见过很多同学把Transactional加在 Controller 上或者加在 Service 接口实现类的私有方法上结果事务完全没有隔离效果。验证方法很直接在assign方法中手动插入一条throw new RuntimeException(test)然后在studentMapper.updateRoomId之前抛出执行后看房间里occupied_num是否增加了。如果增加了说明increaseOccupiedNum已经提交事务没有回滚如果没有增加说明事务生效了。大多数时候是因为spring-mybatis.xml里没配置tx:annotation-driven transaction-managertransactionManager/或者配置了但在 method 拦截下不匹配。这是一个能在答辩时实际演示的排错过程比回答“事务是保证一致性”的桌面概念高出一个层次。最后如果你想把这个项目往更规范的工程靠可以在 Mapper 查询中砍掉所有SELECT *明确列名给所有表加create_time和update_time字段用 MySQLDEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动维护。这两点虽然不直接影响功能但代码评审者通常会因此认为你很注意细节。做完这些之后回到你解压的那个 zip试着不看原来的业务代码用第一性原理重新实现一次学生入住这条链路你会发现那些在 java 面试里反复出现的动态代理、SqlSession 生命周期、Spring 容器初始化到底是怎么落地到这个宿舍管理系统里的。本文还有配套的精品资源点击获取