☰
Spring Boot学生公寓管理系统实战:从数据库设计到并发权限避坑指南
2026/10/28 19:08:44 网站建设 项目流程

Springboot学生公寓管理系统,这类项目我前后接触过好几茬。最早是给一个做智慧校园的朋友做技术评审,后来陆续有学生拿来当毕业设计找我帮忙看代码。接触多了发现一个问题:很多人不是写不出来,而是没想清楚这东西到底要管哪些事,结果数据库表建了三十多张,功能互相打架,源码看着热闹,跑起来一团糟。

如果你正在做或者打算做一个基于Spring Boot的学生公寓管理系统,这篇内容应该能帮你省下不少弯路。我整理了完整的设计思路、表结构、核心接口和部署踩坑记录,会告诉你每一步为什么这么设计,也会把最容易翻车的并发分配、权限控制、跨域处理这些点拆开讲。不管你是拿来当毕业设计、课程设计,还是想在公司内部做一套公寓管理工具,照着这个思路走,至少能少改三十次表结构。

1. 先别急着写代码,把"公寓管理到底管什么"盘清楚

很多同学打开IDEA敲代码之前根本没有完整梳理过业务。看网上开源项目,有个学生表就往里塞宿舍ID,有个宿舍表就当所有寝室结构一样,结果验收的时候甲方要求来访登记要能查历史,退宿之后床位数要对得上,你才发现原先的模型根本撑不住。

1.1 传统手工管理模式下,最痛的是哪几个环节

我们逐个场景代入一下。宿管阿姨的登记本上,住着哪个学生、哪个床位、电话多少、住哪栋哪层,全靠手写。新学期迎新,几百个新生入住,一个一个手抄,宿管大爷眼睛都快看花。半夜学生没带卡回不了楼,登记本上找名字要翻三分钟。更麻烦的是月底水电费统计,宿管拿着抄表本和入住名单对,不对上半天根本算不清。

再往后走,公寓的椅子坏了、灯管不亮了,学生找辅导员,辅导员通知后勤,后勤找维修工,整个过程没有任何流转记录,修没修、谁修的、修了几次全靠人脑记忆。外来回访客人进楼,证件登记完全凭自觉,出了安全问题根本追溯不到人。

如果你把上述流程抽象成后端需求,就会发现它其实涵盖了很多典型模块:人员管理、房间状态管理、流程性事务处理、权限区分、甚至费用计算。用Spring Boot来做恰好合适,因为它生态成熟,做增删改查非常快,应付这种中小型系统绰绰有余。

1.2 使用者的角色到底有几个,每种角色关心什么

做管理系统第一件事就是把“谁在用”定清楚。学生公寓管理系统通常有四种角色,很多新手一开始只设计了管理员和学生,这绝对不够。

系统管理员管的是全局,比如楼栋录入、宿舍类型维护、账号分配、公告发布、数据统计,他看到的是宏观数字。

宿管员管的是具体某栋楼,负责入住和退宿办理、晚间查寝信息录入、访客登记、报修单审核,他关心的是床位数据准不准、今天哪些房间有报修、月底水电费怎么分摊。

学生是日常使用的最大群体,需要能提交报修、查看账单、登记访客信息、接收公告,偶尔还能查一下自己住哪间宿舍,舍友是谁。

还有一类是辅导员或院系老师,虽然不直接操作后台,但需要查看学生入住分布、晚归记录、违规用电记录,走的是“只读+审核”权限。

角色模型设计清楚之后,后端权限模块才能有的放矢,否则后面做Shiro或者Spring Security的时候,你根本不知道接口该给谁放行。

1.3 功能边界:必修项和加分项划分

我综合自己接过几个实际项目的经验,整理了一张功能清单。

类型功能模块说明
核心必修楼栋与房间管理维护楼栋、楼层、房间号、房间类型、床位、朝向等基础档案
核心必修学生入住退宿流程支持入住登记、分配宿舍、换宿、退宿,记录完整历史
核心必修宿管签到/查寝管理支持宿管按楼栋录入晚归、未归、查寝异常情况
核心必修报修工单流转学生提交、宿管审核、维修工接单、完成回执与评价
核心必修访客及大件物品出入登记记录访客姓名、证件号码、被访人、进出时间
核心必修公告通知管理员发布,学生列表浏览
核心必修水电费与住宿费账单定期生成账单,学生查询缴纳状态
加分拓展门禁数据同步对接硬件或模拟门禁刷卡记录
加分拓展可视化统计入住率、楼栋空床位、性别分布图表
加分拓展学生晚归推送对接微信/邮件通知辅导员

强烈建议第一版只做核心必修项。很多做毕设的同学失败就失败在功能膨胀上,宿舍、学生、座位、卫生检查、活动报名一把抓,单个模块都做成了半吊子,数据库关系乱成一团。

2. 技术选型不是越新越好,我用这套组合的原因

2.1 为什么Spring Boot而不是SSH或者Spring Cloud

学生公寓管理系统属于典型的中小型单体应用,用Spring Boot来承载再合适不过。Spring Boot最大的优势是自动配置和约定优于配置,你不需要自己拼装一堆XML配置文件,一个Maven工程拉下来就能启动。同时它对内嵌Tomcat的支持非常友好,打包成可执行Jar包扔到Linux服务器就能跑,部署成本极低。

有些同学觉得学Spring Cloud微服务很高级,非要拆成用户服务、房间服务、工单服务,项目弄成三四套工程。我只能说这属于过度设计。公寓管理系统的并发量可能一天也就几百个请求,单体应用完全能扛住,而且调试和部署都很轻松。真正搞微服务之后,你还要处理服务注册、负载均衡、分布式事务,对毕设而言纯粹是给自己挖坑。

2.2 ORM框架为什么要选MyBatis-Plus而不是纯MyBatis

这里我非常推荐MyBatis-Plus。原生MyBatis写简单CRUD确实也比较方便,但你要写很多Mapper接口和XML映射文件,尤其是分页查询这种高频操作,手写复杂SQL非常繁琐。MyBatis-Plus在MyBatis之上提供了BaseMapper,用户表、宿舍表这些实体直接继承,不需要写一行XML就能完成基本增删改查,分页插件也是现成的。

我目前的推荐组合是:

  • Java 8 或 11,别盲目用Java 17或21,原因后面讲
  • Spring Boot 2.7.x
  • MyBatis-Plus 3.5.x
  • MySQL 5.7 或 8.0
  • Redis(可选,主要做验证码缓存和接口防重复提交)
  • Knife4j 用于接口文档管理
  • JWT + 拦截器做认证授权,不引入Spring Security,因为角色简单,自己控制更清晰
  • 前端根据情况选择,如果走前后端分离就使用Vue 2 + Element-UI,如果用模板引擎则Thymeleaf

这套选型的中庸之处在于,它既不会让项目看起来特别简单,也不会给自己增加太多新框架的学习成本。

2.3 Spring Boot版本的选择暗藏一个大坑

这里我要专门提醒,热词里有一条是“springboot版本太高”,这确实是很多人爆炸的点。Spring Boot 3.x发布于2022年底,本身没有问题,但它基于Jakarta EE规范,过去用javax.servlet、javax.validation这一套包名全部改成了jakarta.*,意味着大量老牌第三方组件的兼容版本都要跟着升级。

很多同学从Gitee上拉下来一个老项目,发现pom里写的是Spring Boot 2.7,自己手动改成3.1之后,Tomcat容器换了,JSTL标签库也不兼容,页面直接500。我的建议是:如果项目涉及毕业设计,而且你主要用MyBatis-Plus和通用工具类,Spring Boot 2.7.x是更稳妥的选择,教程多、问题答案多、时间也不会浪费在配置地狱里。

2.4 业务架构上如何分层

源码的组织架构也值得说明一下。很多初学者把所有代码写进Controller,一个方法两百行,这是在给自己埋雷。我习惯的分层方式是:

  • Controller层:只负责参数接收、调用Service、返回统一结果
  • Service层:处理真正的业务规则,具备事务边界
  • Mapper层:数据访问,杜绝业务代码侵入
  • entity包:与数据库表对应的实体
  • dto包:接收前端请求相关的传输对象
  • vo包:返回给前端的数据视图对象,隐藏敏感字段
  • config包:框架配置类
  • common包:全局异常处理、统一返回封装、JWT工具、常量定义

注意一个容易犯的问题是实体类直接返回给前端。比如学生实体内包含手机号、身份证号,如果直接序列化返回,接口一旦暴露,个人隐私全部泄漏。这种需求场景下,后端一定要写VO来做字段裁剪。这个习惯越早养成越好,上班以后没人会给你机会把数据库表直接返给前端。

3. 数据库设计直接决定后面要改多少代码

学生公寓管理系统的数据库设计是整个项目的灵魂。我常说,宿舍和学生的关系如果设计错了,后面每一个功能都会别扭。别再像有些半吊子教程那样,直接在student表里搞一个room_id字段,入住退宿全靠更新这个字段,那么你根本回答不了“上学期这个学生住哪间”的问题。

3.1 核心表拆解:档案数据和流转数据要分开

我们来梳理一下,这个系统到底需要哪些核心表。

第一类是基础档案表:楼栋表building、宿舍表dormitory、床位表bed、学生档案表student、账号表sys_user、公告表notice。这一层是静态数据,数据量相对稳定,维护频率不高。

第二类是业务流转表:入住记录表live_record、报修工单表repair_order、水电费账单表fee_bill、访客记录表visitor_record、查寝记录表check_record。这一层是动态数据,记录了“谁在什么时间干了什么”。

第三类是关联配置表,比如角色表role、菜单权限表menu、角色菜单关系表,如果要用比较完整的权限管理可以考虑RBAC模型。

很多新手会把student和live_record混淆。其实student表里存的是学生自然信息:学号、姓名、性别、学院、班级、联系方式;而live_record表存的是入住行为:某个学生在某段时间住进了哪个宿舍哪个床位。业务上,一个学生可能住了四年,中间换过宿舍,如果没有live_record表,他的住宿历史就丢了。

3.2 宿舍-床位-入住记录之间的关系该怎么落

宿舍表设计上有些字段需要提前想清楚。比如一个房间是几人间,是否混合住宿(不同性别混住通常不允许,靠字段约束),是否为公共卫生间,是否有空调。这些看似业务上的细节,将来都会变成筛选条件。

床位表bed可以单独建,也可以不建。如果宿舍类型都是标准四人间、六人间,不涉及随机分配床位,很多项目选择在入住时为每个宿舍动态编号床位。但我建议还是建立bed表,因为实际公寓管理系统中,很多床位会处于故障、维修、临时封闭等状态,不单独存床位数据,根本表达不了这种精细状态。并且宿舍分配时直接落到床位上,后续换宿、退宿追踪会更准确。

关键关联设计如下:

  • dormitory表有building_id外键,room_no房间号,bed_count床位总数,bed_used当前已用床位数,gender_type枚举男寝/女寝
  • bed表中dorm_id指宿舍,bed_no是床号,status字段表示可用/占用/维修
  • live_record表记录student_id、dorm_id、bed_id、check_in_time、check_out_time,check_out_time为空代表当前正在住

为了确保一个学生同一时间不能有两处有效住宿,可以在live_record表中添加一个条件唯一索引,专门约束“未退宿数据的学号唯一”。MySQL支持函数索引或者虚拟列,简单点可以做退宿标记字段: UNIQUE KEY uk_student_active (student_id, is_active) 这样在数据库层面就成了最后防线,比代码判重可靠得多。

3.3 流水表最容易忽略的几个字段

预算充足的设计总是提前把“审计”字段留好。比如每个表都安排create_time、update_time两个时间字段,新增记录时自动填充。MyBatis-Plus可以用MetaObjectHandler实现,也可以直接在实体类上使用@TableField(fill = FieldFill.INSERT)。

还有一个常见错误是使用浮点数存储水电费金额。金融计算之后很容易出现小数点精度问题,比如0.1+0.2不等于0.3。账单表里面金额字段请使用DECIMAL(10,2),也就是定点数。这个坑我在代码评审的时候至少看到十次以上,大家引以为戒。

报修单表和访客登记表里有个外键概念容易搞乱。报修单的提交人可能是学生,处理人是后勤维修师傅,而维修师傅也可能单独作为一张员工表存在。如果不想把员工系统做太复杂,可以做一张简单的handler表或者直接用账号表的用户ID来取代。访客登记表则需要记录访问的宿舍楼栋和宿舍房间号,但和student表的关系要注意,因为这些年来宿舍变迁,访客登记时要以当时地址为准,最好冗余一个dorm_info文本字段,防止学生已经毕业导致关联数据查不出来。

3.4 状态字段别用字符串随缘填,枚举化之后少很多Bug

我在项目里习惯把状态相关的字段全部设计成字符类型,但约束值范围。例如bed.status只有0可用、1占用、2维修、3停用几个取值;repair_order.status有0待审核、1已派单、2维修中、3待确认、4已完成、5已关闭。这些取值在Java层用枚举类维护,数据库中写注释说明,配合索引可以做状态筛选。

你要是不好理解,打个比方说,状态机正如洗衣机面板上的模式,你让洗衣模式保持“标准、轻柔、快洗”几个选项,使用洗衣机的用户永远不会想到去拧一个不存在的“烘干加热”按钮。把状态约束住,前端的下拉选项、后端逻辑判断、数据库统计三处就不会出现鸡同鸭讲。

4. 后端实现时的核心场景与代码落地

当我评估一个学生公寓管理系统的源码好坏时,不会只看它能不能跑通登录注册,而是重点看三个场景:权限机制、入住分配、以及状态怎么流转。

4.1 认证授权方案:为什么我用JWT而不是Session

做管理系统的登录,目前大家普遍采用JWT。JWT的原理可以简单理解成:用户登录成功后,后端根据用户身份信息生成一段带签名的令牌字符串,前端携带这个令牌访问受保护接口,后端验签通过即放行。它天然适合前后端分离项目,多终端登录也比较自然。

流程:

  1. 用户提交用户名密码
  2. 后端校验通过后返回token
  3. 前端存储token,每次请求头自动带上Authorization: Bearer token
  4. 后端的拦截器解析token并获取当前用户角色,判断是否有权限

与Session的区别在于,Session数据存在服务端内存中,适用于同一个Servlet容器内的请求;一旦需要做多节点部署或者小程序、App共用登录态,就非常麻烦。JWT自包含令牌,服务端不需要保留会话状态,但要注意token过期逻辑,不然会过期无感。

学生公寓管理系统的角色不多,因此可以按角色写一个判断注解,哪些接口允许宿管、哪些只允许管理员。我自己的简便做法是使用一个自定义注解@RequireRole,在HandlerInterceptor中判断用户角色是否在允许集合中。如果角色多、权限细,就可以考虑接入Spring Security或Sa-Token的权限框架。

4.2 最核心的批量入住场景,怎么写才不至于半夜被电话吵醒

宿舍入住的分配功能,隐藏着一个典型并发问题。每年开学会有大批新生同时办理入住,假设现在只剩下最后一个床位,两个宿管员同时办理分配到最后一间,两个请求同时查到床位可用,同时执行更新,就会导致一个床位被分给两个学生。

我先给你看一个错误示例:

// 错误示例:先查再改,不锁资源 Bed bed = bedMapper.selectById(bedId); if ("0".equals(bed.getStatus())) { bed.setStatus("1"); bedMapper.updateById(bed); liveRecordService.insert(studentId, bed.getDormId(), bedId); }

这样在高并发下,两个线程看到的都是status=0,于是都执行了更新。正确做法是在事务内部加锁行,锁定该床位后再判断状态,保证每个时刻只有一个线程在操作该床位。

@Transactional(rollbackFor = Exception.class) public void checkIn(CheckInRequest request) { // 1. 查询学生档案是否存在且未注销 Student student = studentMapper.selectById(request.getStudentId()); if (student == null) { throw new BusinessException("学生不存在"); } // 2. 检查学生是否已有有效入住记录 LambdaQueryWrapper<LiveRecord> activeWrapper = Wrappers.lambdaQuery(); activeWrapper.eq(LiveRecord::getStudentId, request.getStudentId()) .eq(LiveRecord::getIsActive, "1"); if (liveRecordMapper.selectCount(activeWrapper) > 0) { throw new BusinessException("该学生当前仍有有效入住记录,请先办理退宿"); } // 3. 锁定床位行,防止并发抢占 Bed bed = bedMapper.selectBedByIdForUpdate(request.getBedId()); if (bed == null || !"0".equals(bed.getStatus())) { throw new BusinessException("床位状态不可用"); } // 4. 更新床位状态 bed.setStatus("1"); bedMapper.updateById(bed); // 5. 插入入住记录 LiveRecord record = new LiveRecord(); record.setStudentId(request.getStudentId()); record.setDormId(bed.getDormId()); record.setBedId(bed.getId()); record.setCheckInTime(new Date()); record.setIsActive("1"); liveRecordMapper.insert(record); // 6. 同步宿舍已用床位数量,并判断是否已满 dormitoryMapper.increaseUsedCount(bed.getDormId()); }

注意,selectBedByIdForUpdate需要自定义SQL,本质是执行SELECT ... FOR UPDATE,这样MySQL Innodb引擎会给该行加排他锁。本事务未提交前,其他事务必须等待。这是解决并发分配的底层保障。

再配合唯一索引,数据库层兜底,这个问题才算真正稳住。这样的代码写出来,拿来当面试亮点也是完全能打的。

4.3 退宿舍和换宿舍容易忽略的逻辑

退宿不是把床位状态改成0就结束了。退宿要同时完成以下动作:

  1. 从业务关系上关闭这张入住单:把live_record的check_out_time置为当前时间,将is_active置为0
  2. 检查学生名下有没有未结清的水电账单或未完成报修单,如果有则系统拦截或提示
  3. 床位状态改为0可用,宿舍usedCount减一

换宿可以看成是“退宿+入住”的组合操作,不过为了保留历史链路,需要一口气包在一个@Transactional里:先把旧入住记录关闭,再清理新床位的占用状态,再插入新的live_record。如果第一步成功、第二步失败,就必须整体回滚,所以千万别在两个事务里分开写。

如果你想要文档里把这些说明写清楚,我会在配套文档的“核心流程设计”部分单列一张状态流转表。

4.4 报修工单怎么做流转闭环

报修模块很多人做成最简单的“学生提交,管理员修改状态”就完事,这缺少了流程感。我建议至少包含五个阶段:

  • 学生提交报修单,附上文字描述和图片地址
  • 宿管审核是否同意维修,如果是人为损坏则驳回并备注原因
  • 维修人员接单,状态转为维修中
  • 维修完成,填完处理结果,状态待确认
  • 学生确认维修完成,工单关闭

每张报修单要有创建时间、修改时间、记录操作人。再往后可以扩展“维修评价”,但第一版不必做。前端只要一个按钮就能流转,后端通过updateStatus接口做状态机校验,防止学生跳过宿管直接改成已处理。状态机校验这部分,本质是把“流程状态允许的下一步变迁”写成一个Map,状态变迁不合法直接抛异常。

4.5 水电账单到底怎么生成

公寓的水电费往往一个月一个周期。管理处每月导入一次各房间表读数,系统算出本期用量和金额,给每位在住学生生成一条费用记录。这里需要注意产生账单的时间点应与live_record表有效入住记录关联,若月中入住,可以按入住日期计算分摊。

我在源代码里默认实现了两种账单模式:

  • 按宿舍总费用平摊到每个在住学生
  • 按房间独立计费,缴费责任到室长或全体住宿生

哪种更合理要看不同学校的管理规定,所以代码里把账单策略类单独放置,方便调整。金额一律用BigDecimal计算,账单状态从待缴、已缴费、已开票逐步推进。

5. 前端和接口设计,直接影响项目好不好演示

很多人写后端完全不考虑前端调用是否方便,结果自己做演示页面的时候越写越难受。我建议从开发第一天就约定好统一返回结构。

5.1 后端统一返回体该怎么设计

一个典型的前后端分离项目统一响应类通常包含code、message、data三个字段。例如:

{ "code": 200, "message": "操作成功", "data": { "list": [], "total": 10 } }

后端创建一个Result类泛型封装成功、失败、分页,全局异常处理器兜住所有业务异常,确保传给前端的错误信息是中文可读的。JWT过期时返回401状态码,前端全局拦截之后跳到登录页,这才是完整的处理链路。

接口文档使用Knife4j生成,相比老牌Swagger,Knife4j的界面更清爽,接口分组和调试也顺手得多。下载源码之后,只要在配置中开启Knife4j相关页面,就可以在本地直接查看和调试每个接口了。

5.2 前端宿舍分配页面怎么避免反人类

宿舍分配页面是所有前端页面里最容易做烂的。一个合理的分配界面应该具备两级选择:先筛选楼栋和性别,再展示空闲宿舍及床位状态。床位的可用状态用不同颜色标识,绿色代表可用、灰色代表占用、红色代表维修。宿管点击某个空床位后,右侧回显学生信息,二次确认完成分配。

如果前端选择直接在后端传宿舍ID而不限制性别,系统就会出现男生入女寝的严重业务事故。因此后端接口一定要根据当前登录宿管管辖楼栋判断,也要在宿舍数据校验时同时校验性别类型。前后端双重校验不是为了给谁添麻烦,而是为了真实业务安全。

5.3 常用接口清单预览

我在源码中整理过一份典型的接口清单,可以对照检查自己的项目是否完整:

  • POST /api/auth/login:登录
  • POST /api/auth/logout:退出
  • GET /api/dorm/page:宿舍分页查询,支持楼栋、状态、房间号条件
  • POST /api/dorm:新增宿舍
  • PUT /api/dorm:修改宿舍信息
  • GET /api/bed/list?dormId=xx:查询某宿舍所有床位
  • POST /api/live/checkIn:办理入住
  • POST /api/live/checkOut:办理退宿
  • POST /api/live/changeRoom:换宿
  • GET /api/repair/page:报修单分页
  • POST /api/repair:提交报修
  • PUT /api/repair/handle:处理报修单
  • POST /api/visitor:访客登记
  • GET /api/visitor/page:访客查询
  • GET /api/fee/page:费用流水
  • POST /api/fee/order:生成账单
  • GET /api/notice/list:公告列表

接口命名尽量使用REST语义,但考虑到国内项目的实际成熟度,POST配合具体动作接口也是常见做法,关键是风格要统一,不要一个方法叫/getUser另一个叫deleteStudentById。

6. 安全与数据保护,答辩时最容易被追问的点

学生公寓管理系统涉及大量学生隐私数据,你在源码里如果手机号明文返回、身份证号到处打印、密码在数据库里直接存原文,那面试官或老师肯定要抓住追问。这块做好了反而能成为加分项。

6.1 密码不能明文存储

原则非常简单:用户注册或初始密码生成后,不直接以明文形式入库,而应进行不可逆加密。Spring Security中自带的BCryptPasswordEncoder是常见做法,但如果不引入Spring Security,也可以使用jBCrypt库。

比如一个初始密码是123456,加密后存到数据库里的结果是: $2a$10$NecpS4S...

即使数据库泄露,攻击者也不能直接拿到明文密码。登录校验时,也是拿用户输入明文密码去和密文比对。这部分做好,无论毕设还是生产项目都是基本素养。

需要提醒一点:MD5不是不能用,但一定要加盐。不加盐的MD5在彩虹表面前基本等于裸奔。我仍然推荐BCrypt,它在生成时自带了随机盐,性价比最高。

6.2 接口层应该拦截和防什么

哪怕是一个毕设,基本安全规则也值得做。首先是统一统一鉴权拦截器,放行登录、验证码、Knife4j资源等公开接口;其余所有/api/**请求都必须校验token。

其次是防SQL注入。使用MyBatis-Plus的LambdaQueryWrapper基本能规避大部分拼接SQL注入风险,但在编写原生XML SQL时,绝对不要使用${}接收用户输入,必须一律使用#{}参数占位。

然后是防XSS,虽然前后端分离项目中React或Vue本身有默认转义,但演示系统也要关注用户留言、公告内容中的

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

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

立即咨询