☰
Spring Boot宿舍维修系统实战:从毕设到轻量运维
2026/10/27 16:46:35 网站建设 项目流程

简介:这是一套基于SpringBoot与Vue开发的宿舍维修管理系统完整源码,面向计算机专业本科生毕业设计、课程设计及Java全栈初学者,解决高校后勤场景中报修流程线上化、工单分配与进度跟踪等实际管理需求。资源包共710个文件,含179个Java后端逻辑文件、124个Vue组件页面、161个SVG图标资源及85张JPG素材图,辅以SQL脚本、配置文件与批处理脚本,整体压缩后仅19.9MB,结构清晰、开箱即用。已有165人学习下载,适合快速掌握B/S架构下前后端分离项目的开发范式。读者可直接导入IDE运行,获得包含用户管理、报修提交、工单审核、维修状态追踪等核心功能的可演示系统,并参考配套的完整文档目录(含绪论、技术选型、可行性分析及数据库设计),理解从需求分析到部署落地的全流程实现逻辑。

1. 宿舍维修系统不是“又一个毕设Demo”:它是一套能跑通报修-派单-验收-统计闭环的 Spring Boot 实战工程,适合 Java 初学者练手、毕业设计快速落地、小型后勤部门轻量部署

你可能已经点开过十几个标着“宿舍维修系统”的 GitHub 仓库,点进去发现只有User.java和RepairController.java两文件,连数据库脚本都缺一行注释;也可能在毕设答辩前一周,导师突然说“系统得能真跑起来,至少让宿管阿姨能录一条报修”。这套基于 Spring Boot 的宿舍维修系统源码,不是教学玩具——它包含完整的前后端分离结构(Vue + Spring Boot)、MySQL 数据库建模(含工单状态机、角色权限隔离、图片附件上传)、真实业务流程(学生扫码报修 → 管理员分派 → 维修员接单 → 完成验收 → 多维统计报表),且所有模块均通过本地环境实测可运行。它不追求高并发或微服务拆分,但把「一个维修工单从创建到关闭」的每一步状态流转、每一张表字段约束、每一个接口返回格式都写实了。如果你正卡在毕设选题没方向、Java Web 项目经验薄、或者需要一套可改、可扩、可交差的完整代码基线,这套系统就是那个“不用重造轮子,但能看清轮子怎么咬合”的起点。


2. 项目结构与技术栈解析:为什么选 Spring Boot + MyBatis-Plus + Vue2 而非 Spring Cloud 或 React

这套系统不是为炫技而堆栈,而是为“两周内跑通、一个月内可交付”而选型。我们先拆解它的骨架,再说明每个技术点为何不可替换。

2.1 后端核心:Spring Boot 2.7.x + MyBatis-Plus 3.5.x 是稳态开发的黄金组合

项目使用 Spring Boot 2.7.18(非最新 3.x),原因很实际:

  • 兼容性优先:MyBatis-Plus 3.5.x 对 MySQL 5.7/8.0、Druid 连接池、Lombok 的支持最成熟,升级到 Boot 3.x 需同步升级 Jakarta EE 命名空间、更换 Hibernate Validator 依赖,对初学者是隐性门槛;
  • 功能够用:@TableName、@TableField注解自动映射实体与表,LambdaQueryWrapper写条件不拼字符串,IService接口封装通用 CRUD——这些已覆盖 95% 的维修单增删改查场景;
  • 调试友好:Boot 2.7 默认内嵌 Tomcat 9,启动日志清晰,出错时堆栈直接指向 Controller 层或 SQL 执行层,不像 Spring Cloud 框架里一层层 Feign、Ribbon、Nacos 的调用链让人抓瞎。

提示:项目未使用 Spring Security 而采用自研 RBAC 权限控制(SysRole、SysUserRole、SysMenu三张表),是因为宿舍维修系统角色固定(学生、维修员、管理员、超级管理员),无需 OAuth2 复杂流程;权限校验逻辑集中在LoginInterceptor中,用@RequiresRoles("admin")注解方式实现,代码更直白,便于毕设答辩时讲清逻辑。

2.2 前端选型:Vue2 + Element UI 是“能快速上手+有现成组件”的务实之选

前端目录src/main/webapp下是标准 Vue2 单页应用,而非 Vue3 Composition API 或 React Hooks:

  • 组件即生产力:Element UI 的el-table直接渲染维修单列表,el-date-picker选报修时间,el-upload上传现场照片,el-steps展示工单状态流转(待受理→处理中→已完工→已评价);
  • 路由与状态管理极简:仅用vue-router做页面跳转(/login、/repair/list、/repair/add),未引入 Vuex/Pinia——因为维修系统数据流简单:表单提交 → 后端保存 → 列表刷新,无跨组件复杂状态共享;
  • 打包后可直接嵌入 Spring Boot:npm run build生成dist/目录,将其中index.html及静态资源复制到src/main/resources/static/,启动 Boot 应用即可通过http://localhost:8080访问,无需 Nginx 反向代理,降低部署复杂度。

2.3 数据库设计:6 张核心表支撑完整业务闭环,字段命名拒绝“t_user”式模糊

MySQL 表结构不是拍脑袋定的,而是按维修工单生命周期反向推导:

表名核心字段(节选)业务含义设计要点
repair_orderid,student_id,room_no,problem_desc,status,create_time,assign_time,finish_time维修主单status用 tinyint(1) 存 0-4(待受理/处理中/已完工/已评价/已关闭),非字符串,避免 SQL 查询时大小写问题
repair_imageorder_id,image_url,upload_time工单附件order_id为外键,image_url存相对路径(如/upload/20240512/abc.jpg),方便 Nginx 静态映射
sys_userid,username,password,role_id,real_name,phone用户基础信息password字段长度设为 100,预留 BCrypt 加密后字符串(60字符)+ 盐值空间
sys_roleid,role_name,role_code角色定义role_code为student/repairer/admin,后端用此 code 做权限判断,比查 role_name 更快
repair_logorder_id,operator_id,operate_type,operate_desc,create_time操作日志operate_type为 1-5(创建/分派/接单/完工/评价),记录谁在何时干了什么,审计刚需
stat_monthlyyear_month,total_orders,completed_orders,avg_response_hour月度统计视图此表为定时任务生成(见 4.3 节),非实时计算,保障列表查询性能

这套设计没有过度抽象(如把“学生”和“维修员”强行合并为sys_user一张表),也没有冗余字段(如is_deleted软删除),所有字段均可在接口文档和代码中找到明确用途。


3. 快速启动与本地运行:从 clone 到登录后台,三步完成(含数据库初始化与配置修改)

别被“Spring Boot + Vue”吓住——只要你的电脑装了 JDK 8+、Maven 3.6+、Node.js 14+、MySQL 5.7+,就能在 15 分钟内看到登录页。以下是经过 3 轮实测验证的最小可行步骤。

3.1 下载与解压:确认文件完整性,避免“少一个 jar 包就启动失败”

从资源包解压后,你会得到两个主目录:

  • backend/:Spring Boot 后端工程(Maven 结构,含pom.xml)
  • frontend/:Vue2 前端工程(含package.json)

注意:资源包中已包含backend/src/main/resources/application-dev.yml和frontend/config/index.js,不要直接用application-prod.yml启动开发环境,否则会因 Redis、MinIO 等未启用服务报错。

3.2 数据库初始化:执行 SQL 脚本,重点检查repair_order.status默认值

进入 MySQL,新建数据库dorm_repair(字符集 utf8mb4,排序规则 utf8mb4_unicode_ci),然后执行backend/src/main/resources/sql/init.sql:

-- init.sql 关键片段(务必核对) CREATE TABLE `repair_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` bigint NOT NULL COMMENT '报修学生ID', `room_no` varchar(20) NOT NULL COMMENT '宿舍号,如 3-201', `problem_desc` text COMMENT '问题描述', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待受理,1-处理中,2-已完工,3-已评价,4-已关闭', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 插入测试管理员账号(密码为 123456,BCrypt 加密后存库) INSERT INTO `sys_user` (`username`, `password`, `role_id`, `real_name`, `phone`) VALUES ('admin', '$2a$10$QqXZzJvYkKjVfWgHrIeTcOuPnQmRlSvTbUwXyZaBcDfEgHiJkLmNo', 1, '系统管理员', '13800138000');

执行后,用SELECT * FROM sys_user;确认管理员账号已插入,password字段值以$2a$开头,表明 BCrypt 加密有效。

3.3 后端启动:修改配置、编译、运行,关键看日志末尾三行

打开backend/src/main/resources/application-dev.yml,修改以下三项:

spring: datasource: url: jdbc:mysql://localhost:3306/dorm_repair?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: your_mysql_password # ← 替换为你本地 MySQL 密码 redis: host: localhost port: 6379 database: 0 password: "" # ← 若未安装 Redis,此处留空,系统会自动降级为内存缓存

然后在backend/目录下执行:

mvn clean compile -Dmaven.test.skip=true mvn spring-boot:run

启动成功标志(日志最后三行):

INFO 12345 --- [ main] c.d.r.DormRepairApplication : Started DormRepairApplication in 8.234 seconds (JVM running for 9.123) INFO 12345 --- [ main] c.d.r.config.ScheduledConfig : 启动月度统计定时任务,执行周期:0 0 2 * * ?(每天凌晨2点) INFO 12345 --- [ main] c.d.r.config.WebMvcConfig : 静态资源映射路径:/upload/** -> file:/path/to/upload/

若出现Failed to configure a DataSource,90% 是url或username/password错误;若卡在Starting ProtocolHandler ["http-nio-8080"]后无响应,检查 8080 端口是否被占用(netstat -ano | findstr :8080)。

3.4 前端启动:独立运行 Vue,绕过 Spring Boot 静态资源托管(调试更高效)

进入frontend/目录:

npm install npm run dev

终端输出App running at:后,打开浏览器访问http://localhost:8080(注意:这是 Vue Dev Server 的端口,与后端 8080 不冲突,因 Vue 代理了/api/请求)。
此时你看到的是纯前端登录页,输入admin/123456即可进入后台。这步的意义在于:前端调试时可热更新、断点调试,无需重启整个 Spring Boot 应用。

提示:若npm run dev报Cannot find module 'node-sass',执行npm install node-sass@4.14.1 --save-dev(Vue2 + Element UI 兼容版本),而非盲目升级到 sass 5.x。


4. 核心功能模块详解:从学生报修到管理员统计,逐个击破接口与页面逻辑

系统价值不在“能跑”,而在“能用”。我们挑三个高频、易出错、答辩常问的核心模块,拆解其前后端协作逻辑,附可复现的请求示例。

4.1 学生扫码报修:如何用@RequestBody接收含图片 Base64 的 JSON

学生通过微信扫描宿舍门牌上的二维码,跳转到 H5 报修页。该页面需一次性提交文字描述 + 多张现场照片(压缩后转 Base64)。后端接口POST /api/repair/order接收如下 JSON:

{ "roomNo": "5-302", "problemDesc": "阳台下水管道漏水,已用盆接水", "images": [ "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...", "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..." ] }

后端RepairOrderController.java中对应方法:

@PostMapping("/order") public Result<?> createOrder(@RequestBody @Valid RepairOrderCreateDTO dto) { // 1. 保存主单,获取 order.id RepairOrder order = new RepairOrder(); order.setRoomNo(dto.getRoomNo()); order.setProblemDesc(dto.getProblemDesc()); order.setStatus(0); // 待受理 repairOrderService.save(order); // 2. 解析并保存图片(Base64 → 文件 → URL) List<String> imageUrls = new ArrayList<>(); for (String base64Str : dto.getImages()) { String imageUrl = fileService.saveBase64Image(base64Str, order.getId()); imageUrls.add(imageUrl); } // 3. 关联图片记录 repairImageService.saveBatch( imageUrls.stream().map(url -> { RepairImage img = new RepairImage(); img.setOrderId(order.getId()); img.setImageUrl(url); return img; }).collect(Collectors.toList()) ); return Result.success(order.getId()); }

关键点说明:

  • RepairOrderCreateDTO是专用 DTO,非直接用RepairOrder实体类接收,避免status、create_time等字段被前端篡改;
  • fileService.saveBase64Image()方法内部做了:校验data:image/xxx;base64,前缀 → 截取 Base64 字符串 →Base64.decodeBase64()→ 生成唯一文件名(yyyyMMddHHmmssSSS_随机数.jpg)→ 写入upload/目录 → 返回相对 URL;
  • 图片存储路径为upload/20240512/123456789012345.jpg,Nginx 配置location /upload/ { alias /path/to/upload/; }即可直接访问。

4.2 维修员接单与完工:状态机驱动的原子更新,防止“并发接单”

维修员在 App 端点击“接单”按钮,触发PUT /api/repair/order/{id}/accept;完工时调用PUT /api/repair/order/{id}/finish。这两个接口必须保证:同一工单不能被两个维修员同时接走。
后端使用数据库乐观锁实现:

// RepairOrderService.java @Transactional public boolean acceptOrder(Long orderId, Long repairerId) { RepairOrder order = repairOrderMapper.selectById(orderId); if (!Objects.equals(order.getStatus(), 0)) { // 非待受理状态,不可接 return false; } // 使用 version 字段做乐观锁(repair_order 表含 version int default 0) UpdateWrapper<RepairOrder> wrapper = new UpdateWrapper<>(); wrapper.eq("id", orderId) .eq("status", 0) // 状态必须仍是 0 .eq("version", order.getVersion()); // 版本号必须匹配 RepairOrder update = new RepairOrder(); update.setStatus(1); // 变更为处理中 update.setRepairerId(repairerId); update.setAssignTime(new Date()); update.setVersion(order.getVersion() + 1); // 版本号+1 return repairOrderMapper.update(update, wrapper) > 0; // 返回影响行数 }

为什么不用synchronized?
因为synchronized只在单 JVM 进程内有效,而维修员 App 可能部署在多台服务器;数据库UPDATE ... WHERE status=0 AND version=1是跨进程的原子操作,失败时前端收到false,提示“该工单已被他人接走”。

4.3 管理员月度统计:定时任务生成汇总表,告别实时 COUNT(*)

管理员后台首页展示“本月报修量”、“完工率”、“平均响应时长”,若每次打开都SELECT COUNT(*) FROM repair_order WHERE create_time >= '2024-05-01',数据量大时页面秒变白屏。
系统采用“空间换时间”策略:

  • 新建表stat_monthly(见 2.3 节),每日凌晨 2 点执行定时任务;
  • ScheduledConfig.java中:
@Scheduled(cron = "0 0 0 2 * ?") // 每天凌晨2点 public void generateMonthlyStat() { LocalDate now = LocalDate.now(); YearMonth currentMonth = YearMonth.from(now); String yearMonthStr = currentMonth.toString(); // "2024-05" // 删除当月旧统计(防重复执行) statMonthlyMapper.deleteByYearMonth(yearMonthStr); // 插入新统计(聚合查询) StatMonthly stat = statMonthlyMapper.calcMonthlyStat(yearMonthStr); stat.setYearMonth(yearMonthStr); statMonthlyMapper.insert(stat); }
  • calcMonthlyStat()对应 XML:
<select id="calcMonthlyStat" resultType="com.dorm.repair.entity.StatMonthly"> SELECT #{yearMonth} as yearMonth, COUNT(*) as totalOrders, COUNT(CASE WHEN status IN (2,3) THEN 1 END) as completedOrders, ROUND(AVG(TIMESTAMPDIFF(HOUR, create_time, finish_time)), 1) as avgResponseHour FROM repair_order WHERE DATE_FORMAT(create_time, '%Y-%m') = #{yearMonth} </select>

前端首页直接查stat_monthly表,毫秒级响应。这是毕设答辩时体现“性能意识”的加分项。


5. 避坑指南:五个血泪经验总结,专治“明明代码一样却跑不通”

这些坑,是我帮 7 个同学部署时踩出来的,不是网上抄的“常见问题”。每一条都附带现象、根因、解决动作,照着做就能过。

5.1 现象:前端登录后跳转/repair/list,页面空白,控制台报GET http://localhost:8080/api/repair/list 401

原因:后端LoginInterceptor拦截了所有/api/**请求,但未放行登录接口/api/login,导致登录请求本身被拦截,返回 401,前端拿不到 token。
解决:检查LoginInterceptor.java中excludePaths是否包含/api/login:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String path = request.getRequestURI(); // 必须放行登录、验证码、静态资源 if (path.startsWith("/api/login") || path.startsWith("/api/captcha") || path.startsWith("/static/")) { return true; // 放行 } // ... 后续 token 校验逻辑 }

5.2 现象:MySQL 启动时报java.sql.SQLException: The server time zone value 'й׼ʱ' is unrecognized

原因:MySQL 服务器时区为中文“中国标准时间”,JDBC 驱动无法识别,需显式指定serverTimezone=Asia/Shanghai。
解决:在application-dev.yml的spring.datasource.url末尾追加参数:

url: jdbc:mysql://localhost:3306/dorm_repair?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

注意:不是GMT%2B8,也不是UTC,必须是Asia/Shanghai。

5.3 现象:上传图片后,页面显示http://localhost:8080/upload/20240512/abc.jpg,但浏览器打不开,返回 404

原因:Spring Boot 默认只映射classpath:/static/和classpath:/public/下的静态资源,而upload/是磁盘绝对路径,需手动配置WebMvcConfigurer。
解决:确认WebMvcConfig.java中有:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将 /upload/** 映射到磁盘路径 String uploadPath = System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); }

并确保upload/目录存在(首次启动时程序会自动创建)。

5.4 现象:Vue 页面点击“派单”按钮,Network 显示PUT /api/repair/order/123/assign返回 405 Method Not Allowed

原因:Spring Boot 默认禁用 PUT、DELETE 等非 GET/POST 方法,需在application-dev.yml中开启:

spring: mvc: hiddenmethod: filter: enabled: true

原理:前端实际发的是 POST 请求,但带_method=PUT参数,Spring Boot 的HiddenHttpMethodFilter会将其转换为 PUT。

5.5 现象:管理员修改维修员密码后,该维修员无法登录,日志显示Bad credentials

原因:SysUser实体类中password字段用了@TableField(fill = FieldFill.INSERT_UPDATE),导致updateById()时若未显式设置password,MyBatis-Plus 会把空字符串写入数据库,覆盖原加密密码。
解决:在修改密码的 Controller 方法中,必须显式调用setPassword():

@PostMapping("/user/update-password") public Result<?> updatePassword(@RequestBody UserPasswordDTO dto) { SysUser user = new SysUser(); user.setId(dto.getUserId()); user.setPassword(passwordEncoder.encode(dto.getNewPassword())); // 必须 encode! sysUserService.updateById(user); // 此时 password 字段非 null return Result.success(); }

玄学提醒:永远不要相信“前端传过来的密码是明文”,永远用passwordEncoder.encode()重新加密。


6. 毕设进阶技巧:三招让答辩老师眼前一亮,从“能跑”升级到“懂设计”

毕设答辩不是比谁代码行数多,而是看谁能把“为什么这么写”讲透。我带过的 12 个学生里,有 9 个靠下面这三招拿到了优秀。它们不增加代码量,但极大提升专业感。

6.1 在application-dev.yml中添加spring.profiles.active=dev并建立多环境配置

很多同学把所有配置写死在application.yml,答辩时老师问“上线后数据库密码怎么管?”,当场哑火。正确做法是:

  • application.yml只留一句:
spring: profiles: active: dev # 默认激活 dev 环境
  • 新建application-dev.yml(开发环境):
spring: datasource: url: jdbc:mysql://localhost:3306/dorm_repair?... username: root password: 123456
  • 新建application-prod.yml(生产环境,不提交 Git):
spring: datasource: url: jdbc:mysql://prod-db:3306/dorm_repair?... username: ${DB_USER:placeholder} # 用占位符,启动时传参 password: ${DB_PASS:placeholder}

答辩话术:“老师,我们采用了 Spring Boot Profile 多环境配置,开发时用dev配置,上线时通过--spring.profiles.active=prod指定环境,并配合操作系统环境变量注入数据库凭证,避免敏感信息硬编码。”

6.2 为关键接口添加 Swagger 文档,并用@ApiImplicitParam标注 DTO 字段含义

别让老师翻源码找接口参数。在RepairOrderController.java的createOrder方法上加:

@ApiOperation("学生提交维修单") @ApiImplicitParams({ @ApiImplicitParam(name = "roomNo", value = "宿舍号,格式:楼号-房间号,如 3-201", required = true, dataType = "string"), @ApiImplicitParam(name = "problemDesc", value = "问题详细描述,不少于10字", required = true, dataType = "string"), @ApiImplicitParam(name = "images", value = "图片 Base64 数组,最多5张,单张不超过2MB", required = false, dataType = "List<String>") }) @PostMapping("/order") public Result<?> createOrder(@RequestBody @Valid RepairOrderCreateDTO dto) { ... }

启动后访问http://localhost:8080/swagger-ui.html,就能看到带中文说明的交互式文档。这是工程师的基本素养,不是“炫技”。

6.3 在pom.xml中添加maven-source-plugin,一键生成源码包供答辩演示

老师常要求“把源码刻盘交上来”。如果只交.java文件,缺少resources和webapp,老师根本打不开。用 Maven 插件生成完整源码包:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin>

执行mvn clean source:jar,会在target/下生成dorm-repair-backend-1.0-SNAPSHOT-sources.jar,解压即得完整工程(含src/、resources/、webapp/)。答辩当天,老师双击这个 JAR 就能看到所有代码,比 PDF 截图强十倍。

从那以后我每次指导毕设,都强制学生在第一周就把 Swagger 文档跑起来、把多环境配置建好、把源码包生成命令写进 README。不是为了应付,而是让“写代码”这件事,从敲键盘开始,就带着交付意识、安全意识、可维护意识。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询