ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java+Vue宿舍管理系统实战:前后端分离完整搭建指南

Java+Vue宿舍管理系统实战:前后端分离完整搭建指南 简介这是一套面向Java与前端初学者及毕业设计学生的实战型宿舍管理系统完整工程基于Spring Boot Vue实现前后端分离架构覆盖用户管理、宿舍分配、报修审批、入住退宿等核心业务场景。资源共207个文件包含69个Java后端服务类含清晰注释、27个JS与18个Vue组件文件构成前端交互逻辑辅以18个配置与环境文件如.env.development、.editorconfig、yml等整体压缩包仅7.82MB轻量易部署。已有515人学习下载项目经导师评审获98分代码结构规范、文档齐全适合作为课程设计、期末大作业或毕业设计参考范本。下载解压后可快速运行配套数据库脚本与部署说明完备新手也能通过注释理解模块职责与接口调用关系是少有的兼顾教学性、工程性与可扩展性的高分实践案例。1. 为什么一个宿舍管理系统成了 Java Vue 前后端分离的“练手标尺”刚带实习生做毕设时常被问“学完 Spring Boot 和 Vue到底该做什么项目才不算假大空”答案往往就落在——宿舍管理系统。它不追求高并发或复杂算法但完整覆盖了权限分级宿管/辅导员/学生、房间状态实时更新、报修流程闭环、出入记录归档等真实业务链路。更重要的是它天然适配前后端分离架构前端用 Vue 实现动态表单、弹窗审批、Tab 切换视图后端用 Spring Boot 搭建 RESTful 接口、集成 MyBatis-Plus 操作 MySQL、通过 Spring Security 控制菜单级和按钮级权限。这不是玩具 Demo而是企业中台类系统最基础的形态缩影——登录态管理、文件上传、分页查询、跨域处理、Token 自动续期全得实打实写出来。如果你正卡在“会写 Hello World 却不会搭真实项目”这个系统就是你验证四层架构Controller–Service–Mapper–Entity是否真正落地的试金石。2. 搭建最小可运行骨架Spring Boot 后端接口 Vue 前端路由双启动2.1 初始化 Spring Boot 工程并配置核心依赖使用 Spring Initializrhttps://start.spring.io/生成基础工程勾选以下必选项Spring Web提供 REST 接口能力MyBatis Framework数据库操作注意不选 Spring Data JPA本项目更倾向原生 SQL 控制力Spring Security权限拦截与登录认证Lombok减少 Getter/Setter 模板代码MySQL Driver数据库驱动Validation参数校验生成后在pom.xml中补充 MyBatis-Plus Starter替代原生 MyBatis和 Hutool 工具包简化日期、JSON、加密等操作dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency提示MyBatis-Plus 版本需与 Spring Boot 2.7.x 兼容若用 Spring Boot 3.x则必须升至4.1.0并启用 Jakarta EE 9 命名空间。本项目默认按 Spring Boot 2.7.18 构建避免因版本错配导致TableName注解失效或分页插件不生效。2.2 定义宿舍领域模型与数据库初始化脚本创建entity包定义核心实体类。以Dormitory宿舍楼和Student学生为例体现一对多关系// entity/Dormitory.java Data TableName(dormitory) public class Dormitory { TableId(type IdType.AUTO) private Long id; private String buildingNo; // 楼号如 A栋 private String floor; // 楼层如 3F private String roomNo; // 房间号如 301 private Integer capacity; // 床位数 private Integer currentOccupancy; // 当前入住人数 private String status; // 状态空闲/已满/维修中 }对应 MySQL 初始化脚本src/main/resources/sql/init.sql需包含建表与基础数据CREATE TABLE dormitory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, building_no VARCHAR(20) NOT NULL, floor VARCHAR(10) NOT NULL, room_no VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 4, current_occupancy INT NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 空闲, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_building_floor_room (building_no, floor, room_no) ); INSERT INTO dormitory (building_no, floor, room_no, capacity, status) VALUES (A栋, 1F, 101, 4, 空闲), (A栋, 1F, 102, 4, 已满), (B栋, 2F, 205, 6, 维修中);注意UNIQUE KEY uk_building_floor_room是关键约束防止同一楼层重复录入房间update_time使用ON UPDATE CURRENT_TIMESTAMP确保 MyBatis-Plus 的自动填充功能无需额外配置即可生效。2.3 Vue 前端项目初始化与路由结构设计使用 Vue CLI 创建项目Vue 3 Composition API Router 4npm install -g vue/cli vue create dorm-admin # 选择 Manually select features → Choose features: Router, Linter # Use history mode for router? Yes # Pick a linter / formatter config: ESLint Prettier # Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files初始化后按业务模块组织src/router/index.js路由import { createRouter, createWebHistory } from vue-router import Login from /views/Login.vue import Layout from /layout/Layout.vue import DormList from /views/dorm/DormList.vue import StudentList from /views/student/StudentList.vue import RepairOrder from /views/repair/RepairOrder.vue const routes [ { path: /login, name: Login, component: Login, meta: { public: true } }, { path: /, name: Layout, component: Layout, children: [ { path: , redirect: /dorm }, { path: dorm, name: DormList, component: DormList, meta: { title: 宿舍管理 } }, { path: student, name: StudentList, component: StudentList, meta: { title: 学生入住 } }, { path: repair, name: RepairOrder, component: RepairOrder, meta: { title: 报修中心 } } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 全局前置守卫未登录跳转登录页 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token !to.meta.public) { next({ name: Login }) } else { next() } }) export default router关键点说明meta: { public: true }标记登录页为免鉴权页面router.beforeEach是 Vue Router 4 的标准守卫写法比旧版beforeEach更清晰Layout.vue是带侧边栏和顶部导航的壳组件所有业务页作为其子路由嵌入符合中后台系统通用布局。3. 实现核心业务流从登录鉴权到宿舍状态联动更新3.1 Spring Security 配置 Token 认证与权限控制本项目不采用 Session而是基于 JWT 的无状态 Token 认证。在config包下新建SecurityConfig.javaConfiguration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() // 前后端分离项目通常禁用 CSRF由前端 Token 保障 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz - authz .requestMatchers(/api/auth/login, /api/auth/captcha).permitAll() // 登录与验证码放行 .requestMatchers(/api/dorm/**).hasAuthority(ROLE_ADMIN) // 宿舍管理仅管理员可访问 .requestMatchers(/api/student/**).hasAnyAuthority(ROLE_ADMIN, ROLE_STAFF) // 辅导员也可查学生 .anyRequest().authenticated() ) .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) // 未登录返回 401 .accessDeniedHandler(new JwtAccessDeniedHandler()); // 权限不足返回 403 http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } }JwtAuthenticationFilter是核心过滤器负责解析请求头中的Authorization: Bearer xxx并校验签名public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (StringUtils.hasText(token) JwtUtil.validateToken(token)) { Long userId JwtUtil.getUserId(token); UserDetails userDetails userDetailsService.loadUserByUsername(String.valueOf(userId)); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }参数说明SessionCreationPolicy.STATELESS强制关闭 Session所有状态交由前端维护hasAuthority(ROLE_ADMIN)对应数据库中sys_user_role表的权限标识JwtUtil.validateToken()内部使用HMACSHA256算法校验签名密钥存于application.yml的jwt.secret字段不可硬编码。3.2 Vue 前端统一请求封装与 Token 自动注入在src/utils/request.js中封装 Axios 实例实现请求拦截、响应拦截与错误统一处理import axios from axios import { ElMessage } from element-plus // 创建 axios 实例 const service axios.create({ baseURL: /api, // 所有请求前缀由 vue.config.js 的 devServer.proxy 代理到后端 timeout: 10000 }) // 请求拦截自动添加 Token service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) ) // 成功响应拦截提取 data 层 service.interceptors.response.use( response { const { code, data, msg } response.data if (code 200) { return data } else { ElMessage.error(msg || 请求失败) return Promise.reject(new Error(msg)) } }, error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } else if (error.response?.status 403) { ElMessage.error(权限不足请联系管理员) } return Promise.reject(error) } ) export default service配套vue.config.js配置开发代理解决跨域module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, // 后端 Spring Boot 默认端口 changeOrigin: true, pathRewrite: { ^/api: // 去掉 /api 前缀再转发 } } } } }关键逻辑interceptors.request在每次请求头注入Authorization前端无需每个 API 调用都手动拼interceptors.response将后端统一返回格式{ code: 200, data: {}, msg: }解构为直接可用的data大幅减少组件内.then(res res.data)的冗余401 错误触发自动登出是真实项目中必须的体验闭环。3.3 宿舍状态联动学生入住/退宿时自动更新房间 occupancy 字段这是体现业务逻辑深度的关键环节。当学生办理入住POST /api/student/checkin时不能只插入student表还必须原子化更新对应宿舍的current_occupancy// controller/StudentController.java PostMapping(/checkin) public Result checkIn(RequestBody Valid StudentCheckInDTO dto) { boolean success studentService.checkIn(dto); return success ? Result.success() : Result.fail(入住失败); } // service/impl/StudentServiceImpl.java Transactional(rollbackFor Exception.class) Override public boolean checkIn(StudentCheckInDTO dto) { // 1. 查询目标宿舍 Dormitory dorm dormitoryMapper.selectOne( new QueryWrapperDormitory() .eq(building_no, dto.getBuildingNo()) .eq(floor, dto.getFloor()) .eq(room_no, dto.getRoomNo()) ); if (dorm null) { throw new ServiceException(宿舍不存在 dto.getBuildingNo() dto.getFloor() dto.getRoomNo()); } if (dorm.getCurrentOccupancy() dorm.getCapacity()) { throw new ServiceException(宿舍已满无法入住); } // 2. 插入学生记录 Student student new Student(); student.setStudentId(dto.getStudentId()); student.setName(dto.getName()); student.setDormId(dorm.getId()); student.setCheckInTime(new Date()); studentMapper.insert(student); // 3. 原子化更新宿舍 occupancy使用数据库行锁防超卖 int updated dormitoryMapper.update(null, new UpdateWrapperDormitory() .setSql(current_occupancy current_occupancy 1) .eq(id, dorm.getId()) .gt(current_occupancy, dorm.getCapacity() - 1) // 再次校验双重保险 ); return updated 1; }技术要点Transactional保证事务一致性UpdateWrapper.setSql()直接执行SET current_occupancy current_occupancy 1避免先查后更新的并发问题gt(current_occupancy, dorm.getCapacity() - 1)是乐观锁式二次校验即使并发请求同时通过第一步判断第二步UPDATE也只会有一条成功因current_occupancy已被另一条更新其余返回 0 行影响从而抛出异常。这是高并发场景下防止超住的核心手段。4. 数据库与文档协同用 Swagger 生成接口文档 用 PlantUML 绘制 ER 图4.1 Spring Boot 集成 Swagger3 生成实时 API 文档在pom.xml中引入springdoc-openapi-uiSwagger3 官方推荐替代过时的 springfoxdependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.6.14/version /dependency配置类SwaggerConfig.java启用并定制Configuration public class SwaggerConfig { Bean public Docket api() { return new Docket(DocumentationType.OAS_30) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(com.example.dorm.controller)) .paths(PathSelectors.any()) .build() .securityContexts(Arrays.asList(securityContext())) .securitySchemes(Arrays.asList(apiKey())); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(宿舍管理系统 API 文档) .description(基于 Spring Boot 2.7 的 RESTful 接口规范) .version(v1.0) .build(); } private ApiKey apiKey() { return new ApiKey(JWT, Authorization, header); } private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(defaultAuth()) .build(); } private ListSecurityReference defaultAuth() { AuthorizationScope authorizationScope new AuthorizationScope(global, accessEverything); AuthorizationScope[] scopes new AuthorizationScope[]{authorizationScope}; return Arrays.asList(new SecurityReference(JWT, scopes)); } }启动应用后访问http://localhost:8080/swagger-ui.html即可查看交互式文档。所有PostMapping、GetMapping方法自动注册参数类型、示例值、响应结构一目了然。注意生产环境务必关闭 Swaggerspringdoc.api-docs.enabledfalse避免接口暴露风险开发阶段开启后前端同学可直接在 UI 上调试/api/dorm/list等接口无需等待后端提供 Postman 链接。4.2 用 PlantUML 编写可执行的 ER 图并嵌入 Markdown 文档在项目根目录新建docs/er-diagram.puml用文本描述数据库关系startuml 宿舍管理系统 ER 图 使用 https://www.plantuml.com/plantuml/ 在线渲染 entity dormitory as dorm { * id PK -- * building_no * floor * room_no -- capacity current_occupancy status } entity student as stu { * id PK -- student_id UK name gender dorm_id FK check_in_time } entity repair_order as repair { * id PK -- student_id FK dorm_id FK content status create_time } dorm ||--o{ stu : 入住 dorm ||--o{ repair : 报修 stu }o--|| repair : 发起 enduml将此文件内容粘贴至 PlantUML 在线编辑器https://www.plantuml.com/plantuml/即可生成高清 PNG 或 SVG 图。将其保存为docs/er-diagram.png并在README.md中引用## 数据库设计 核心实体关系如下图所示 ![ER Diagram](docs/er-diagram.png) - dormitory宿舍楼信息主键 id - student学生信息外键 dorm_id 关联宿舍 - repair_order报修单关联学生与宿舍支持状态流转待处理→处理中→已完成优势说明PlantUML 是纯文本绘图可纳入 Git 版本管理修改字段只需改.puml文件无需打开 Visio 或 draw.io生成的图清晰表达1:N关系||--o{符号比口头描述“学生属于某个宿舍”更精确文档与代码同源避免设计稿与实际建表语句脱节。5. 进阶技巧用 MyBatis-Plus 分页插件优化列表性能 Vue Element Plus 表格懒加载5.1 后端分页MyBatis-Plus Page 对象与自定义 SQL 结合宿舍列表页常需按楼号、状态、容量范围多条件筛选。若用PageHelper需手动写COUNT(*)而 MyBatis-Plus 的IPage可自动完成// controller/DormitoryController.java GetMapping(/list) public Result list( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String buildingNo, RequestParam(required false) String status) { PageDormitory page new Page(current, size); QueryWrapperDormitory wrapper new QueryWrapper(); if (StringUtils.hasText(buildingNo)) { wrapper.eq(building_no, buildingNo); } if (StringUtils.hasText(status)) { wrapper.eq(status, status); } IPageDormitory result dormitoryService.page(page, wrapper); return Result.success(result); }对应 XML 中的自定义 SQLmapper/DormitoryMapper.xml需显式支持分页select idselectPage resultTypecom.example.dorm.entity.Dormitory SELECT * FROM dormitory where if testbuildingNo ! null and buildingNo ! AND building_no #{buildingNo} /if if teststatus ! null and status ! AND status #{status} /if /where ORDER BY create_time DESC /select关键点PageDormitory构造时传入current和sizeMyBatis-Plus 会自动在执行SELECT *前先执行SELECT COUNT(*)获取总条数并在最终 SQL 中添加LIMIT #{size} OFFSET #{offset}XML 中的where标签智能拼接AND避免WHERE 11resultType必须指向完整类路径否则分页后records为空。5.2 前端表格懒加载Element Plus Table 的load事件与节流当宿舍数据量达万级时一次性加载所有页签卡顿。Element Plus 的el-table支持load事件实现滚动加载!-- views/dorm/DormList.vue -- template el-table :datatableData v-loadingloading scrollhandleScroll el-table-column propbuildingNo label楼号 width100 / el-table-column proproomNo label房间号 width100 / el-table-column propstatus label状态 width100 / /el-table /template script setup import { ref, onMounted } from vue import request from /utils/request const tableData ref([]) const loading ref(false) const currentPage ref(1) const pageSize ref(20) const hasMore ref(true) // 首次加载 onMounted(() { loadData() }) const loadData async () { if (!hasMore.value) return loading.value true try { const res await request.get(/dorm/list, { params: { current: currentPage.value, size: pageSize.value } }) tableData.value [...tableData.value, ...res.records] if (res.current res.pages) { currentPage.value } else { hasMore.value false } } finally { loading.value false } } // 滚动到底部触发加载加节流防频繁调用 let scrollTimer null const handleScroll () { const table document.querySelector(.el-table__body-wrapper) if (table.scrollTop table.clientHeight table.scrollHeight - 10 hasMore.value) { if (scrollTimer) clearTimeout(scrollTimer) scrollTimer setTimeout(() { loadData() }, 300) } } /script技术细节scroll监听表格容器滚动通过scrollTop clientHeight scrollHeight - 10判断是否接近底部setTimeout节流确保 300ms 内只触发一次loadDatares.pages是 MyBatis-PlusIPage返回的总页数比res.total更适合判断是否还有下一页...tableData.value展开已有数据实现无缝追加避免闪烁。5.3 生产部署检查清单Nginx 静态资源托管 Spring Boot Profile 切换最后一步让系统真正跑在线上。nginx.conf配置静态文件托管server { listen 80; server_name dorm.example.com; # 前端静态资源 location / { root /var/www/dorm-admin/dist; try_files $uri $uri/ /index.html; } # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }Spring Boot 通过application-prod.yml切换生产配置# src/main/resources/application-prod.yml spring: datasource: url: jdbc:mysql://prod-db:3306/dorm_db?useSSLfalseserverTimezoneAsia/Shanghai username: dorm_prod password: ${DB_PASSWORD:changeme} # 密码从环境变量读取 redis: host: prod-redis port: 6379 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 生产环境关闭 SQL 日志 logging: level: com.example.dorm.mapper: warn # Mapper 层日志降为 warn启动命令# 构建前端 cd dorm-admin npm run build cp -r dist ../dorm-backend/src/main/resources/static # 启动后端指定 profile cd dorm-backend java -jar -Dspring.profiles.activeprod dorm-backend.jar部署要点Nginx 的try_files确保 Vue Router 的 History 模式正常工作proxy_pass末尾的/保证/api/user正确转发为http://127.0.0.1:8080/userDB_PASSWORD从环境变量注入避免密码硬编码生产 profile 关闭 MyBatis SQL 日志防止敏感信息泄露。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进