SpringBoot宿舍管理系统实战:权限控制、数据库设计与业务闭环
2026/9/19 20:36:39 网站建设 项目流程

简介:这是一份面向高校计算机相关专业毕业设计的完整论文资源,以SpringBoot框架为核心,结合MySQL数据库与B/S架构,系统讲解学生宿舍管理系统的设计实现全过程。资源适合需要完成毕业设计、课程设计或学习SpringBoot项目开发的学生参考。论文围绕管理员、宿管员、学生、维修员四类角色,覆盖楼宇管理、宿舍管理、学生管理、申请换寝、请假报备、报修申请、问题反馈、缺寝登记、迁出记录、公告管理等核心业务模块,从需求分析、系统设计到编码测试均有详细论述。内容预览中可见中英文摘要、目录及功能划分,结构完整,逻辑清晰,可作为项目开发与论文撰写的直接范本。压缩包共1个文件,为docx格式电子文档,大小3.03MB,便于编辑与排版。目前已有81人学习浏览,适合正在筹备毕业设计或希望了解宿舍管理业务流程的读者下载使用,有助于快速搭建同类管理系统的认知框架并节省前期调研时间。

1. 一个 SpringBoot 宿舍管理系统到底要管什么

学生宿舍管理是个典型的「看着简单、做起来碎」的业务。楼宇、宿舍、床位、学生、报修、请假、换寝、缺寝、迁出、公告,这些对象彼此关联,而且使用人群分四类:管理员、宿管员、学生、维修员。如果靠 Excel 和微信群,信息必然零散,报修进度靠催,缺寝情况靠问,换寝有没有床位靠猜。这套 SpringBoot 学生宿舍管理系统,就是把上述所有动作从线下搬到线上:管理员维护楼宇宿舍和学生档案,宿管员处理缺寝登记和报修申请,学生自助提交换寝、请假、报修和问题反馈,维修员接收报修通知并回填记录。技术栈是 SpringBoot + MySQL,B/S 架构,四类角色共用一套登录入口,按角色动态渲染菜单和权限。要拆这个项目,先别急着看页面,核心价值在数据模型的设计和几个高频业务闭环的实现上。

2. 先别写代码:四角色权限模型与核心表结构设计

2.1 角色权限:用 RBAC 还是直接字段区分

很多毕设项目一上来就引入 Spring Security + RBAC 五张表,结果业务没做多少,光配权限就耗掉一半时间。这个系统的角色是固定的四种,不存在动态创建角色、给角色分配菜单的需求,所以更务实的做法是在用户表上直接用一个role字段区分角色,配合拦截器做 URL 级别的访问控制。这样表设计简单,查询高效,调试的时候一眼就能看出登录用户是什么身份。

但要注意一个边界:如果后续要做「宿管员只能管理自己负责楼宇的数据」,就不能只靠role字段了,需要增加楼宇和用户的关联关系。我的建议是用户表加一个building_id字段,宿管员创建时绑定楼宇,查询学生数据时按building_id过滤。这个设计不影响当前功能,但给数据隔离留了口子。

2.2 核心数据表:从业务对象反推建表语句

这个系统的核心业务对象不算多,但表间关系要理清楚。先看角色:学生属于某个宿舍,宿舍属于某栋楼宇,楼宇有宿管员负责。再看业务流转:学生提交请假、报修、换寝申请,宿管员审批或登记,维修员处理报修,所有操作产生记录。

从这些关系反推,最少需要这几张表:

表名用途关键字段
sys_user四类角色的统一用户表role,building_id,dorm_id
building楼宇信息building_no,name,floors
dormitory宿舍信息building_id,room_no,bed_count,used_count
student_info学生扩展信息user_id,student_no,major,class_name
repair_record报修单apply_user_id,dorm_id,type,status,handler_id
leave_apply请假报备student_id,start_time,end_time,reason,status
change_bed_apply换寝申请student_id,from_dorm_id,to_dorm_id,status
absence_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码字段用 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 3,JDK 17 的要求会把一部分部署环境卡死,尤其是有些机房服务器还跑着 JDK 8。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

用 MyBatis-Plus 而不是原生 MyBatis,是因为这个项目里有大量单表 CRUD,MyBatis-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 Result<UserVO> 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 LambdaQueryWrapper<SysUser>() .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 Map<Integer, 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里面未来如果还要维护一条报修日志表或者通知记录表,同一个事务保证报修单和日志要么一起成功要么一起回滚。

状态校验放在更新之前,但这里有个并发风险:两个请求同时读到status=0,同时执行更新,就会导致状态错乱。悲观的做法是在selectById时加上FOR UPDATE行锁;在 MySQL 的 InnoDB 引擎下,selectById走主键索引,锁的是一行记录,不会影响其他宿舍的报修单并发处理。

-- 在 Mapper 中手动处理 SELECT * FROM repair_record WHERE id = #{id} FOR UPDATE

4.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 mysqld

application.yml里最容易配错的是时区和 SSL 参数。MySQL 8 的连接字符串如果不加时区参数,会报The server time zone value错误。推荐直接使用下面的配置:

spring: datasource: url: jdbc:mysql://localhost:3306/dormitory?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

页面加载慢但接口响应快,优先看静态资源是不是走到了 SpringBoot 默认的 classpath 路径;部署到服务器后访问不到,先确认是不是防火墙或安全组没放行端口,再看server.address是否绑定到了0.0.0.0。日志级别调成 DEBUG 后启动,观察 MyBatis-Plus 打印的 SQL,很多数据不一致问题通过 SQL 日志能直接定位到是哪一步更新操作丢了这个字段。

日志文件过大时,在application.yml里加logging.file.name=logs/dormitory.log之外,用logback-spring.xml配置按天滚动和保留天数,避免一张表的问题变成一个占用磁盘的问题。演示环境可用--spring.profiles.active=dev快速切换开发/生产配置。

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

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

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

立即咨询