🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Claude 随心用,限时 5 折。 👉 点击领海量免费额度
每到毕业季,计算机专业的学生们最头疼的往往不是论文,而是那个必须完成的“毕业设计”。选题太简单,显得没技术含量;选题太复杂,又怕时间不够、代码跑不通。一个能兼顾技术栈学习、业务逻辑清晰、又能快速上手的项目,就成了刚需。
今天要讨论的“基于SpringBoot+Vue的学生宿舍报修信息管理系统”,就是一个非常典型的毕业设计选题。它看似简单,一个“报修系统”而已,但如果你只把它当作一个增删改查的练习,那就错过了它真正的价值。这个项目的核心,不在于实现一个功能,而在于如何通过一个具体的业务场景,串联起Java后端、Vue前端、数据库设计、权限控制、前后端分离架构等一系列企业级开发的核心技能点。
很多同学在开发类似系统时,容易陷入两个误区:要么前端页面写得漂亮,后端逻辑却一团糟,接口设计混乱;要么后端CRUD写得飞起,前端却交互生硬,用户体验极差。更常见的问题是,代码虽然能跑,但结构混乱,毫无扩展性,经不起任何追问。这篇文章,我们就来彻底拆解这个项目,不仅告诉你“怎么做”,更会深入分析“为什么这么做”,以及在实际开发中“有哪些坑”。无论你是正在寻找毕业设计题目的同学,还是想通过一个完整项目巩固SpringBoot和Vue技能的开发者,这篇文章都将提供一份从零到一的实战指南。
1. 这个项目真正要解决什么问题?
学生宿舍报修管理系统,表面上是解决“宿舍东西坏了找谁修”的问题。但从软件开发的角度看,它要解决的是 多角色协同业务流程的数字化建模与实现 。这听起来有点抽象,我们拆开来看:
- 业务痛点 :传统报修靠纸条、电话或跑腿,信息易丢失、进度不透明、责任难追溯。系统要解决信息流转低效、状态不透明、数据无法统计的问题。
- 技术痛点(对开发者而言) :如何设计一个清晰、可扩展的数据库来支撑“报修单”这个核心实体的全生命周期状态流转?如何为不同角色(学生、维修工、管理员)设计合理的前端界面和后端接口权限?如何保证前后端数据交互的效率和安全性?
- 学习痛点(对毕业设计而言) :如何选择一个技术栈既能体现技术能力,又不会过于冷门或复杂?如何确保项目结构清晰、代码规范,便于答辩讲解?如何让项目除了基础功能外,还有那么一两个“亮点”?
因此,这个项目的价值远不止于实现功能。它是一次完整的、微缩版的企业应用开发实践。通过它,你可以系统地练习:
- 后端 :SpringBoot的快速构建、MyBatis-Plus的高效数据操作、RESTful API设计、全局异常处理、权限拦截(如JWT)。
- 前端 :Vue的组件化开发、路由管理(Vue Router)、状态管理(Vuex/Pinia)、异步请求(axios)、Element UI等组件库的使用。
- 工程化 :前后端分离的协作模式、接口文档管理、基础的Git版本控制。
- 业务思维 :从需求分析到数据库设计,再到功能模块划分的完整流程。
如果你能跟着本文的思路,不仅跑通代码,更能理解每个技术选型背后的原因,那么这份毕业设计的含金量将远超一个简单的“作业”。
2. 技术栈选型与核心概念
为什么是SpringBoot + Vue?这是目前国内Java领域最主流、最成熟的 前后端分离 开发组合之一,社区活跃、资料丰富,非常适合学习和毕业设计。
2.1 后端技术栈:SpringBoot生态
- SpringBoot :核心框架。它最大的优点是“约定大于配置”,内置了Tomcat服务器,让你无需复杂配置就能快速启动一个Web应用。这对于需要快速交付的毕业设计来说,至关重要。
- Spring MVC :处理Web请求的模型。它帮你优雅地组织Controller、Service、Dao(Mapper)三层架构,这是企业级项目的标准分层。
- MyBatis-Plus :数据持久层框架。它是MyBatis的增强工具,提供了强大的CRUD封装。你几乎不用写简单的SQL,就能完成绝大多数数据操作,极大提升开发效率。它的
QueryWrapper等条件构造器也非常好用。 - MySQL :关系型数据库。稳定、易用、免费,是学习和小型项目的首选。
- Maven :项目构建与依赖管理工具。通过一个
pom.xml文件管理所有第三方库(Jar包),解决令人头疼的依赖冲突问题。
2.2 前端技术栈:Vue生态
- Vue.js :渐进式JavaScript框架。核心是数据驱动和组件化,学习曲线相对平缓,能快速构建出交互丰富的单页面应用(SPA)。
- Vue Router :官方路由管理器。负责管理前端页面之间的跳转,在SPA中实现无刷新切换视图。
- Vuex/Pinia :状态管理模式库。用于集中管理所有组件共享的状态(例如:当前登录的用户信息)。Pinia是Vuex的升级版,更简单、更符合组合式API风格,新项目建议使用Pinia。
- Axios :基于Promise的HTTP客户端。用于浏览器和Node.js中发送请求,是前后端通信的桥梁。
- Element Plus :基于Vue 3的桌面端组件库。提供了丰富的、美观的UI组件(按钮、表单、表格、弹窗等),让你能快速搭建出专业的管理后台界面,无需从零设计CSS。
2.3 核心架构:前后端分离(B/S)
这是本项目的关键架构。理解它,就理解了现代Web开发的主流模式。
- 前端(Vue) :运行在用户的浏览器中,负责渲染页面、处理用户交互。它通过Axios调用后端提供的 RESTful API 接口来获取或提交数据(JSON格式)。
- 后端(SpringBoot) :运行在服务器上,负责处理业务逻辑、操作数据库。它接收前端的API请求,处理完成后返回JSON数据,不关心页面如何渲染。
- 优势 :前后端职责清晰,可以并行开发;前端技术选型灵活;后端接口可以供多种客户端(Web、App、小程序)复用。
3. 开发环境与工具准备
工欲善其事,必先利其器。以下是推荐的环境配置清单,版本号请根据实际情况调整,但大版本建议保持一致以避免兼容性问题。
3.1 后端环境准备
- JDK :版本 8、11 或 17(LTS长期支持版)。推荐JDK 17,它是目前的主流选择。安装后配置
JAVA_HOME环境变量。 - IDE :IntelliJ IDEA(社区版或旗舰版)。它对Java和SpringBoot的支持是最好的,能极大提升开发效率。
- Maven :版本 3.6+。IDEA通常自带,但建议独立安装并配置
MAVEN_HOME和仓库镜像(如阿里云镜像)以加速依赖下载。 - MySQL :版本 5.7 或 8.0。安装后记住root密码,并安装一个图形化管理工具,如Navicat、MySQL Workbench或IDEA自带的Database工具。
3.2 前端环境准备
- Node.js :版本 16+。它是JavaScript的运行环境,自带包管理工具npm。安装Node.js后,npm即可使用。
- 包管理器 :可以使用npm,但更推荐 yarn 或 pnpm ,它们速度更快、依赖管理更清晰。可以通过
npm install -g yarn安装yarn。 - IDE :Visual Studio Code (VSCode)。轻量且强大的代码编辑器,拥有丰富的Vue插件生态。
- Vue CLI 或 Vite :项目脚手架。Vue CLI是传统选择,功能全面。Vite是新一代构建工具,启动和热更新速度极快。本文示例将使用更现代的 Vite 来创建Vue项目。
3.3 初始化项目结构
在开始编码前,你的工作空间应该有两个独立的项目文件夹:
workspace/
├── dorm-repair-backend/ # SpringBoot后端项目
└── dorm-repair-frontend/ # Vue前端项目
两者完全独立,通过API接口通信。
4. 后端核心设计与实现
我们从后端开始,因为后端定义了数据的结构和业务的规则。
4.1 数据库设计
数据库设计是项目的基石。一个糟糕的设计会让后续编码举步维艰。围绕“报修单”这个核心,我们至少需要以下表:
- 用户表 (sys_user) :存储所有系统用户(学生、维修工、管理员)。通过
user_type字段区分角色。 - 报修单表 (repair_order) :核心表。记录报修详情、状态、时间等。
- 宿舍楼表 (dorm_building) 和 宿舍房间表 (dorm_room) :维护宿舍资源信息。报修单需要关联到具体房间。
- 维修记录表 (repair_record) :可选。用于记录维修工每次的处理反馈,实现更细粒度的跟踪。
以下是 repair_order 表的一个简化DDL示例:
CREATE TABLE `repair_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_number` varchar(32) NOT NULL COMMENT '报修单号(可规则生成,如REPAIR202411110001)',
`student_id` bigint(20) NOT NULL COMMENT '报修学生ID',
`room_id` bigint(20) NOT NULL COMMENT '报修房间ID',
`title` varchar(100) NOT NULL COMMENT '报修标题',
`description` text COMMENT '问题详细描述',
`image_urls` varchar(500) DEFAULT NULL COMMENT '现场图片URL,多个用逗号分隔',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待受理,1-已受理/维修中,2-已完成,3-已取消',
`handler_id` bigint(20) DEFAULT NULL COMMENT '处理人(维修工)ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`appointment_time` datetime DEFAULT NULL COMMENT '预约维修时间',
`finish_time` datetime DEFAULT NULL COMMENT '完成时间',
`rating` tinyint(4) DEFAULT NULL COMMENT '评分(1-5星)',
`comment` varchar(255) DEFAULT NULL COMMENT '评价内容',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_number` (`order_number`),
KEY `idx_student_id` (`student_id`),
KEY `idx_handler_id` (`handler_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修单表';
设计要点 :
-
order_number唯一单号,便于线下沟通和查询。 -
status字段使用字典值,清晰定义工单生命周期。 - 记录关键时间点(
create_time,update_time,finish_time),便于统计和分析。 -
image_urls字段存储图片路径,实际文件可上传至服务器目录或云存储(如OSS),数据库中只存访问地址。
4.2 创建SpringBoot项目
使用IDEA的Spring Initializr(或访问 start.spring.io )创建项目。
- Project : Maven
- Language : Java
- Spring Boot : 2.7.x 或 3.x(注意JDK版本对应关系,Spring Boot 3.x需要JDK 17+)
- Dependencies : 勾选
Spring Web,MyBatis Framework,MySQL Driver。创建完成后,在pom.xml中手动添加MyBatis-Plus的依赖。
<!-- pom.xml 中添加 MyBatis-Plus 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version> <!-- 请使用最新稳定版 -->
</dependency>
<!-- 代码生成器(可选,但强烈推荐用于快速生成基础代码) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
<scope>provided</scope>
</dependency>
4.3 项目结构与核心代码
标准的MVC分层结构如下:
src/main/java/com/yourcompany/dormrepair/
├── DormRepairApplication.java # 启动类
├── config/ # 配置类(如MyBatis-Plus配置、跨域配置)
├── controller/ # 控制器层,接收请求,返回响应
├── entity/ # 实体类,与数据库表对应
├── mapper/ # Mapper接口,即Dao层
├── service/ # 业务逻辑层
│ └── impl/ # 业务逻辑实现类
├── dto/ # 数据传输对象(用于前后端交互)
├── vo/ # 视图对象(用于返回给前端的特定数据模型)
└── common/ # 通用类(如统一返回结果、异常、常量)
实体类示例 (RepairOrder.java) :
package com.yourcompany.dormrepair.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("repair_order") // 指定表名
public class RepairOrder {
@TableId(type = IdType.AUTO) // 主键自增
private Long id;
private String orderNumber;
private Long studentId;
private Long roomId;
private String title;
private String description;
private String imageUrls;
private Integer status; // 使用枚举更佳
private Long handlerId;
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
private LocalDateTime updateTime;
private LocalDateTime appointmentTime;
private LocalDateTime finishTime;
private Integer rating;
private String comment;
// 非数据库字段,用于关联查询显示
@TableField(exist = false)
private String studentName;
@TableField(exist = false)
private String handlerName;
@TableField(exist = false)
private String roomNumber;
}
Mapper接口 (RepairOrderMapper.java) :
package com.yourcompany.dormrepair.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yourcompany.dormrepair.entity.RepairOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 重要:让MyBatis-Plus扫描到
public interface RepairOrderMapper extends BaseMapper<RepairOrder> {
// 继承BaseMapper后,基础的CRUD方法已自动拥有
// 复杂查询可以在这里定义方法,并在对应的XML中写SQL
}
Service层 (RepairOrderService.java) :
package com.yourcompany.dormrepair.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yourcompany.dormrepair.entity.RepairOrder;
import com.yourcompany.dormrepair.vo.RepairOrderVO;
public interface RepairOrderService extends IService<RepairOrder> {
// 分页条件查询报修单(带关联信息)
Page<RepairOrderVO> getOrderPage(Page<RepairOrder> page, String keyword, Integer status, Long userId, Integer role);
// 学生提交报修单
boolean submitOrder(RepairOrder order, Long studentId);
// 维修工接单
boolean acceptOrder(Long orderId, Long handlerId);
// 维修工完成维修
boolean completeOrder(Long orderId, String comment);
}
Controller层 (RepairOrderController.java) :
package com.yourcompany.dormrepair.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yourcompany.dormrepair.common.Result;
import com.yourcompany.dormrepair.entity.RepairOrder;
import com.yourcompany.dormrepair.service.RepairOrderService;
import com.yourcompany.dormrepair.vo.RepairOrderVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/repair-order")
public class RepairOrderController {
@Autowired
private RepairOrderService repairOrderService;
// 学生提交报修
@PostMapping("/submit")
public Result<?> submitOrder(@RequestBody RepairOrder order,
@RequestAttribute Long currentUserId) { // 从Token中获取当前用户ID
boolean success = repairOrderService.submitOrder(order, currentUserId);
return success ? Result.success("提交成功") : Result.error("提交失败");
}
// 分页查询报修单(不同角色看到的数据不同)
@GetMapping("/page")
public Result<Page<RepairOrderVO>> getPage(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer status,
@RequestAttribute Integer userType) { // 用户角色
Page<RepairOrder> page = new Page<>(pageNum, pageSize);
Page<RepairOrderVO> orderPage = repairOrderService.getOrderPage(page, keyword, status, null, userType);
return Result.success(orderPage);
}
// 维修工接单
@PostMapping("/{orderId}/accept")
public Result<?> acceptOrder(@PathVariable Long orderId,
@RequestAttribute Long currentUserId) {
boolean success = repairOrderService.acceptOrder(orderId, currentUserId);
return success ? Result.success("接单成功") : Result.error("接单失败,工单可能已被处理");
}
}
统一返回结果封装 (Result.java) :
package com.yourcompany.dormrepair.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> success(String msg) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg(msg);
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
// 可以定义更多状态码,如 400(参数错误)、401(未授权)、403(禁止访问)等
}
4.4 关键配置
application.yml 配置文件:
server:
port: 8080 # 后端服务端口
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dorm_repair_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
servlet:
multipart:
max-file-size: 10MB # 文件上传大小限制
max-request-size: 20MB
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,调试用,生产环境关闭
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名(如果表中有此字段)
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
mapper-locations: classpath:mapper/*.xml # XML映射文件位置
# 自定义配置,如JWT密钥、文件上传路径
app:
upload-path: /path/to/upload # 文件上传本地路径
jwt:
secret: your-jwt-secret-key-here-minimum-32-chars
expire: 7200 # token过期时间(秒)
5. 前端核心设计与实现
后端API准备好后,前端的工作就是调用这些接口,构建用户界面。
5.1 使用Vite创建Vue 3项目
在 dorm-repair-frontend 目录下,打开终端执行:
# 使用 npm
npm create vue@latest
# 或使用 yarn
yarn create vue
按照提示选择项目特性: TypeScript (推荐)、 Vue Router 、 Pinia 、 ESLint 。 创建完成后,安装Element Plus和Axios:
cd dorm-repair-frontend
npm install element-plus axios
# 或
yarn add element-plus axios
5.2 项目结构
src/
├── api/ # 所有API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── views/ # 页面组件
│ ├── Login.vue # 登录页
│ ├── Student/ # 学生相关页面
│ │ ├── Dashboard.vue
│ │ ├── RepairSubmit.vue
│ │ └── MyOrders.vue
│ ├── Worker/ # 维修工相关页面
│ │ ├── TaskList.vue
│ │ └── TaskDetail.vue
│ └── Admin/ # 管理员相关页面
│ ├── UserManage.vue
│ └── OrderManage.vue
├── utils/ # 工具函数(如请求拦截器、日期格式化)
├── App.vue
└── main.ts
5.3 核心代码示例
1. 配置Axios和请求拦截器 (utils/request.ts) :
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router';
const service = axios.create({
baseURL: 'http://localhost:8080/api', // 后端API地址
timeout: 10000,
});
// 请求拦截器:在请求头中添加Token
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器:统一处理错误
service.interceptors.response.use(
(response) => {
const res = response.data;
// 假设后端统一返回 { code: 200, msg: 'success', data: ... } 格式
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败');
// 如果是未授权,跳转到登录页
if (res.code === 401) {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
router.push('/login');
}
return Promise.reject(new Error(res.msg || 'Error'));
}
return res.data; // 直接返回后端封装里的data字段
},
(error) => {
console.error('请求错误:', error);
ElMessage.error('网络错误或服务器异常');
return Promise.reject(error);
}
);
export default service;
2. 封装API模块 (api/repairOrder.ts) :
import request from '@/utils/request';
// 定义接口返回的数据类型(根据后端VO定义)
export interface RepairOrderVO {
id: number;
orderNumber: string;
title: string;
status: number;
studentName: string;
handlerName?: string;
createTime: string;
// ... 其他字段
}
export interface PageResult<T> {
records: T[];
total: number;
size: number;
current: number;
}
// API函数
export const repairOrderApi = {
// 提交报修单
submitOrder(data: any) {
return request.post('/repair-order/submit', data);
},
// 分页查询报修单
getOrderPage(params: { pageNum: number; pageSize: number; keyword?: string; status?: number }) {
return request.get<PageResult<RepairOrderVO>>('/repair-order/page', { params });
},
// 维修工接单
acceptOrder(orderId: number) {
return request.post(`/repair-order/${orderId}/accept`);
},
// 维修工完成维修
completeOrder(orderId: number, comment: string) {
return request.post(`/repair-order/${orderId}/complete`, { comment });
},
// 学生取消报修
cancelOrder(orderId: number) {
return request.post(`/repair-order/${orderId}/cancel`);
},
};
3. 学生提交报修页面组件 (views/Student/RepairSubmit.vue) :
<template>
<div class="repair-submit-container">
<el-card class="box-card">
<template #header>
<span>提交报修申请</span>
</template>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="报修标题" prop="title">
<el-input v-model="form.title" placeholder="请输入报修问题摘要,如:宿舍空调不制冷" />
</el-form-item>
<el-form-item label="宿舍房间" prop="roomId">
<el-select v-model="form.roomId" placeholder="请选择房间">
<el-option v-for="room in roomList" :key="room.id" :label="room.fullName" :value="room.id" />
</el-select>
</el-form-item>
<el-form-item label="问题描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请详细描述故障现象..." />
</el-form-item>
<el-form-item label="上传图片">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
list-type="picture-card"
:limit="3"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="预约时间">
<el-date-picker
v-model="form.appointmentTime"
type="datetime"
placeholder="选择预约维修时间(可选)"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="submitting">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElUploadFile, type FormInstance, type FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { repairOrderApi } from '@/api/repairOrder';
import { roomApi } from '@/api/room'; // 假设有获取房间列表的API
const formRef = ref<FormInstance>();
const submitting = ref(false);
const roomList = ref<any[]>([]);
const form = reactive({
title: '',
roomId: undefined as number | undefined,
description: '',
appointmentTime: '',
});
const rules: FormRules = {
title: [{ required: true, message: '请输入报修标题', trigger: 'blur' }],
roomId: [{ required: true, message: '请选择宿舍房间', trigger: 'change' }],
description: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
};
const fileList = ref<ElUploadFile[]>([]);
// 处理文件选择
const handleFileChange = (file: ElUploadFile) => {
// 这里可以预览或限制文件大小/类型
console.log('file changed', file);
};
// 加载房间列表
const loadRooms = async () => {
try {
const res = await roomApi.getMyRooms(); // 假设此API返回学生所属房间
roomList.value = res;
} catch (error) {
console.error('加载房间列表失败', error);
}
};
// 提交表单
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) {
ElMessage.warning('请完善表单信息');
return;
}
submitting.value = true;
try {
// 处理图片上传(实际项目中需先上传到文件服务器,获取URL)
const imageUrls = fileList.value.map(f => f.url).join(',');
const submitData = {
...form,
imageUrls
};
await repairOrderApi.submitOrder(submitData);
ElMessage.success('报修申请提交成功!');
resetForm();
} catch (error) {
console.error('提交失败', error);
ElMessage.error('提交失败,请重试');
} finally {
submitting.value = false;
}
});
};
const resetForm = () => {
formRef.value?.resetFields();
fileList.value = [];
};
onMounted(() => {
loadRooms();
});
</script>
<style scoped>
.repair-submit-container {
padding: 20px;
}
.box-card {
max-width: 800px;
margin: 0 auto;
}
</style>
4. 路由守卫与权限控制 (router/index.ts) :
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteLocationNormalized } from 'vue-router';
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/student',
name: 'Student',
component: () => import('@/layouts/StudentLayout.vue'), // 学生端布局
meta: { requiresAuth: true, role: 'student' },
children: [
{ path: 'dashboard', component: () => import('@/views/Student/Dashboard.vue') },
{ path: 'submit', component: () => import('@/views/Student/RepairSubmit.vue') },
{ path: 'orders', component: () => import('@/views/Student/MyOrders.vue') },
]
},
// ... 维修工和管理员路由类似
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 路由守卫:检查登录状态和权限
router.beforeEach((to: RouteLocationNormalized, from, next) => {
const token = localStorage.getItem('token');
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
// 如果目标路由需要认证
if (to.meta.requiresAuth) {
if (!token) {
// 未登录,跳转到登录页
next('/login');
return;
}
// 检查角色权限
const requiredRole = to.meta.role;
if (requiredRole && userInfo.userType !== requiredRole) {
// 角色不符,跳转到无权限页面或首页
ElMessage.warning('无权访问此页面');
next(`/${userInfo.userType}/dashboard`); // 跳转到对应角色的首页
return;
}
}
next();
});
export default router;
6. 系统运行与效果验证
6.1 启动后端服务
- 确保MySQL服务已启动,并创建了数据库
dorm_repair_db。 - 在IDEA中,找到主启动类
DormRepairApplication.java,右键运行。 - 观察控制台日志,看到类似
Tomcat started on port(s): 8080的日志,表示启动成功。 - 可以在浏览器访问
http://localhost:8080(如果配置了简单的测试接口),或使用Postman测试API。
6.2 启动前端服务
- 在
dorm-repair-frontend目录下打开终端。 - 运行
npm run dev或yarn dev。 - 控制台会输出本地访问地址,通常是
http://localhost:5173(Vite默认端口)。 - 在浏览器打开该地址,即可看到前端应用。
6.3 功能验证流程
- 登录 :使用不同角色的测试账号(学生、维修工、管理员)登录。
- 学生端 :
- 进入“提交报修”页面,填写表单并提交。
- 在“我的报修”页面,查看刚提交的工单状态应为“待受理”。
- 维修工端 :
- 登录后,在“待处理任务”列表中应能看到学生提交的工单。
- 点击“接单”,工单状态变为“维修中”。
- 维修完成后,点击“完成”,填写维修备注,状态变为“已完成”。
- 学生端 :
- 刷新“我的报修”页面,看到工单状态已更新为“已完成”。
- 可以对已完成工单进行“评价”。
- 管理员端 :
- 查看所有报修单的统计。
- 管理用户和宿舍信息。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 前端访问后端API报跨域错误 (CORS) | 浏览器安全策略阻止跨域请求 | 浏览器开发者工具Console或Network面板查看错误信息 | 在后端SpringBoot中添加全局CORS配置类,允许前端域名访问。 |
| 前端页面空白,控制台报JS错误 | 1. 依赖未正确安装 2. 组件引入错误 3. TypeScript类型错误 | 查看浏览器Console具体错误信息,定位到文件和行号 | 1. 重新运行 npm install 。 2. 检查组件导入路径和命名。 3. 根据TS错误提示修复类型。 |
| 后端启动失败,端口被占用 | 8080端口已被其他程序使用 | 查看启动日志中的错误信息 | 1. 关闭占用端口的进程。 2. 在 application.yml 中修改 server.port 为其他端口(如8081)。 |
| 数据库连接失败 | 1. MySQL服务未启动 2. 连接URL、用户名、密码错误 3. 时区配置问题 | 查看后端启动日志中的SQL异常堆栈 | 1. 启动MySQL服务。 2. 检查 application.yml 中的数据库配置。 3. 在连接URL中添加 &serverTimezone=Asia/Shanghai 。 |
| MyBatis-Plus查询不到数据 | 1. 实体类字段名与数据库列名未正确映射 2. 表名未指定 | 开启SQL日志( mybatis-plus.configuration.log-impl ),查看实际执行的SQL | 1. 使用 @TableField 注解指定映射关系。 2. 使用 @TableName 注解指定表名。 |
| 文件上传失败 | 1. 上传大小超过限制 2. 服务器存储路径无写权限 | 查看后端日志 | 1. 调整 spring.servlet.multipart.max-file-size 。 2. 检查 app.upload-path 配置的目录是否存在且有权限。 |
| 前端路由跳转后页面空白 | 1. 路由配置错误 2. 组件未正确导出或引入 | 查看浏览器Console和Vue Devtools中的路由状态 | 1. 检查 router/index.ts 中的 component 导入路径。 2. 确保Vue组件使用了 <script setup> 或正确导出了 default 。 |
| 登录成功后Token无效 | 1. Token未正确存储在localStorage 2. 请求拦截器未正确添加Token 3. 后端JWT解析失败 | 1. 检查Application -> Local Storage。 2. 检查Network请求头中是否有Authorization。 3. 查看后端日志。 | 1. 确保登录接口返回Token后正确存储。 2. 检查 utils/request.ts 中的拦截器逻辑。 3. 检查后端JWT生成和验证的密钥是否一致。 |
8. 项目优化与进阶建议(毕业设计亮点)
完成基础功能后,以下优化点可以让你的项目脱颖而出:
- 引入Redis缓存 :将频繁访问且变化不频繁的数据(如宿舍楼列表、字典数据)缓存到Redis,减轻数据库压力。
- 集成消息推送 :当工单状态变更(如被接单、完成)时,通过WebSocket或第三方推送服务(如极光推送)实时通知学生和维修工。
- 实现文件云存储 :将报修图片上传至阿里云OSS、腾讯云COS等对象存储服务,避免占用应用服务器磁盘,并提升访问速度。
- 添加数据可视化 :使用ECharts等库,为管理员后台添加统计图表,如“每月报修量趋势”、“各类故障占比”、“维修工接单排行”等。
- 编写单元测试 :为后端的Service层关键方法编写JUnit单元测试,体现工程化思维。
- 使用Docker容器化部署 :编写
Dockerfile和docker-compose.yml,将MySQL、Redis、后端应用、前端应用容器化,一键部署,展示运维能力。 - 实现简单的权限控制(RBAC) :不仅通过角色控制页面访问,更细化到接口级别和数据级别(例如,维修工只能看到分配给自己的工单)。
- 接入第三方登录 :允许学生通过学校统一身份认证系统(模拟)或微信扫码登录。
- 生成接口文档 :使用Swagger或Knife4j自动生成后端API文档,方便前后端联调,也体现专业度。
9. 总结与学习路径建议
通过这个“学生宿舍报修信息管理系统”的完整开发实践,我们走通了一个典型前后端分离Web应用的全流程。从需求分析、技术选型、数据库设计,到SpringBoot后端开发、Vue前端组件化实现,再到最后的联调测试,每一步都紧扣实际开发场景。
这个项目最大的价值在于它的 完整性和典型性 。它几乎涵盖了本科阶段软件工程课程要求的所有核心知识点。在答辩时,你可以清晰地阐述:
- 为什么选择这个架构? (前后端分离的优势)
- 数据库表是如何设计的? (ER关系、状态字段设计)
- 前后端是如何交互的? (RESTful API设计、Axios封装)
- 权限是如何控制的? (路由守卫、接口拦截)
- 项目有哪些可扩展性? (缓存、消息、云存储等优化方向)
对于学习者而言,不要满足于仅仅复制代码跑通。建议你:
- 理解 :读懂每一行代码的作用,特别是MyBatis-Plus的封装、Vue的响应式原理、Pinia的状态管理。
- 改造 :尝试修改需求,比如增加“紧急报修”类型,并优先显示;或者为维修工增加“签到打卡”功能。
- 调试 :故意制造一些Bug(如传错参数、关掉数据库),学习如何根据错误信息排查问题。
- 重构 :思考代码结构是否可以优化,比如将一些工具函数抽离,将复杂的组件拆分成更小的子组件。
技术的学习在于举一反三。掌握了这个项目的精髓,你就能快速上手开发其他类似的管理系统,如实验室设备预约、图书借阅、社团活动报名等。项目的完整源码可以作为你技术学习的起点和未来求职时的一个扎实作品。建议你在开发过程中,养成良好的代码注释和Git提交习惯,这本身也是一项重要的工程能力。
🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Claude 随心用,限时 5 折。 👉 点击领海量免费额度

1794


被折叠的 条评论
为什么被折叠?



