☰
Spring Boot高校宿舍管理系统:数据建模、权限与并发分配实战
2026/10/14 5:47:26 网站建设 项目流程

做高校宿舍管理系统这类项目,我的第一反应从来不是“这不就是几个 CRUD 吗”,而是先想清楚一件事:宿舍管理的日常到底涉及多少种角色、多少条状态流、多少种容易在细节上翻车的数据变更。很多看起来简单的系统,做出来的版本五花八门,区别就在于有没有把业务边界和数据一致性想明白。这篇文章我就围绕“基于 Spring Boot 的高校宿舍管理系统”这个项目,从业务梳理、数据建模、核心逻辑、权限设计一直到具体落地代码,把整个实现过程完整拆开讲一遍。适合准备用 Spring Boot 做管理系统练手的人,也适合正在做类似项目、想知道哪些环节容易出问题的朋友。

1. 宿舍管理系统到底在管些什么?——先把业务边界理清楚

1.1 角色比你想象的多

很多第一次看这个需求的人,会把角色简单分成“管理员”和“学生”两种。真去宿舍楼里蹲几天,你会发现至少三类角色是必须的:

  1. 学生。学生是系统最主要的用户。他们要查自己住哪栋楼、哪个房间、哪个床位,提交宿舍报修单,查看公告通知,还有可能要发起调宿或退宿申请。
  2. 宿管员。负责一栋或几栋楼的日常管理。新生来了要分配床位,学生毕业或退宿要办理退宿,报修单要受理和流转,还要登记访客信息和晚间查寝情况。
  3. 系统管理员。管的是“学校级”的数据,比如楼栋和房间的基础资料维护、全校用户账号管理、楼栋分配策略配置、各类统计报表查询。

在一些使用场景里还会出现辅导员这种角色,他们不需要操作床位,只需要查看本院学生的住宿分布,方便辅助日常管理。

这个角色维度不梳理清楚,数据库表的设计很容易出问题。比如把宿管员和管理员混在一张大表里,权限控制就会变得混乱,后面每个接口都要写复杂的判断。

1.2 核心业务链路:迎新、报修、调宿、退宿

正常情况下,宿舍系统的核心业务流程可以归纳成四条链路。

第一条是新生入住。学校把新生信息批量导入系统,宿管员按学院预先分好楼栋,然后在楼内分配具体房间和床位,系统同步生成一条住宿记录,房间的已住人数加一。

第二条是日常报修。学生提交报修信息,比如空调不制冷、水龙头漏水,宿管员收到后登记维修工单,维修工接单处理,完成后由学生或宿管员确认关闭。这个流程看着简单,但报修单的状态字段特别容易设计成“一锤子买卖”,只有“处理中”和“已完成”,一旦需要记录拒修、转派、退回等状态,字段就要推倒重来。

第三条是调宿。学生因为转专业、实习等原因需要换宿舍。调宿在数据上的本质是一次退宿加一次入住,而且要求两个操作在同一个事务里完成。处理不好就会出现“人走了,房间占用数没减”的经典脏数据。

第四条是退宿。毕业生离校或者休学退宿,宿管员办理后,床位释放,住宿记录变为退宿状态。重要的是,这条历史记录不能被物理删除,后面要查出“这个学生什么时候住过哪栋哪间”,全靠它。

这四条链路里面,迎新和报修的频率最高,调宿和退宿频率低但数据一致性要求高。做系统的时候,前者要比拼操作流畅度,后者要比拼数据严谨度。

1.3 明确“不做什么”,反而决定项目能走多远

很多项目做几个月收不了尾,不是核心功能没做完,而是需求调研阶段没把边界说清楚。我之前接手一个项目的时候,对接方列了一堆期望:要对接校园一卡通门禁,要做水电表自动抄表,还要支持在线支付水电费。这些功能听着合理,但实际落地的时候,门禁设备和水电表硬件的数据协议都要设备厂商配合,支付则牵涉财务和多个部门审批,已经远远超出“宿舍管理系统”的范畴。

我的做法是,第一版只做管理系统本身能闭环的事情:人员、楼栋、房间、床位、入住、报修、公告、访客。一卡通、水电表、支付这些,等核心系统稳定之后通过接口预留扩展。明确“不做什么”之后,开发节奏反而快很多,核心系统先跑出价值,对方也有信心谈下一步。

2. 为什么这套业务和 Spring Boot 这么合拍

2.1 框架选择的三个现实理由

冷静地讲,宿舍管理系统并不是高并发分布式应用,用不用 Spring Boot 都能做出来。但为什么在真实项目和教学场景中,它几乎成了标准选择?我看到的理由有三个。

第一,配置成本低。早期用 Spring MVC 做项目,要写很多 XML 配置,部署到 Tomcat 还有一堆环境问题。Spring Boot 自带内嵌容器,一个 jar 包直接跑起来,不用纠结环境。

第二,生态成熟。这类系统最关心的数据库访问、权限控制、接口文档、文件上传,Spring Boot 都有对应的成熟组件,选型组合非常稳定,不需要自己造轮子。

第三,资料多、好维护。不管你是自己学还是团队协作,遇到问题搜一下基本就有答案。对于团队来说,后续维护门槛低,接手的人容易上手。

所以我常对准备做这类项目的人说,如果你要做一个信息管理系统,Spring Boot 是最不容易出错的选择。它不炫技,但它把工程成本和维护成本压到了最低。

2.2 常规技术栈清单与版本取舍

一个宿舍管理系统常用的技术栈大概是这样的:

层面推荐方案说明
框架Spring Boot 2.7 / 3.x两者都行,按 JDK 版本定
数据库MySQL 8免费稳定,通用性最强
数据访问MyBatis-Plus单表查询基本不用写 SQL
缓存Redis登录令牌、验证码、公告缓存
安全Spring Security + JWT比手写拦截器规范得多
接口文档knife4j前后端联调利器
工具库Lombok、MapStruct少写样板代码

关于版本选择,我一般这么判断:本机是 JDK 8,就用 Spring Boot 2.7;本机已经上 JDK 17,就直接用 Spring Boot 3.x。不要为了“最新版本”强行升级,稳定性和团队熟悉度更重要。

这里还有一个争论点:要不要做前后端分离。我的建议是分。管理后台配一个 Vue 或 React 页面,学生端配一个小程序,后端只负责提供 RESTful 接口。前后端分开之后,两边的开发节奏互不干扰,一个小团队能跑得很顺。

至于微服务,这个系统的业务范围很小,拆分成多个服务只会带来更多部署、联调和运维成本,完全没有必要上。单体应用加合理分层,已经是这类系统的最优解。

3. 数据库设计:先把宿舍这张“账本”立住

3.1 核心表模型概览

宿舍系统的数据模型,围绕“楼栋—房间—床位—住宿记录”这条主线展开。我们常见的表结构如下:

表名主要职责
t_user账号表,存登录凭证、角色、状态
t_student学生扩展信息,存学号、学院、班级、手机号
t_building楼栋表,存楼号、楼名、楼层数、楼栋类型
t_room房间表,存楼栋 ID、房间号、床位总数、已住人数、性别限制
t_dorm_record住宿记录表,一次住宿一条,核心表
t_repair报修单表
t_announcement公告表
t_visit_log访客登记表

有人会问,为什么账号和学生要分开两张表?实用原因是,学生账号只是账号体系中的一种,宿管员、维修人员、管理员也都要有账号。账号表管登录和角色,学生表管学号和学院信息,两者演化节奏不一样。如果混在一张表里,每种角色的字段都往里塞,表结构很快就会变得失控。

3.2 住宿记录表为什么必须做快照

t_dorm_record 是整张“账本”的记账明细。关键字段大致有这些:

  • id
  • student_id
  • building_id 和 building_name
  • room_id 和 dorm_no
  • bed_no
  • check_in_time
  • check_out_time
  • status

我特别强调一个设计:要在表里面冗余保存当时的楼栋名、房间号和床位号。

为什么?假设系统只关联 room_id,不存房间号,那么所有历史数据都要靠 room 表现查。万一以后某栋楼重新装修,房间号从 101 改成 A101,甚至房间被拆并了,历史住宿记录就全部失联。住宿记录是已经发生的事实,事实不能被静态数据的变化改写。把当时住的是哪一间快照保存在记录里,以后不管基础数据怎么变,历史都可以还原。

这个设计看起来多写了几列,实际恰恰省掉了大概率会踩的坑。比如毕业季要打印住宿证明,或者学生投诉说当年住过某间房,你翻记录的时候就能直接拿出证据。

3.3 索引与状态字段的设计

宿舍系统的高频查询很固定:按学号查学生当前住宿、按楼栋查空房间、按状态查未处理报修。查询一旦明确,索引就好设计了。我建议至少在 t_dorm_record 的 (student_id, status) 上建联合索引,在 t_room 的 (building_id, status) 上建联合索引,在 t_repair 的 (status, created_time) 上建索引。

状态字段比想象中重要。报修单至少要有“待处理 / 处理中 / 已完成 / 已关闭”四态,房间至少要有“空闲 / 部分入住 / 已满”等状态。我见过有项目用两个布尔字段硬凑状态,结果什么组合都能拼出来,业务逻辑全乱套。状态一定要用枚举值定义清楚,数据库里存 int,代码里用枚举对应,别存中文,也别用布尔值硬凑。

4. 宿位分配这类核心逻辑,到底难在哪

4.1 状态流转的两个视角

宿舍分配表面看很简单:选一个有空床位的房间,插一条住宿记录,房间占用数加一。但仔细看,状态其实在两条维度上同时变化。

一条维度是房间和床位的占用状态:空 → 已占用 → 释放后再空。另一条维度是学生的住宿记录状态:未入住 → 正常入住 → 调宿转出 → 已退宿。两条维度必须保持一致,宿舍系统才不会乱账。

一旦把调宿、退宿、换楼栋这些场景引入进来,“单纯改一个字段”的思路就不够用了,必须看成状态机来做。每一步操作要么同时更新房间占用和住宿记录,要么不执行。这里不存在中间状态,这也是为什么事务会成为这套系统的核心工具。

4.2 并发床位分配,如何避免超卖

如果只有一个人操作宿舍分配,怎么写都不会出问题。问题恰恰出在新生报到日:多个宿管员同时在系统里操作,看到同一个“还有空床”的房间,同时点击分配。这时候如果代码写成先查数量再判断,就会出现典型的并发超卖。

我第一次实现这个功能时,习惯性写了最简单的逻辑:

Room room = roomMapper.selectById(roomId); if (room.getOccupiedCount() < room.getBedCount()) { room.setOccupiedCount(room.getOccupiedCount() + 1); roomMapper.updateById(room); }

这个代码平时测试没问题,一旦并发请求进来,两个线程都读到一样的 occupiedCount,都认为还没满,然后都执行加一,房间就真的超载了。

正确的做法是在数据库层面加锁。用事务加select ... for update,让同时进入的请求排队处理:

@Transactional public void assignRoom(Long studentId, Long roomId) { Room room = roomMapper.selectByIdForUpdate(roomId); if (room.getOccupiedCount() >= room.getBedCount()) { throw new BusinessException("该宿舍已满"); } roomMapper.increaseOccupiedCount(roomId); DormRecord record = new DormRecord(); record.setStudentId(studentId); record.setRoomId(roomId); record.setDormNo(room.getRoomNo()); record.setBedNo(nextBedNo); record.setStatus(LIVING); dormRecordMapper.insert(record); }

这里的 selectByIdForUpdate 对应 MyBatis 的select ... for update,数据库会把这条房间记录锁住,其他事务在它提交前都会等待。事务结束后锁释放,下一个请求再判断时看到的是更新后的已住人数,超卖问题就杜绝了。

用悲观锁还是乐观锁,各有各的理由。这里我选悲观锁,一是宿舍分配的并发峰值不算高,锁等待时间很短;二是逻辑直观,团队里其他同学接手也好理解。

4.3 住宿记录为什么必须“带历史”而不能直接覆盖

还有一个容易偷懒的地方:在学生表里直接加一个“当前宿舍”字段,换宿就改这个字段。这种做法做演示很爽,真实业务却撑不住。大学期间一个学生可能住过三个地方:大一住普通宿舍,大二转专业换到另一个学院楼,大四实习前后又调了床位。如果学生表里的字段被不断覆盖,不仅历史查不出来,毕业季需要提供在校住宿证明时,你无法证明学生确实住过某栋楼。

所以我坚持把 DormRecord 设计成“持续追加”的模式:每次入住、换宿、退宿都生成独立记录,历史记录保留 status 字段区分。查询当前住宿取最新一条,查询历史直接按 student_id 拉集合。

账目清楚,后面无论做报表还是出证明,都会很省事。这也是一个信息管理系统和一个演示 Demo 的本质区别。

5. 权限设计:把学生、宿管员、系统管理员分开

5.1 JWT 还是 Session:按场景定

宿舍管理系统的前端可能同时有管理后台和学生小程序。两种端点的认证方式,用 Session 也能做,但我更推荐 JWT。

原因是 JWT 令牌天然适合跨端和跨域场景。服务端不需要保存会话状态,用户在小程序登录后得到的 token 也能在 Web 管理端使用。实现起来,登录成功后签发一个带角色的 token,前端每次请求时放入 Authorization 头,后端过滤器解析即可。

如果你不想引入整套 Spring Security,手写一个 JWT 过滤器加自定义权限注解也够用。但两个方案对比下来,我还是建议直接用 Spring Security。因为这类系统虽然现在只有三个角色,后面大概率会冒出更多角色和资源权限,Security 的资源管理、注解鉴权、授权规则都要比手写实现严谨。

JWT 在代码里大致这样用:

@PostMapping("/login") public Result<TokenVO> login(@RequestBody LoginRequest request) { User user = userService.checkLogin(request.getUsername(), request.getPassword()); String token = JwtUtil.createToken(user.getId(), user.getRoleId()); return Result.ok(new TokenVO(token)); }

过滤器只负责两件事:验证 token 是否有效,把用户 ID 和角色放到上下文里。

5.2 接口级权限与数据级权限

权限要分两层理解。

第一层是接口级权限,控制“谁能调用这个接口”。比如分配宿舍的接口只能宿管员和管理员调用。用 Spring Security 的注解就能搞定:

@PreAuthorize("hasRole('DORM_ADMIN')") @PostMapping("/assign") public Result<Long> assign(@RequestBody AssignRequest request) { return Result.ok(roomService.assignRoom(request)); }

第二层是数据级权限。同一个接口,宿管员只能看到自己负责的楼栋数据,系统管理员才能看全部。实现上,可以在用户表里给宿管员配置 dormId,查询时把当前用户的楼栋范围作为查询条件。

只做接口级别权限,不做数据级权限,宿管员很容易串楼,能查到并操作其他楼栋的记录。权限矩阵设计可以一开始就做成表格:

功能学生宿管员系统管理员
查看公告可以可以可以
提交报修可以不可以不可以
分配床位不可以可以可以
办理退宿申请可以可以
用户管理不可以不可以可以
统计报表不可以本楼数据全部数据

这张表格会指导接口设计,也方便后面补充新角色,不至于把权限逻辑写散。

6. 从零搭建项目骨架的实操步骤与关键代码

6.1 初始化和配置文件

如果是新项目,我习惯的初始化顺序是:用 Spring Initializr 生成工程,选上 Web、MySQL Driver、Validation、Lombok 依赖;然后单独引入 MyBatis-Plus、JWT 工具包和 knife4j。

基础配置文件参考如下:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dorm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里有个细节:Jackson 的日期格式必须一开始就配置好。LocalDateTime 默认序列化出来是一长串数字,前端拿过去没法直接显示。这个坑我们第 7 节还会展开。

6.2 统一返回体与统一异常处理

管理系统的接口特别多,如果每个 Controller 都各自返回 Map 或散装 JSON,前端接口联调会很痛苦。我习惯在项目第一天就定好统一返回结构:

public class Result<T> { private int code; private String message; private T data; }

再加一个全局异常处理器,把业务异常、参数校验异常、系统异常统一转成这个结构。前端只需要处理 code=0 成功、code≠0 失败两种情况,逻辑收敛很多。

6.3 分房接口的完整链路代码

分配房间是宿舍系统的核心场景,把这条链路串起来看,整个骨架就清晰了。

Controller 层负责接收请求和参数校验:

@RestController @RequestMapping("/dorm/room") public class RoomController { private final RoomService roomService; @PostMapping("/assign") public Result<Long> assign(@RequestBody @Valid AssignRequest request) { return Result.ok(roomService.assignRoom(request)); } }

Service 层负责控制事务和核心业务逻辑:

@Service @Transactional public class RoomServiceImpl implements RoomService { @Override public Long assignRoom(AssignRequest request) { Room room = roomMapper.selectByIdForUpdate(request.getRoomId()); if (room.getOccupiedCount() >= room.getBedCount()) { throw new BusinessException("该宿舍已满"); } roomMapper.increaseOccupiedCount(request.getRoomId()); DormRecord record = new DormRecord(); record.setStudentId(request.getStudentId()); record.setRoomId(request.getRoomId()); record.setBuildingId(room.getBuildingId()); record.setDormNo(room.getRoomNo()); record.setBedNo(room.getNextBedNo()); record.setStatus(DormStatus.LIVING); return dormRecordMapper.insert(record); } }

两个地方特别要说清楚。

第一,selectByIdForUpdate 必须在事务里执行才有效果,所以整个方法用 @Transactional 包裹。没有事务,行锁不会持续到期望的范围。

第二,业务逻辑不要写在 Controller 里。分房至少涉及房间占用数变更和住宿记录新增两步,一旦中途出现异常,事务必须回滚。业务逻辑拆到 Service 层并统一挂事务,是多表操作不出脏数据的前提。

搭建到这一步,登录认证、房间管理、住宿记录、报修单、公告这些模块,都可以按同样的模式逐步补齐。骨架在、套路在,剩下的都是沿着同一条路径生长出来的。

7. 我在开发过程中踩过的三个坑与最终改进

7.1 时间序列化和时区问题

第一次联调的时候,前端同学反馈所有接口返回的时间都是一串数字,没法显示。查下来原因是项目没配置 jackson.date-format,LocalDateTime 序列化成了数组结构。

解决办法就是一开始在配置文件中统一日期格式,并在数据库连接串里显式指定 serverTimezone=Asia/Shanghai。

第二个坑更隐蔽:如果服务器时区和数据库时区不一致,统计日报数据会偏几个小时,查起来特别费劲。我的建议是,项目初始化当天就把这两个配置写好,宁可多写三行,不要等联调时再补。

7.2 物理删除造成的脏数据

有一次在处理学生离校时,只是简单执行了删除学生表里的记录,住宿记录表和房间占用数没有一起处理。结果过了几天统计空房间,数据对不上:明明某个床位已经空出来了,房间的已住人数仍然没有下降;学生已经删掉了,住宿历史里却还留着一条“幽灵记录”。

后来我把所有核心表的删除改成逻辑删除。学生离校变成一个状态变更事件,退宿操作也通过显式的记录状态释放床位,不再直接删数据。这样历史完整,统计准确,数据也不会因为手误而丢失。

逻辑删除有个注意点:唯一约束的字段,比如学号,逻辑删除后再新增同号学生,数据库唯一索引会冲突。这时候要么去掉物理唯一索引,改为代码判断;要么在唯一字段里拼接删除标记。这个细节提前想好,不然上线后会很被动。

7.3 漏掉后端接口权限校验

有一次做前端页面时,把宿管员的管理入口隐藏了,就觉得后端一定安全。后来用接口工具直接请求分配宿舍的 URL,发现居然能调通。根因很简单:后端接口没有做角色校验,任何登录用户都能调用管理类接口。

修正方法就是前面说的,用 Spring Security 的权限注解把所有写操作和管理类接口保护起来,不依赖前端隐藏来保安全。权限这块应该是后端主动设防,不能把安全寄希望于界面不可见。这是个特别基础但又特别容易被新人忽略的问题。

7.4 性能上的两个小优化

宿舍系统并发量本身不大,但有两个点优化之后体验提升明显。

一是公告列表用 Redis 做缓存。公告读多写少,缓存有效期设置十分钟,数据库压力小很多,前端打开首页也不会转圈。

二是报修单创建后的通知逻辑改为 Spring 异步事件。学生提交报修后,接口不需要等通知发送完成,使用异步执行后响应速度明显提升。这两步都不复杂,但对用户体感改善很明显。


最后说说自己的体会。宿舍管理系统这类项目,业务本身并不复杂,真正体现技术水平的地方,全在“边界”和“一致性”上。边界画清楚了,需求不会像雪球一样越滚越大;一致性想明白了,分配不会超卖,调宿不会丢数据,报表也不会对不上。把这套思路迁移到其他管理系统里,比如资产、工单、会议室预订,你会发现它们的骨架都是相通的。项目做完,最大的收获往往不是某个 CRUD 写得多漂亮,而是你已经形成了一套面对不确定需求时仍然能保持数据正确的设计习惯。

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

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

立即咨询