简介:本资源为基于SSM与Vue3实现的设备维修管理系统毕业设计项目,面向计算机相关专业需要完成毕设的学生及Java全栈初学者。系统采用B/S架构与前后端分离模式,后端以Java语言开发,前端使用Vue3配合ElementPlus构建界面,覆盖设备管理、维修申请、维修审批、维修过程记录、维修验收与统计分析等模块,并为各节点加入审批功能,实现从申请到交付的全流程信息化管理与回溯。压缩包共约2000个文件,以1011个js脚本、892个md说明文档、87个json配置为主,另含sql建库脚本、html页面与docx设计文档,整体约129.49MB,目录结构清晰便于按模块查阅。目前已有284人学习下载,适合作为毕设选题参考、课程设计模板或SSM与Vue3前后端分离项目的实战学习素材。
1. 毕设选 SSM+Vue3 做设备维修管理系统:这套组合到底值不值得投入
设备维修这件事,很多中小工厂和物业还在用微信群喊人、Excel 记台账。设备坏了谁报修、派给谁、修了多久、换了什么件、下次保养什么时候,全靠人脑记。一旦设备数量上百、维修工超过五个,信息就开始乱:报修单丢了、责任分不清、备件库存对不上。设备维修管理系统要解决的就是这条链路——从报修、派单、维修、验收、备件消耗到统计报表,全部线上留痕。
标题里的 SSM 是 Spring + SpringMVC + MyBatis 这套经典 Java 后端组合,Vue3 是前端框架。为什么毕设场景下这套组合依然能打?因为 SSM 资料多、结构清晰、答辩时老师听得懂,Vue3 的 Composition API 写后台管理页面比 Vue2 更顺手,配合 Element Plus 能快速搭出表格、表单、弹窗。这套方案适合计算机相关专业的本科毕设,也适合想练手前后端分离的中级开发者。它不追求高并发,追求的是业务闭环完整、代码可读、能讲清楚每一层在干什么。下面我按实际落地的顺序,把选型、建表、接口、前端页面和踩坑一条条拆开讲。
2. 环境搭建与 SSM 后端骨架:从零跑通第一个接口
2.1 版本选型:为什么是 JDK8 + Spring 5 + MyBatis 3
毕设最怕的是环境装三天跑不起来。SSM 这套东西版本兼容性经过多年验证,我一般锁定 JDK8、Spring 5.3.x、MyBatis 3.5.x、MySQL 8.0、Maven 3.8。JDK8 是因为大量 SSM 教程和依赖都基于它,用 JDK17 会遇到反射和模块化报错,徒增麻烦。Spring 5.3 是 5.x 最后一个稳定分支,和 MyBatis 整合成熟。MySQL 8.0 注意驱动类改成com.mysql.cj.jdbc.Driver,连接串要带时区和allowPublicKeyRetrieval。
Vue3 这边用 Vite 构建,Node 建议 18 LTS。Element Plus 选 2.x,和 Vue3 配套。这里有个热词里常被问到的点:vue3 和 vue2 的区别,落到项目里最直接的就是 Vue3 用createApp挂载、用 Composition API 组织逻辑、用ref和reactive管理响应式数据,而 Vue2 是new Vue加 Options API。后台管理系统这种表单表格密集的场景,Composition API 把同一功能的逻辑聚在一起,比 Vue2 的 data/methods/computed 分散写法好维护得多。
2.2 Maven 依赖与目录结构
后端用 Maven 管理依赖,核心依赖如下。注意 SSM 是三个框架整合,不是单个 starter,需要手动配 Spring 和 MyBatis 的整合包。
<!-- pom.xml 核心依赖,版本按需微调 --> <dependencies> <!-- Spring 核心与 Web MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.30</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.1</version> </dependency> <!-- MySQL 驱动,8.0 用 cj 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <!-- 连接池、JSON、日志 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies>目录结构按经典分层走:controller接请求、service写业务、mapper做持久化、entity放实体、config放 Spring 配置。这个分层不是形式主义,答辩时老师问「你的业务逻辑写在哪」,你能指着 service 层说清楚,比全塞 controller 里强太多。
2.3 Spring 与 MyBatis 整合配置
整合的关键是把SqlSessionFactory交给 Spring 管理,再用MapperScannerConfigurer扫描 mapper 接口。下面是一段 Java Config 写法,比 XML 更直观。
@Configuration @MapperScan("com.repair.mapper") // 扫描 mapper 接口,省去逐个注册 public class MyBatisConfig { @Bean public DataSource dataSource() { DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/repair_db?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"); ds.setUsername("root"); ds.setPassword("your_password"); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); return ds; } @Bean public SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(ds); // 指定 mapper xml 位置,接口和 xml 分离是 SSM 常见做法 bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return bean.getObject(); } }@MapperScan是 SSM 里最常用的注解之一,热词里 ssm 常用注解 经常被搜,它解决的就是「每个 mapper 都要手动注册」的痛点。dataSource里连接串的三个参数别漏:serverTimezone不设会报时区错误,useSSL=false避免本地连接警告,allowPublicKeyRetrieval=true是 MySQL8 用 caching_sha2 认证时需要的。跑通标准是:启动 Tomcat 后访问一个测试接口能返回 JSON,且日志里没有No qualifying bean或CannotGetJdbcConnection报错。
3. 设备维修业务建模:数据表设计与核心接口实现
3.1 五张核心表撑起维修闭环
设备维修管理系统的业务闭环是:设备档案 → 报修单 → 派单 → 维修记录 → 备件消耗。对应五张核心表,字段设计直接决定后面接口好不好写。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| device | 设备档案 | id, device_no, device_name, location, status, buy_date |
| repair_order | 报修单 | id, device_id, fault_desc, reporter, report_time, status |
| dispatch | 派单记录 | id, order_id, repairer_id, dispatch_time, dispatch_status |
| repair_record | 维修记录 | id, order_id, repairer_id, repair_desc, cost, finish_time |
| spare_part | 备件库存 | id, part_name, stock, unit, warn_stock |
repair_order.status用数字或枚举表示流转状态:0 待派单、1 已派单、2 维修中、3 待验收、4 已完成。这个状态机是整个系统的骨架,前端所有按钮的显示隐藏都依赖它。spare_part.warn_stock是预警阈值,库存低于它就标红,这是答辩时容易被问到的「亮点功能」。
3.2 报修单分页查询接口
后台列表页几乎都是分页查询,这里用 MyBatis 的limit配合 PageHelper 或者手写分页。手写分页更可控,毕设推荐手写,逻辑清楚。
// RepairOrderController.java @RestController @RequestMapping("/api/order") public class RepairOrderController { @Autowired private RepairOrderService orderService; // 分页查询报修单,支持按状态和设备名筛选 @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer status, @RequestParam(required = false) String deviceName) { // 计算偏移量,pageNum 从 1 开始 int offset = (pageNum - 1) * pageSize; List<RepairOrderVO> list = orderService.pageQuery(offset, pageSize, status, deviceName); int total = orderService.countQuery(status, deviceName); return Result.success(new PageResult<>(list, total)); } }pageNum和pageSize用@RequestParam接收,前端传参时注意 pageNum 从 1 开始,后端算 offset 要减 1。status和deviceName是可选筛选条件,用required = false,service 层用动态 SQL 拼接。返回统一用Result包装,包含 code、msg、data,前端拦截器统一处理,这是前后端分离的常见约定。
3.3 MyBatis 动态 SQL 写筛选条件
mapper XML 里用<if>标签拼动态条件,这是 MyBatis 最实用的特性之一。
<!-- RepairOrderMapper.xml --> <select id="pageQuery" resultType="com.repair.vo.RepairOrderVO"> SELECT o.id, o.fault_desc, o.reporter, o.report_time, o.status, d.device_name, d.location FROM repair_order o LEFT JOIN device d ON o.device_id = d.id <where> <if test="status != null"> AND o.status = #{status} </if> <if test="deviceName != null and deviceName != ''"> AND d.device_name LIKE CONCAT('%', #{deviceName}, '%') </if> </where> ORDER BY o.report_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理第一个AND,避免WHERE AND的语法错误,这是新手最容易翻车的地方。LIKE CONCAT('%', #{deviceName}, '%')用#{}预编译防注入,别用${}拼接字符串。LEFT JOIN保证即使设备被删了报修单也能查出来,实际项目里设备一般做逻辑删除,不会真删。
3.4 派单与状态流转接口
派单是业务核心动作,涉及更新报修单状态和插入派单记录,必须放在同一个事务里。
@Service public class DispatchServiceImpl implements DispatchService { @Autowired private RepairOrderMapper orderMapper; @Autowired private DispatchMapper dispatchMapper; @Override @Transactional(rollbackFor = Exception.class) // 任一失败整体回滚 public void dispatch(Integer orderId, Integer repairerId) { // 先校验当前状态必须是待派单,防止重复派单 RepairOrder order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 0) { throw new BizException("该工单状态不允许派单"); } // 更新工单状态为已派单 orderMapper.updateStatus(orderId, 1); // 插入派单记录 Dispatch dispatch = new Dispatch(); dispatch.setOrderId(orderId); dispatch.setRepairerId(repairerId); dispatch.setDispatchTime(new Date()); dispatchMapper.insert(dispatch); } }@Transactional的rollbackFor = Exception.class很关键,默认只回滚运行时异常,业务里抛的自定义异常如果是受检异常就不会回滚,这是血泪经验。状态校验放在更新前,用乐观锁思路防止并发重复派单。BizException是自定义业务异常,配合全局异常处理器返回友好提示。
4. Vue3 前端页面:从登录到维修工单列表
4.1 Vite 初始化与 Element Plus 接入
前端用 Vite 创建 Vue3 项目,命令很直接。
# 创建 Vue3 项目,选 Vue 模板 npm create vite@latest repair-frontend -- --template vue cd repair-frontend npm install # 安装路由、状态管理、UI 库、请求库 npm install vue-router@4 pinia element-plus axios装完后在main.js里注册 Element Plus 和路由。Vue3 的挂载方式和 Vue2 不同,用createApp。
// main.js import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(ElementPlus) // 全局注册组件,省去逐个引入 app.use(router) app.mount('#app')createApp是 Vue3 的入口,和 Vue2 的new Vue({ render: h => h(App) })相比更简洁。Element Plus 全局注册后,模板里直接用<el-table>、<el-form>等标签。热词里 vue3 使用 elementui 其实指的是 Element Plus,因为原版 Element UI 只支持 Vue2,Vue3 必须用 Plus 版本,这点别搞混。
4.2 用 Composition API 写工单列表页
工单列表页是后台最典型的页面:搜索栏 + 表格 + 分页 + 操作按钮。用<script setup>语法糖写,逻辑最紧凑。
<template> <div class="order-page"> <!-- 搜索栏 --> <el-form :inline="true" :model="query"> <el-form-item label="设备名称"> <el-input v-model="query.deviceName" placeholder="请输入" /> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.status" clearable> <el-option label="待派单" :value="0" /> <el-option label="已派单" :value="1" /> <el-option label="维修中" :value="2" /> </el-select> </el-form-item> <el-button type="primary" @click="loadData">查询</el-button> </el-form> <!-- 表格 --> <el-table :data="tableData" border> <el-table-column prop="deviceName" label="设备名称" /> <el-table-column prop="faultDesc" label="故障描述" /> <el-table-column prop="reporter" label="报修人" /> <el-table-column label="状态"> <template #default="{ row }"> <el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag> </template> </el-table-column> <el-table-column label="操作"> <template #default="{ row }"> <el-button v-if="row.status === 0" @click="handleDispatch(row)">派单</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination v-model:current-page="query.pageNum" :page-size="query.pageSize" :total="total" @current-change="loadData" /> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import axios from 'axios' const query = reactive({ pageNum: 1, pageSize: 10, status: null, deviceName: '' }) const tableData = ref([]) const total = ref(0) // 加载列表数据 const loadData = async () => { const res = await axios.get('/api/order/page', { params: query }) tableData.value = res.data.data.list total.value = res.data.data.total } onMounted(loadData) // 页面挂载后自动加载 </script>ref用于基本类型和数组,reactive用于对象,这是 Composition API 里最容易混淆的点。热词里 vue3 reactive 和 vue3 ref 万能对象 被反复搜,实际规则是:ref返回带.value的响应式引用,模板里自动解包;reactive返回 Proxy 对象,直接访问属性。表格数据用ref([]),查询条件用reactive({}),这是我在项目里固定的习惯。v-model:current-page是 Vue3 里v-model带参数的写法,替代 Vue2 的.sync。
4.3 axios 封装与跨域处理
前后端分离必然遇到跨域。开发阶段在vite.config.js里配代理最省事。
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true } } } })配了代理后,前端请求/api/order/page会被转发到后端 8080 端口,浏览器不认为是跨域。生产环境则在后端配 CORS 或 Nginx 反向代理。changeOrigin: true会把请求头里的 host 改成目标地址,某些后端校验 host 时必须开。这里提醒一句,代理只在开发服务器生效,打包后的静态文件部署到 Nginx 时要另配。
5. 联调避坑:那些让毕设卡三天的常见问题
5.1 后端接口返回 404 或 500
现象:前端请求报 404,或者 500 但控制台没详细信息。原因通常是 SpringMVC 没扫描到 controller,或者@RequestMapping路径拼错。解决:检查spring-mvc.xml或 Java Config 里的component-scan包路径是否覆盖 controller,检查类上和方法上的路径拼接结果。500 的话看后端日志堆栈,多半是 mapper 没找到或 SQL 语法错。
5.2 前端表格数据不显示
现象:接口返回了数据,但表格空白。原因常见两种:一是tableData.value赋值时忘了.value,直接写tableData = res.data,响应式丢失;二是后端返回字段名和prop不一致,比如后端是device_name下划线,前端写deviceName。解决:统一用驼峰,在 MyBatis 里开mapUnderscoreToCamelCase=true自动转换,或者 SQL 里用别名。
5.3 时间字段显示成一串数字或乱码
现象:report_time显示成1699999999000或带 T 的 ISO 格式。原因:Jackson 默认把 Date 序列化成时间戳。解决:在实体字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),或者在配置文件里全局设置。时区不写GMT+8会差 8 小时,这个坑几乎人人踩过。
5.4 派单后状态没变或重复派单
现象:点了派单按钮,工单状态还是待派单,或者同一条被派了两次。原因:事务没生效,或者状态校验缺失。解决:确认@Transactional所在方法是被 Spring 代理调用的(同类内部调用不生效),确认派单前查了状态。并发场景下可以在 update 语句里加AND status = 0做条件更新,根据影响行数判断是否成功。
5.5 Vue3 打包后页面空白
现象:npm run build后部署,页面白屏,控制台报资源 404。原因:Vite 默认打包路径是绝对路径/,部署到子目录就找不到。解决:在vite.config.js里设base: './'改成相对路径。另外路由模式如果用 history,Nginx 要配try_files回退到 index.html,否则刷新 404。
6. 让毕设加分:状态机可视化与备件预警的进阶做法
基础功能跑通后,答辩想拿高分得有点亮点。我一般会加两个东西:工单状态流转可视化和备件库存预警。状态机可视化不用引重型库,用 Element Plus 的<el-steps>就能画出「报修 → 派单 → 维修 → 验收 → 完成」的进度条,把当前工单的 status 映射到 active 索引即可。
<el-steps :active="currentStep" finish-status="success"> <el-step title="报修" /> <el-step title="派单" /> <el-step title="维修" /> <el-step title="验收" /> <el-step title="完成" /> </el-steps>currentStep由工单状态计算:status 为 0 时 active 是 1,为 4 时是 5。这样老师一眼就能看懂业务流转,比干巴巴的表格有说服力。
备件预警则是在维修完成时扣减库存,并检查是否低于warn_stock。扣减库存要用UPDATE spare_part SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},把库存判断放进 SQL 的 where 条件里,根据影响行数判断是否扣减成功,避免并发超卖。低于预警值的备件在首页用红色标签列出,这就是一个完整的「发现—处理—预警」闭环。
验证方法上,我习惯用 Postman 先把所有接口跑一遍,确认状态码和数据正确,再联调前端。数据库层面用几条 SQL 造测试数据,覆盖各种状态。最后压测不用做,毕设规模没必要,但可以手动模拟并发派单,验证条件更新是否挡住了重复操作。
说个我自己的教训:当年做类似系统时,图省事把状态判断全写在前端,后端接口裸奔,结果答辩演示时老师直接调接口把已完成的工单又派了一次,当场翻车。从那以后我坚持一条习惯——所有状态校验和权限判断必须落在后端,前端只做展示。这个习惯后来帮我省了无数麻烦。希望帮到你。
本文还有配套的精品资源,点击获取