简介:一份基于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”,大多数人的第一反应是解压、导入、跑起来。但只做到这一步,等于把最好的练手机会扔掉了。SSM(Spring + 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, status
- student 表:id, user_id, student_no, name, gender, phone, room_id, bed_no, create_time
- dormitory 表:id, building_name, manager, phone
- room 表:id, dormitory_id, room_no, floor, capacity, occupied_num, detail
- repair 表:id, student_id, room_id, content, repair_time, status, reply
2.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段 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. 从数据库到 Java:SSM 三层架构下的核心功能实现
3.1 Mapper 层:写一个带条件查询的 MyBatis 映射
随便打开一个 SSM 毕设项目的 mapper 目录,你大概率会看到一堆 XML 文件,每个文件对应一个实体类的增删改查。我的建议是,不要把简单查询也塞在 XML 里。像selectById这种单表主键查询,用注解@Select写在 Mapper 接口上就够了;复杂一点的多表联查、动态排序、模糊搜索,再用 XML 写。这样既保留了 MyBatis 的优势,又不至于让 XML 文件膨胀到几百行。
下面这段代码是学生列表查询的 Mapper,它接受一个查询条件对象StudentQuery,里面包含姓名、学号、宿舍楼 id 三个可空字段:
public interface StudentMapper { List<Student> selectByCondition(@Param("query") StudentQuery query); }对应 XML:
<select id="selectByCondition" resultMap="StudentResultMap"> 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 test="query.name != null and query.name != ''"> AND s.name LIKE CONCAT('%', #{query.name}, '%') </if> <if test="query.studentNo != null and query.studentNo != ''"> AND s.student_no = #{query.studentNo} </if> <if test="query.dormitoryId != null"> AND d.id = #{query.dormitoryId} </if> </where> ORDER BY s.id DESC </select>逻辑说明:<where>标签会自动去掉第一个多余的 AND,<if>是 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。很多同学在这里踩坑:前端传studentNo,Form 类里写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:interceptor><mvc: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.xml | Maven 依赖和打包配置 | IDEA 无法识别依赖 |
| jdbc.properties | 数据库连接参数 | 启动找不到数据源 |
| mybatis-config.xml | MyBatis 全局配置 | 无法扫描 mapper |
| spring-mvc.xml | Controller 扫描和视图配置 | 请求 404 |
| spring-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.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dormitory_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456serverTimezone不设置的话,MySQL 8 会报 CST 和 UTC 时区错误。allowPublicKeyRetrieval=true是很多新手首次连接 MySQL 8 时遇到的坑,如果不加,发送方会因无法获取公钥而抛出Public Key Retrieval is not allowed。这两个参数看起来不起眼,却是整个项目能不能跑起来的关键。
4.2.2 MyBatis 映射与实体类别名
在spring-mybatis.xml中有这样一段:
<bean class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.dorm.mapper"/> </bean>mapperLocations指定了 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 9,Spring 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 里没有characterEncoding=utf8,以及 Tomcat 未设置请求编码。后者建议在 web.xml 中加入 CharacterEncodingFilter:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</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 层返回一个 List<Map>,前端用 ECharts 画饼图。现在很多宿舍管理系统课程设计都要求可视化界面,你提前把这个做进去,等于一个功能同时考核了 SQL 聚合、级联联合查询和前端图表库。
第三个技巧是验证事务是否真的生效。我见过很多同学把@Transactional加在 Controller 上,或者加在 Service 接口实现类的私有方法上,结果事务完全没有隔离效果。验证方法很直接:在assign方法中手动插入一条throw new RuntimeException("test"),然后在studentMapper.updateRoomId之前抛出,执行后看房间里occupied_num是否增加了。如果增加了,说明increaseOccupiedNum已经提交,事务没有回滚;如果没有增加,说明事务生效了。大多数时候是因为spring-mybatis.xml里没配置<tx:annotation-driven transaction-manager="transactionManager"/>,或者配置了但在 method 拦截下不匹配。这是一个能在答辩时实际演示的排错过程,比回答“事务是保证一致性”的桌面概念高出一个层次。
最后,如果你想把这个项目往更规范的工程靠,可以在 Mapper 查询中砍掉所有SELECT *,明确列名;给所有表加create_time和update_time字段,用 MySQLDEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动维护。这两点虽然不直接影响功能,但代码评审者通常会因此认为你很注意细节。做完这些之后,回到你解压的那个 zip,试着不看原来的业务代码,用第一性原理重新实现一次学生入住这条链路,你会发现那些在 java 面试里反复出现的动态代理、SqlSession 生命周期、Spring 容器初始化到底是怎么落地到这个宿舍管理系统里的。
本文还有配套的精品资源,点击获取