简介:这是一套基于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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修单表';这段脚本里的几个关键决策要说清楚。password字段用了VARCHAR(100)而不是VARCHAR(20),因为BCrypt加密后的密文长度是60个字符左右,如果你按明文密码的长度去设计字段,存进去就会被截断,后面登录校验怎么比对都是false。check约束让数据库在底层兜底,防止「已住人数大于可住人数」这种脏数据出现——别小看这一行,它会在并发分配宿舍时帮你挡住一次大事故。
角色字段用了字符串常量而不是单独的权限表。这是刻意的取舍:学生宿舍管理系统只有两种角色,控制粒度也就是「学生权限」和「管理员权限」,为这个拆一套RBAC五张表属于过度设计。课设答辩时老师问「权限怎么设计的」,你回答「基于角色的访问控制,通过拦截器区分ADMIN和STUDENT的路由」完全够用,而且比背一套Spring Security配置更容易讲清楚。
2.3 查询可住房间:把多表关联写成Mapper
有了表结构,第一个要写的核心查询就是「还有哪些房间可以入住」。这个查询会用在管理员分配宿舍的列表页,它涉及房间表、楼栋表,并且要过滤掉满员和停用的房间。用MyBatis Plus的条件构造器可以写,但如果使用原生的XML Mapper,会更直观地体现SQL功底:
<select id="selectAvailableRooms" resultType="map"> 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 < 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.version,Maven插件用了默认版本,而IDEA的Project Structure里设置的SDK是另一个版本。
解决方法是三处统一:pom.xml里加上maven.compiler.source和maven.compiler.target,IDEA的Project Structure里把Project SDK和Project language level改成一致,Maven的JDK环境变量也要指向同一个目录。很多人在环境变量配置教程里看到「JAVA_HOME要配」,至少要知道它是给Maven、Tomcat这些外部工具找JDK用的,配错了就会出现这种版本错乱问题。
4.2 数据库查询到中文全部变成问号
现象:页面上显示的学生姓名和房间信息里所有中文字符都变成「???」。原因分两种:一种是JDBC连接串里缺少characterEncoding参数,另一种是建表时用了latin1字符集。解决方法是双管齐下——建立数据库时指定utf8mb4,JDBC连接串里加上characterEncoding=utf8&useUnicode=true。注意在MySQL 8.0以上版本,连接串还需要加serverTimezone参数,否则时间字段会报错(详见下一条)。这个坑建议写完建表脚本就顺手规避,不要等到答辩前一天再排查。
4.3 时间字段报错或相差8小时
现象:插入的报修时间比实际时间少8小时,或者数据库连接直接报「The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized」。原因是MySQL驱动版本过高时,对未明确指定的时区会报错;而如果应用服务器和数据库服务器时区不一致,查询出来的时间就会偏移8小时。
解决方法是两条同时做:JDBC连接串加上serverTimezone=Asia/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-data=true自定义开关来控制,写成一个配置项,不同场景启动时用不同的参数。
这个技巧做起来只有几十分钟,但它解决的是课设演示里最常见的一个尴尬瞬间:什么都准备好了,就是没数据,或者数据早就被折腾得七零八落。如果你的项目已经能跑通,我建议先花点时间把初始化这块加上,把答辩的稳定性前置到代码层,而不是靠现场手速去弥补。这条习惯我一直保留到现在,任何需要演示的临时环境,我都会留一条一条初始化入口,省去重复造数据的工夫。希望帮到你。
本文还有配套的精品资源,点击获取