简介:这是一套面向高校计算机专业学生与Java初学者、可用于毕业设计或课程实训的学生宿舍管理系统完整源码。项目前端基于Bootstrap搭建管理界面,后端采用SpringBoot框架与MySQL数据库,按管理员、宿管、学生三类角色划分权限:管理员可对学生、楼宇、宿舍、入住、宿管及管理员信息进行增删改查与Excel导入导出,宿管负责学生、宿舍与入住管理,学生端则提供学生与宿舍信息查询,功能闭环完整。资源包共661个文件,以js、css、less、scss等前端资源为主,辅以35个java源文件、40个class编译文件及sql脚本、yml配置、xml映射等,压缩包约72.37MB,并附MySQL5与MySQL8两套可运行项目。已有469人学习下载,适合作为毕业设计参考、框架入门练手或二次开发基础,帮助读者快速理解角色权限设计与前后端数据交互流程。
1. 需求先立住:宿舍管理为什么值得单独做一套系统
每年毕业季,后台被问得最多的一类问题就是“管理系统到底怎么才算做完”。拿学生宿舍管理来说,表面看只是几张表:学生、楼栋、房间、床位,好像一个 CRUD 就能交差。真动手才发现坑在后面——床位要能分配和退换、晚归和报修要留痕、宿管和辅导员看到的数据范围不一样、学期末还要出一张能对得上的统计表。这套基于 Java 的学生宿舍管理系统,解决的就是“人、房、床、事”四件事在一个闭环里跑通的问题,适合拿来做毕业设计或课程设计的完整交付,也适合作为管理系统类项目的骨架去改。关键词里的项目源码、java源码、毕业设计、管理系统,落到这里就是一套能跑、能讲、能验收的工程,而不是一堆散装接口。
2. 总体架构与数据模型:先把四张核心表和三层结构定死
2.1 技术选型:为什么是 Spring Boot + MyBatis + MySQL
管理系统类项目最怕选型花哨。我的习惯是后端用 Spring Boot 打底,持久层用 MyBatis,数据库 MySQL,前端可以是服务端渲染的模板页,也可以是前后端分离的 Vue 页面,看你的答辩要求。选 Spring Boot 的理由很实在:内嵌容器、起步依赖、配置集中,一个application.yml就能把数据源、端口、日志管住,答辩现场换台机器也能跑起来。MyBatis 相比全自动 ORM 的好处是 SQL 可控,宿舍管理里“按楼栋统计空床位”“查某学生历史住宿记录”这类查询,手写 SQL 比拼条件构造器清楚得多,出了问题也能直接看 SQL 日志定位。
依赖清单通常控制在下面这个范围,版本号按你本地环境对齐即可,不要盲目追新:
<!-- pom.xml 关键依赖,版本按本地 JDK 与 Spring Boot 主线对齐 --> <dependencies> <!-- Web 层:REST 接口与参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 持久层:MyBatis 与 MySQL 驱动 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 登录鉴权与密码加密,避免明文存密码这种硬伤 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>这段依赖里,spring-boot-starter-web负责接口和静态资源,mybatis-spring-boot-starter负责 Mapper 扫描和 SQL 会话,MySQL 驱动只在运行期需要所以标runtime。安全 starter 不是必须,但宿舍系统有学生、宿管、管理员三类角色,登录和权限绕不开,早引入比后期补要省事。参数上要注意的是 MyBatis 的mapper-locations和type-aliases-package两个配置,前者指向 XML 映射文件目录,后者让实体类不用写全限定名,配错了启动就报绑定失败,这是新手最常见的翻车点之一。
2.2 数据模型:学生、楼栋、房间、床位四张主表加两张流水表
宿舍管理的本质是资源分配,所以表结构要围绕“一个床位在某个时间段属于某个学生”来设计。四张主表是学生表、楼栋表、房间表、床位表,两张流水表是住宿记录表和事件记录表(晚归、报修、卫生检查都往这里放)。床位表里不要直接存学生姓名,只存当前占用学生的外键,历史归属交给住宿记录表,这样退宿换宿都不会丢数据。
-- 床位表:核心是状态与当前占用人的解耦 CREATE TABLE bed ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL COMMENT '所属房间', bed_no VARCHAR(16) NOT NULL COMMENT '床位编号,如 A-01', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已占用 2维修', student_id BIGINT NULL COMMENT '当前占用学生,空闲时为 NULL', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY uk_room_bed (room_id, bed_no) ); -- 住宿记录表:一条记录就是一段住宿生命周期 CREATE TABLE stay_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, bed_id BIGINT NOT NULL, check_in DATETIME NOT NULL COMMENT '入住时间', check_out DATETIME NULL COMMENT '退宿时间,NULL 表示在住', operator VARCHAR(32) NOT NULL COMMENT '经办宿管账号' );床位表加version字段是为了处理并发分配。两个人同时点“分配床位”,如果不加锁,可能都读到空闲状态然后各写一次,最后一条覆盖另一条,学生住哪就成玄学了。用乐观锁在更新时带上版本条件,更新影响行数为 0 就提示“该床位已被占用,请刷新重试”,这是最省事也最可靠的做法。住宿记录表的check_out允许为空,查询“当前在住”就是check_out IS NULL,比在床位表上反复改状态更不容易出错。
2.3 分层结构:Controller 只做参数校验,业务规则全压到 Service
三层结构谁都会说,但真正决定项目好不好维护的是职责边界。我的做法是 Controller 只接收请求、做参数格式校验、调 Service、包装返回;Service 承担全部业务规则,比如“一个学生同时只能有一个在住床位”“维修中的床位不能分配”“退宿必须先结清报修单”;Mapper 只做数据存取,不写业务判断。这样答辩时被问到“换宿逻辑在哪”,你能一句话指到 Service 的具体方法,而不是在 Controller 里翻半天。
@Service public class BedAllocateService { @Transactional(rollbackFor = Exception.class) public void allocate(Long bedId, Long studentId, String operator) { // 规则一:学生不能已有在住床位 Long living = stayRecordMapper.countLivingByStudent(studentId); if (living != null && living > 0) { throw new BizException("该学生已有在住床位,请先办理退宿"); } // 规则二:床位必须空闲,用乐观锁防止并发覆盖 int updated = bedMapper.occupy(bedId, studentId); if (updated == 0) { throw new BizException("床位已被占用或处于维修状态,请刷新后重试"); } // 规则三:写入住宿流水,保留完整生命周期 stayRecordMapper.insert(studentId, bedId, operator); } }@Transactional保证占床和写流水要么都成功要么都回滚,rollbackFor = Exception.class是因为默认只回滚运行时异常,业务异常如果继承的是受检异常就会漏回滚。occupy对应的 SQL 里要带status = 0 AND version = #{version}条件并version = version + 1,这就是乐观锁的落地方式。参数operator从登录会话里取,不要由前端传,否则谁操作的都说不清,审计就失去意义了。
3. 核心模块实现:分配、退换、事件与权限四条主线
3.1 床位分配与退换宿:状态机比 if-else 可靠
床位从空闲到占用再到空闲,中间还可能进维修,这是一个典型状态机。用枚举把状态和允许的迁移写死,比散落各处的 if 判断稳得多。分配走“空闲→占用”,退宿走“占用→空闲”,报修走“空闲→维修”,维修完成再回“空闲”。每次迁移都校验前置状态,非法迁移直接抛业务异常。换宿本质是“退宿 + 分配”的组合,但要在同一个事务里完成,否则中途失败会出现学生两边都没床的尴尬局面。
public enum BedStatus { FREE(0), OCCUPIED(1), REPAIR(2); private final int code; BedStatus(int code) { this.code = code; } // 允许的状态迁移表,集中管理避免逻辑分散 public boolean canTransitTo(BedStatus target) { return switch (this) { case FREE -> target == OCCUPIED || target == REPAIR; case OCCUPIED -> target == FREE; case REPAIR -> target == FREE; }; } }这段枚举把迁移规则收在一处,Service 里只调canTransitTo,新增状态时改这里就行。switch表达式要求 JDK 14 以上,如果你的环境是 JDK 8,改成传统switch加return即可,别为了语法糖把环境搞崩。换宿方法里先调退宿、再调分配,两步都在@Transactional内,任何一步抛异常整体回滚,学生不会出现“无床可住”的中间态。
3.2 晚归、报修与卫生检查:事件表统一收口
很多同学把晚归、报修、卫生检查各建一张表,结果统计时要 union 三张表,越写越乱。我的做法是一张事件表,用event_type区分类型,用payload存类型相关的扩展信息(JSON 字符串即可)。晚归记时间,报修记故障描述和处理状态,卫生检查记分数。查询“某学生本学期所有异常事件”就是一条按学生和时间范围的查询,统计报表也好做。
CREATE TABLE dorm_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, event_type VARCHAR(16) NOT NULL COMMENT 'LATE_LATE/RREPAIR/HYGIENE', payload JSON NULL COMMENT '类型相关扩展字段', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已处理', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_student_time (student_id, created_at) );event_type用字符串而不是数字,是为了日志和排查时一眼能看懂,代价是占点空间,管理系统这个量级完全无所谓。idx_student_time联合索引支撑“按学生查历史事件”的高频查询。payload用 JSON 类型需要 MySQL 5.7 以上,低版本改TEXT也行,只是失去 JSON 函数校验能力。报修的处理状态流转同样建议走状态机,避免“已处理”被反复改回“待处理”这种数据污染。
3.3 角色权限:三类角色看到的数据范围必须不同
学生只能看自己的住宿信息和自己的事件记录;宿管能看本楼栋的学生和床位,能办理入住退宿;管理员能管楼栋、房间、账号,能看全局统计。实现上不要在每个接口里手写 if 判断角色,用 Spring Security 的方法级注解加自定义数据范围校验更清晰。角色存role字段,楼栋归属存building_id,宿管查询时强制拼上楼栋条件,这是数据隔离的底线。
@PreAuthorize("hasAnyRole('ADMIN','WARDEN')") @GetMapping("/beds") public Result<List<BedVO>> listBeds(@RequestParam Long buildingId) { // 宿管只能查自己管辖楼栋,管理员不受限 Long scope = SecurityUtils.currentBuildingScope(); if (scope != null && !scope.equals(buildingId)) { throw new BizException("无权查看该楼栋数据"); } return Result.ok(bedService.listByBuilding(buildingId)); }@PreAuthorize做角色粗筛,currentBuildingScope做数据细筛,两层配合才能既挡住越权又不用写重复代码。currentBuildingScope从登录用户上下文取,管理员返回null表示不限。这里容易翻车的是把楼栋 ID 交给前端传却不校验,学生改个参数就能看别人楼栋,答辩时被追问一句就露馅。
4. 避坑与排查:这几处翻车点我踩过不止一次
4.1 并发分配导致一个床位两个人
现象是压测或两人同时操作时,同一床位出现两条在住记录。原因是先查空闲再更新,中间没有锁。解决办法是床位表加version乐观锁,更新语句带版本条件,影响行数为 0 就提示重试;对分配接口再加一层按床位 ID 的分布式锁或数据库行锁作为兜底。血泪经验是别指望前端按钮置灰能防并发,接口才是唯一防线。
4.2 退宿后历史记录丢失
现象是学生退宿再入住,之前的住宿记录查不到了。原因多半是退宿时直接删了住宿记录,或者床位表覆盖了学生字段。正确做法是住宿记录只更新check_out时间,永不物理删除;床位表只改状态和当前学生,历史归属靠流水表还原。统计“某床位历年住过谁”时,查流水表按bed_id过滤即可。
4.3 事务不生效导致半成功
现象是分配床位成功但流水没写进去,或者换宿只退没分。原因通常是@Transactional加在同类内部调用的方法上,或者异常被 catch 后没重新抛出。排查时先看方法是不是被 Spring 代理调用(跨类调用才走代理),再看异常类型是否在回滚范围内。我的习惯是业务异常统一继承RuntimeException,并在全局异常处理器里记日志,出问题能顺着日志找到具体事务边界。
4.4 统计报表数字对不上
现象是首页显示的空床位数和床位列表实际数量不一致。原因一般是统计 SQL 和列表 SQL 用了不同的过滤条件,比如一个算了维修床位一个没算。解决办法是把“有效床位”的定义收敛到一个常量或视图里,统计和列表都引用同一套条件。答辩前务必拿一批造好的数据跑一遍交叉核对,别等老师现场点开才发现数字打架。
5. 验收与答辩:用一张检查表把项目讲圆
验收这件事,我的建议是别等做完再想,而是在开发中期就把检查表列出来,边做边对。下面这张表可以直接拿去用,左边是验收项,右边是判断标准和演示方式,答辩时按顺序走一遍,基本不会卡壳。
| 验收项 | 判断标准 | 演示方式 |
|---|---|---|
| 登录与角色隔离 | 三类角色登录后菜单和数据范围不同 | 分别登录学生、宿管、管理员各点一遍 |
| 床位分配 | 空闲床位可分配,已占用提示失败 | 同一床位连续分配两次,第二次报错 |
| 退宿与换宿 | 退宿后床位回空闲,历史记录可查 | 退宿后查流水表,再换宿验证事务 |
| 事件记录 | 晚归、报修、卫生检查可录入可查询 | 录一条报修并改状态,按学生查历史 |
| 统计报表 | 空床位数与列表一致 | 首页数字与床位列表逐楼栋核对 |
| 异常处理 | 非法参数返回统一错误结构 | 传空 ID、越权楼栋 ID 各试一次 |
答辩技巧上,最容易被问倒的不是功能多少,而是“你为什么这么设计”。所以每个模块准备一句话的设计理由:床位用乐观锁是因为并发分配、住宿记录不删除是因为要留历史、事件表统一收口是因为统计方便。被问到没做的功能,坦诚说“当前版本没覆盖,扩展点在某某表/某某接口”,比硬编一个不存在的功能强。演示前把数据造好,至少准备两个楼栋、若干学生、几条事件记录,空库演示是答辩大忌。
从那以后我每次带人做管理系统类项目,都强制先画数据模型再写第一行代码,表结构定不下来就不动 Controller。这套习惯帮我省掉了大量返工,也让答辩时的每个“为什么”都有据可依。希望帮到你。
本文还有配套的精品资源,点击获取