恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java学生宿舍管理系统毕设:Spring Boot+Vue全流程与避坑指南
首页
资讯中心
/
Java学生宿舍管理系统毕设:Spring Boot+Vue全流程与避坑指南
Java学生宿舍管理系统毕设:Spring Boot+Vue全流程与避坑指南
发布时间:2026/10/9 1:57:56
简介这份资源是面向高校计算机相关专业学生与Java Web初学者的一份学生宿舍管理系统毕业设计文档以塔里木大学宿舍管理为背景针对传统人工、单机管理方式效率低、数据易错易丢失等问题给出了一套基于B/S模式的Web化解决方案。系统采用JSPMySQL技术栈划分系统管理员、宿舍管理员和学生三类角色涵盖用户登录、学生注册、学生与管理员管理、宿舍管理、组合查询及宿舍管理员管理等核心功能。文档完整梳理了J2EE、JSP、B/S架构、MySQL、TOMCAT等关键技术并依次展开需求分析、业务流程分析、功能模块设计、数据库概念与逻辑结构设计、系统实现及功能测试等章节目录结构清晰便于读者按模块学习与参考。资源包为1个docx文件约1.44MB已有204人学习适合作为课程设计或毕业设计的参考范本帮助读者快速理解系统整体架构与开发流程。1. 从一份“学生宿舍管理系统”说起Java 毕设项目到底该怎么落地很多 Java 方向的同学在选毕设题目时都会碰到“学生宿舍管理系统”这个经典选题。它看起来简单真动手才发现宿舍楼栋、房间、床位、学生、入住退宿、调宿、报修、晚归登记、访客记录这些实体之间的关系比想象中复杂得多。更现实的问题是答辩老师不会只看你跑起来一个界面他会追问床位状态怎么保证不冲突一个学生同时被分到两个房间怎么办退宿后床位怎么释放这些才是这个题目真正的技术含量所在。这篇笔记面向三类人正在做 Java 毕设、需要一套能跑通且经得起追问的方案的应届生想用 Spring Boot Vue 练手一个完整 CRUD 业务系统的初中级开发者以及需要快速搭出宿舍管理原型、后续再扩展成后勤系统的工程同学。我会按“需求拆解 → 数据库设计 → 后端接口 → 前端联调 → 避坑 → 进阶”的顺序把基于 Java 的学生宿舍管理系统设计与实现讲透重点放在那些真正会翻车的地方而不是堆一堆用不上的功能。2. 需求拆解与数据库设计先把实体关系理清楚再写代码2.1 宿舍管理系统的核心实体与关系做这个系统最容易犯的错是一上来就打开 IDEA 建 Controller。我的习惯是先在纸上画实体关系。核心实体其实就六个楼栋building、房间room、床位bed、学生student、入住记录checkin_record、报修单repair_order。其中床位是连接房间和学生的关键中间实体很多同学把床位直接做成房间的一个字段结果一个四人间就没法单独管理每个床位了。关系上一个楼栋有多个房间一个房间有多个床位一个床位在某一时间段内只能被一个学生占用一个学生同一时间只能占用一个床位。这个“同一时间”是重点意味着入住记录必须带入住时间和退宿时间而不是简单在床位表上挂一个 student_id。用时间区间来表达占用关系才能支持历史查询和调宿记录。提示如果答辩老师问“为什么不用床位表直接存学生ID”你就回答无法保留历史入住记录也无法处理调宿和退宿后的床位释放这是设计上的硬伤。2.2 建表 SQL 与字段说明下面是我一般会用的建表脚本MySQL 8.0 语法字段命名用下划线方便 MyBatis-Plus 映射。注意 bed 表里不存 student_id占用关系全部放在 checkin_record 里。-- 楼栋表 CREATE TABLE building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 楼栋名称如1号楼, floor_count INT NOT NULL DEFAULT 6 COMMENT 楼层数, gender_type TINYINT NOT NULL DEFAULT 0 COMMENT 0混合 1男生 2女生, remark VARCHAR(200) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房间表 CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL, room_no VARCHAR(20) NOT NULL COMMENT 房间号如301, floor_no INT NOT NULL, bed_count INT NOT NULL DEFAULT 4, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可用 0停用, UNIQUE KEY uk_building_room (building_id, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 床位表 CREATE TABLE bed ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, bed_no VARCHAR(10) NOT NULL COMMENT 床位号如A/B/C/D, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2维修, UNIQUE KEY uk_room_bed (room_id, bed_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL, name VARCHAR(30) NOT NULL, gender TINYINT NOT NULL COMMENT 1男 2女, college VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, UNIQUE KEY uk_stu_no (stu_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 入住记录表核心用时间区间表达占用 CREATE TABLE checkin_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, bed_id BIGINT NOT NULL, checkin_time DATETIME NOT NULL, checkout_time DATETIME DEFAULT NULL COMMENT NULL表示仍在住, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在住 0已退宿, KEY idx_bed_status (bed_id, status), KEY idx_student_status (student_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得说清楚。checkin_record 上的两个联合索引不是随便加的idx_bed_status 用来快速判断某个床位当前是否被占用idx_student_status 用来判断某个学生当前是否已有床位。这两个查询在分配床位和调宿时是高频操作没有索引在数据量上千后会明显变慢。status 字段和 checkout_time 是冗余的目的是避免每次都用checkout_time IS NULL去判断查询更直观代价是要保证两者一致这个一致性放在 Service 层用事务保证。2.3 用 MyBatis-Plus 根据实体类反向生成建表语句热搜里经常有人问“MyBatis-Plus 怎么根据 Java 实体类生成建表 SQL”。MyBatis-Plus 本身不提供 DDL 生成但它的代码生成器Generator能根据表生成实体反过来要自己写。我一般用反射读实体注解拼 SQL适合快速验证表结构。下面是一个最小实现public class TableSqlGenerator { // 读取实体类上的 TableName 和字段上的 TableField拼出 CREATE TABLE public static String generate(Class? entityClass) { TableName tableName entityClass.getAnnotation(TableName.class); String table tableName ! null ? tableName.value() : entityClass.getSimpleName(); StringBuilder sb new StringBuilder(CREATE TABLE table (\n); for (Field f : entityClass.getDeclaredFields()) { // 跳过 serialVersionUID 和静态字段 if (Modifier.isStatic(f.getModifiers())) continue; String col camelToUnderline(f.getName()); String type mapJavaTypeToSql(f.getType()); sb.append( ).append(col).append( ).append(type).append(,\n); } sb.setLength(sb.length() - 2); // 去掉最后一个逗号 sb.append(\n);); return sb.toString(); } private static String camelToUnderline(String s) { StringBuilder r new StringBuilder(); for (char c : s.toCharArray()) { if (Character.isUpperCase(c)) r.append(_).append(Character.toLowerCase(c)); else r.append(c); } return r.toString(); } private static String mapJavaTypeToSql(Class? t) { if (t Long.class || t long.class) return BIGINT; if (t Integer.class || t int.class) return INT; if (t String.class) return VARCHAR(255); if (t Date.class || t LocalDateTime.class) return DATETIME; return VARCHAR(255); } }这段代码的逻辑是遍历实体字段把驼峰转下划线把 Java 类型映射成 SQL 类型。参数上要注意两点一是TableId标注的主键要额外拼PRIMARY KEY AUTO_INCREMENT上面为了简洁省略了实际用的时候要补二是TableField(exist false)的字段要跳过否则会生成多余列。这个工具只适合开发期快速建表生产环境还是老老实实维护 SQL 迁移脚本别指望它处理索引和外键。3. 后端接口实现床位分配、退宿与调宿的事务处理3.1 床位分配接口的并发安全写法床位分配是这个系统里唯一真正有并发风险的地方。两个管理员同时给两个学生分配同一个床位如果不加控制就会产生两条 status1 的入住记录。常见做法有三种数据库唯一索引、悲观锁、乐观锁。我一般用“唯一索引 事务”兜底简单可靠。思路是给 checkin_record 加一个“在住床位唯一”的约束。MySQL 不支持部分索引所以用一个技巧增加一个冗余列active_bed_id在住时等于 bed_id退宿时置为 NULL然后对它加唯一索引。NULL 不参与唯一性判断正好满足需求。ALTER TABLE checkin_record ADD COLUMN active_bed_id BIGINT DEFAULT NULL; CREATE UNIQUE INDEX uk_active_bed ON checkin_record(active_bed_id);Service 层分配床位的核心逻辑Transactional(rollbackFor Exception.class) public void assignBed(Long studentId, Long bedId) { // 1. 校验学生当前没有在住记录 Long cnt checkinRecordMapper.selectCount( new LambdaQueryWrapperCheckinRecord() .eq(CheckinRecord::getStudentId, studentId) .eq(CheckinRecord::getStatus, 1)); if (cnt 0) throw new BizException(该学生已有床位请先退宿或调宿); // 2. 校验床位空闲 Bed bed bedMapper.selectById(bedId); if (bed null || bed.getStatus() ! 0) throw new BizException(床位不可用); // 3. 插入入住记录active_bed_id 触发唯一索引冲突则抛异常回滚 CheckinRecord record new CheckinRecord(); record.setStudentId(studentId); record.setBedId(bedId); record.setActiveBedId(bedId); record.setCheckinTime(new Date()); record.setStatus(1); checkinRecordMapper.insert(record); // 4. 更新床位状态 bed.setStatus(1); bedMapper.updateById(bed); }这里的关键是第 3 步如果两个线程同时插入同一个 bedId唯一索引会让其中一个抛 DuplicateKeyException事务回滚前端收到“床位已被占用”。这比先查再插的写法安全得多因为“先查再插”之间存在时间窗口。参数上Transactional的 rollbackFor 一定要写 Exception.class否则默认只回滚 RuntimeException业务异常如果是受检异常就不会回滚这是血泪经验。3.2 退宿与调宿状态流转不能乱退宿的逻辑看起来简单其实有个坑退宿时要同时做三件事——把入住记录的 status 改成 0、checkout_time 设为当前时间、active_bed_id 置为 NULL然后把 bed 的 status 改回 0。这三步必须在一个事务里否则中途失败会出现“床位显示空闲但记录还在住”的脏数据。调宿本质上是“退宿 重新分配”但要在一条记录链上体现。我的做法是把原记录退宿再插入一条新记录两条记录通过一个transfer_group字段关联方便追溯。不要直接改原记录的 bed_id那样历史就丢了答辩时老师一问“这个学生之前住哪”就答不上来。Transactional(rollbackFor Exception.class) public void transferBed(Long studentId, Long newBedId) { CheckinRecord old checkinRecordMapper.selectOne( new LambdaQueryWrapperCheckinRecord() .eq(CheckinRecord::getStudentId, studentId) .eq(CheckinRecord::getStatus, 1)); if (old null) throw new BizException(该学生当前无床位); // 退宿原床位 old.setStatus(0); old.setCheckoutTime(new Date()); old.setActiveBedId(null); checkinRecordMapper.updateById(old); Bed oldBed bedMapper.selectById(old.getBedId()); oldBed.setStatus(0); bedMapper.updateById(oldBed); // 分配新床位复用 assignBed 的逻辑 assignBed(studentId, newBedId); }注意old.setActiveBedId(null)这一步MyBatis-Plus 默认的 updateById 会忽略 null 字段导致 active_bed_id 改不掉唯一索引依然占着新记录插不进去。解决办法是在实体类的 activeBedId 字段上加TableField(updateStrategy FieldStrategy.IGNORED)强制更新 null。这个坑我第一次做的时候卡了整整一个下午日志里只看到 DuplicateKeyException完全想不到是 null 没更新。3.3 分页查询与多条件筛选接口宿舍管理系统的列表页通常要支持按楼栋、楼层、状态筛选还要分页。用 MyBatis-Plus 的 Page 对象配合 LambdaQueryWrapper 就够了不需要手写 XML。public PageBedVO pageBeds(int pageNum, int pageSize, Long buildingId, Integer status) { PageBedVO page new Page(pageNum, pageSize); // 自定义 XML 做三表关联返回床位房间楼栋信息 return bedMapper.selectBedPage(page, buildingId, status); }对应的 XML 里用 LEFT JOIN 把 room 和 building 关联进来WHERE 条件用if testbuildingId ! null动态拼接。参数上pageNum 从 1 开始pageSize 建议限制在 100 以内防止前端传个 10000 把数据库拖垮。这个接口本身不难难的是返回的 VO 要包含“当前入住学生姓名”需要在 SQL 里再 LEFT JOIN 一次 checkin_record 和 student条件是在住状态。这个 JOIN 写不好会有性能问题建议在 checkin_record 的 (bed_id, status) 索引上做别全表扫。4. 前端联调与跨浏览器支持Vue 页面里那些容易忽略的细节4.1 用 Vue3 Axios 封装宿舍管理页面前端我一般用 Vue3 Element Plus请求统一走 Axios 封装。核心是把 token、错误提示、loading 状态集中处理页面里只关心业务。// request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) service.interceptors.response.use( res { // 后端统一返回 { code, msg, data } if (res.data.code ! 200) { ElMessage.error(res.data.msg || 请求失败) return Promise.reject(new Error(res.data.msg)) } return res.data.data }, err { ElMessage.error(err.message || 网络异常) return Promise.reject(err) } ) export default service这段封装的价值在于所有页面不用重复写错误处理后端返回非 200 时自动弹提示。参数上timeout 设 10 秒超过就中断避免用户对着转圈页面干等。baseURL 用/api配合开发环境的 proxy生产环境用 Nginx 转发这样前端代码不用改。2.2 跨浏览器支持宿舍管理系统也要考虑热搜里有“跨浏览器支持的设计与实现”很多人觉得管理系统只在内网用 Chrome 就行但实际交付时经常遇到学校机房还在用旧版 Edge 或者 360 浏览器兼容模式。我的经验是Element Plus 本身对现代浏览器支持没问题真正会翻车的是日期选择器和文件上传。日期格式不要用toLocaleDateString()不同浏览器输出不一致统一用 dayjs 格式化。文件上传比如导入学生名单要用 FormData并且后端要允许跨域否则在 Firefox 下会被拦。注意如果要在 IE 或旧内核浏览器上跑Vue3 直接不支持只能退回 Vue2 Element UI。选型前先确认运行环境别做完才发现打不开。5. 避坑与排查做宿舍管理系统时最容易翻车的 5 个地方5.1 床位状态和入住记录不一致现象床位表显示空闲但学生查询显示已入住或者反过来。原因更新床位状态和插入入住记录不在同一个事务里中途异常导致只成功了一半。解决所有涉及床位状态变更的操作必须加Transactional并且把两张表的更新放在同一个方法内不要拆成两个 Service 调用。5.2 MyBatis-Plus 更新 null 字段失效现象退宿时 active_bed_id 置 null 没生效导致调宿时唯一索引冲突。原因MyBatis-Plus 默认 updateStrategy 是 NOT_NULLnull 字段被忽略。解决在需要更新为 null 的字段上加TableField(updateStrategy FieldStrategy.IGNORED)或者改用 UpdateWrapper 显式 set。5.3 分页查询总数不对现象列表数据正确但总条数偏大翻页出现空白页。原因自定义 XML 里 count 语句和查询语句的 JOIN 不一致或者用了SELECT *导致 count 把关联表的行也算进去。解决count 语句单独写只 count 主表或者用 MyBatis-Plus 的page.setSearchCount(false)手动查总数。5.4 日期格式前后端不一致现象前端传2024-01-01后端 LocalDateTime 解析报错。原因Spring Boot 默认不支持这种格式直接转 LocalDateTime。解决在字段上加JsonFormat(pattern yyyy-MM-dd)和DateTimeFormat或者全局配置 Jackson 的日期格式。5.5 并发分配床位产生重复记录现象压测或多人同时操作时同一床位出现两条在住记录。原因先查后插存在竞态。解决用第 3 章讲的 active_bed_id 唯一索引兜底捕获 DuplicateKeyException 转成友好提示不要只靠代码里的 if 判断。6. 进阶技巧把宿舍管理系统做成能写进简历的项目如果只是完成基本 CRUD这个项目在简历上很难出彩。我的做法是加两个能体现工程能力的点。第一是操作日志用 AOP 拦截所有增删改接口记录操作人、时间、参数、结果存到一张 log 表。这样答辩时可以说“系统具备审计能力”而且实现成本很低。Aspect Component public class OpLogAspect { Around(annotation(opLog)) public Object around(ProceedingJoinPoint pjp, OpLog opLog) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); // 异步写入日志表避免影响主流程 logService.saveAsync(opLog.value(), pjp.getArgs(), System.currentTimeMillis() - start); return result; } }第二是数据导出把床位分配结果导出成 Excel用 EasyExcel 一行代码搞定。这两个功能加起来不到 200 行代码但能让项目从“课设水平”提升到“有工程意识”。验证方法上我习惯用 Postman 跑一遍完整流程建楼栋 → 建房间 → 生成床位 → 导入学生 → 分配床位 → 调宿 → 退宿 → 查历史。每一步都检查数据库里两张表的状态是否一致。这个流程跑通基本就能应对答辩。最后一个习惯所有涉及状态变更的接口我都会在本地用两个线程同时调一次看唯一索引是否生效。这个动作花不了五分钟但能避免答辩现场被问到并发时心虚。希望帮到你。本文还有配套的精品资源点击获取