SpringBoot+Vue3前后端分离宿舍管理系统实战开发全解析
做宿舍管理系统这个项目最初是因为帮某高校的后勤处改造一套老掉牙的Excel管理模式。宿舍管理员的日常就是跟一张张表格较劲查个空床位要打开七八个文件来回比对学生调宿要手写登记再抄到电脑里月底统计入住率更是要命。后来我花了大概三周时间用 Java SpringBoot Vue3 MyBatis 这套前后端分离架构配 MySQL 数据库把这套流程彻底搬进了系统里。这篇文章把整个项目的需求分析、数据库建模、后端接口、前端页面、联调排错和部署上线的完整链路全部写出来给正在做类似管理系统或者想参考前后端分离项目源码的同学一个可以直接复用的样板。文章里的代码片段和配置都是实际跑通过的直接抄就能用。1. 宿舍管理员还在用Excel时这套系统是怎么立项的1.1 三个最让人抓狂的日常场景先说清楚这个项目的背景不然你很难理解后面一系列设计选择是从哪来的。第一件事是查宿舍。某高校的宿管中心每学期开学都要做一次床位大盘点以前的做法是把所有楼栋的入住表打印出来一间一间核对空床位的标记全靠人工判断。一栋六层的宿舍楼一百多间房登记一次至少要半天核对完表格上全是涂改痕迹到了下一个学期这张纸基本就不能用了。第二件事是调宿。学生因为换专业、换校区、跟室友闹矛盾要调宿舍流程是学生找辅导员签字拿着纸质申请表去宿管中心管理员在Excel里找到目标宿舍确认有没有空位再手动改两条记录。遇到楼栋数据不统一的情况同一间宿舍在不同表格里可能一个显示有人一个显示空置管理员还得跑现场确认效率极低。第三件事是报修。水管漏水、灯泡坏了学生在值班室手写登记维修师傅每天跑一趟来看登记本修没修、修好了没有全靠师傅自己记。学生问起来宿管老师还得打电话问师傅一圈下来谁都不知道当前进度。这三个场景归纳下来本质上都是同一个问题数据分散在多个表格、多个经办人手里没有一个统一的、实时可见的数据源。立项的时候需求其实就一句话——把宿舍的人、房、物统一管起来。1.2 功能清单哪些必须做哪些可以砍需求方给的单子很长什么访客登记、门禁联动、水电表自动抄录、宿舍评分、失物招领全都有。但实际开发时间只有三周必须学会砍需求砍需求不是偷懒是项目管理的基本功。我最后保留的核心功能是四块基础数据管理包括楼栋、宿舍、床位、学生的增删改查和批量导入导出。这直接解决了查宿舍和调宿的痛点。住宿分配与调宿支持按学号或姓名检索学生、查看空床位分布、执行入住与调宿操作。报修工单学生端提交报修管理员端派单维修人员更新处理状态形成一个简单的闭环。统计报表按楼栋、按年级、按性别统计入住率支持导出Excel。被砍掉的是门禁联动和电表抄录原因是这两块依赖硬件接口不是三周能搞定的。这个取舍我认为是对的先把核心流程跑通再扩展外围功能比一开始就铺一个大而全但处处是坑的系统强得多。系统上线后如果宿管老师觉得好用了后面再加模块也不迟。2. 技术选型不纠结SpringBootVue3MyBatis为什么是这套2.1 后端用Java理由其实很朴素后端用 Java SpringBoot说实话没有什么悬念。原因有三点第一这套系统的使用方是高校后勤和教务部门他们现有的技术栈和运维习惯就是 Java 系后续同事维护不用从零学第二SpringBoot 的生态太成熟了安全框架、数据库连接池、代码生成器都有现成的轮子三周工期不允许从零造轮子第三宿舍管理系统这种典型的 CRUD 密集型业务SpringBoot 的 MVC 分层和注解开发效率非常高写起接口来几乎就是重复劳动不容易出错。2.2 前端跳过Vue2直接用Vue3前端我选了 Vue3 而不是 Vue2。这个选择放在今天已经是共识但项目启动时团队里还有人顾虑觉得 Vue2 资料多、踩坑少。我的判断是第一这个项目没有历史包袱不存在老代码兼容问题完全可以站在新版本生态上做第二Vue3 的 Composition API 在处理表格页面的筛选、分页、弹窗这种复杂交互时逻辑复用比 Options API 舒服太多同一个筛选逻辑可以在多个页面间复用不用写 mixin 那一套第三配套的 Vite 构建工具开发体验比 Webpack 快一个量级热更新时间从秒级变成毫秒级对迭代速度的帮助是实打实的。不过要老实说Vue3 的生态在项目启动时还有些小坑比如部分组件库的 Vue3 版本当时还不太稳定表格组件偶尔有些边缘交互会出问题后面我会专门讲到。2.3 MyBatis半ORM的好处报表SQL就是想怎么写怎么写数据访问层用 MyBatis核心原因在于这类型管理系统的查询需求太无常了。宿舍管理系统的报表统计特别多按楼栋统计入住率、按学院统计人数、按性别统计分布、按年级统计退宿情况……这些统计 SQL 往往要 join 三四张表加上各种 if 条件动态拼 where。用 JPA 写这种动态查询很容易陷入 Specification 或 QueryDSL 的复杂语法里维护成本比 SQL 本身还高。MyBatis 的 XML 文件写动态 SQL 是所见即所得查询逻辑一眼看过去就是一条完整的 SQL哪里改了条件、哪里换了 join改起来非常直观。而且配合 PageHelper 分页插件分页问题也省心了不用手写 limit 和 count 两条 SQL。2.4 数据库选型MySQL就够没必要上重量级数据库用 MySQL 8.0原因不多说了免费、稳定、团队熟悉。宿舍管理系统这种规模的数据量撑死也就几千个学生、几百间宿舍、上万条报修记录MySQL 完全够用没必要上 PostgreSQL 或者搞分布式数据库那一套。真正需要花心思的是表结构设计这个我放到下一章专门讲。3. 数据库设计楼栋、宿舍、床位的三层结构怎么建模3.1 六张核心表的字段说明这个系统一共六张核心表管理员表、学生表、楼栋表、宿舍表、床位表、报修表。先看字段设计后面讲几个关键决策。表名核心字段说明adminid, username, password, real_name, role, phone, create_timerole区分超管和普通宿管员studentid, student_no, name, gender, grade, college, major, phone, status, create_timestatus标记在宿/退宿buildingid, building_name, building_no, floor_count, room_count, manager, remark楼栋基础信息dormid, building_id, room_no, floor, bed_count, gender_limit, statusgender_limit限定男/女/混住bedid, dorm_id, bed_no, student_id, statusstudent_id为空表示空床repairid, student_id, dorm_id, type, content, images, status, create_time, handle_time报修工单流转3.2 为什么宿舍和床位必须拆成两张表这是设计里最值得说的一点。一开始拿到需求时有人提议在宿舍表里放一个床位列表字段用 JSON 格式存查询的时候解析出来。我坚决反对这个方案原因有两条第一JSON 字段无法参与 SQL 查询。比如要查所有空床位如果床位存在 JSON 里你没法用 where 条件去查只能先把所有宿舍捞出来再到 Java 代码里解析过滤。数据量小的时候感觉不出来但代码写起来很别扭而且一旦数据量上来内存过滤的性能问题就暴露了。第二床位和宿舍本身是 1 对多关系拆成两张表后可以精确管理哪个床位住着谁。入住、调宿、退宿操作本质上就是更新床位表的 student_id 字段逻辑非常清晰。而且后续如果要扩展床位费床铺物品清单之类的业务直接在床位表上加字段就行不会影响宿舍表的结构。3.3 物理外键到底建不建我的结论是不建另一个经常被问到的问题是表之间要建物理外键吗我的答案是不建。项目中所有表之间的关联都靠逻辑外键也就是普通索引字段维护不在数据库层面声明 FOREIGN KEY。原因有三第一删数据时物理外键会带来一堆级联限制比如调宿时要先清空床位表对应的旧关联再更新宿舍表如果建了外键操作顺序稍不对就报错业务逻辑会被数据库约束牵着鼻子走第二系统后续上线新功能时如果涉及表结构调整物理外键的维护成本非常高加字段、改类型都得先处理外键关系第三这种业务系统的并发量很低物理外键带来的那点数据一致性保障完全可以用后端事务来替代。建了唯一索引加普通索引查询性能有保障又保留了灵活性。3.4 时间字段用datetime埋一个少踩一个坑最后提一个细节时间字段统一用 datetime 而不是 timestamp。原因后面在踩坑章节会详细讲——timestamp 受数据库时区影响大部署环境一换就容易出现差8小时的经典问题。datetime 虽然没有时区概念但只要在 Java 后端统一用 LocalDateTime 统一时区配置反而更可控。对于宿舍管理系统这种不需要跨时区协作的场景datetime 就是最稳的选择。4. 后端落地JWT鉴权、MyBatis动态SQL和接口设计4.1 项目分层和包结构后端工程我保持了标准的 SpringBoot 分层结构controller 层只做参数接收和结果返回service 层写业务逻辑mapper 层定义 MyBatis 接口XML 文件写 SQL。额外加了一层 dto 用于接收前端查询参数vo 用于返回给前端的视图对象避免直接把实体类暴露给前端。controller 层我写代码时有个原则不允许出现任何业务逻辑只做参数校验、调用 service、返回统一结果。很多初学者喜欢在 controller 里直接拼接业务判断或者写 if 分支短期看是快了后期维护就是灾难。统一结果返回用 R 泛型类结构是 code、message、data 三件套前端 axios 拦截器就靠这个结构做统一判断。4.2 JWT登录鉴权的完整链路登录鉴权用 JWT整体链路是用户登录时校验用户名密码密码用 BCrypt 加密存储比对通过后生成 token 返回前端。token 的有效期设置成 24 小时方便宿管老师白天用一整天不用反复登录。前端拿到 token 后存在 localStorage 里每次请求在 axios 拦截器里把 token 放到 Authorization 请求头。后端用拦截器统一校验不需要校验的接口比如登录接口本身通过白名单机制放行。Token 的生成我用的是现有的 JWT 工具库自定义了一个工具类payload 里塞了 userId、username、role 三个字段。role 字段很重要因为这个系统有两种角色宿管管理员和学生。两种角色的权限差异很大学生只能看自己的住宿信息、提交报修工单管理员可以管理所有数据、派单、统计。前端路由和后端接口都要根据 role 做权限判断所以这个字段从一开始就必须进 token不能等后期再加。4.3 MyBatis动态SQL在统计查询里的具体用法这一节举个例子最能说明 MyBatis 的价值按多条件统计入住率。需求是前端页面可以选楼栋、选楼层、选性别然后展示符合条件的宿舍入住率。这个查询的条件组合有七八种SQL 里要动态拼接 where 和 join。用 MyBatis 的if和where标签可以很优雅地实现select idselectDormStats resultTypemap SELECT b.building_name, d.floor, d.room_no, COUNT(bed.id) AS total_beds, SUM(CASE WHEN bed.student_id IS NOT NULL THEN 1 ELSE 0 END) AS occupied_beds FROM dorm d LEFT JOIN building b ON d.building_id b.id LEFT JOIN bed bed ON bed.dorm_id d.id where if testbuildingId ! null AND b.id #{buildingId} /if if testfloor ! null AND d.floor #{floor} /if if testgender ! null and gender ! AND d.gender_limit #{gender} /if /where GROUP BY b.building_name, d.floor, d.room_no /selectwhere标签会自动处理掉第一个条件前面多余的 AND非常省心。这种写法在 JPA 里要写一堆 Specification 代码在 MyBatis 里就是直观的 SQL。统计结果的 total_beds 和 occupied_beds 两个字段业务层拿去做入住率计算返回前端直接渲染整个链路很清爽。4.4 统一异常处理和事务管理接口开发中还有两个容易忽略但很重要的点统一异常处理和事务。统一异常处理我用 RestControllerAdvice 注解实现。全局捕获四类异常业务异常、参数校验异常、数据库异常、兜底异常分别返回不同的 code。比如参数校验失败返回 400业务规则冲突如宿舍已满、学号重复返回 500前端拿到 code 之后弹对应的提示不会把一堆堆栈信息暴露给用户。事务管理方面报修单的状态流转、调宿操作都是多表更新的场景我统一在 service 方法上加 Transactional(rollbackFor Exception.class)。这里特别强调一下rollbackFor 一定要指定否则遇到 checked exception 是不会自动回滚的这个坑我见过不止一次线上数据错乱之后排查到凌晨才找到原因。5. 前端落地Vue3的路由守卫、axios封装和组件抽取5.1 前端工程结构和页面规划前端用 Vite Vue3 Element Plus Pinia Vue Router 这套组合。Element Plus 是 Element UI 的 Vue3 版本表格、表单、弹窗、日期选择器这些组件在这个项目里用得非常多比手写样式效率高太多。页面规划上登录页独立一个路由登录后的主布局是一个带侧边栏的框架页面里面嵌套各个业务页面首页仪表盘显示总人数、空床位、待处理报修数量、宿舍管理、学生管理、入住调宿、报修管理、统计报表。首页仪表盘做的是几个统计接口的汇总展示给宿管老师一打开系统就能看到全局情况。5.2 路由守卫和角色权限的双层控制路由守卫是前端权限控制的关键。我在路由表里给每个页面配置了 meta.roles 数组比如宿舍管理页面的 roles 是 [admin]我的宿舍页面的 roles 是 [admin, student]两类角色都能进。然后在全局前置守卫里做三件事第一步判断有没有 token没有一律跳转登录页第二步判断目标路由是否需要登录不需要的放行第三步判断当前用户角色是否在 meta.roles 里不在就跳转403页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.meta.roles) { const user JSON.parse(localStorage.getItem(user)) if (to.meta.roles.includes(user.role)) { next() } else { next(/403) } } else { next() } })这里要提醒的是前端路由守卫只是用户体验层面的控制真正的权限校验必须后端接口再做一层。前端可以改代码绕过路由守卫但后端接口如果没校验角色数据照样泄露。我在后端的宿舍管理接口里都加了 RequireRole 注解做二次校验前后端配合才能做到权限可控。5.3 axios封装的三个关键层axios 封装是前后端联调的基础设施我做了三层处理。第一层是请求拦截器统一加上 Authorization 请求头从 localStorage 取 token没有 token 的请求也会正常发出由后端返回未登录错误。第二层是响应拦截器统一处理后端返回的 code。code 为 200 时直接返回 data 给业务层code 不为 200 时弹错误提示。这里特别处理了 401 和 token 过期的情况清空本地存储、跳转登录页避免用户停留在半登录状态。第三层是对 HTTP 错误码的处理比如 500、502、503 这类服务器错误统一提示服务器开小差了避免把原始错误信息直接暴露给用户。封装完成之后业务页面里的请求代码非常干净比如列表页export function getStudentList(params) { return request({ url: /student/list, method: get, params }) }页面里调用就是一个 Promise配合 Element Plus 的 loading 指令表格加载状态不用手动管。5.4 组件抽取的时机不要为了封装而封装前端开发里最容易犯的错是过度封装——页面还没写完就开始抽公共组件结果抽出来的组件接口参数比业务代码还复杂改一个需求要改三个地方。我这个项目的经验是一个组件至少被三个页面用到且交互模式基本一致的时候才值得抽出来。宿舍管理系统里真正值得抽的组件其实就两个一个是楼栋树宿舍表格的组合查询组件在宿舍管理、入住调宿、报表三个页面都用了一个是学生选择弹窗调宿和入住时都要选学生。这两个组件抽出来之后三个页面的代码量各自减少了大概三分之一而且后续改需求比如表格里要加一列是否党员只需要改组件本身三个页面同时生效。这就是组件抽取真正的价值不在于代码行数减少而在于需求变更时的响应速度。6. 联调踩坑记录跨域、时区、路由懒加载6.1 跨域配置看着简单实际踩了三种姿势的坑前后端分离项目第一个遇到的就是跨域。我在后端写了 CORS 配置类允许所有来源、所有方法、所有请求头。但在测试时发现浏览器还是报跨域错误排查了很久最后发现是三层问题叠加。第一自定义拦截器先于 CorsFilter 处理了请求OPTIONS 预检请求被拦截器拦截返回了 401导致预检失败。解决方法是在拦截器里对 OPTIONS 请求直接放行让预检请求能正常通过。第二我在前端 axios 里自定义了请求头 Authorization这在 CORS 配置里如果不显式声明 allowedHeaders部分浏览器会认为这是非简单请求预检时会携带 Access-Control-Request-Headers 头后端如果没匹配到就报错。所以 allowedHeaders 要明确写出来不能只写个简单的通配。第三如果将来用 Nginx 做反向代理跨域问题可以通过 /api 前缀转发彻底消解前端请求同源的 /apiNginx 转发到后端端口。这个方案我在部署环节采用了比后端 CORS 更省心建议做前后端分离项目的同学优先考虑这个方案。6.2 时间字段差8小时从数据库到前端的连环坑报修工单列表页有一个创建时间列联调测试时发现凡是下午提交的工单显示时间总是比实际早8小时。比如学生下午3点提交前端显示早上7点。排查链路是这样的先看数据库存的值发现存进去的就是早上7点说明问题出在写入环节。后端日志显示 LocalDateTime.now() 生成的时间是正确的下午3点那问题就出在 MyBatis 映射或 JDBC 驱动上。再查连接串配置发现 JDBC 连接串里没有指定 serverTimezone而 Java 8 时间类型映射到数据库时驱动默认用了 UTC 时区解析导致写入时被减掉了8小时。解决方式是在 JDBC 连接串里显式加上 serverTimezoneAsia/Shanghai 和 useLegacyDatetimeCodefalse同时把表里的时间字段类型统一改用 datetime。改完之后重新测试下午3点提交的工单数据库存下午3点前端显示下午3点问题彻底消失。6.3 刷新页面404路由history模式的经典问题用 Vue Router 的 history 模式配合懒加载动态 import本地开发一切正常但打包部署到服务器后用户在某个子页面按 F5 刷新返回 404。原因是页面刷新时浏览器直接请求当前 URL比如 /dorm/123但服务器上并没有这个物理路径对应的文件只有 index.html而 Nginx 默认不会做前端路由的回退。解决方法是在 Nginx 配置里加 try_files 指令location / { try_files $uri $uri/ /index.html; }这样所有找不到静态资源的请求都会回退到 index.html由前端路由接管刷新404就解决了。这个配置是 history 模式部署的标配但每聊一次就会有人踩一次这里单独写出来。6.4 懒加载组件加载失败的一个组合问题另外再补充一个前端的小坑懒加载的组件在部署到服务器后偶尔报Failed to fetch dynamically imported module刷新又好了。这个现象的原因是部署新版本时index.html 引用的 JS 文件名就是 Vite 构建时带哈希的那个和浏览器缓存里的旧文件名不一致。用户在旧页面上操作时路由懒加载去拉取已经被替换掉的旧 chunk自然就失败了。解决方式是配合版本号缓存策略部署新版本后让 Nginx 对 index.html 设置 no-cache对带 hash 的静态资源设置长缓存。这样用户在刷新后一定能拿到最新的 index.html其中的文件引用也一定是最新的不会再拿到被替换掉的旧 chunk。7. 上线的最后一公里打包、部署和线上优化7.1 前端打包与后端发布的标准姿势前端部署用命令就能完成。本地开发时用代理解决跨域生产环境用 Nginx 反向代理。前端打包后把 dist 目录放到服务器指定路径配置 Nginx 把 /api 前缀的请求转发到 SpringBoot 的 8080 端口这样前端和后端域同源CORS 配置反而变得不是必须的了这也符合我之前说的部署优先方案。后端打包用 Maven 的 package 命令生成可执行 jar部署时直接 java -jar 运行。为了稳妥我在 jar 启动脚本里设置了 JVM 内存参数和 UTF-8 编码java -Xms256m -Xmx512m -jar dormitory-system.jar --spring.profiles.activeprod生产环境配置文件里单独配置了数据库地址、端口、日志级别跟开发环境隔离。日志级别生产环境设为 info开发环境设为 debug避免生产环境输出过多无用日志撑满磁盘。7.2 数据库索引优化的一次实际观察部署上线后我实际观察了一次慢查询日志。发现学生列表的模糊查询在数据量增长后开始变慢EXPLAIN 一看student_no 和 name 字段的查询都没有走索引。解决方式是在 student_no 上加唯一索引在 name 字段上加普通索引。另外报修表的 dorm_id 和 status 是高频查询字段也建了联合索引。这几条索引建完后列表页的响应时间从几百毫秒降到 50 毫秒以内效果非常明显。这里要提醒的是索引不是越多越好写多的表索引建太多反而拖慢插入和更新。宿舍管理系统的写入频率很低所以多建几个索引问题不大但如果是订单系统那种高频写入场景索引数量就得慎重。上线后观察慢查询来反推索引设计比上线前盲目建索引靠谱得多。7.3 系统上线后可以继续做的三个方向系统上线后我在实际使用中收集到一些反馈后续可以继续做的方向包括第一给报修工单加催单功能超过48小时未处理允许学生催办这个对宿舍管理的服务质量提升很明显本质上是在工单闭环上增加一个超时机制。第二宿舍分配支持批量导入加自动分配算法按学院、班级优先就近分配。目前是手动分配遇到开学季几百个新生同时入住管理员操作量还是很大自动分配能大大缓解这个高峰压力。第三把 Excel 导出做成异步任务数据量大时前端先显示生成中后端后台线程生成后提供下载链接。目前是同步导出几百条记录没问题但如果未来扩展到几千条同步导出会卡住接口影响其他请求的响应。这三个方向都不涉及架构调整是在现有表结构上做增量开发这也是当初设计时把核心表拆细带来的好处——扩展点都是在边缘加表加字段不会动主干。我在整个开发过程中最大的体会是宿舍管理系统这种项目技术难度其实不高真正的难点在于把宿管老师的日常习惯翻译成系统能理解的数据结构。比如调宿舍这个动作在业务上是一套复杂的流程——先查目标宿舍、确认性别限制、看空位、最后更新两条记录每一步在系统里都要找到对应的功能入口少一个环节宿管老师就宁可回去用Excel。所以开发这种系统时多花时间跟实际使用者聊比多写几行代码重要得多。另外一个很实用的建议是所有管理类系统的源码里数据库初始化脚本一定要写好新同事拿到项目第一步就是建库建表脚本写得清楚整个项目的上手成本能降一大半。如果你也在做类似的校园后勤管理系统记住这两点会少走很多弯路。