☰
Java学生宿舍管理系统实战:Spring Boot+MySQL从设计到部署
2026/9/28 1:21:44 网站建设 项目流程

简介:这是一套面向高校计算机专业学生与Java Web初学者、课程设计或毕业设计开发者的学生宿舍管理系统完整项目包,围绕住宿信息管理、宿舍与床位分配、日常行为记录、权限控制等场景,提供可运行的后端逻辑与数据库方案。压缩包共661个文件,约77.19MB,以400个js、46个css、28个less与28个scss等前端资源为主,辅以35个java源码、40个class、8个xml与8个html,并含2个sql脚本、5个mp4辅导视频、部署文档及两套适配MySQL5与MySQL8的工程包,覆盖MVC分层、DAO数据访问、Servlet与JSP交互等实现细节。目前已有1615人学习下载。读者可据此获得完整源码、数据库结构、部署指南与视频讲解,用于快速搭建环境、理解模块划分与排错思路,提升数据库设计与Java Web开发能力。

1. 从一份 Java 课设压缩包说起:学生宿舍管理系统到底要解决什么

每年毕业季,计算机相关专业的群里都会刷屏同一类文件——「基于 Java 的学生宿舍管理系统设计与实现(源代码+数据库+部署文档+辅导视频).zip」。很多人第一反应是「课设作业」,但真正做过一线开发的人会告诉你,这类系统的骨架和中小型管理后台几乎一模一样:用户角色分层、宿舍楼栋与床位建模、入住退宿的状态流转、数据增删改查、权限校验、报表导出。它之所以被反复选作课程设计案例,不是因为简单,而是因为它把 Java Web 开发里最核心的几件事全串了一遍。

这篇文章不讲空话,我会按一个真实可落地的思路,把「学生宿舍管理系统」从技术选型、数据库设计、核心模块编码,到部署上线、常见翻车点,一层层拆开。适合三类人:正在做 Java 课程设计、需要一套能跑通的完整方案的学生;想用一个小型项目练手 Spring Boot + MySQL 的转行者;以及需要快速交付一个宿舍管理内部工具的开发者。读完你应该能自己从零搭出一套能登录、能分配床位、能查空寝、能导出报表的系统,而不是只会改别人压缩包里的代码。

2. 技术选型与数据库设计:为什么这套组合最稳

2.1 后端框架选 Spring Boot 而不是原生 Servlet

很多人拿到课设第一反应是用 JSP + Servlet 硬写,理由是「老师只教了这个」。但从工程角度看,原生 Servlet 做宿舍管理系统会遇到几个绕不开的麻烦:每个请求都要手动解析参数、手动管理数据库连接、手动处理事务,代码量翻倍还容易出 bug。Spring Boot 把这些全部封装好,一个@RestController就能暴露接口,@Transactional就能保证退宿时床位状态和入住记录同时更新。

我一般推荐的组合是:Spring Boot 2.7.x(Java 8 兼容性最好,课设环境普遍是 JDK 8 或 11)+ MyBatis-Plus(比 JPA 更贴近 SQL,方便课设答辩时讲清楚每条语句)+ MySQL 8.0 + Thymeleaf 或 Vue 二选一。如果时间紧、只想快速出效果,用 Thymeleaf 服务端渲染,不用单独起前端工程;如果想让简历好看一点,用 Vue3 + Element Plus 前后端分离,但部署会多一个 Nginx 环节。

选型理由说白了就三条:生态成熟、资料多、出问题能搜到答案。宿舍管理系统这种业务不复杂但角色和状态多的项目,最怕的是框架本身出玄学问题,Spring Boot 的稳定性正好避开这个坑。

2.2 数据库表设计:五张核心表撑起整个系统

数据库是整个系统的地基,表设计错了后面全是血泪。宿舍管理系统的核心实体其实就五个:用户、角色、楼栋、宿舍、床位,再加一张入住记录表做状态流转。下面是我常用的建表 SQL,直接可以在 MySQL 8.0 里执行。

-- 用户表:学生和管理员共用,用 role 区分 CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '学号/工号,登录名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1宿管 2管理员', `gender` TINYINT DEFAULT 0 COMMENT '0男 1女', `phone` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 楼栋表 CREATE TABLE `building` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '如 1号楼', `gender_type` TINYINT NOT NULL COMMENT '0男生楼 1女生楼', `floor_count` INT NOT NULL DEFAULT 6, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='楼栋表'; -- 宿舍表 CREATE TABLE `dorm` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `building_id` BIGINT NOT NULL, `room_no` VARCHAR(20) NOT NULL COMMENT '如 302', `capacity` INT NOT NULL DEFAULT 4 COMMENT '可住人数', `occupied` INT NOT NULL DEFAULT 0 COMMENT '已住人数', PRIMARY KEY (`id`), UNIQUE KEY `uk_building_room` (`building_id`,`room_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍表'; -- 床位表:一个床位一条记录,状态流转的核心 CREATE TABLE `bed` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `dorm_id` BIGINT NOT NULL, `bed_no` INT NOT NULL COMMENT '床位号 1-4', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已入住 2维修中', PRIMARY KEY (`id`), UNIQUE KEY `uk_dorm_bed` (`dorm_id`,`bed_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='床位表'; -- 入住记录表:谁在哪个床位、什么时候入住退宿 CREATE TABLE `checkin_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `bed_id` BIGINT NOT NULL, `checkin_time` DATETIME NOT NULL, `checkout_time` DATETIME DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1在住 0已退宿', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_bed` (`bed_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入住记录表';

这段 SQL 有几个设计点值得说清楚。第一,bed表把床位拆成独立记录,而不是在dorm表里存一个「已住人数」字段就完事。原因是宿舍分配本质是「抢床位」,如果只存人数,两个人同时申请同一个宿舍时会出现超卖,拆成床位后可以用唯一索引 + 状态更新来保证一致性。第二,checkin_record表用status标记在住和已退宿,退宿时不删记录,保留历史,方便后面做住宿统计。第三,sys_user的密码字段长度给到 100,因为 BCrypt 加密后的字符串是 60 位,留足余量。

参数上,capacity默认 4 是本科宿舍常见配置,如果做研究生 2 人间或 6 人间,改这个值即可,床位初始化时按 capacity 循环插入。字符集统一用utf8mb4,避免学生姓名里有生僻字或 emoji 时插入失败。

2.3 角色权限怎么划分才不返工

宿舍管理系统的角色一般分三层:学生只能看自己的入住信息、申请换宿;宿管能管理本楼栋的床位、审批换宿;管理员能管所有楼栋、导入学生名单、导出报表。很多人一开始图省事只做「学生/管理员」两种角色,做到一半发现宿管要独立权限,回头改代码非常痛苦。

我的做法是在sys_user.role里预留三个值,权限校验用 Spring Security 或简单的拦截器 + 注解。如果课设时间紧,用拦截器判断 role 就够了,不必上完整的 Spring Security,否则配置量会拖慢进度。关键是角色字段和权限判断的入口要提前留好,后面加角色只是加分支,不用动表结构。

3. 核心模块编码:从登录到床位分配的最小闭环

3.1 登录与密码加密:别再用明文存密码

登录是整个系统的入口,也是最容易出安全问题的地方。课设里最常见的翻车是密码明文存数据库,答辩时被老师一问就尴尬。正确做法是用 BCrypt 加密,Spring Security 自带的BCryptPasswordEncoder就能用,不引入额外依赖。

@Service public class UserService { @Autowired private UserMapper userMapper; // BCrypt 编码器,强度默认 10,够用且性能可接受 private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); public User login(String username, String rawPassword) { // 1. 按用户名查用户,查不到直接返回 null,不暴露"用户不存在" User user = userMapper.selectByUsername(username); if (user == null) { return null; } // 2. 用 matches 比对明文和密文,BCrypt 每次加密结果不同但都能匹配 if (!encoder.matches(rawPassword, user.getPassword())) { return null; } return user; } public void register(User user, String rawPassword) { // 注册时加密存储 user.setPassword(encoder.encode(rawPassword)); userMapper.insert(user); } }

逻辑说明:matches方法内部会从密文中解析出盐值,再用同样的盐加密明文比对,所以同一个密码每次encode结果不同但都能验证通过。参数上,BCrypt 强度默认 10,加密一次约 50-100ms,登录场景完全够用;如果调到 12 以上,单次加密会超过 300ms,高并发下会拖慢登录接口,课设没必要。

注意:不要把encoder每次 new 出来,虽然 BCryptPasswordEncoder 是无状态的,但养成注入单例的习惯更好。另外登录失败时统一返回「用户名或密码错误」,不要分别提示,避免被枚举用户名。

3.2 床位分配:用事务和状态机避免超卖

床位分配是宿舍管理系统最核心也最容易出 bug 的模块。典型场景是:一个宿舍 4 个床位,两个学生同时点「申请入住」,如果代码写得随意,可能出现 5 个人住进 4 人间。解决办法是把「查空床位」和「更新床位状态」放在同一个事务里,并且用数据库的行锁或乐观锁保证原子性。

@Service public class BedService { @Autowired private BedMapper bedMapper; @Autowired private CheckinRecordMapper recordMapper; @Transactional(rollbackFor = Exception.class) public void assignBed(Long userId, Long dormId) { // 1. 查询该宿舍第一个空闲床位,FOR UPDATE 加行锁 Bed bed = bedMapper.selectFreeBedForUpdate(dormId); if (bed == null) { throw new BizException("该宿舍已满,请选择其他宿舍"); } // 2. 更新床位状态为空闲->已入住,带 status 条件防止并发覆盖 int updated = bedMapper.updateStatus(bed.getId(), 0, 1); if (updated == 0) { // 说明在查和改之间被别人抢了,事务回滚 throw new BizException("床位刚被占用,请重试"); } // 3. 写入住记录 CheckinRecord record = new CheckinRecord(); record.setUserId(userId); record.setBedId(bed.getId()); record.setCheckinTime(new Date()); record.setStatus(1); recordMapper.insert(record); } }

对应的 Mapper SQL 关键两条:

<!-- 查询空闲床位并加行锁,注意 FOR UPDATE 必须在事务中才生效 --> <select id="selectFreeBedForUpdate" resultType="Bed"> SELECT * FROM bed WHERE dorm_id = #{dormId} AND status = 0 ORDER BY bed_no ASC LIMIT 1 FOR UPDATE </select> <!-- 带旧状态条件的更新,返回影响行数用于判断是否抢到 --> <update id="updateStatus"> UPDATE bed SET status = #{newStatus} WHERE id = #{id} AND status = #{oldStatus} </update>

逻辑说明:FOR UPDATE会在事务提交前锁住查询到的行,其他事务查同一行会阻塞,从而避免两个请求拿到同一个床位。updateStatus里带status = oldStatus条件是第二道保险,即使锁没生效,更新影响行数为 0 也能让事务回滚。参数上,rollbackFor = Exception.class保证任何异常都回滚,不要只写默认的 RuntimeException,否则自定义的受检异常不会触发回滚。

这套写法在课设答辩时是加分项,因为它体现了对并发和数据一致性的理解,比单纯 CRUD 高一个层次。

3.3 退宿与换宿:状态流转要留后悔药

退宿看起来简单,把床位状态改回空闲、入住记录标记已退宿就行。但实际业务里有两个坑:一是退宿后床位可能马上被别人入住,历史记录要能追溯;二是换宿本质是「先退后住」,如果中间失败会导致学生无床可住。

我的做法是换宿接口里先执行退宿逻辑,再执行分配逻辑,整个过程包在一个事务里。如果新床位分配失败,事务回滚,学生还是住在原床位,不会出现「退了没地方住」的情况。退宿时checkin_record不删除,只把status改成 0、填上checkout_time,这样后面做「本学期住宿人数统计」时能按时间范围查。

@Transactional(rollbackFor = Exception.class) public void changeBed(Long userId, Long newDormId) { // 1. 查当前在住记录 CheckinRecord current = recordMapper.selectActiveByUser(userId); if (current == null) { throw new BizException("当前没有在住记录"); } // 2. 释放原床位 bedMapper.updateStatus(current.getBedId(), 1, 0); current.setStatus(0); current.setCheckoutTime(new Date()); recordMapper.updateById(current); // 3. 分配新床位,失败则整体回滚 assignBed(userId, newDormId); }

参数说明:selectActiveByUser查的是status = 1的记录,一个学生同时只能有一条在住记录,这个约束建议在业务层保证,数据库层可以加唯一索引(user_id, status)的变体,但 MySQL 不支持部分索引,所以用业务校验即可。

4. 部署上线:从本地跑通到别人能访问

4.1 本地环境搭建与依赖安装

部署文档里最常见的问题是「在我电脑上能跑」,换台机器就报错。根因通常是 JDK 版本、MySQL 版本、Maven 依赖没对齐。我一般把环境要求写死在 README 里,并用 Maven 的maven-compiler-plugin锁定编译版本。

# 1. 确认 JDK 版本,必须是 8 或 11,17 会有模块化问题 java -version # 2. 确认 Maven 版本 mvn -v # 3. 创建数据库并导入表结构 mysql -u root -p -e "CREATE DATABASE dorm_db DEFAULT CHARSET utf8mb4;" mysql -u root -p dorm_db < sql/schema.sql # 4. 修改 application.yml 里的数据库连接 # spring.datasource.url: jdbc:mysql://localhost:3306/dorm_db?useSSL=false&serverTimezone=Asia/Shanghai # spring.datasource.username: root # spring.datasource.password: 你的密码 # 5. 打包并运行 mvn clean package -DskipTests java -jar target/dorm-system-1.0.jar

逻辑说明:serverTimezone=Asia/Shanghai必须加,否则 MySQL 8.0 驱动会报时区错误,这是最高频的翻车点。useSSL=false在本地开发可以关掉,生产环境建议开启。-DskipTests跳过测试加速打包,课设阶段测试用例通常没写全,跳过能省时间。

参数上,数据库连接池用默认的 HikariCP 即可,maximum-pool-size默认 10,课设并发量完全够。如果部署到云服务器,把server.port改成 80 或 8080,注意安全组要放行对应端口。

4.2 打包成可执行 jar 与常见启动报错

Spring Boot 默认打包成可执行 jar,内嵌 Tomcat,java -jar直接跑。但有几个报错几乎每个人都会遇到,提前知道能省半天时间。

第一个是「Port 8080 was already in use」,说明端口被占用,用netstat -ano | findstr 8080(Windows)或lsof -i:8080(Linux/Mac)找到进程杀掉,或者改server.port。第二个是「Access denied for user 'root'@'localhost'」,密码错了或 MySQL 没启动,先确认mysql -u root -p能登录。第三个是「Table 'dorm_db.sys_user' doesn't exist」,schema.sql 没导入或导入了错误的库,重新执行导入命令。

如果要把系统部署到服务器让别人访问,最简方案是买一台云服务器,装 JDK 和 MySQL,把 jar 传上去nohup java -jar xxx.jar &后台运行。想更规范一点,用 Docker 打包,但课设阶段没必要,反而增加学习成本。

4.3 数据库备份与迁移的实用做法

部署文档里经常忽略备份,但宿舍数据一旦丢了就是事故。MySQL 自带mysqldump,一条命令就能导出整个库:

# 导出结构和数据 mysqldump -u root -p dorm_db > backup_$(date +%Y%m%d).sql # 只导出结构,用于初始化 mysqldump -u root -p --no-data dorm_db > schema.sql # 恢复 mysql -u root -p dorm_db < backup_20250101.sql

参数说明:--no-data只导表结构,适合放进项目作为初始化脚本;不加则连数据一起导,适合备份。如果数据量大,加--single-transaction避免锁表。课设阶段数据量小,直接全量导出即可。

5. 避坑与排查:那些让我熬夜的翻车现场

5.1 中文乱码:从数据库到页面的全链路排查

现象:学生姓名显示成问号或乱码。原因通常有三层:数据库字符集不是 utf8mb4、连接 URL 没指定编码、页面响应头编码不对。解决顺序是先查数据库SHOW CREATE TABLE sys_user看字符集,再查连接 URL 加characterEncoding=utf8,最后查 Spring Boot 的server.servlet.encoding.charset=utf-8。三层都对齐基本不会再乱。

5.2 事务不生效:方法内部调用是元凶

现象:退宿时床位状态改了但入住记录没更新,数据不一致。原因是在同一个类里用this.assignBed()调用带@Transactional的方法,Spring 的代理没生效。解决办法是把事务方法抽到另一个 Service 里,或者注入自己@Autowired private BedService self;用self.assignBed()调用。这个坑我踩过不止一次,记住事务注解只在外部调用时生效。

5.3 时间字段差 8 小时:时区配置漏了一处

现象:入住时间存进去是下午 3 点,查出来变成早上 7 点。原因是 JVM 时区和 MySQL 时区不一致。解决是在application.yml的 JDBC URL 加serverTimezone=Asia/Shanghai,同时在启动参数加-Duser.timezone=Asia/Shanghai。两处都配才能保证从数据库到 Java 对象时间一致。

5.4 分页查询总数不对:count 语句写错

现象:分页显示共 100 条,实际只有 20 条。原因是 MyBatis-Plus 的分页插件没配置,或者自定义 count 语句里带了LIMIT。解决是确认配置了PaginationInnerInterceptor,自定义 SQL 的 count 语句去掉 order by 和 limit。这个坑在列表页特别明显,答辩时被问到很尴尬。

5.5 静态资源 404:打包后路径变了

现象:本地跑页面样式正常,打成 jar 后 CSS 和 JS 全 404。原因是静态资源放在了src/main/webapp而不是src/main/resources/static。Spring Boot 打包成 jar 后不识别 webapp 目录,必须放 static 下。解决是把资源挪到resources/static,引用路径用/css/style.css这种绝对路径。

6. 进阶技巧:把课设做成能写进简历的项目

如果你不想只交个课设,而是想让这个宿舍管理系统在面试里能聊,有几个投入产出比很高的进阶点。第一是加操作日志,用 AOP 拦截所有增删改接口,记录操作人、时间、参数,存一张operation_log表。面试官问「你怎么做审计」时,这就是现成答案。第二是加数据导出,用 EasyExcel 把住宿名单导出成 Excel,比 POI 简单很多,代码量少还不容易 OOM。

// AOP 记录操作日志的核心切面 @Aspect @Component public class LogAspect { @Autowired private OperationLogMapper logMapper; // 拦截所有带 @Log 注解的方法 @Around("@annotation(logAnnotation)") public Object around(ProceedingJoinPoint point, Log logAnnotation) throws Throwable { long start = System.currentTimeMillis(); Object result = point.proceed(); // 记录方法名、耗时、参数 OperationLog log = new OperationLog(); log.setMethod(point.getSignature().getName()); log.setCost(System.currentTimeMillis() - start); log.setParams(Arrays.toString(point.getArgs())); log.setCreateTime(new Date()); logMapper.insert(log); return result; } }

参数说明:@Around能拿到方法执行前后的完整信息,point.proceed()是实际执行业务方法。注意日志表插入不要和业务方法共用事务,否则业务回滚日志也没了,可以在切面方法上加@Transactional(propagation = Propagation.REQUIRES_NEW)开新事务。

第三是加接口文档,用 SpringDoc 或 Knife4j,启动后自动生成 Swagger 页面,面试时直接打开给面试官看接口设计,比口头描述有说服力。第四是把项目部署到云服务器并配个域名,简历上写「在线可访问」,比「本地运行」高一个档次。

我自己做这类项目最大的教训是:不要一上来就追求功能全,先把登录、床位分配、退宿这条主链路跑通,再逐步加报表、日志、导出。主链路稳了,后面加功能都是锦上添花;主链路有并发 bug,功能再多也是空中楼阁。另外,数据库表设计阶段多花两小时想清楚状态流转,比编码阶段花两天改 bug 划算得多。希望帮到你。

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

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

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

立即咨询