SpringBoot+Vue前后端分离宿舍管理系统:从零到部署全解析
1. 这块“毕设级”宿舍管理系统到底在做什么每年到毕业季总有一大批人捧着同一个题目来找我“宿舍管理系统”。说实话这题在计算机类毕设、课设里的出现频率和电商系统、图书管理系统基本上是同一梯队。但正因为做得人太多反而拉开了差距——有人交上去的是一堆拼凑代码面试官一问就露馅有人把这个题目做成了能讲清楚、能跑通、还能演示亮点的小型全栈项目直接变成简历上的加分项。这次我们聊的项目标题很直白基于SpringBootMyBatis-PlusMySQLVue前后端分离的宿舍管理系统设计与实现。技术栈是经典的Java后端Vue前端架构上走前后端分离数据落在MySQL。它覆盖了一个宿舍管理场景的核心需求楼栋管理、房间管理、学生入住/退宿、报修登记、访客管理、卫生检查、系统用户与权限控制。这个项目最典型的受众有三类第一类是正在做毕设或课设的在校生需要一个完整、可复现、能答辩的工程第二类是刚学完SpringBoot和Vue、想练手第一个完整项目的前端或后端初学者第三类是准备校招、想往简历里塞一个“前后端分离全栈项目”的求职者。不管你属于哪一类这篇内容的目标只有一个把整个项目从头到尾的关键环节拆开把每个技术选型背后的原因讲清楚把最容易踩的坑提前指出来。我先说说整体感受。这套系统如果只是照着网上千篇一律的代码敲一遍三天能跑起来但没什么价值。真正值钱的地方在于三个点权限模型怎么设计、前后端联调时Token怎么处理、以及MyBatis-Plus这种半自动ORM到底在项目里替我们省了哪些事。把这些想透了你答辩的时候才有东西可讲。2. 技术选型拆解为什么偏偏是这一套组合2.1 SpringBoot负责什么为什么离不开它SpringBoot在这套系统里的定位是“后端地基”。它不是一个新框架而是对Spring生态的二次封装把SpringMVC、Spring、事务管理、Jackson序列化这些底层组件整合到一起通过自动配置大幅减少了配置文件的量。很多人刚接触SpringBoot时会有一个误解以为引入SpringBoot就是用了微服务。其实在宿舍管理系统这个体量下我们根本不需要微服务那套分布式治理能力需要的是一个能快速把接口写出来、能正常处理HTTP请求、能管好数据库事务和依赖注入的Web服务程序。SpringBoot刚好满足而且它的内嵌Tomcat让应用直接打包成JAR就能跑不需要单独装一个Tomcat服务器部署成本低到离谱。实际开发里我建议使用SpringBoot 2.7.x这个版本线。为什么不用最新的SpringBoot 3.x因为3.x强制JDK 17而很多东西——比如教学视频里常用的旧版MyBatis-Plus插件、某些老教程的pom依赖写法——在3.x下会踩各种兼容性问题。宿舍管理系统是拿来练手和过答辩的不要在最底层环境上浪费大量时间。JDK 8 SpringBoot 2.7.x Maven 3.6 这套组合稳定性极高网上的解决方案也最全。2.2 MyBatis-Plus到底比MyBatis强在哪关于MyBatis和MyBatis-Plus的区别我直接用一句话说清楚MyBatis是一个半自动SQL映射框架SQL要自己写MyBatis-Plus是MyBatis的一个增强工具在保留手写SQL能力的基础上把单表的增删改查、分页查询、条件构造这些高频操作全都做成了内置方法。举个例子在宿舍管理系统里我们有一个student表里面存学生的学号、姓名、班级、入住楼栋和房间ID。如果只用原生MyBatis想实现“按条件分页查询学生列表”你得先写一个查询方法、配一个XML、写对应的SQL再写一个统计总数的SQL两个SQL还要保证条件一致。而在MyBatis-Plus里只需要继承BaseMapperStudent然后调用selectPage方法配合LambdaQueryWrapper把条件拼出来就行SQL由框架自动生成还自动生成了COUNT查询。LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(name), Student::getName, name) .eq(Student::getStatus, 1) .orderByDesc(Student::getCreateTime); PageStudent page studentMapper.selectPage(new Page(current, size), wrapper);这段代码里没有一条SQL但跑出来的效果和手写SQL完全一致。它值钱的地方不只是少写几行代码而是让“简单操作”的代码量断崖式下降让开发者的精力集中在复杂查询和业务规则上。但注意MyBatis-Plus不是万能的。它只适合单表CRUD和简单的单表条件查询。宿舍管理系统里“查询某栋楼的所有在住学生及其宿舍号同时统计每间宿舍的剩余床位”这种多表关联查询依然要手写SQL。项目里最合理的做法是单表操作全交给MyBatis-Plus多表关联和复杂统计单独写在Mapper XML里。这样既有开发效率又不至于让性能失控。2.3 MySQL表结构为什么不能随手建MySQL是这套系统的数据库表结构设计的好坏直接决定了后续开发是顺畅还是处处难受。宿舍管理系统这种业务表不多但表之间的关联关系比较典型是练习数据库设计的绝佳场景。我的建议是使用MySQL 8.0字符集用utf8mb4。这里有一个常见误区很多人图省事用utf8但MySQL的utf8实际上是utf8mb3它存不了emoji和一些生僻字而utf8mb4才是完整的四字节UTF-8。宿舍管理系统的报修描述、留言反馈这些字段完全有可能出现特殊字符选对字符集可以避免很多脏数据问题。数据库设计层面最核心的一张表是user——因为宿舍管理系统的登录用户不只有学生还有宿管员和系统管理员。不同角色看到的功能菜单、能操作的接口完全不同。所以在表设计时用户表和角色表要拆开通过中间表建立多对多关系。后面我会专门用一节来讲表的详细设计。2.4 Vue全家桶Vue2还是Vue3怎么选前端这边选择Vue是一个共识因为Vue在国内的生态和使用门槛都非常适合中小型项目。但Vue2和Vue3的选择却是一个值得单独讨论的问题。如果是一年前我大概率会让你用Vue2 Element UI因为这组合的教程最全、遇到问题能搜到的答案最多。但到了现在Vue2已经停止维护Element UI也进入了维护冻结期新项目再用Vue2其实是给自己埋坑。我的建议是直接用Vue3 Vite Element Plus Pinia。不是说不考虑学习成本。Vue3对新手最不友好的地方是组合式APIComposition API带来的思维转变但从长远看学Vue3是必须的。而且Vue3在逻辑复用上比Vue2的mixin清晰太多哪怕只是做一个简单的管理系统用组合式API写出来的代码可维护性也明显更好。前端技术上还有一个不可回避的点HTTP请求库和Token处理。项目里我会选择axios封装一个带请求拦截和响应拦截的实例请求时自动附带Authorization请求头响应时统一处理业务码和HTTP错误码。这部分是整个前后端分离项目里最容易出问题、但讲起来最有含金量的地方后面单独展开。2.5 前后端分离架构解决了什么问题所谓前后端分离就是把前端页面和后端接口当作两个独立的工程来开发和部署。前端通过HTTP协议调用后端的RESTful API数据格式统一用JSON。在宿舍管理系统里Vue开发服务器占用一个端口比如8080SpringBoot后端占用另一个端口比如9090两者各自独立开发、独立部署。这个架构最大的好处是“解耦”。前端开发不需要关心Java代码怎么写的后端开发也不用碰HTML和CSS只要提前约定好接口文档两边就可以并行推进。对毕设来说这个架构还有一个隐藏加分项答辩时可以把系统拆成“前端工程”和“后端工程”分别讲解显得你对软件工程的理解更有层次感。但前后端分离也引入了一个经典问题——跨域。前端在http://localhost:8080后端在http://localhost:9090浏览器出于同源策略会拦截跨域请求。解决办法有几种后端接口加CrossOrigin、配置CORS过滤器、通过Nginx反向代理统一入口。开发阶段我用Vite的proxy代理解决生产部署则交给Nginx统一转发这种方式最干净。3. 数据库设计一套宿舍管理系统最见功力的地方3.1 核心表梳理与字段规划宿舍管理系统的表不算多但设计得好不好直接决定后面代码的复杂程度。我这里给出一个经过实际项目验证的表结构方案你可以直接拿过去改再用。第一张表是用户表sys_user不要叫user因为user在MySQL里虽然不是严格保留字但容易引发不必要的麻烦。这张表存登录账号、密码、真实姓名、性别、联系电话、头像、状态以及创建时间和更新时间。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(200) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, gender TINYINT COMMENT 性别 0未知 1男 2女, phone VARCHAR(20) COMMENT 联系电话, avatar VARCHAR(255) COMMENT 头像地址, status TINYINT DEFAULT 1 COMMENT 状态 1启用 0禁用, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间 );第二张是角色表sys_role和用户角色关联表sys_user_role。角色表字段简单大概就是角色ID、角色名称、角色编码、备注。用户和角色通过中间表关联中间表就两个字段user_id和role_id。这种设计支撑了“一个用户可以有多个角色”的场景虽然宿舍管理系统一般一个用户只有一个角色但多对多设计扩展性好也符合通用权限模型的做法。然后是宿舍楼栋表building、房间表room、学生信息表student。这里我特别想讲一讲room表的设计。一个房间最基本的属性是楼栋ID、房间号、宿舍类型几人寝、当前已住人数、容量上限、状态是否可用。这里的“已住人数”其实是一个冗余字段——正常应该通过查student表里住在这个房间的人数来统计但每次查询房间列表时都去统计一次列表接口会变得很慢。所以实际项目中我选择在room表里直接维护cur_count在入住、退宿时做同步更新。这就是数据库设计里的“冗余换性能”思想面试或答辩时提到这一点会显得你的设计有思考深度。接着是入住记录表live_record记录学生入住和退宿的历史。每次学生入住时插入一条记录包含学生ID、楼栋ID、房间ID、入住时间、退宿时间为空表示在住、操作人。这张表是房间状态变更的审计日志也是统计床位利用率的数据来源。报修表repair、访客表visitor、**卫生检查表hygiene**这三张是业务扩展表。报修表包含报修人ID、房间ID、报修内容、报修时间、处理状态、处理人ID、处理结果和完成时间。访客表包含被访学生ID、来访人姓名、身份证号可选、来访时间、离开时间、登记人。卫生检查表包含检查人ID、房间ID、评分、检查备注、检查时间。3.2 逻辑删除一个让我又爱又恨的设计在很多管理系统里删数据是一个敏感操作。学生误点击退宿、管理员误删除报修记录如果物理删除了数据就找不回来了。MyBatis-Plus对这个问题提供了逻辑删除支持——用一个字段标记数据是否被删除查询时自动过滤已删除数据看起来像是删掉了其实数据还在表里。宿舍管理系统里我建议启用逻辑删除但只对部分表启用。student、live_record、repair、visitor这些表建议逻辑删除因为它们是业务数据未来可能需要回溯。而sys_user_role这种中间表不需要因为它的数据本身就是关联关系删掉就是删掉。MyBatis-Plus启用逻辑删除只需要两步先在配置文件中声明全局逻辑删除配置mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0然后在实体类字段上加TableLogic注解TableLogic private Integer deleted;这里有个非常容易踩的坑如果你用MyBatis-Plus自动生成的deleteById去删除数据它执行的是UPDATE而不是DELETE这没问题。但如果你在Mapper XML里手写了DELETE FROM room WHERE id ?这条SQL不会走逻辑删除会直接把数据物理干掉。所以项目里凡是有逻辑删除需求的表手写SQL时必须加上WHERE deleted 0条件或者尽量用MyBatis-Plus内置方法完成删除操作。3.3 唯一索引与常用索引建议索引这东西只有在数据量上来了以后才能感觉到它的价值。宿舍管理系统一般不会有大并发但作为毕设该有的索引还是得有否则面试官问起来没得说。主键索引不用说了。值得单独建立的索引包括sys_user.username登录时要按账号查用户这个字段有唯一约束天然就是唯一索引。student.user_id学生表和用户表通过用户ID关联查询学生详情时常用。room.building_id按楼栋查房间列表时高频使用。live_record.student_id查一个学生的入住历史时高频使用。repair.room_id报修记录按房间ID查询时使用同时可联合status建立复合索引因为场景经常是“查某个房间的未处理报修”。索引不是越多越好因为每次插入和更新数据时索引也要维护索引过多会影响写入性能。宿舍管理系统这个量级上述这些够用了。答辩时能说清楚“为什么给这个字段加索引、为什么不用给性别字段加索引”就比只会复制建表SQL的人强出一截。4. 后端核心功能实现从登录鉴权到权限控制4.1 项目初始化与依赖配置后端工程创建这一步我推荐直接用Spring InitializrIDEA里可以选Spring Initializr也可以在start.spring.io网页生成。坐标信息不用太纠结Group写com.exampleArtifact写dormitoryJava版本选8依赖勾选Spring Web、MySQL Driver、Lombok。然后手动在pom.xml里加入MyBatis-Plus和JWT相关的依赖。MyBatis-Plus的依赖一直以mybatis-plus-boot-starter为坐标SpringBoot 2.7版本对应的是3.5.x版本的MyBatis-Plusdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependency这里提醒一句JDK 8用jjwt 0.9.1没问题但如果你用的是JDK 9以上需要额外添加javax.xml.bind相关依赖否则会报ClassNotFoundException: javax.xml.bind.DatatypeConverter。最好的办法是老老实实JDK 8。application.yml是后端配置文件的核心几个关键配置项数据源、MyBatis-Plus、JWT密钥、端口号。我在项目里习惯把端口设为9090避免和前端的8080冲突同时开启server.servlet.session.timeout这类会话配置。不过用了JWT之后服务端不保存会话Session配置基本就不需要了。4.2 JWT登录鉴权的完整流程前后端分离架构里Session方案有点尴尬因为浏览器和服务端不在同一个域Cookie跨域处理非常麻烦。更通用的做法是用Token而JWTJSON Web Token就是Token方案里最常见的一种。JWT的原理一句话就能说清用户登录成功后服务端生成一个包含用户信息、角色信息和过期时间的加密字符串返回给前端。前端把这段Token存起来之后每次请求都在HTTP请求头里带上Authorization: Bearer token。服务端收到请求后先校验Token的签名和有效期解析出用户信息再做业务处理。因为Token本身携带了用户身份信息所以服务端不需要存储会话状态这叫无状态认证。登录接口大概长这样PostMapping(/auth/login) public Result? login(RequestBody LoginDTO loginDTO) { // 1. 查询用户是否存在 SysUser user userService.getUserByUsername(loginDTO.getUsername()); if (user null) { return Result.error(用户名或密码错误); } // 2. 校验密码BCrypt加密后的密码比对 if (!BCrypt.matches(loginDTO.getPassword())) { return Result.error(用户名或密码错误); } // 3. 检查用户状态 if (user.getStatus() 0) { return Result.error(该账号已被禁用); } // 4. 生成Token String token JwtUtil.createToken(user); // 5. 返回用户信息 Token return Result.success(new LoginVO(token, user)); }密码存储不要用明文更不要用MD5。MD5是哈希算法虽然不可逆但撞库攻击很容易破解常见密码。项目里应该用BCryptPasswordEncoder它在哈希时自动加盐而且是慢哈希暴力破解成本高得多。Spring Security如果不想引入的话也可以单独使用spring-security-crypto这个依赖只用到加密工具类不引入整个权限框架。4.3 拦截器与注解实现接口权限控制有了JWT之后接下来要解决的是“哪些接口需要登录才能访问、哪些接口只有管理员能访问”。这是一个经典的鉴权加授权问题。宿舍管理系统的接口权限可以分三层公开接口、登录用户接口、管理员接口。公开接口比如登录、验证码生成登录用户接口比如学生查询自己的入住信息、提交报修申请管理员接口比如新增楼栋、分配房间、删除用户。这些权限控制我用两个组件来实现一个是HandlerInterceptor拦截器负责拦截所有请求并解析Token一个是自定义注解RequireRole标注在Controller方法上声明这个接口需要什么角色。拦截器的核心逻辑public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } // 从请求头获取Token String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(未登录); } // 解析Token LoginUser loginUser JwtUtil.parseToken(token.replace(Bearer , )); if (loginUser null) { throw new BusinessException(Token无效或已过期); } // 校验角色权限 if (handler instanceof HandlerMethod) { RequireRole requireRole ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (requireRole ! null) { if (!loginUser.getRoles().contains(requireRole.value())) { throw new BusinessException(无权限访问); } } } request.setAttribute(loginUser, loginUser); return true; }这个设计的好处是权限控制非常灵活。像“学生查询自己的信息”和“管理员查询所有学生信息”这种同一个业务但不同权限的接口可以共用一个Controller方法方法内部通过RequireRole指定允许的角色然后从request里取出当前登录用户判断数据归属。4.4 MyBatis-Plus在业务中的几个高阶用法前面说了MyBatis-Plus的CRUD很省事但项目里真正让它发光的是另外几个能力。第一个是自动填充。create_time和update_time这两个字段如果每次插入、更新都手动set长期写就是灾难。MyBatis-Plus提供MetaObjectHandler接口可以定义自动填充的逻辑Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }然后在实体类的对应字段上加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)之后插入和更新时就不需要手动处理时间了。第二个是条件构造器LambdaQueryWrapper。它是MyBatis-Plus的灵魂。需要注意的一个细节使用LambdaQueryWrapper可以避免硬编码字段名因为它是通过方法引用取字段名的例如Student::getGender。这样重构字段名时代码会直接编译报错而不是运行时报SQL异常。第三个是分页插件。MyBatis-Plus的分页查询需要先配置一个分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }如果不加这个配置selectPage方法只会执行普通查询根本不分页而且不会执行COUNT查询。这个坑很多人踩过所以提醒一句分页插件是分页的前提不是可选项。5. 前端工程实现Vue3项目搭建与联调5.1 从零创建Vue3项目并配置基础能力前端工程方面我用Vite创建Vue3项目npm create vitelatest dormitory-web -- --template vue项目创建后安装依赖npm install npm install vue-router4 pinia axios element-plus这里有一个小知识点Vite脚手架生成的默认项目里package.json是没有vue-router和pinia这些路由状态管理依赖的需要手动安装。Element Plus按需引入也可以但对新人来说全量引入更省心就是在main.js里import ElementPlus from element-plus import element-plus/dist/index.css app.use(ElementPlus)按需引入能减少打包体积但会增加配置复杂度。宿舍管理系统这类内部项目完全不需要优化首屏体积到极致全量引入足够。目录结构我这样安排src/ |-- api/ // 接口请求模块 |-- assets/ // 静态资源 |-- components/ // 公共组件 |-- layout/ // 主布局 |-- router/ // 路由配置 |-- stores/ // Pinia状态管理 |-- views/ // 页面视图 |-- utils/ // 工具函数 |-- App.vue |-- main.js这个目录是标准的中小型Vue项目结构模块边界清晰。以后新增页面在views里建一个文件夹在router里加一条路由在api里加一个接口文件三个地方改完就完成了一个功能模块的接入。5.2 axios请求封装与Token处理方案前后端分离项目里axios封装质量直接决定联调体验。我的做法是创建一个request.js导出封装后的axios实例import axios from axios import { ElMessage } from element-plus import { useUserStore } from ../stores/user import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带上Token request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录状态已过期请重新登录) const userStore useUserStore() userStore.clearUserInfo() router.push(/login) } else { ElMessage.error(error.message || 网络错误) } return Promise.reject(error) } ) export default request请求拦截器里把Pinia中存储的Token拼到请求头上响应拦截器里统一处理业务异常和HTTP 401过期状态。这套方案在开发前后端分离项目时几乎是标配理解它也等于理解了为什么前端不能直接拿后端返回的原始异常信息给用户看——因为中间这一层已经把错误转换成用户能理解的语言了。Token的存储位置我一直用Pinia加localStorage双写Pinia负责运行时读取localStorage负责刷新后持久化。刷新页面后Pinia里存的数据会丢所以要在初始化时从localStorage里恢复Token和用户信息。还要注意localStorage有XSS风险但管理系统属于内部系统跨站脚本攻击风险相对可控用起来成本最低。相比之下Cookie因为可以设置HttpOnly更安全但前端就无法用JS读取处理逻辑复杂很多。5.3 路由守卫与动态菜单Vue Router 4提供全局前置守卫在路由跳转前检查Token是否存在、决定是否放行router.beforeEach((to, from, next) { const userStore useUserStore() if (to.path /login) { next() return } if (!userStore.token) { next(/login) return } if (userStore.roles.includes(ADMIN) !userStore.menuLoaded) { // 管理员动态生成菜单路由 } next() })这里的动态菜单是项目的一个亮点。不同角色登录后看到的侧边栏菜单不一样学生看到的是我的入住、我的报修、访客登记这些功能宿管员看到的是入住管理、卫生检查、访客管理管理员则额外看到用户管理、楼栋管理、角色权限配置。实现方案有几种最简单的是前端根据角色编码判断渲染哪些菜单数据库里不存菜单表。这种方案代码量少但不够灵活。更正规的做法是在后端维护一张sys_menu表和角色菜单关联表登录后根据用户角色查询可访问菜单列表前端再动态添加路由。第二种方案能讲的东西更多对毕设来说含金量更高我也更推荐动手实现一下因为它实际上就是RBAC权限模型中资源权限部分的前端体现。5.4 Element Plus实现核心页面前端界面上登录页、主布局、学生管理页面、房间管理页面、报修处理页面是必须有的。我挑几个核心页面的实现逻辑说。登录页是最简单的一个表单用户名加密码提交时调用/auth/login接口成功后存Token、存用户信息然后跳转到首页。这里注意一个细节登录接口返回的用户信息里必须包含角色编码前端要根据角色决定跳转到不同的默认首页。比如管理员跳到“系统概览”学生跳到“我的宿舍”。学生管理页面是一个典型的“搜索加分页”页面。顶部是搜索条件表单下面是表格右侧是操作按钮。它的核心逻辑是调用分页接口请求参数带上current、size、name、buildingId等字段后端返回records和total前端把结果绑定到表格和分页组件上。初次点击分页时很容易出问题因为当前页变化后没有重新触发查询。解决办法是把分页组件绑定的current-page和page-size设计为响应式数据在current-change和size-change事件里重新调用接口。房间管理页面有个特色功能楼栋和房间的联动选择。选择楼栋后房间列表只显示该楼栋的房间。这个功能前后端都能做后端接buildingId参数过滤前端监听楼栋选择变化清空并刷新房间列表。我在实战中发现用el-cascader级联选择器可以一次性完成楼栋到房间的两级选择体验稍微好一点但普通的下拉框嵌套也完全够用。6. 部署与上线把项目从本地搬到服务器6.1 开发环境下的跨域联调在本地开发时前端通过Vite启动在8080端口后端SpringBoot在9090端口。浏览器访问前端页面时页面里的axios请求指向了/api这个相对路径它会被Vite的开发服务器转发到后端。Vite在vite.config.js里配置代理export default defineConfig({ server: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这个配置的核心是rewrite因为前端请求的路径是/api/auth/login而后端接口路径是/auth/login代理时需要把/api前缀去掉。如果你后端Controller类上定义了RequestMapping(/api)那就不需要rewrite。这个前后缀统一的问题建议在接口联调前就和后端约定好不然一会去掉一会不去掉容易出现404。6.2 打包与Nginx部署后端打包很直接Maven生命周期里执行package生成一个JAR包然后在服务器上执行java -jar dormitory-admin.jar --spring.profiles.activeprod配置文件里区分dev和prod两套环境prod环境的数据源地址改成服务器上的MySQL地址数据库账号密码使用环境变量或外部配置文件注入不要硬编码在代码里。前端打包执行npm run build会生成dist目录里面是静态文件。把这些文件上传到服务器的Nginx目录下然后配置一个Nginx站点server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/dormitory-web; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /prod-api/ { proxy_pass http://localhost:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意try_files $uri $uri/ /index.html这个配置它是前端路由history模式的关键。Vue Router如果用的是HTML5 History模式直接刷新一个子路由页面时Nginx会返回404加上这个配置后所有未知路径都回退到index.html由前端路由接管。如果用Hash模式就没有这个问题但URL上会有一个#号观感稍差。服务器选择上阿里云、腾讯云的轻量应用服务器都够用2核4G配置跑这套系统毫无压力。数据库不需要单独装MySQL服务端直接用云数据库或者服务器上自建MySQL都行。6.3 上线前必须检查的几个细节项目上线的坑往往不在功能开发阶段而在部署细节上。我梳理几个自己踩过的坑。第一个是时区问题。MySQL连接串里的serverTimezone如果不设置SpringBoot和MySQL之间的时间可能会差8个小时。推荐在JDBC连接串中显式声明jdbc:mysql://localhost:3306/dormitory?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai第二个是上传文件的大小限制。如果做头像上传或报修图片上传SpringBoot默认限制上传文件大小为1MB需要手动调整spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB第三个是JWT密钥。上线环境必须更换默认的JWT签名密钥否则任何人都能伪造Token。密钥要足够长而且包含随机字符最好用环境变量注入不要写在代码里。第四个是CORS和代理的取舍。上线后前后端同一个Nginx域名下走Nginx反向代理就不存在跨域问题后端不需要额外配置CORS。如果前端静态资源部署在OSS上后端接口在服务器上那就要在后端配置跨域过滤器。最好保持生产和开发环境行为一致开发时用Vite代理生产用Nginx代理后端统一不开启CORS这样能少处理很多奇怪的问题。7. 常见问题排查与避坑实录7.1 表名和字段名冲突这个问题在宿舍管理系统里很容易遇到。比如有个实体类叫room但MySQL 8.0里room并不是保留字所以问题不大。但user表就不一样了虽然能用但总有一天会碰到一条SQL因为USER被解析成系统关键字而报错。所以我从一开始就建议把用户表命名为sys_user。还有一类问题实体类里叫buildingId数据库表里字段叫building_id如果没配map-underscore-to-camel-case: true查询结果会一直映射不上。MyBatis-Plus的默认配置里这个值是true但如果手写SQL返回自定义VO有时候字段映射还是会出问题。排查思路是查出来的结果为空或某些字段为null先看SQL的别名和实体类属性名是否对上了。7.2 MyBatis-Plus分页不生效这个问题的典型表现是调用了selectPage方法返回的total是0或者records里包含了全部数据。绝大多数原因是没配置分页插件。有人认为MyBatis-Plus天然支持分页其实它只是提供了分页方法真正拦截改写SQL靠的是PaginationInnerInterceptor。这个方法在前面MyBatis-Plus配置那一节已经写了没配置就回去补上。另外一个不太常见但确实存在的情况在某些低版本MyBatis-Plus 3.4.x中分页插件在配置DbType.MYSQL时如果数据源类型判断错误也会导致分页失效。检查方法很简单给分页日志打开SQL日志打印看有没有LIMIT关键字以及是否执行了COUNT查询。7.3 日期格式前后端不一致SpringBoot默认返回的日期格式是yyyy-MM-dd HH:mm:ss但Vue的Element Plus日期选择器默认绑定的是Date对象或yyyy-MM-dd格式的字符串。如果不做统一前端展示时间经常变成“2024-12-26T10:30:00.00008:00”这种格式非常难看。解决办法有两种一种是在Jackson配置里统一格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8另一种是在实体类的日期字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。如果项目里用了LocalDateTime推荐后者因为spring.jackson.date-format对LocalDateTime不一定生效。7.4 逻辑删除后唯一索引冲突这个坑很隐蔽。sys_user表的username字段有唯一索引。用户A删除后管理员新增了一个用户名相同的新用户按常理说应该是可以的——因为旧用户已经“删掉”了。但逻辑删除并没有真正把数据从表里删掉唯一索引仍然约束着这一列导致新增用户时报“用户名已存在”。这种情况的解决办法有几种把逻辑删除字段deleted放进唯一索引里创建复合唯一索引(username, deleted)。但MyBatis-Plus逻辑删除后删除行的deleted值是1新增行的deleted值是0复合唯一索引允许同时存在(zhangsan, 0)和(zhangsan, 1)。不过如果想删除两次同名用户第二次删除时还是会发生冲突。更彻底的做法是逻辑删除时把username改成username _deleted_ id。这个细节体现了对产品逻辑的理解答辩时可以重点讲。7.5 前端Token过期后页面卡死前端常见的糟糕体验是Token过期后用户点按钮没有反应也不跳登录页页面看起来像死掉了一样。正确做法是在响应拦截器里遇到HTTP 401时统一清理登录状态并跳转登录页。但这里有个细节请求并发问题时如果两个请求同时返回401会触发两次router.push(/login)和两次ElMessage.error体验差。解决办法是设置一个标志位用一个Promise队列确保只有第一个401触发跳转其他401等待刷新完成或者直接静默失败。对宿舍管理系统这个复杂度来说简单的做法是加一个isRedirecting状态let isRedirecting false if (error.response error.response.status 401) { if (!isRedirecting) { isRedirecting true userStore.clearUserInfo() router.push(/login) ElMessage.error(登录状态已过期请重新登录) setTimeout(() { isRedirecting false }, 3000) } }8. 资料使用建议与二次开发方向8.1 拿到项目资料后的正确打开方式标题里特意标了“附资料”说明现在网上分享这个项目的资源包里通常会有完整的代码、SQL脚本、演示视频和毕业论文文档。资源是辅助学习的好工具但不少人拿到手就陷入了“复制粘贴把系统跑起来、以为万事大吉”的状态这是最可惜的用法。我建议按照这样的顺序使用资料第一步把SQL脚本导入MySQL启动后端启动前端整体跑通系统感受一下业务流程。第二步只保留数据库表把后端代码按照自己的理解重新写一遍遇到不会的再参考原代码。第三步给系统加一个原项目没有的功能比如数据分析看板、全校宿舍空位统计。做完这三步这个项目才能真正长在你身上。直接拿现成代码去交差答辩时问两个问题就露馅了。8.2 适合扩展的三个方向宿舍管理系统虽然小但扩展空间其实很大。我建议根据自己的技术水平和兴趣选择方向。第一个方向是加消息通知功能。比如报修处理完成后系统自动通知学生卫生检查评分过低时通知辅导员。技术实现上可以在后端接入WebSocket或者简单的邮件通知服务会给项目增加实时交互能力。第二个方向是加数据可视化。宿舍大楼每个楼层的住宿率、各楼栋报修类型的占比、最近一周访客人数的变化趋势这些都可以用ECharts画成图表放在首页。前端用ECharts很成熟后端只需要写好统计SQL返回聚合结果。这个方向难度适中视觉效果好答辩演示时特别加分。第三个方向是加移动端适配。不需要单独做App用Vue的响应式布局适配手机浏览器或者用uni-app做一个简单的H5页面方便宿管员在手机上查看入住率、处理报修。这个方向能体现你对多端适配的理解也可以让项目在实际场景中更可用。8.3 我给这套项目的最终评价宿舍管理系统是这个技术栈组合的绝佳练习场。它的业务复杂度不高不低——刚好能让你完整走一遍建表、写接口、写页面、联调、部署的全流程又不会因为过于复杂的业务把你困在需求分析里。SpringBoot、MyBatis-Plus、MySQL、Vue这四件套本身也是国内中小型后端项目的主流配置熟练之后不管是实习还是校招面试都有得聊。我从自己的使用体会说一句这个项目真正难的地方不在代码量而在“想清楚”。权限模型怎么建、房间状态怎么流转、Token怎么管理这些在设计阶段多花一点时间后期开发速度会成倍提升。如果你在做的过程中遇到某个具体报错或者某个功能不知道怎么实现拿着问题去搜索解决方案比自己闷头摸索效率高得多。希望这篇拆解能帮你少踩几个坑早点把项目做扎实。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →