基于Java Spring Boot的学生宿舍管理系统设计与实现
发布时间:2026/9/25 4:18:34来源:尧图网络
简介这是一套基于Java的学生宿舍管理系统源码采用SpringBoot框架与MySQL数据库搭建前端基于Bootstrap设计适合计算机相关专业学生用于毕业设计或课程设计。系统围绕管理员、宿管、学生三类角色设计覆盖学生管理、楼宇管理、宿舍管理、入住管理等核心模块并支持多类信息的导入导出业务逻辑清晰便于二次开发。资源包共661个文件压缩后72.37MB主要包含Java源码、class编译文件、前端js/css样式、HTML页面、SQL数据库脚本及mp4演示视频等其中还附有适配MySQL5和MySQL8的两份项目压缩包方便不同环境直接运行。导入导出功能在对应Controller类中有完整实现可对照学习。目前已有467人学习下载这套资源既能帮助快速理解宿舍管理系统的整体功能与代码组织也可作为答辩演示和项目实战的参考资料实用价值较高。1. 基于Java的学生宿舍管理系统为什么它是课设清单里的常青树如果你打开过近几年任意一届Java课程设计的选题列表大概率会在第几页看到「学生宿舍管理系统」这个名字。它之所以常年出现不是因为它新而是因为它刚好卡在了一个适合练手的复杂度区间有登录、有权限、有CRUD、有关联表查询还带那么一点事务和并发——难到能体现工作量又不至于让人在答辩前一周心态崩塌。这套系统覆盖的需求非常实在学生信息管理、宿舍分配、调宿退宿、报修处理每一块都能对应到现实业务里。这篇文章要做的是把这个题目拆成一条可复现的落地路径数据库怎么设计、权限怎么做、宿舍分配和报修这两个核心业务闭环怎么实现、答辩前哪些故障最容易把你绊倒。我采用的框架以Spring Boot为主但不依赖太偏门的东西底层的思路你换成Servlet或SSM一样能用。无论你是要做课程设计还是想给毕设找一个不太容易翻车的方向这条路径都能让你从「不知道从哪下手」直接走到「能跑通、能讲清楚」。2. 先拆库表再写代码5张表加一个角色字段工作量直接少一半很多做宿舍管理系统的新手一上来就急着搭项目、写登录结果写到宿舍分配的时候发现表结构撑不住需求只能回头重构。我一般会先干一件事把所有需求翻译成数据库表。这个动作看起来慢实际是把后期返工的活提前干完了。2.1 为什么是这5张表把业务对象理清楚先明确系统的核心角色学生和管理员。学生要查到自己的宿舍信息、发起报修管理员要管理房间、分配宿舍、处理报修。围绕这两个角色最少需要五张表才能把闭环跑起来用户表、楼栋表、房间表、入住记录表和报修单表。楼栋和房间拆成两张表而不是把楼栋名直接写在房间表里原因是查询和统计的时候很省事。比如「计算每栋楼的入住率」只需要房间表关联楼栋表按building_id分组即可。如果你把楼栋名字符串存在房间表里遇到「东区1号楼」改名你就要写一堆UPDATE语句去刷数据这就是典型的设计短路。入住记录表极其重要但它经常被课设模板忽略。很多人的做法是直接在用户表里存一个room_id就完事退宿时置空。这种做法的问题是历史不可追溯——宿舍管理系统天然有「谁什么时候住进了哪间宿舍、什么时候搬走」这条审计线索有入住记录表你才能在答辩时说清楚「调宿、退宿怎么管理的」。2.2 建表SQL把字段约束一次设对这里给出一个可以直接执行的MySQL建表脚本覆盖上述五张表CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, real_name VARCHAR(50) NOT NULL COMMENT 姓名, role VARCHAR(10) NOT NULL DEFAULT STUDENT COMMENT 角色: ADMIN/STUDENT, dorm_room_id BIGINT DEFAULT NULL COMMENT 当前所在房间ID管理员为NULL, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE dorm_building ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, building_no VARCHAR(20) NOT NULL COMMENT 楼栋编号如东区1号楼, floor_count INT NOT NULL DEFAULT 6 COMMENT 楼层数, PRIMARY KEY (id), UNIQUE KEY uk_building_no (building_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宿舍楼栋表; CREATE TABLE dorm_room ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, building_id BIGINT NOT NULL COMMENT 所属楼栋ID, room_no VARCHAR(20) NOT NULL COMMENT 房间号如301, capacity INT NOT NULL COMMENT 可住人数如4, occupied INT NOT NULL DEFAULT 0 COMMENT 已住人数必须小于等于capacity, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可住 0停用, PRIMARY KEY (id), KEY idx_building_id (building_id), CONSTRAINT chk_occupied CHECK (occupied 0 AND occupied capacity) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房间表; CREATE TABLE check_in_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, student_id BIGINT NOT NULL COMMENT 学生ID, room_id BIGINT NOT NULL COMMENT 房间ID, action VARCHAR(10) NOT NULL COMMENT IN入住 OUT退宿, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, operator_id BIGINT NOT NULL COMMENT 操作人ID, PRIMARY KEY (id), KEY idx_student_id (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入住记录表; CREATE TABLE repair_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, student_id BIGINT NOT NULL COMMENT 报修学生ID, room_id BIGINT NOT NULL COMMENT 房间ID, description VARCHAR(500) NOT NULL COMMENT 问题描述, status VARCHAR(20) NOT NULL DEFAULT PENDING COMMENT PENDING待处理 PROCESSING处理中 DONE已完成, handle_note VARCHAR(500) DEFAULT NULL COMMENT 处理结果备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, handle_time DATETIME DEFAULT NULL COMMENT 处理完成时间, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修单表;这段脚本里的几个关键决策要说清楚。password字段用了VARCHAR(100)而不是VARCHAR(20)因为BCrypt加密后的密文长度是60个字符左右如果你按明文密码的长度去设计字段存进去就会被截断后面登录校验怎么比对都是false。check约束让数据库在底层兜底防止「已住人数大于可住人数」这种脏数据出现——别小看这一行它会在并发分配宿舍时帮你挡住一次大事故。角色字段用了字符串常量而不是单独的权限表。这是刻意的取舍学生宿舍管理系统只有两种角色控制粒度也就是「学生权限」和「管理员权限」为这个拆一套RBAC五张表属于过度设计。课设答辩时老师问「权限怎么设计的」你回答「基于角色的访问控制通过拦截器区分ADMIN和STUDENT的路由」完全够用而且比背一套Spring Security配置更容易讲清楚。2.3 查询可住房间把多表关联写成Mapper有了表结构第一个要写的核心查询就是「还有哪些房间可以入住」。这个查询会用在管理员分配宿舍的列表页它涉及房间表、楼栋表并且要过滤掉满员和停用的房间。用MyBatis Plus的条件构造器可以写但如果使用原生的XML Mapper会更直观地体现SQL功底select idselectAvailableRooms resultTypemap SELECT r.id AS room_id, b.building_no, r.room_no, r.capacity, r.occupied, (r.capacity - r.occupied) AS free_beds FROM dorm_room r INNER JOIN dorm_building b ON r.building_id b.id WHERE r.status 1 AND r.occupied lt; r.capacity ORDER BY b.building_no, r.room_no /select这段SQL的筛选逻辑是status为启用、occupied小于capacity两者缺一不可。只判断occupied capacity而忽略status会出现「停用待改造的房间被分配给学生」这种低级事故。排序我用building_no加room_no是为了让前端列表按楼栋和房间号自然分组而不是按创建时间乱序排列——宿舍管理这种场景用户找房间是按位置找的不是按创建时间找的。MyBatis Plus的分页插件配置也提一句在配置类里注册PaginationInnerInterceptor传入DbType.MYSQL之后Page对象就能自动执行COUNT和LIMIT。如果你用的是原生JDBC分页就要手写LIMIT #{offset}, #{pageSize}注意计算offset时不要忘了减一这是新手最常见的分页翻车点。3. 登录鉴权与宿舍分配把三个业务闭环跑通表结构定下来之后剩下的工作就是按业务闭环逐个实现。这里我不打算把每一个Controller都贴出来而是挑选三个最容易卡住的环节登录与鉴权、宿舍分配的并发安全、报修工单的状态流转。这三个闭环能跑通系统的主体功能就完成百分之八十了。3.1 登录与鉴权BCrypt加密 拦截器别再存明文密码登录模块是每个系统都有的但很多课设模板里居然还在做明文密码比对。如果你答辩时被问到「密码安全怎么考虑的」回答「加了密但展示不出来加密过程」会当场扣分。我用Spring Security中的BCryptPasswordEncoder来做密码哈希它不需要引入完整的Spring Security框架单独引入spring-security-crypto依赖即可。Service public class AuthService { private final PasswordEncoder passwordEncoder new BCryptPasswordEncoder(); public SysUser login(String username, String rawPassword) { SysUser user userMapper.selectByUsername(username); if (user null) { throw new BusinessException(账号不存在); } if (user.getStatus() ! 1) { throw new BusinessException(账号已被禁用); } if (!passwordEncoder.matches(rawPassword, user.getPassword())) { throw new BusinessException(密码错误); } return user; } }matches方法的第一个参数是用户输入的明文密码第二个参数是数据库里存的BCrypt密文。BCrypt的特点是每次哈希的结果都不一样所以千万不要用「先加密再比较密文是否相等」的方式去校验而是要调用matches方法它内部会把盐从密文中解析出来重新计算。这一点在答辩时经常被追问能说清楚就是加分项。登录成功后把用户对象塞进HttpSession然后在Spring MVC的拦截器里做路由级的权限控制。拦截器是比在Controller里重复写if判断更优雅的方案Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); SysUser loginUser session null ? null : (SysUser) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } if (request.getRequestURI().startsWith(/admin) !ADMIN.equals(loginUser.getRole())) { response.sendRedirect(/403); return false; } return true; } }这里有一个细节request.getSession(false)传了false意思是「没有就返回null不要新建一个Session」。如果不传这个参数访问静态资源或AJAX轮询接口时会被强制创建一堆无效的Session白占服务器内存。拦截规则上把管理员专属接口统一挂到/admin前缀下学生接口挂在/student前缀下拦截器就只需要判断两件事是否登录、角色是否匹配。这种基于URL前缀的约定式权限管理代码量最少也最容易向答辩老师解释。3.2 宿舍分配并发是最大陷阱条件更新才是兜底宿舍分配是整个系统里最容易看起来「跑通了但实际是错的」的功能。先看常规逻辑查询房间剩余床位剩余大于0把学生宿舍ID改成该房间房间已住人数加一。这个逻辑在单用户操作时没有任何问题但一旦两个管理员同时给两名学生分配最后一间宿舍的最后一个床位两个请求都查到剩余1然后都执行入住最终房间的occupied会变成2超过capacity。解决思路分三层。第一层是给service方法加Transactional保证房间人数更新和学生宿舍ID更新在一个事务里要么都成功要么都失败。第二层是把「增加已住人数」做成条件更新让数据库来校验剩余床位。第三层才是在极端场景下引入分布式锁课设阶段完全用不到。Transactional(rollbackFor Exception.class) public void assignRoom(Long studentId, Long roomId, Long operatorId) { int updated dormRoomMapper.increaseOccupied(roomId); if (updated ! 1) { throw new BusinessException(该房间已满员请选择其他房间); } SysUser student userMapper.selectById(studentId); if (student null || !STUDENT.equals(student.getRole())) { throw new BusinessException(学生不存在); } if (student.getDormRoomId() ! null) { throw new BusinessException(该学生已分配宿舍如需调整请先退宿); } userMapper.updateDormRoomId(studentId, roomId); checkInRecordMapper.insert(studentId, roomId, IN, operatorId); }对应的SQL是这条UPDATE dorm_room SET occupied occupied 1 WHERE id #{roomId} AND occupied capacity AND status 1这条UPDATE语句的巧妙之处在于把并发校验下推到数据库数据库的行锁保证了两个事务操作同一行时是串行的第二个事务执行时occupied已经被第一个事务改成满员了条件occupied capacity不成立所以影响行数是0代码里的updated ! 1就会抛异常并回滚。这是比「先查后更」更可靠的做法也是这道题在答辩时最能体现水平的点。入住记录、学生宿舍ID、房间人数这三步在同一个事务里才是一条完整的分配链路。注意在事务里先查学生再更新房间可能导致锁顺序不一致引发死锁更稳妥的顺序是先更新房间拿到房间行锁再查学生。我在上面的代码里就是这个顺序。死锁问题在课设数据量下很少出现但答辩老师如果追问你能说出「房间锁优先」这个策略印象分会高不少。3.3 报修工单状态机比一堆if else更容易扩展报修功能看起来普通但它是系统里唯一一个涉及状态流转的模块。状态有三个待处理PENDING、处理中PROCESSING、已完成DONE。最笨的写法是在Controller里接收一个status参数直接UPDATE这样做的隐患是状态可以任意跳转——管理员可以把DONE改回PENDING逻辑就乱了。我习惯在Service里把状态流转收口成几个明确的方法Transactional public void acceptRepair(Long orderId, Long operatorId) { RepairOrder order repairMapper.selectById(orderId); if (order null) { throw new BusinessException(工单不存在); } if (!PENDING.equals(order.getStatus())) { throw new BusinessException(只有待处理工单才能受理); } repairMapper.updateStatus(orderId, PROCESSING, null, null); } Transactional public void finishRepair(Long orderId, String handleNote, Long operatorId) { RepairOrder order repairMapper.selectById(orderId); if (order null) { throw new BusinessException(工单不存在); } if (!PROCESSING.equals(order.getStatus())) { throw new BusinessException(只有处理中的工单才能完成); } repairMapper.updateStatus(orderId, DONE, handleNote, new Date()); }这样设计的收益是状态机的合法路径被硬编码在方法里非法流转直接抛异常。如果以后要加「已驳回」状态只需要增加对应的方法和判断条件不需要改动已有的受理和完成逻辑。每个方法先查再判再更新三步走逻辑完全透明。updateStatus方法里同时更新handle_note和handle_time注意这两个字段在受理阶段要传null否则会把上一轮的处理信息覆盖成空。报修单的查询列表还有一个隐蔽的问题学生只能看到自己的报修单所以查询方法里必须带上当前登录学生的ID而不是用「传一个studentId参数进来」由前端决定。前端传参意味着学生把studentId改成别人的就能看到他人的报修记录这就是典型的横向越权。正确做法是从Session里拿当前用户ID再传给Mapper。4. 答辩前必查的避坑清单并发入住、横向越权、中文乱码和JDK版本到了这里系统的核心功能已经能跑了。但根据我见过的大量翻车案例真正让课设在答辩现场出洋相的往往不是业务逻辑而是几个看起来很基础的配置问题。这一章把最高频的五个坑列出来每一条都按照现象、原因、解决的顺序写你可以对照着逐项排查。4.1 启动报错源发行版17需要目标发行版17现象项目启动或打包时Maven控制台输出「java: 警告: 源发行版 17 需要目标发行版 17」然后编译失败。原因几乎都是系统默认JDK版本和项目编译级别不一致——你电脑装的是JDK17但pom.xml里没有显式声明java.versionMaven插件用了默认版本而IDEA的Project Structure里设置的SDK是另一个版本。解决方法是三处统一pom.xml里加上maven.compiler.source和maven.compiler.targetIDEA的Project Structure里把Project SDK和Project language level改成一致Maven的JDK环境变量也要指向同一个目录。很多人在环境变量配置教程里看到「JAVA_HOME要配」至少要知道它是给Maven、Tomcat这些外部工具找JDK用的配错了就会出现这种版本错乱问题。4.2 数据库查询到中文全部变成问号现象页面上显示的学生姓名和房间信息里所有中文字符都变成「???」。原因分两种一种是JDBC连接串里缺少characterEncoding参数另一种是建表时用了latin1字符集。解决方法是双管齐下——建立数据库时指定utf8mb4JDBC连接串里加上characterEncodingutf8useUnicodetrue。注意在MySQL 8.0以上版本连接串还需要加serverTimezone参数否则时间字段会报错详见下一条。这个坑建议写完建表脚本就顺手规避不要等到答辩前一天再排查。4.3 时间字段报错或相差8小时现象插入的报修时间比实际时间少8小时或者数据库连接直接报「The server time zone value Öйú±ê׼ʱ¼ä is unrecognized」。原因是MySQL驱动版本过高时对未明确指定的时区会报错而如果应用服务器和数据库服务器时区不一致查询出来的时间就会偏移8小时。解决方法是两条同时做JDBC连接串加上serverTimezoneAsia/Shanghai项目启动前确认操作系统时区是东八区。另外不要为了省事在Java代码里手动给LocalDateTime加减8小时那是把数据弄脏的典型做法。时区问题应该由连接串统一解决代码里不该出现任何时区换算逻辑。4.4 学生能改别人的宿舍信息现象学生登录系统后把浏览器的请求参数改了就能查看甚至修改其他同学的宿舍信息。原因是所有查询和更新都直接用了前端传过来的ID没有做归属校验。解决方法是前面提到的原则学生相关的操作一律从Session取当前用户ID后端只认Session不认参数。如果你发现代码里出现了「从request.getParameter拿studentId然后去查数据」的写法直接改成从登录态里取。权限漏洞在答辩时被老师点到比功能做不出来扣分还狠。4.5 房间只剩一个床位但两个学生同时入住成功现象演示时老师要求同时点两个分配按钮结果满员房间的occupancy变成了capacity加一。原因是只做了「先查再更」的方式没有利用数据库的条件更新。这也就是第3.2节里那条UPDATE语句的价值所在——坚持用UPDATE with condition做并发兜底。如果你图省事把「查剩余、改房间」分成两条SQL裸奔这条踩坑记录早晚会轮到你的项目。5. 启动时自动灌入测试数据答辩演示不慌的最后一招最后一层技巧要解决一个教学场景特有的尴尬答辩当天老师可能要求你用新机器、新数据库跑一遍项目。结果一启动管理员账号是空的宿舍列表是空的演示从「录入数据」开始五分钟就浪费了观感还极差。解决办法是让项目在启动时自动写入初始化数据只要项目能连上数据库跑起来就是一套可演示的完整系统。Spring Boot的CommandLineRunner接口可以做到这件事在应用启动完成后、对外提供请求之前执行一段初始化逻辑。我通常把这段代码放在单独的组件里只负责灌入管理员账号、两栋宿舍楼和若干测试学生。核心实现是判断数据是否存在不存在才插入避免每次启动都重复灌入Component public class DataInitializer implements CommandLineRunner { Resource private UserMapper userMapper; Resource private DormBuildingMapper dormBuildingMapper; Resource private PasswordEncoder passwordEncoder; Override public void run(String... args) { initAdminUser(); initBuildings(); initStudents(); } private void initAdminUser() { Long count userMapper.countByRole(ADMIN); if (count 0) { return; } SysUser admin new SysUser(); admin.setUsername(admin); admin.setPassword(passwordEncoder.encode(123456)); admin.setRealName(系统管理员); admin.setRole(ADMIN); userMapper.insert(admin); log.info(已初始化管理员账号默认密码 123456请登录后尽快修改); } }这段代码的逻辑核心是「幂等初始化」不判断用户是否为空就直接插入会导致每次重启项目都往数据库里塞一条新管理员加了count查询之后只有第一次启动才会灌数据。密码用passwordEncoder.encode而不是明文和前面登录校验呼应保证初始化数据和正常业务走的是同一套加密逻辑。初始化楼栋和测试学生时也有讲究测试学生最好直接关联到具体的房间这样演示时打开学生列表就能看到完整的宿舍信息而不必现场一步步分配。另外建议在resource目录下的application.yml里通过spring.profiles.active配置切换开发环境与演示环境的初始化行为——开发环境每次都重置数据演示环境保留上一次的操作痕迹。比如你可以用dorm.init-datatrue自定义开关来控制写成一个配置项不同场景启动时用不同的参数。这个技巧做起来只有几十分钟但它解决的是课设演示里最常见的一个尴尬瞬间什么都准备好了就是没数据或者数据早就被折腾得七零八落。如果你的项目已经能跑通我建议先花点时间把初始化这块加上把答辩的稳定性前置到代码层而不是靠现场手速去弥补。这条习惯我一直保留到现在任何需要演示的临时环境我都会留一条一条初始化入口省去重复造数据的工夫。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网