简介:本资源是一套基于Spring Boot与Vue开发的公寓报修管理系统完整毕业设计项目,面向计算机专业本科生及Java全栈初学者,解决高校或中小型公寓物业中报修流程线上化、工单闭环管理、多角色协同等实际问题。压缩包共460个文件,9.85MB,涵盖139个Java后端核心代码文件(含MyBatisPlus数据层与Spring Security权限控制)、51个Vue组件(如update-password.vue.bak等管理界面原型)、161个SVG图标资源及配套JPG/PNG素材、YML配置与SQL脚本等,结构清晰,模块对应绪论、系统分析、数据库设计、前后端实现等毕设标准章节。目前已有126人学习下载,读者可直接导入IDE运行调试,获取含用户管理、图片素材上传、工单状态流转、响应式后台界面在内的可部署系统源码,以及完整的项目文档目录与技术选型说明,是实践B/S架构、前后端分离与MySQL数据库设计的优质教学参考案例。
1. 为什么一个「公寓报修管理系统」能成为 Java 毕设高频选题?它真只是个 CRUD 吗?
很多计算机专业学生在毕设选题时点开「公寓报修管理系统」,第一反应是:这不就是增删改查+上传图片+发邮件?但实际交付中,90% 的挂科或答辩卡点,恰恰出在「看似简单」的环节——比如报修单状态流转被并发修改导致工单丢失、维修人员接单后超时未响应却无自动预警、多栋楼多管理员权限隔离失效、图片上传后缩略图生成失败引发前端白屏。这些不是理论问题,而是 Spring Boot 在真实业务流中必须面对的事务边界、异步解耦、文件处理与权限模型落地细节。本系统面向高校宿舍/长租公寓场景,核心价值不在界面美观,而在用最小技术栈(Spring Boot + MyBatis Plus + Thymeleaf + MySQL)跑通「用户提报→客服分派→师傅接单→现场处理→用户评价→数据统计」全链路闭环。适合 Java 基础扎实、想通过毕设夯实 Web 开发全流程能力的学生,尤其适合作为 Spring Boot 面试题的实战载体——比如「如何保证接单操作的幂等性」「怎样设计报修单状态机」「上传图片如何防超大文件阻塞线程」,全是面试官爱问的硬核点。
2. 用 Spring Boot 3.2 + JDK 17 搭建最小可运行骨架:从空项目到能登录的后台
2.1 初始化项目并确认关键依赖版本兼容性
Spring Boot 版本选择直接影响后续集成稳定性。当前主流毕设环境推荐 Spring Boot 3.2.x(非 3.3.x),因其对 JDK 17 支持成熟,且与 MyBatis Plus 3.5.5+、Thymeleaf 3.1.2 兼容性经过大量学生验证。避免使用 Spring Boot 3.3+,该版本默认启用虚拟线程(Virtual Threads),在毕设级部署(如学生本地 Tomcat 或阿里云轻量应用服务器)易因 JVM 参数未调优导致线程调度异常。初始化命令如下:
# 使用 Spring Initializr CLI(需提前安装)生成基础项目 spring init \ --build=maven \ --java-version=17 \ --packaging=jar \ --name=apartment-repair-system \ --package-name=com.example.repair \ --dependencies=web,thymeleaf,mybatis-plus,mysql,validation,lombok,actuator \ --boot-version=3.2.12 \ ./apartment-repair-system提示:
actuator用于后期暴露/actuator/health等端点验证服务状态,validation是表单校验必需依赖,lombok减少@Data等样板代码。若使用 IDEA 创建,务必在 New Project → Spring Initializr 页面手动将 Spring Boot Version 设为3.2.12,而非默认最新版。
2.2 配置 MySQL 数据源与 MyBatis Plus 自动建表
毕设数据库建议使用 MySQL 8.0+(避免 MySQL 5.7 的datetime默认值兼容问题)。在application.yml中配置连接池参数,重点控制最大连接数与超时时间,防止本地开发时因连接泄漏导致服务假死:
spring: datasource: url: jdbc:mysql://localhost:3306/apartment_repair?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id # 使用雪花算法生成 Long 类型 ID,避免 UUID 字符串索引性能损耗 table-prefix: t_ # 所有表名加前缀,便于区分系统表启动类添加@MapperScan注解扫描 Mapper 接口:
@SpringBootApplication @MapperScan("com.example.repair.mapper") public class ApartmentRepairSystemApplication { public static void main(String[] args) { SpringApplication.run(ApartmentRepairSystemApplication.class, args); } }2.3 实现基于角色的登录认证:管理员、维修员、住户三类用户
不引入 Spring Security 复杂配置,采用轻量级 Session + 自定义拦截器实现权限控制。创建User实体类,字段包含id,username,password,role(枚举:ADMIN/TECHNICIAN/RESIDENT),status(启用/禁用):
@Data @TableName("t_user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String username; private String password; // 存储 BCrypt 加密后的密文 private Integer role; // 0-管理员, 1-维修员, 2-住户 private Integer status; // 0-禁用, 1-启用 }密码加密使用BCryptPasswordEncoder,在用户注册/修改密码时调用:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }登录逻辑封装在LoginController中,关键校验点:
- 用户名存在且状态为启用;
- 密码匹配(
passwordEncoder.matches(rawPassword, user.getPassword())); - 根据
role值跳转不同首页(/admin/index,/tech/index,/resident/index)。
注意:Session 超时时间需在
application.yml中显式设置,避免默认 30 分钟过短导致操作中断:server: servlet: session: timeout: 1800 # 单位秒,即 30 分钟
3. 报修单全生命周期管理:状态机驱动的业务逻辑与数据库设计
3.1 报修单状态流转模型与数据库表结构设计
报修单(RepairOrder)不是静态记录,而是具备明确状态变迁规则的业务实体。常见状态包括:WAITING(待受理)、ASSIGNED(已指派)、PROCESSING(处理中)、COMPLETED(已完成)、REJECTED(已拒绝)、CLOSED(已关闭)。状态变更必须满足前置条件,例如「只有状态为WAITING的单子才能被指派」,否则应抛出业务异常。数据库表t_repair_order关键字段设计如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键,雪花ID |
| title | VARCHAR(100) | 报修标题,非空 |
| description | TEXT | 详细描述,支持换行 |
| image_urls | JSON | 存储图片 URL 数组,如["/upload/abc.jpg","/upload/def.png"] |
| status | TINYINT | 状态码:0-WAITING, 1-ASSIGNED, 2-PROCESSING, 3-COMPLETED, 4-REJECTED, 5-CLOSED |
| applicant_id | BIGINT | 提报人 ID(关联 t_user.id) |
| assignee_id | BIGINT | 指派人 ID(管理员) |
| technician_id | BIGINT | 维修员 ID(可为空,接单后填充) |
| created_time | DATETIME | 创建时间 |
| updated_time | DATETIME | 最后更新时间 |
提示:
image_urls使用 JSON 类型存储,而非单独建图库表。毕设场景下简化设计,避免多表关联复杂度;生产环境则需拆分为t_repair_image表并建立外键。
3.2 状态变更服务层实现:用枚举约束状态合法性
定义RepairStatus枚举,明确每个状态的合法前驱状态(allowedFrom),确保状态机不可绕过:
public enum RepairStatus { WAITING((byte) 0, Collections.emptySet()), ASSIGNED((byte) 1, Set.of(WAITING)), PROCESSING((byte) 2, Set.of(ASSIGNED, WAITING)), // 允许管理员直派处理中 COMPLETED((byte) 3, Set.of(PROCESSING)), REJECTED((byte) 4, Set.of(WAITING, ASSIGNED)), CLOSED((byte) 5, Set.of(COMPLETED, REJECTED)); private final byte code; private final Set<RepairStatus> allowedFrom; RepairStatus(byte code, Set<RepairStatus> allowedFrom) { this.code = code; this.allowedFrom = allowedFrom; } public boolean canTransitionFrom(RepairStatus from) { return this.allowedFrom.contains(from); } public static RepairStatus fromCode(byte code) { for (RepairStatus status : values()) { if (status.code == code) return status; } throw new IllegalArgumentException("Unknown status code: " + code); } }状态更新方法在RepairOrderService中实现,强制校验:
@Transactional public void updateStatus(Long orderId, RepairStatus newStatus, Long operatorId) { RepairOrder order = repairOrderMapper.selectById(orderId); if (order == null) { throw new BusinessException("报修单不存在"); } RepairStatus currentStatus = RepairStatus.fromCode(order.getStatus()); if (!newStatus.canTransitionFrom(currentStatus)) { throw new BusinessException("状态变更非法:从 " + currentStatus + " 不可变为 " + newStatus); } order.setStatus(newStatus.getCode()); order.setUpdatedTime(LocalDateTime.now()); // 若变为 PROCESSING 或 COMPLETED,记录操作人 if (newStatus == PROCESSING || newStatus == COMPLETED) { order.setTechnicianId(operatorId); // operatorId 此时为维修员ID } repairOrderMapper.updateById(order); }3.3 住户提报与维修员接单的并发安全控制
提报和接单操作均涉及数据库写入,需防止并发冲突。例如两个维修员同时点击同一单的「接单」按钮,应只允许一人成功。传统SELECT ... FOR UPDATE在高并发下易锁表,毕设场景推荐乐观锁方案:在t_repair_order表增加version字段(INT类型,默认 0),MyBatis Plus 自动识别@Version注解:
@TableField(fill = FieldFill.INSERT) @Version private Integer version;接单方法中,SQL WHERE 条件加入version和当前状态双重校验:
// 更新语句自动生成:UPDATE t_repair_order SET status=?, technician_id=?, version=? // WHERE id=? AND version=? AND status=? int rows = repairOrderMapper.update(order, Wrappers.<RepairOrder>lambdaUpdate() .eq(RepairOrder::getId, orderId) .eq(RepairOrder::getStatus, RepairStatus.WAITING.getCode()) .eq(RepairOrder::getVersion, oldVersion)); if (rows == 0) { throw new BusinessException("接单失败:该报修单已被他人处理"); }4. 文件上传与图片处理:本地存储 + 缩略图生成的轻量级方案
4.1 基于 Spring Boot MultipartFile 的安全上传实现
毕设无需接入 OSS,采用本地目录存储更可控。关键安全措施:
- 限制单文件大小 ≤ 5MB(
spring.servlet.multipart.max-file-size=5MB); - 校验文件扩展名,仅允许
jpg/jpeg/png/gif; - 重命名文件为 UUID + 时间戳,避免恶意文件名(如
../../../webshell.jsp); - 存储路径隔离:按日期分目录(
/upload/2024/06/15/xxx.jpg),防止单目录文件过多。
配置项在application.yml中:
spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB上传控制器代码:
@PostMapping("/upload") @ResponseBody public Result<String> uploadImage(@RequestParam("file") MultipartFile file) { // 1. 校验文件类型 String contentType = file.getContentType(); if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType) && !"image/gif".equals(contentType)) { return Result.fail("仅支持 JPG/PNG/GIF 格式"); } // 2. 生成安全文件名 String originalFilename = file.getOriginalFilename(); String extension = "." + FilenameUtils.getExtension(originalFilename); String safeName = UUID.randomUUID().toString().replace("-", "") + System.currentTimeMillis() + extension; // 3. 构建存储路径(按日期分层) LocalDate now = LocalDate.now(); String subPath = String.format("%d/%d/%d", now.getYear(), now.getMonthValue(), now.getDayOfMonth()); Path uploadDir = Paths.get("upload", subPath); try { Files.createDirectories(uploadDir); Path targetPath = uploadDir.resolve(safeName); file.transferTo(targetPath); String url = "/upload/" + subPath + "/" + safeName; return Result.success(url); } catch (IOException e) { log.error("文件上传失败", e); return Result.fail("上传失败,请重试"); } }4.2 使用 Thumbnailator 生成缩略图并嵌入 Thymeleaf 模板
原图直接展示易拖慢页面加载,需生成 200x200 缩略图。引入thumbnailator依赖:
<dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.19</version> </dependency>在UploadService中添加缩略图生成逻辑,与原图同目录保存,命名规则为original_name_thumb.jpg:
public String generateThumbnail(String originalPath) throws IOException { Path original = Paths.get(originalPath); String thumbName = FilenameUtils.getBaseName(original.getFileName().toString()) + "_thumb" + FilenameUtils.getExtension(original.getFileName().toString()); Path thumbPath = original.getParent().resolve(thumbName); Thumbnails.of(original.toFile()) .size(200, 200) .outputQuality(0.8) .toFile(thumbPath.toFile()); return "/upload/" + thumbPath.toString().substring("upload".length() + 1); }Thymeleaf 模板中使用缩略图:
<div th:each="imgUrl : ${repairOrder.imageUrls}"> <img th:src="${imgUrl.replace('.jpg', '_thumb.jpg').replace('.png', '_thumb.png')}" th:alt="报修图片" width="200" height="200"/> </div>注意:
replace操作需确保原图命名规范,否则缩略图路径错误。更健壮做法是在RepairOrder实体中额外维护thumbnailUrls字段,上传时一并生成并存库。
5. 毕设答辩高频问题应对:从代码细节到架构演进的深度准备
5.1 面试官必问的 3 个 Spring Boot 配置陷阱与修复方案
| 问题场景 | 错误配置 | 正确配置 | 原因说明 |
|---|---|---|---|
启动报错Failed to configure a DataSource | spring.datasource.url未配置或拼写错误(如datasoruce) | 检查application.yml中spring.datasource.url是否完整,含?useSSL=false&serverTimezone=Asia/Shanghai | Spring Boot 2.4+ 默认启用DataSource自动配置,URL 缺失会触发健康检查失败 |
| Thymeleaf 模板不渲染,显示原始 HTML | spring.thymeleaf.cache: true(开发时未关闭) | 开发环境设为false:spring.thymeleaf.cache: falsespring.thymeleaf.enabled: true | 缓存开启导致修改模板后需重启,毕设调试阶段必须关闭 |
| 日志输出中文乱码(IDEA 控制台) | 未指定 JVM 参数 | 在 IDEA 的 Run Configuration → VM Options 中添加:-Dfile.encoding=UTF-8 | Windows 系统默认编码为 GBK,Spring Boot 日志框架读取配置文件时解码异常 |
5.2 如何向答辩老师证明你理解了「为什么用 MyBatis Plus 而不是 JPA」?
不要只说「JPA 学不会」,要从技术选型依据切入:
- SQL 可控性:报修系统需复杂查询(如「统计各楼栋本月报修量 Top5」),MyBatis Plus 的
QueryWrapper可精准构造GROUP BY+ORDER BY+LIMIT,而 JPA 的 Criteria API 写法冗长且易出错; - 学习成本与调试效率:MyBatis Plus 自动生成 SQL 并打印到控制台(
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl),学生能直接看到执行语句,快速定位 N+1 查询问题;JPA 的@Query注解需反复编译验证; - 生态适配性:本系统需对接 EasyExcel 导出报表,MyBatis Plus 的
selectList返回List<Map<String, Object>>与 Excel 模板无缝衔接;JPA 实体需额外转换。
5.3 毕设代码质量提升的 3 个具体动作
为所有 Service 方法添加
@Transactional显式声明
即使当前只有单表操作,也养成习惯。未来扩展「提报报修单同时发送站内信」时,事务能保证两者原子性。避免依赖 Spring Boot 默认的@Transactional代理机制失效(如自调用)。用
Result<T>统一封装返回值,禁止裸return new HashMap<>()
定义标准响应体:public class Result<T> { private int code; // 200-成功, 500-失败 private String msg; private T data; // getter/setter }所有 Controller 返回
Result<?>,前端统一处理code,避免if (res.data != null)等散落判断。在
application-dev.yml中配置 HikariCP 连接池监控端点
添加 Actuator 依赖后,访问http://localhost:8080/actuator/metrics/hikaricp.connections.active可实时查看活跃连接数。答辩时演示「模拟 100 并发提报,观察连接池是否打满」,体现工程化思维。
提示:答辩前务必导出
mvn dependency:tree -Dverbose输出,确认无冲突依赖(如spring-boot-starter-web与spring-boot-starter-webflux共存)。使用mvn clean compile全流程验证,避免 IDEA 缓存导致的编译通过但打包失败。
本文还有配套的精品资源,点击获取