
简介这是一份面向高校计算机相关专业毕业设计的完整论文资源以SpringBoot框架为核心结合MySQL数据库与B/S架构系统讲解学生宿舍管理系统的设计实现全过程。资源适合需要完成毕业设计、课程设计或学习SpringBoot项目开发的学生参考。论文围绕管理员、宿管员、学生、维修员四类角色覆盖楼宇管理、宿舍管理、学生管理、申请换寝、请假报备、报修申请、问题反馈、缺寝登记、迁出记录、公告管理等核心业务模块从需求分析、系统设计到编码测试均有详细论述。内容预览中可见中英文摘要、目录及功能划分结构完整逻辑清晰可作为项目开发与论文撰写的直接范本。压缩包共1个文件为docx格式电子文档大小3.03MB便于编辑与排版。目前已有81人学习浏览适合正在筹备毕业设计或希望了解宿舍管理业务流程的读者下载使用有助于快速搭建同类管理系统的认知框架并节省前期调研时间。1. 一个 SpringBoot 宿舍管理系统到底要管什么学生宿舍管理是个典型的「看着简单、做起来碎」的业务。楼宇、宿舍、床位、学生、报修、请假、换寝、缺寝、迁出、公告这些对象彼此关联而且使用人群分四类管理员、宿管员、学生、维修员。如果靠 Excel 和微信群信息必然零散报修进度靠催缺寝情况靠问换寝有没有床位靠猜。这套 SpringBoot 学生宿舍管理系统就是把上述所有动作从线下搬到线上管理员维护楼宇宿舍和学生档案宿管员处理缺寝登记和报修申请学生自助提交换寝、请假、报修和问题反馈维修员接收报修通知并回填记录。技术栈是 SpringBoot MySQLB/S 架构四类角色共用一套登录入口按角色动态渲染菜单和权限。要拆这个项目先别急着看页面核心价值在数据模型的设计和几个高频业务闭环的实现上。2. 先别写代码四角色权限模型与核心表结构设计2.1 角色权限用 RBAC 还是直接字段区分很多毕设项目一上来就引入 Spring Security RBAC 五张表结果业务没做多少光配权限就耗掉一半时间。这个系统的角色是固定的四种不存在动态创建角色、给角色分配菜单的需求所以更务实的做法是在用户表上直接用一个role字段区分角色配合拦截器做 URL 级别的访问控制。这样表设计简单查询高效调试的时候一眼就能看出登录用户是什么身份。但要注意一个边界如果后续要做「宿管员只能管理自己负责楼宇的数据」就不能只靠role字段了需要增加楼宇和用户的关联关系。我的建议是用户表加一个building_id字段宿管员创建时绑定楼宇查询学生数据时按building_id过滤。这个设计不影响当前功能但给数据隔离留了口子。2.2 核心数据表从业务对象反推建表语句这个系统的核心业务对象不算多但表间关系要理清楚。先看角色学生属于某个宿舍宿舍属于某栋楼宇楼宇有宿管员负责。再看业务流转学生提交请假、报修、换寝申请宿管员审批或登记维修员处理报修所有操作产生记录。从这些关系反推最少需要这几张表表名用途关键字段sys_user四类角色的统一用户表role,building_id,dorm_idbuilding楼宇信息building_no,name,floorsdormitory宿舍信息building_id,room_no,bed_count,used_countstudent_info学生扩展信息user_id,student_no,major,class_namerepair_record报修单apply_user_id,dorm_id,type,status,handler_idleave_apply请假报备student_id,start_time,end_time,reason,statuschange_bed_apply换寝申请student_id,from_dorm_id,to_dorm_id,statusabsence_record缺寝登记student_id,date,reason这里有个取舍学生信息单独建一张student_info还是直接合并进sys_user如果只是毕设演示合并成一张表完全够用但如果要体现学生换班、转专业、学号变更这些历史轨迹拆开更合理。我在这个项目里选择拆开因为学生管理的字段学号、专业、班级和账号字段用户名、密码的变更频率完全不同拆开后sys_user表保持精简登录查询性能更好。2.3 建表 SQL 与字段约束说明直接给出核心表的建表语句注意几个容易踩坑的点CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt加密存储, real_name varchar(50) DEFAULT NULL, role tinyint NOT NULL COMMENT 1管理员 2宿管员 3学生 4维修员, building_id bigint DEFAULT NULL COMMENT 宿管员绑定的楼宇ID, dorm_id bigint DEFAULT NULL COMMENT 学生所在宿舍ID, phone varchar(20) DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dormitory ( id bigint NOT NULL AUTO_INCREMENT, building_id bigint NOT NULL, room_no varchar(20) NOT NULL, bed_count int NOT NULL DEFAULT 4, used_count int NOT NULL DEFAULT 0, gender tinyint NOT NULL COMMENT 1男生宿舍 2女生宿舍, PRIMARY KEY (id), UNIQUE KEY uk_building_room (building_id, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段用 BCrypt 加密存储不要用 MD5。MD5 查重成本极低泄露一次就等于明文。Spring Security Crypto 包里自带BCryptPasswordEncoder不需要引入完整的安全框架也能用。uk_building_room唯一索引很关键防止同一栋楼录入两个相同房号。dormitory表的used_count是一个冗余计数添加学生入住时1迁出时-1。这个字段的维护必须和宿舍分配逻辑放在同一个事务里否则会出现「宿舍实际住满但系统显示还有床位」的问题。后文换寝部分会专门讲这个事务边界。3. SpringBoot 骨架搭建与登录鉴权实现3.1 依赖选型与工程目录SpringBoot 版本选择 2.7.x原因很实际官方文档全、社区问题沉淀多、对 JDK 8 兼容最好。别一上来就用 SpringBoot 3JDK 17 的要求会把一部分部署环境卡死尤其是有些机房服务器还跑着 JDK 8。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies用 MyBatis-Plus 而不是原生 MyBatis是因为这个项目里有大量单表 CRUDMyBatis-Plus 的BaseMapper可以直接省掉 mapper XML 的编写分页插件也是现成的。但不建议把业务逻辑写死在 QueryWrapper 链式调用里复杂查询还是要写 SQL 映射。工程目录按职责分包而不是按技术层分包com.dormitory ├── controller # 接口层 ├── service # 业务逻辑层 │ └── impl ├── mapper # MyBatis-Plus 数据访问层 ├── entity # 实体类 ├── dto # 入参/出参对象 ├── config # 拦截器、全局异常、CORS 配置 └── common # 统一返回结果、常量、工具类3.2 登录接口从 Controller 到 Service 的完整链路登录接口是整个系统的入口四类角色共用。核心逻辑是接收用户名密码查询用户校验密码写入 Session。用 Session 而不是 JWT是因为这个系统是服务端渲染页面为主Session 的过期管理由容器接管代码量最少。如果前后端分离才考虑 JWT。RestController RequestMapping(/api/auth) public class AuthController { Resource private AuthService authService; PostMapping(/login) public ResultUserVO login(RequestBody LoginDTO dto, HttpServletRequest request) { // 1. 非空校验 if (StringUtils.isBlank(dto.getUsername()) || StringUtils.isBlank(dto.getPassword())) { return Result.error(用户名和密码不能为空); } // 2. 调登录服务 UserVO user authService.login(dto.getUsername(), dto.getPassword()); if (user ! null) { // 3. 写入 Session request.getSession().setAttribute(loginUser, user); return Result.ok(user); } return Result.error(用户名或密码错误); } }Service 层是核心关注密码校验和用户状态检查Service public class AuthServiceImpl implements AuthService { Resource private SysUserMapper sysUserMapper; private final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); Override public UserVO login(String username, String rawPassword) { SysUser user sysUserMapper.selectOne( new LambdaQueryWrapperSysUser() .eq(SysUser::getUsername, username) ); if (user null) { return null; } if (user.getStatus() ! 1) { throw new BusinessException(账号已被禁用请联系管理员); } // 用 BCrypt 校验密码 if (!encoder.matches(rawPassword, user.getPassword())) { return null; } // 组装 VO 时剔除敏感字段 UserVO vo new UserVO(); vo.setId(user.getId()); vo.setUsername(user.getUsername()); vo.setRealName(user.getRealName()); vo.setRole(user.getRole()); if (user.getRole() 3) { // 学生角色带上宿舍信息供前端展示 vo.setDormId(user.getDormId()); } return vo; } }encoder.matches(rawPassword, user.getPassword())的作用是拿明文密码和数据库中已加密的密文做比对BCrypt 每次生成的盐不同但matches可以正确校验。UserVO里不要回传password字段这是最低要求更稳妥的做法是直接用JsonIgnore注解在实体类的密码字段上从序列化源头杜绝泄露。3.3 基于拦截器的 URL 权限控制四类角色能访问的接口不同用拦截器控制 URL 模式最直接。SpringBoot 的自动配置机制会自动注册WebMvcConfigurer的实现类我们只需要在配置类里添加拦截器规则。Component public class AuthInterceptor implements HandlerInterceptor { private static final MapInteger, String ROLE_PREFIX Map.of( 1, /admin, 2, /dormitory, 3, /student, 4, /maintainer ); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserVO loginUser (UserVO) request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } String uri request.getRequestURI(); String prefix ROLE_PREFIX.get(loginUser.getRole()); // 管理员可以访问所有接口其他角色只能访问对应前缀 if (loginUser.getRole() 1 || uri.startsWith(prefix)) { return true; } response.setStatus(403); return false; } }Configuration public class WebConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /login, /css/**, /js/**); } }这段拦截器的设计哲学是「角色前缀即权限」学生的接口 URL 都以/student开头宿管员以/dormitory开头维修员以/maintainer开头只有管理员可以跨越前缀限制访问全部接口。好处是权限规则一眼可见新增一个学生端接口时不需要改配置文件坏处是如果未来要让学生也能调用宿管员的某几个接口这个设计就不够用了需要改成基于注解的细粒度权限控制。4. 报修与换寝两个高频业务闭环的落地4.1 报修流程的状态机设计报修是这个系统里流转链路最长的一个业务学生发起、宿管员审核、维修员接单、维修完成回填。这种多角色协作的业务最忌讳在代码里散落魔法数字判断状态。先用状态机把流转理清。待处理(0) - 已派单(1) - 维修中(2) - 已完成(3) | | v v 已取消(4) 已验收(5)学生提交报修单状态为待处理(0)宿管员审核通过并指派给维修员状态变为已派单(1)维修员接单开始维修状态变为维修中(2)维修完成回填记录状态变为已完成(3)学生在宿舍端确认状态变为已验收(5)如果宿管员审核不通过状态直接置为已取消(4)。状态流转在后端要做合法性校验不允许跳跃。比如维修员不能把待处理(0)直接改成已完成(3)必须经过派单环节。这个校验写在 Service 层不写在 Controller。4.2 报修接口实现与事务边界Service public class RepairServiceImpl implements RepairService { Resource private RepairRecordMapper repairRecordMapper; Resource private DormitoryMapper dormitoryMapper; Override Transactional(rollbackFor Exception.class) public Long createRepair(RepairApplyDTO dto, Long studentId) { RepairRecord record new RepairRecord(); record.setApplyUserId(studentId); record.setDormId(dto.getDormId()); record.setType(dto.getType()); record.setDescription(dto.getDescription()); record.setStatus(0); // 初始状态待处理 record.setCreateTime(LocalDateTime.now()); repairRecordMapper.insert(record); return record.getId(); } Override Transactional(rollbackFor Exception.class) public void dispatch(Long recordId, Long maintainerId) { // 校验当前状态必须是待处理 RepairRecord record repairRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BusinessException(当前状态不允许派单); } record.setStatus(1); record.setHandlerId(maintainerId); record.setDispatchTime(LocalDateTime.now()); repairRecordMapper.updateById(record); } }Transactional加在 Service 方法上而不是 Controller 方法上。原因在于 Controller 只做参数接收和结果返回事务边界应该划在业务逻辑的起止位置。createRepair里面未来如果还要维护一条报修日志表或者通知记录表同一个事务保证报修单和日志要么一起成功要么一起回滚。状态校验放在更新之前但这里有个并发风险两个请求同时读到status0同时执行更新就会导致状态错乱。悲观的做法是在selectById时加上FOR UPDATE行锁在 MySQL 的 InnoDB 引擎下selectById走主键索引锁的是一行记录不会影响其他宿舍的报修单并发处理。-- 在 Mapper 中手动处理 SELECT * FROM repair_record WHERE id #{id} FOR UPDATE4.3 换寝申请如何保证宿舍资源不超卖换寝是宿舍管理系统里最容易出并发问题的功能。学生 A 申请从 401 换到 502学生 B 同时申请从 301 换到 502如果 502 只剩一个空位两个申请都通过了「剩余床位 0」的校验就会超卖。解决办法是把「校验空位 扣减床位 更新学生宿舍」放在同一个事务里并且对宿舍行加锁。Override Transactional(rollbackFor Exception.class) public void approveChangeBed(Long applyId, Long approverId) { // 1. 获取申请单 ChangeBedApply apply changeBedApplyMapper.selectById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BusinessException(申请单不存在或已处理); } // 2. 悲观锁锁定目标宿舍防止并发超卖 Dormitory targetDorm dormitoryMapper.selectByIdForUpdate(apply.getToDormId()); if (targetDorm.getUsedCount() targetDorm.getBedCount()) { throw new BusinessException(目标宿舍已满员); } // 3. 原宿舍释放床位 Dormitory fromDorm dormitoryMapper.selectById(apply.getFromDormId()); if (fromDorm.getUsedCount() 0) { fromDorm.setUsedCount(fromDorm.getUsedCount() - 1); dormitoryMapper.updateById(fromDorm); } // 4. 目标宿舍占用床位 targetDorm.setUsedCount(targetDorm.getUsedCount() 1); dormitoryMapper.updateById(targetDorm); // 5. 更新学生的宿舍关联 SysUser student sysUserMapper.selectById(apply.getStudentId()); student.setDormId(apply.getToDormId()); sysUserMapper.updateById(student); // 6. 申请单置为通过写入迁出记录 apply.setStatus(1); apply.setApproveTime(LocalDateTime.now()); changeBedApplyMapper.updateById(apply); MoveOutRecord moveOut new MoveOutRecord(); moveOut.setStudentId(apply.getStudentId()); moveOut.setFromDormId(apply.getFromDormId()); moveOut.setToDormId(apply.getToDormId()); moveOut.setCreateTime(LocalDateTime.now()); moveOutRecordMapper.insert(moveOut); }selectByIdForUpdate是关键对应 SQL 是SELECT * FROM dormitory WHERE id ? FOR UPDATE。在事务里这行记录会被加上排他锁直到事务提交才释放。第二个申请就算同时进来也只能在这个锁后面排队等第一个事务提交后才能读到最新数据此时used_count已更新超卖自然被拦住。换寝事务的边界还需注意如果目标宿舍和原宿舍是同一栋楼先释放再占用的顺序没问题如果后续要支持跨校区换寝需要考虑原宿舍释放失败时的补偿逻辑当前这个项目用同一个事务包住任何一个环节抛异常都会回滚。另外approveChangeBed的入参approverId当前只做记录用途如果要做更严格的权限控制还需要校验审批人是否为该生所在楼宇的宿管员。5. 功能验收清单与部署排错技巧5.1 四类角色的核心验收点系统开发完不能直接交按角色过一遍验收清单每个功能都要能走通正向和反向流程。角色核心功能验收点管理员楼宇/宿舍管理新增宿舍时bed_count used_count必须被拦截删除宿舍时若有学生关联应提示管理员学生管理学生的宿舍变更后sys_user.dorm_id和宿舍used_count必须一致宿管员缺寝登记同一天同一学生不能重复登记两次宿管员报修申请处理不通过时必须填写原因学生端能看到拒绝理由学生请假报备请假结束时间必须晚于开始时间冲突时明确提示学生问题反馈提交后学生能看到反馈状态待处理/已回复维修员报修通知维修员只能看到已派给自己的报修单不能看到全部最值得仔细验收的是数据一致性随便找一个学生看他所在的宿舍再查这个宿舍的used_count两边必须对得上。如果出现不一致八成是某个直接改数据库的 SQL 没有同步更新冗余字段。5.2 常见问题排查启动报错先看端口占用和 MySQL 连接八成的问题都出在这两处。# 检查 8080 端口是否被占用 lsof -i :8080 # 查看 MySQL 是否在运行Linux 下使用 systemctl status mysqldapplication.yml里最容易配错的是时区和 SSL 参数。MySQL 8 的连接字符串如果不加时区参数会报The server time zone value错误。推荐直接使用下面的配置spring: datasource: url: jdbc:mysql://localhost:3306/dormitory?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8页面加载慢但接口响应快优先看静态资源是不是走到了 SpringBoot 默认的 classpath 路径部署到服务器后访问不到先确认是不是防火墙或安全组没放行端口再看server.address是否绑定到了0.0.0.0。日志级别调成 DEBUG 后启动观察 MyBatis-Plus 打印的 SQL很多数据不一致问题通过 SQL 日志能直接定位到是哪一步更新操作丢了这个字段。日志文件过大时在application.yml里加logging.file.namelogs/dormitory.log之外用logback-spring.xml配置按天滚动和保留天数避免一张表的问题变成一个占用磁盘的问题。演示环境可用--spring.profiles.activedev快速切换开发/生产配置。本文还有配套的精品资源点击获取