1. 项目背景与需求梳理宿舍管理到底在管什么做这个宿舍信息管理系统的初衷得从一次真切的工作经历说起。我当时在帮朋友所在的某高校做数字化改造的调研发现宿管科老师的桌面上堆着好几本厚厚的手工登记册学生入住登记、床位分配、来访记录、报修工单、晚归登记、宿舍评分……全部靠纸质和Excel表格在跑。每到月底汇总两位老师要加班一星期还经常出现数据对不上、重复统计的情况。更要命的是学生想报修、想调宿、想查电费除了跑一趟宿管办公室没有任何线上渠道。这个项目的目标很简单就是给这类场景做一套真正能落地的管理系统。不是那种毕设演示用的增删改查Demo而是要把宿舍管理的实际业务流程走通。核算下来系统的核心用户有三类学生、宿管老师、系统管理员。学生要能查床位、在线报修、查水电余额、办退宿申请宿管老师要能管理楼栋房间床位、处理报修工单、登记来访、做宿舍检查评分管理员则负责账号权限分配和基础数据维护。需求分析阶段我花了不少时间泡在宿管办公室看他们一天天的工作流。举个具体的例子报修流程看起来简单——学生上报、师傅维修、学生确认——但实际执行中派单给谁就是个细节问题。宿舍楼的维修是分区域的水工、电工、木工各有负责的楼栋不是随便派一个人就能搞定。所以系统里必须要有维修项目分类和楼栋维修负责人的匹配关系这两层设计。这类软需求只有蹲过现场才能总结出来。再把非功能需求列一下并发量按照几千名学生同时访问来设计因为选课季、开学季会出现明显的峰值数据准确性和关系一致性要可靠学生调宿、毕业退宿这类操作必须事务化页面操作要足够简单宿管老师的电脑水平参差不齐很多老师不习惯复杂的筛选条件大按钮、明显的状态提示、分步向导式的表单是刚需。这些需求听起来朴素却是决定系统上手率的生死线。2. 技术选型背后的取舍逻辑技术栈从标题就能看出来SpringBoot Vue MySQL MyBatis。但为什么是这一套组合很多人没真正想清楚我在这里把选型逻辑掰开讲。2.1 为什么是SpringBoot而不是SSH或纯Servlet如果是五六年前做这种项目SSHSpring Struts Hibernate还是主流现在再选SSH就是给自己找麻烦。Struts的配置繁琐程度令人发指XML里塞几百行Action映射调试一个参数透传能耗费半天。SpringBoot的核心价值在于约定大于配置内嵌Tomcat解决了部署环境差异问题自动配置机制把数据源、事务管理器这些基础设施全部托管开发者只需要关心业务代码。对一个宿舍管理系统这种CRUD密集型的应用SpringBoot能把开发重心拉回到业务逻辑本身。再对比一下纯Servlet JSP的方案。Servlet虽然轻量但所有代码都要手动管理事务、手动处理JSON序列化、手动做参数绑定代码量差不多是SpringBoot的三倍。JSP的页面渲染模式在前后端分离时代也基本被淘汰了所有渲染逻辑靠后端输出HTML前端交互受限性能同步压力大。SpringBoot的生态成熟度也是重要考量遇到问题随手一搜就能找到大量案例。我甚至可以说这个项目选SpringBoot最大的收益不是某个具体功能而是它带来的可维护性和开发效率量级的提升。2.2 前端框架为什么Vue是这种管理系统的稳妥选项前端技术选型上我在Vue和React之间权衡过一阵。宿舍管理系统本质上属于中后台管理类应用特点是什么页面多但交互相对规整核心是表格、表单、弹窗、状态标签对实时数据流的需求不是特别高。Vue在这类场景下有天然优势——模板语法直观v-model双向绑定让表单处理异常顺手渐进式架构允许我们用Vue Router挂路由、用Vuex或Pinia管全局状态用Element UI作为组件库就能拼装出全部界面。React当然也能做JSX的灵活性在处理复杂嵌套组件时更强但代价是团队成员需要更高的React功底函数式状态管理的思想对后端出身的人来说学习曲线更陡。本着团队平均能力水平最合适的技术才是好技术这个原则Vue Element UI的组合让前端开发成本直线下降。另外我选了Vue 2而不是Vue 3这个决定今天看可能显得保守——但从生态兼容性来说Element UI对Vue 2的支持最成熟部分插件在Vue 3初期还不稳定。真正跑过生产环境的人会明白稳定的旧版本比时髦的新版本更让人安心。2.3 MyBatis与MyBatis-Plus的选择这个项目标题写了MySQL MyBatis但实际开发中我用的是MyBatis-Plus它是对MyBatis的增强封装。为什么不直接用原生MyBatis因为宿舍管理系统的数据表多、字段碎、单表CRUD占了八成以上。用原生MyBatis每个实体类都要配一堆XML映射文件写insert、selectById、updateById这些千篇一律的标签纯粹是体力活。MyBatis-Plus提供了通用Mapper单表操作一行代码都不用写它自带的IService接口把分页查询、条件构造器、逻辑删除这些高频能力全部内置了。我说的尽可能用MyBatis-Plus并不代表放弃原生的SQL能力——恰恰相反多表关联查询、复杂的统计报表、按状态聚合的Dashboard数据这些我用的是自定义SQL。在XML中手写resultMap和动态SQL既能享受MyBatis的SQL控制力又有Plus帮我们摆脱样板代码。一个很实在的场景首页需要一个各楼栋入住率统计的图表数据源涉及楼栋表、房间表、住宿记录表三表关联还要计算已入住床位除以总床位的百分比这种查询用Plus的Wrapper写非常别扭直接在XML里写联合查询SQL就顺手得多。所以结论是MyBatis-Plus打底 手写SQL补充两条腿走路。技术栈这块我最后强调一点MySQL里有个细节需要提前处理就是数据库字符集和排序规则。建库时一定要用utf8mb4而不是utf8因为utf8在MySQL里最多只支持3字节字符而一些生僻字和emoji字符需要4字节历史的教训是挺痛的。后面我在踩坑章节会再具体说。3. 数据库设计一张表一个字段都不能马虎数据库是管理系统的地基。宿舍系统表面上业务清晰但表与表之间的关联关系很容易设计得乱七八糟。我在这里把主要表结构梳理一遍这部分直接决定后面所有功能好不好写。3.1 核心表结构一览系统总共设计了11张表主表就是下面这些表名用途关键字段user用户表含学生/宿管/管理员id, username, password, role, name, phone, avatardorm_building楼栋表id, building_name, manager_name, manager_phone, area_typedorm_room房间表id, building_id, room_no, bed_count, used_bed_count, room_type, floorstudent_info学生信息表id, user_id, student_no, class_name, major, phone, emergency_contactbed_assignment床位分配表id, student_id, room_id, bed_no, assign_date, statusrepair_order维修工单表id, student_id, repair_type, description, images, status, repairer_id, create_timevisit_record来访登记表id, student_id, visitor_name, visitor_phone, visit_time, leave_timedorm_check宿舍检查表id, room_id, check_date, score, violation_desc, checker_idfee_record水电费记录表id, student_id, room_id, water_amount, electric_amount, period, is_paidleave_apply退宿/调宿申请表id, student_id, apply_type, reason, status, approve_timenotice公告表id, title, content, publisher_id, publish_time, top_flag这里说几个我在实际建模时反复推敲的细节。床位分配表的独立性。最开始我想把床位号直接做成房间表里的字段用逗号分隔存所有已分配床位。后来果断推翻了——这违背第一范式查询某学生住哪个床位要拆分字符串统计空床位要写存储过程完全是自己埋雷。独立的bed_assignment表用student_id关联学生用room_id关联房间床位状态在assign_date和status上做控制所有床位相关的查询都能用简单SQL完成。user表和student_info表的拆分。学生信息里的学号、班级、专业、紧急联系人这些是相对固定的基础数据而登录密码、手机号可能换、头像、角色属于账号维度数据混在一张表里会让更新逻辑变复杂。拆开之后安全性和扩展性都更好——未来如果要接入校园统一身份认证只需要替换user表的认证逻辑无需动学生档案。房间表里的used_bed_count字段。这是典型的冗余换性能设计。每次分配床位后除了在bed_assignment表插记录还要同步更新dorm_room表的used_bed_count字段。完全可以在查询时用count聚合拿到但宿舍管理场景下房间剩余床位这个信息几乎在每一个列表页面都要展示实时查count会让SQL很繁重。用冗余字段的代价是必须保证更新逻辑不遗漏——后面讲到事务控制时我会说这个字段的更新和床位分配操作必须在同一个事务里完成。3.2 权限与数据隔离的设计思路宿舍系统看起来是三个角色实际上权限模型要比表面复杂。学生只能操作自己的数据宿管可以管理自己所辖楼栋的数据管理员拥有全部权限。比如一个A栋的宿管登录系统后他不应该看到B栋的房间基础信息和报修工单。我的做法是在表设计阶段就预留归属字段dorm_building表有manager_id字段指向宿管老师的user_id宿舍检查表有checker_id维修工单有repairer_id。在SQL层面做数据隔离而不是靠前端粗暴地隐藏按钮。比如宿管查看报修单列表时后端SQL强制加上WHERE building_id IN (SELECT building_id FROM dorm_building WHERE manager_id 当前用户ID)。这种后端强制隔离比前端跳转拦截靠谱得多就算有人伪造接口请求也拿不到其他楼栋的数据。4. 后端核心模块设计与实现数据库落定后后端开发就有章可循了。这个章节我把项目分层、登录认证、报修流程和事务处理这几个关键模块的实战过程捋一遍。4.1 项目分层package结构直接决定代码的整洁度SpringBoot项目的package分层我采用的是标准的三层架构变体cn.school.dorm ├── common // 全局异常处理、统一返回结果、工具类 ├── config // CORS配置、拦截器注册、MyBatis-Plus配置 ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务逻辑层事务边界的所在层 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 实体类 ├── dto // 数据传输对象用于接口参数封装 └── vo // 视图对象用于响应数据的聚合封装这里有个非常容易犯的错把entity直接当作接口出入参使用。学生查询报修记录前端需要的字段可能是报修类型描述一个字典值的中文名而entity里存的是repair_type数字编码宿管统计房间入住率需要的是房间号加已住/空余数量而entity里是整个房间记录。如果直接用entity返回轻则字段冗余、接口文档混乱重则把不该暴露的字段比如密码哈希泄漏给前端。我的约定是所有接口入参必须有对应的DTO类所有响应必须经过VO包装。多写几个类的成本远低于后续接口变更时的返工成本。统一返回结果也是必须养的代码习惯。我定义了一个Result类包含code、message、data三个字段配合全局异常处理器所有接口无论成功失败都返回统一结构。这样前端axios拦截器可以统一处理错误码无需每个页面各自try-catch。项目做到一半的时候我数了一下接口数量已经到了四十多个如果没有统一的返回结构联调阶段会是一场灾难。4.2 登录认证与JWT权限控制认证方案我选了JWT 拦截器没有引入Spring Security。为什么不引入Spring Security功能强大是事实但对这种角色简单的系统来说配置门槛带来的收益不成比例——SecurityConfig里的一大堆过滤器链、密码加密策略、CSRF配置新手很容易在配置阶段就迷失方向。而宿舍系统的核心诉求就一条登录后签发token请求带上token拦截器解析token拿到用户ID和角色放行或拒绝。JWT的实现我用的是jjwt库。登录成功后把userId和role塞进token的claims里设置过期时间为24小时。为了应对上次登录后改密这类常见问题用户修改密码后我会把该用户的token加入一个黑名单——Redis里存一份失效token集合拦截器解析token后先查黑名单。这样设计有两个好处不依赖session、天然支持后续水平扩展多实例部署时JWT无需共享session。拦截器的角色控制逻辑如下自定义一个RequireRole注解标注在Controller方法上值可以是student、admin等。拦截器在preHandle阶段解析token拿角色和注解要求做比对不符合直接返回403。学生端的接口全部加这个方法级别的角色控制宿管接口和管理员接口同理。做这个设计时我特意查了前端的路由守卫配置——前端只能做到隐藏入口真正的安全边界必须在后端。4.3 报修工单的核心流程与状态流转报修是宿舍系统里交互路径最长的功能状态流转要设计得严谨学生提交工单待派单→ 宿管查看并指派维修师傅待维修→ 师傅标记开始维修维修中→ 师傅完成维修并填写耗材情况待验收→ 学生确认验收已完成。外加一个用户取消和超时自动关闭的兜底状态。这个状态机用一张repair_type_code整数字段来维护配合一张状态字典表做中文名映射。核心状态变更都写在了Service层通过一个统一的updateRepairStatus(orderId, targetStatus, operatorId)方法完成方法内部校验当前状态能否跳到目标状态。这种集中式状态管理比在各处自由update字段要安全得多——杜绝了从已完成状态倒退到维修中这种业务乱象。事务处理上报修流程里有三个地方的原子性要求很高。一是创建工单时要同时更新工单表和对应的通知记录二是分配维修师傅时工单状态、师傅负载计数要同步变化三是确认验收时工单状态完成同时要更新房间的待维修标记。这些我全部用了Transactional注解并指定rollbackFor Exception.class。这里有个坑必须说Transactional默认只在运行时异常时回滚如果代码里catch了异常又没有重新抛出事务是不会回滚的。所以我在创建工单时宁可让异常直接往上抛由全局异常处理器统一拦截也不要自己吞掉。床位的分配/退宿流程也是事务控制的重点。用前面那张bed_assignment表和dorm_room表的used_bed_count字段来做分配床位时插入一条床位分配记录同时把房间的已住数量1退宿时反向操作。这两个操作要么一起成功要么一起失败。我用一个Transactional方法包裹细节上还加了乐观锁房间表里加了一个version字段更新used_bed_count时用UPDATE dorm_room SET used_bed_count used_bed_count 1, version version 1 WHERE id ? AND version ?做条件更新。如果影响行数为0说明并发下数据被修改过重新读取再重试。这个设计在开学季集中分配床位时特别重要不加锁的话两个操作同时执行可能把已住数量算错。5. 前端页面与交互实现后端的体系搭起来之后前端开发是另一个主战场。我用的技术组合是Vue Vue Router Vuex Element UI Axios下面讲几个核心实现点和我在数据联调时遇到的典型问题。5.1 路由与权限控制的配合方式路由是前端应用的骨架。我的router配置分两层设计基础路由登录页、欢迎页和需要登录的嵌套路由。嵌套路由的访问权限用路由守卫控制router.beforeEach钩子里检查本地存储的token没有token就去踢回登录页有token则用meta.roles字段和当前用户角色比对。另外在登录成功后动态加载当前用户有权限的路由模块这一步可以避免把全部路由直接暴露给前端代码——虽然前端隐藏路由不能作为安全手段但至少能减少误导用户和减少无谓的接口请求。菜单的动态生成也值得提一句。后端在登录接口的返回数据里会带一个permissions数组里面是本用户可访问的菜单编码。前端拿到这个数组后通过一个菜单渲染组件循环生成侧边栏。这样新增一种角色时前端代码完全不用改只要后端返回不同的菜单列表就行。我后来替系统新增财务处查询的角色时这种设计确实省了很多事——因为财务角色只能查看费用相关页面不需要在代码里写if-elif判断。5.2 核心页面的实现细节宿舍床位分配页面是我认为整个前端最有价值的一个页面。宿管老师需要直观看到楼栋-房间-床位的层级关系以及每一张床位的当前状态已入住/空闲/维护中。我用的是一个左中右三栏的布局左侧树形组件选择楼栋中间是房间列表卡片右侧是选中房间的床位网格图。床位图用Element UI的卡片组件加上动态Class实现三色状态区分——绿色空闲、红色已住、灰色维修。这个页面免去了宿管面对表格时还要想象房间长什么样的认知负担实测反馈说老人们也能很快上手。工单处理的对话式界面。维修师傅端我做了移动适配的页面——毕竟他们基本是在楼里跑来跑去用手机处理工单是刚需。工单列表在手机上以卡片流呈现重点信息是状态彩色标签和楼栋房间号点击进入详情页后有一个时间线组件展示每一步状态变更的流转记录谁在几点把状态从待维修改成了维修中。这个时间线数据其实就是后端在每次状态变更时写入的一条operation_log记录前端定时轮询刷新。有些场景下需要推送通知考虑到部署成本前端选择了轮询来实现通知5秒间隔的轻量轮询对这个体量的应用完全够用。统计Dashboard。首页给宿管看的是一组实时数据卡片今日待派工单数、本月维修完成率、各楼栋入住率、最近三次宿舍检查的平均分。这些数据全部来自一个聚合统计接口后端一次返回前端用ECharts绘制柱状图和饼图。接口的SQL写得比较长用了嵌套子查询动态计算入住率和平均分。顺便提醒一句Dashboard接口数据更新频率不高前端做了5分钟缓存避免每次切换Tab都打后端。5.3 前后端联调的典型问题清单联调阶段是bug的高发期这里列几个我遇到的典型问题方便大家避坑。跨域配置问题。本地开发时前端跑8080端口后端跑8088端口跨域不可避免。我用了后端CORS全局配置解决同时候把允许的域名限制为前端服务器地址并用allowedMethods限定GET、POST、PUT、DELETE。上线时更彻底的做法是走Nginx反向代理让前端请求同源的/api路径由Nginx把请求转发给后端服务这样浏览器感知不到跨域生产环境也更安全。日期格式的序列化坑。Java后端默认的日期序列化格式是2024-01-18T15:30:00而Element UI的date-picker组件期望的是yyyy-MM-dd HH:mm:ss这类的格式。联调时我见过不少前端刚拿到的数据日期显示又长又怪。解决方案是在全局配置了Jackson的日期格式化同时在返回给前端的VO里对日期字段做精确格式化。这是一个很常见但特别影响体验的细节。文件上传路径的坑。学生提交报修时允许拍照上传后端用本地磁盘存储上传文件返回可访问的URL给前端。本地开发看起来一切正常部署到服务器后图片却访问不了——原因在于启动路径和静态资源映射的冲突。我的解决办法是用一个绝对路径的/upload/映射指向服务器的统一文件目录不走相对路径。如果你也要做文件上传功能这个坑几乎必然会遇到。6. 部署与性能优化上线前必做的几件事开发完成不等于项目完工部署环境和生产调优这两步做不好前面所有工作都白费。这里说说我踩过的几个生产环境的坑和优化手段。6.1 打包部署的细节后端用Maven打jar包命令是mvn clean package -DskipTests。SpringBoot的项目默认内置了Tomcat所以服务器上只要装了JDK就能直接java -jar dorm-system.jar运行。我建议用nohup方式启动同时把标准输出和错误日志分别重定向到日志文件这样排查问题不用满屏翻找。前端则是一个纯静态文件目录在服务器上安装Nginx把构建产物直接放进Nginx的html目录下。Nginx的配置有一个关键点前端静态文件的缓存策略。index.html不要缓存否则每次发版后用户浏览器还在用旧版入口文件而js、css等带哈希文件名的静态资源可以设置长缓存因为文件名变化后浏览器自然去请求新文件。这个策略能显著减少白屏和缓存混淆的问题。6.2 数据库连接池与SQL优化生产环境我一开始用了SpringBoot默认的HikariCP连接池参数但并发一上来就踩了连接池爆满的坑。宿舍系统在开学季的查询量会瞬间飙高默认的maximumPoolSize10根本不够。我调成60同时把minimumIdle设置成20让连接池保持一定的空闲连接。另外所有涉及列表查询的地方都强制加了分页MyBatis-Plus的Page对象能自动拼limit避免一次查全表——有一次宿管老师反馈查维修记录特别卡排查后发现是没有分页导致某个楼栋的几年工单全部加载出来了。慢SQL优化方面我给几个高频查询的字段建了联合索引bed_assignment表的student_id字段、repair_order表的building_id和status组合、visit_record表的student_id和时间组合。索引不是越多越好但这两个查询条件是系统里出现频率最高的没有索引就是灾难。建索引之后有一个查询从3秒降到了200毫秒以内体验对比非常明显。6.3 安全加固登录、密码和接口登录接口要防暴力破解我加了一个简单的失败次数限制同一个用户名连续失败5次锁定10分钟。密码存储用的是BCrypt加密而不是MD5——MD5因为是可快速计算的哈希已经被广泛证明不适合做密码存储了即使加盐也不能完全抵御GPU暴力破解。BCrypt的代价系数我设置成10兼顾安全和登录速度。接口层还加了另一层防刷逻辑。高频只读接口比如Dashboard统计做了限流控制每个用户每秒钟不超过10次请求二次修改类接口比如床位分配、工单状态变更用校验前端传入的数据完整性。另外SQL注入虽然在MyBatis的#{}预编译机制已经被防住了但如果使用${}拼接SQL就必须格外谨慎——我代码里做了强制约束所有用户输入的排序字段和表名字段白名单校验后才允许传入。7. 踩过的坑与复盘这几条经验值得多看一遍每次做完一个完整项目我都会把过程中真正卡壳的事情记下来。这个项目的三个坑我觉得对做类似管理系统的人有普遍参考价值。第一个坑是MyBatis-Plus的字段自动填充失效。我给创建时间、更新时间两个字段配置了自动填充逻辑运行时报错发现填充不生效。检查了半天发现实体类字段上用了TableField(fill FieldFill.INSERT)但忘了写MetaObjectHandler的实现类去填充字段值。这是个低级但极容易遗漏的细节——MyBatis-Plus不会因为注解自动填充必须有配套的Handler处理器。重新实现并注册Bean后恢复正常。第二个坑是前后端联调时的“时间差”。后端返回的时间是UTC格式前端在浏览器中显示时做了本地时区转换导致学生看到的下单时间比实际时间早了8个小时。排查后发现Jackson默认时区是UTC而不是Asia/Shanghai在配置文件中加一行spring.jackson.time-zoneAsia/Shanghai就解决了。这个坑一定要提前预防不然上线后用户报bug你会被搞得焦头烂额。第三个坑是文件上传的临时文件清理。开发阶段问题不大但生产环境的服务器磁盘不够一个多月后磁盘就打满了。排查发现是上传的照片被存到系统临时目录且没有做定时清理大量历史工单的照片堆积在那。后来我加了定时任务定期把一个月之前且工单状态已完成的图片归档到冷存储目录同时清理临时文件。磁盘问题从这个角度看也侧面说明了日志压缩和文件生命周期管理要在一开始就纳入设计。第四个坑让我印象最深是并发分配同一个房间的床位。测试时用两个账号同时操作都能看同一个房间有两个空床位但实际上只有一张空床。当时用JMeter模拟并发请求发现used_bed_count出现了不一致。这段代码没有先查再改而是直接用条件更新的乐观锁方案解决前面4.3节已经详细讲了这里不再重复。只想说一句所有涉及数量扣减的字段先查再改一顿操作十个有九个会出并发问题请务必用数据库层面的原子更新或者乐观锁/悲观锁机制。我个人的体会是做管理系统最容易在“细节正确性”上栽跟头——需求、架构、技术选型这些大方向反而是相对可控的。真正让你随时准备救火的往往是事务边界遗漏、时区没统一、索引没加、缓存策略存疑这些细微之处。宿舍信息管理系统看起来是一个典型的业务CRUD项目但砍掉那些看着唬人的高并发指标之后剩下的每一条CRUD背后都有具体的业务逻辑要严谨对待。系统上线后宿管老师的一句话让我觉得挺有成就感的“以前月底汇总报表要加三天班现在点一个按钮就导出来了。”做这种贴近实际场景的项目最踏实的回报就是用户觉得它真的有用。