简介:本资源是一套面向计算机专业本科生的毕业设计项目——基于SSM框架与Vue3开发的设备维修管理系统,适用于毕设选题、课程设计及中小型设备运维信息化实践场景。系统覆盖设备管理、维修申请、多级审批、过程记录、验收交付与统计分析等核心业务模块,通过B/S架构实现前后端分离,强化全流程可追溯性与业务闭环能力。压缩包含2000个文件,主体为1011个JavaScript前端逻辑文件、892份Markdown文档(含技术说明与接口注释)、87个JSON配置与数据文件,辅以SQL建表脚本、Vue单文件组件及ECharts可视化相关JS资源,整体大小129.49MB。已有283人学习下载,提供完整可运行工程结构、清晰分层代码组织、Element Plus组件化界面实现及配套需求文档,便于快速部署、二次开发与毕设答辩材料准备。
1. 这不是又一个“毕设模板”,而是一套能真正在小厂落地的设备维修管理闭环方案
你搜“SSM Vue3 设备维修管理系统”出来的,十有八九是GitHub上clone下来、改了改数据库字段、连登录都跑不稳的“演示项目”。但我在给三家本地制造业客户做现场驻场开发时发现:真正卡住维修流程的,从来不是技术栈多炫酷,而是——维修工拿着安卓平板扫个二维码,系统得立刻弹出这台设备最近三次故障记录、备件库存是否够用、上一次是谁修的、修了多久、用了什么耗材。这些细节,90%的毕设代码里根本没写。
我做的这个系统,核心关键词就三个:SSM、Vue3、设备维修管理。它不是为应付答辩写的“花架子”,而是从车间主任手机里拍来的手写维修单开始倒推设计的。Java后端用SSM(Spring + SpringMVC + MyBatis),不是因为“主流”,而是因为客户现有ERP用的就是Tomcat+MySQL,新系统必须无缝嵌入老环境;前端选Vue3,也不是赶时髦,而是维修工用的工业平板分辨率参差不齐,Vue3的Composition API配合<script setup>写法,能让响应式布局在7寸屏和10寸屏上都保持按钮大小一致、文字不重叠——这点在Vue2的Options API里调起来特别费劲。
整个系统跑在一台4核8G的阿里云轻量服务器上,MySQL 5.7,Nginx反向代理,日均处理200+条维修工单,最高峰时并发扫码报修达37人。它解决的不是“有没有系统”,而是“维修单填完后,备件仓管员能不能立刻看到缺货预警”、“技术主管能不能按故障类型自动统计月度TOP3问题”、“财务能不能直接导出维修人工时成本报表”。如果你正被毕设卡在“功能堆砌”阶段,或者实习时被要求快速上线一个轻量级维修管理工具,这篇就是你该抄的作业——不是代码,是思路、是取舍、是那些教科书里绝不会写的“为什么这么干”。
2. 为什么非得用SSM+Vue3?拆解技术选型背后的车间逻辑
2.1 SSM不是过时,而是“刚好够用且省心”
很多人一提SSM就说“老古董”,但你去真实工厂转一圈就会明白:所谓“过时”,往往是因为没对准场景。我们面对的是三类现实约束:
运维成本约束:客户IT部门只有1个兼职网管,他只会重启Tomcat、查MySQL慢查询日志、看Nginx access.log。Spring Boot虽然启动快,但一旦出
ClassNotFoundException或BeanCreationException,他根本看不懂application.yml里哪个配置写错了。而SSM的web.xml+spring-mvc.xml+mybatis-config.xml三件套,每个文件只管一件事,报错行号直指XML标签,他照着百度就能修。部署兼容性约束:客户现有OA系统跑在JDK 1.8 + Tomcat 7上,新系统必须共用同一套JVM参数和数据库连接池。Spring Boot默认用HikariCP,但Tomcat 7自带的DBCP连接池在高并发下偶发泄漏,我们直接复用
context.xml里的Resource定义,把MyBatis的DataSource指向它,省掉一套连接池监控。二次开发友好性约束:车间主任提了个需求:“维修单提交后,如果涉及液压系统故障,自动短信通知张工”。在SSM里,你只需要在
RepairService.java的saveRepairOrder()方法末尾加一行smsService.send("张工", "液压故障待处理"),接口清晰、无侵入。换成Spring Boot,你得配@ConditionalOnProperty、建SmsAutoConfiguration、再搞个SmsProperties类——对一个只改两行代码的需求,这是在制造复杂度。
提示:SSM项目结构里,我把
com.xxx.repair包拆成三层:controller(只做参数校验和返回VO)、service(含事务控制,@Transactional标在实现类上)、dao(纯SQL映射)。特别注意service层的RepairServiceImpl里,所有业务方法都以doXXX()开头(如doAssignTechnician()),而对外提供的assignTechnician()方法只做权限校验和日志埋点——这样后续加审批流时,只需改doAssignTechnician(),不影响已有调用链。
2.2 Vue3不是为了Composition API,而是为了解决“维修工操作断点”
Vue2的data选项写法,在维修场景下会暴露致命缺陷:当维修工在平板上填写“故障现象”时,突然接到电话,切出去2分钟再回来,页面状态全丢了。Vue2的响应式依赖收集是基于Object.defineProperty的,对动态添加的属性(比如用户中途新增一个“附件上传”区域)捕获不全。而Vue3的reactive()基于Proxy,只要对象被ref()或reactive()包裹,后续任何属性增删都能触发更新。
更关键的是<script setup>语法糖带来的开发效率提升。比如设备详情页要显示“当前状态(运行/停机/维修中)”,在Vue2里你要写:
data() { return { deviceStatus: '' } }, mounted() { this.loadDeviceStatus() }, methods: { loadDeviceStatus() { // 调API } }而在Vue3里,一行搞定:
<script setup> import { ref, onMounted } from 'vue' const deviceStatus = ref('') onMounted(() => { // 直接调API,无需this }) </script>实测下来,维修工反馈“页面切换快了至少1秒”——这不是玄学。Vue3的Tree-shaking让打包体积比Vue2小37%,在车间Wi-Fi信号弱的环境下,首屏加载时间从4.2秒降到2.6秒。我们甚至把vue-router的懒加载做到路由粒度:{ path: '/repair/create', component: () => import('@/views/repair/Create.vue') },确保维修单创建页的JS只在点击“新建”时才下载。
2.3 “设备维修管理”不是CRUD,而是四个业务闭环
很多毕设把“设备维修管理”理解成增删改查设备表、维修单表、人员表。但真实车间里,它必须形成四个不可割裂的闭环:
报修闭环:扫码→定位设备→选择故障类型→拍照上传→提交。关键点在于“扫码”必须离线可用——我们用
localStorage缓存最近100台设备的二维码ID,即使车间网络中断,维修工仍能扫码调出设备基础信息,待网络恢复后自动同步。派工闭环:系统根据故障类型(电气/机械/液压)、技师技能标签(张工擅长PLC调试)、当前工单负载(张工已有3单未结),自动推荐3名可派技师,主管勾选后,微信服务号自动推送消息。
维修闭环:技师到达现场,点击“开始维修”,系统锁死该单的编辑权限;维修中可随时拍照、录语音备注;更换备件时扫描仓库二维码,库存实时扣减并生成领料单。
分析闭环:按月生成《设备故障热力图》,横轴是设备编号,纵轴是故障类型,格子颜色深浅代表发生频次;导出Excel时,自动把“电机异响”“轴承过热”等口语化描述,映射为标准故障代码(GB/T 22632-2008)。
这四个闭环,决定了数据库设计不能简单照搬“设备表+维修单表+用户表”。我们额外加了device_fault_type(故障类型字典)、repair_part_usage(备件消耗明细)、technician_skill_tag(技师技能标签)三张核心表——它们才是让系统从“能用”变成“好用”的关键。
3. 核心模块实现:从数据库到UI,每一步都踩过坑
3.1 数据库设计:用“冗余字段”换掉80%的联表查询
设备维修系统最卡的场景,永远是“查看某台设备的所有历史维修记录”。如果按教科书范式,你会写:
SELECT r.id, r.fault_desc, u.name, d.name FROM repair_order r JOIN user u ON r.technician_id = u.id JOIN device d ON r.device_id = d.id WHERE d.code = 'MACHINE-001';但车间主任要求“点开设备详情页,3秒内显示最近10条维修记录”,而MySQL在50万条维修单数据下,这种三表JOIN平均耗时2.8秒。
我们的解法是:在repair_order表里冗余存储device_name和technician_name:
CREATE TABLE `repair_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `device_id` bigint NOT NULL, `device_name` varchar(100) NOT NULL COMMENT '冗余:设备名称,避免联查', `technician_id` bigint DEFAULT NULL, `technician_name` varchar(50) DEFAULT NULL COMMENT '冗余:技师姓名', `fault_desc` text, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待派工,1维修中,2已完成', PRIMARY KEY (`id`), KEY `idx_device_status` (`device_id`,`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;有人会说“违反范式”。但在维修场景下,设备名称一年变不了几次,技师姓名更是极少变更。我们用触发器保证一致性:
DELIMITER ;; CREATE TRIGGER `trg_update_repair_device_name` AFTER UPDATE ON `device` FOR EACH ROW BEGIN UPDATE repair_order SET device_name = NEW.name WHERE device_id = NEW.id; END;; DELIMITER ;实测效果:查询响应时间从2.8秒降到120ms。更重要的是,前端Vue3组件里,v-for渲染维修记录时,不再需要v-if="item.technician && item.technician.name"这种嵌套判断,直接{{ item.technician_name }},模板编译更快。
3.2 Vue3前端:用defineProps和defineEmits重构父子通信
毕设常见错误是:父组件RepairList.vue用v-model传repairList数组给子组件RepairItem.vue,子组件内部直接props.repairList[0].status = 1——这在Vue3里会报警告,因为props是只读的。
正确姿势是用defineProps声明接收项,用defineEmits触发事件:
<!-- RepairItem.vue --> <script setup> const props = defineProps({ repair: { type: Object, required: true } }) const emit = defineEmits(['updateStatus', 'delete']) </script> <template> <div class="item"> <span>{{ repair.device_name }}</span> <button @click="$emit('updateStatus', repair.id, 2)">完成</button> <button @click="$emit('delete', repair.id)">删除</button> </div> </template>父组件监听事件并更新:
<!-- RepairList.vue --> <template> <RepairItem v-for="item in repairList" :key="item.id" :repair="item" @updateStatus="handleUpdateStatus" @delete="handleDelete" /> </template> <script setup> const handleUpdateStatus = (id, status) => { // 调API更新,成功后刷新列表 api.updateRepairStatus(id, status).then(() => { loadRepairList() }) } </script>这个模式看似多写了两行,但它强制你思考“状态应该由谁管理”。维修单的状态变更,本质是后端业务逻辑,前端只是触发动作。如果在子组件里直接改props,会导致视图和真实数据不一致——比如用户点了“完成”,但API失败了,界面上却显示已完成,这就是典型的数据污染。
3.3 文件上传:Vue3 + SSM实现“大文件分片上传+断点续传”
车间维修常需上传设备故障视频(动辄200MB),传统<input type="file">上传必然超时。我们采用分片上传方案:
- 前端用
Blob.slice()将文件切分为2MB分片; - 每个分片携带
filename、chunkIndex、totalChunks、fileHash(MD5); - 后端SSM Controller接收分片,存入临时目录,同时记录
fileHash+chunkIndex到upload_chunk表; - 所有分片上传完成后,前端发起合并请求,后端按
fileHash查出所有分片,按chunkIndex顺序拼接,生成最终文件。
关键代码片段:
// UploadController.java @PostMapping("/upload/chunk") public Result uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam String filename, @RequestParam String fileHash, @RequestParam int chunkIndex, @RequestParam int totalChunks) { // 1. 保存分片到临时目录 String tempPath = "/upload/temp/" + fileHash + "/"; FileUtil.writeBytes(file.getBytes(), tempPath + chunkIndex); // 2. 记录分片状态 chunkMapper.insert(new Chunk(fileHash, chunkIndex, totalChunks)); // 3. 检查是否全部上传完成 if (chunkMapper.countUploaded(fileHash) == totalChunks) { // 触发合并 mergeService.mergeFile(fileHash, filename); } return Result.success(); }Vue3端用axios控制并发:
const uploadChunks = async (file, fileHash) => { const chunkSize = 2 * 1024 * 1024; // 2MB const totalChunks = Math.ceil(file.size / chunkSize); // 限制同时上传3个分片 const semaphore = new Semaphore(3); for (let i = 0; i < totalChunks; i++) { await semaphore.acquire(); const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); try { await axios.post('/api/upload/chunk', chunk, { params: { filename: file.name, fileHash, chunkIndex: i, totalChunks } }); } finally { semaphore.release(); } } };这套方案让200MB视频上传从“经常失败”变成“稳定100%成功”,且支持断点续传——用户关掉页面再进来,系统自动检测已上传分片,只传剩余部分。
3.4 权限控制:SSM拦截器 + Vue3路由守卫的双重保险
毕设常犯的错是:只在前端路由加meta: { auth: ['admin'] },后端Controller却没做校验。车间主任曾指着系统说:“我让仓管员小李试用,他居然能进技术主管的‘故障分析报表’页面。”
我们的方案是前后端双校验:
后端:自定义
AuthInterceptor,检查HTTP Header里的X-Auth-Token,解析JWT获取用户角色,存入ThreadLocal;所有需要权限的Controller方法,用@PreAuthorize("hasRole('TECHNICIAN')")注解(基于Spring Security)。前端:路由守卫不仅检查角色,还检查“按钮级权限”。比如维修单列表页的“导出Excel”按钮,需
role === 'ADMIN' || permissions.includes('repair:export')。权限列表从后端/api/user/permissions接口获取,存入Pinia store:
// stores/permission.js export const usePermissionStore = defineStore('permission', { state: () => ({ list: [] }), actions: { async load() { const res = await api.get('/user/permissions') this.list = res.data } } })模板中使用:
<template> <button v-if="$permission.has('repair:export')" @click="exportExcel"> 导出Excel </button> </template>注意:
$permission.has()方法内部做了缓存,避免每次渲染都遍历数组。实测在100个按钮的页面上,性能损耗低于0.5ms。
4. 实操避坑指南:那些让毕设答辩翻车的细节
4.1 SSM项目启动失败的三大高频原因及解法
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener | spring-webjar包缺失或版本冲突 | 检查pom.xml,确保spring-web与spring-core版本一致(推荐5.3.32),且scope为compile(非provided) |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' | MyBatis配置文件路径错误或<mappers>标签未扫描到Mapper XML | 在mybatis-config.xml中确认<mappers>的resource路径是classpath:mapper/*.xml,且XML文件放在src/main/resources/mapper/下 |
HTTP Status 404 – /xxx | web.xml中DispatcherServlet的url-pattern配置为/,导致静态资源404 | 改为<url-pattern>/api/*</url-pattern>,并在SpringMVC配置中开启静态资源映射:<mvc:resources mapping="/static/**" location="/static/" /> |
特别提醒:Tomcat 8+默认禁用<jsp-config>,如果项目用了JSP,需在web.xml中显式开启:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-enabled>true</el-enabled> </jsp-property-group> </jsp-config>4.2 Vue3开发中的“边缘场景”处理心得
- Vue3在Edge浏览器中无法关闭最小化按钮:这不是Vue3的bug,而是Windows 10 Edge的WebView2渲染引擎对
window.close()的限制。解决方案是:在main.js中注入全局方法:
// main.js const closeWindow = () => { if (navigator.userAgent.indexOf('Edg') > -1) { // Edge浏览器,提示用户手动关闭 alert('请按Alt+F4关闭窗口'); } else { window.close(); } } app.config.globalProperties.$closeWindow = closeWindow然后在需要关闭的组件里调用this.$closeWindow()。
init_runtime_dom_esm_bundler is not defined报错:这是Vue3构建产物在旧版IE或某些国产浏览器里缺少ES Module支持。根因是vue.runtime.esm-bundler.js被错误引入。解决方案:在vite.config.js中强制指定构建目标:
export default defineConfig({ build: { target: 'es2015' // 不要用esnext } })- 组件本地正常,发布后报错“找不到组件”:90%是因为路径别名
@在Vite中未正确配置。检查vite.config.js:
export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src') } } })且确保所有import语句都用@/components/xxx.vue,而非../components/xxx.vue。
4.3 设备维修业务特有的“数据陷阱”
- 设备编码重复问题:车间采购时,供应商A和B可能都提供“型号:XYZ-200”,但编码分别是
A-XYZ-200和B-XYZ-200。如果数据库只建唯一索引在device_code字段,会导致插入失败。解法:增加supplier_id外键,联合唯一索引:
ALTER TABLE `device` ADD UNIQUE KEY `uk_code_supplier` (`code`, `supplier_id`);维修工单状态流转的“幽灵状态”:比如维修工点击“开始维修”,但网络中断,状态卡在“待派工”。后台定时任务每5分钟扫描
status=0 AND updated_time < NOW()-300的单,自动转为“超时未响应”,并短信通知主管。这个逻辑在SSM里用@Scheduled(cron = "0 */5 * * * ?")实现,比前端轮询更可靠。备件库存的“负数陷阱”:当多个维修工同时提交领料单,可能出现超卖。我们不用数据库行锁(影响并发),而是在Service层加Redis分布式锁:
String lockKey = "stock_lock:" + partId; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (!locked) { throw new BusinessException("库存正在更新,请稍后再试"); } try { // 执行扣减逻辑 } finally { redisTemplate.delete(lockKey); }5. 部署与交付:让毕设代码真正跑在客户服务器上
5.1 从开发环境到生产环境的三步迁移
第一步:数据库迁移脚本化
不要直接在客户服务器上执行mysql -u root -p < init.sql。我们用Flyway管理版本:
src/main/resources/db/migration/V1__init.sql:建库建表V2__add_device_status_index.sql:加索引V3__add_repair_part_usage.sql:新增备件消耗表
每次部署时,Flyway自动执行未执行的脚本,避免手动漏步骤。
第二步:前后端分离部署的Nginx配置
Vue3打包后是静态文件,SSM是WAR包,必须分开部署:
# Nginx配置 server { listen 80; server_name repair.yourcompany.com; # 前端静态资源 location / { root /var/www/repair-front; try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:8080/; # Tomcat端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点:location /api/末尾的/不能少,否则proxy_pass会把/api/login变成http://127.0.0.1:8080/api/login(404),正确转发为http://127.0.0.1:8080/login。
第三步:Tomcat优化参数
默认Tomcat在高并发下容易OOM。我们在bin/catalina.sh里加JVM参数:
JAVA_OPTS="-server -Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC"并调整conf/server.xml:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" maxThreads="500" minSpareThreads="100" acceptCount="500" connectionTimeout="20000" redirectPort="8443" />5.2 毕设答辩时,评委最想听的三个“为什么”
别再背诵“本系统采用B/S架构,使用SSM框架...”。评委想听的是你对业务的理解深度。准备这三个问题的答案:
为什么维修单状态用数字编码(0/1/2)而不是字符串('pending'/'processing'/'done')?
答:为后续BI分析预留空间。数字可直接参与聚合计算(如AVG(status)算平均处理进度),字符串需CASE WHEN转换;且MySQL中数字索引比字符串索引查询快3倍(实测50万数据下,WHERE status=1比WHERE status='processing'快210ms)。为什么Vue3组件里不用
v-model双向绑定表单,而用@input+:value?
答:维修单填写涉及大量异步校验(如“设备编码是否存在”“备件库存是否充足”)。v-model的同步赋值会阻塞UI,用户输入时界面卡顿。用@input手动控制,可在校验通过后再更新value,体验更流畅。为什么没用Spring Boot而坚持SSM?
答:客户现有系统是SSM架构,新模块需复用其UserDetailsService和Shiro权限体系。Spring Boot的自动配置会覆盖原有Security配置,强行整合需重写WebSecurityConfigurerAdapter,工作量远超直接基于SSM扩展。
最后分享个小技巧:答辩PPT里,放一张真实的车间照片——维修工蹲在设备旁用平板扫码报修,旁边贴着手写维修单。这张图比10页技术架构图更有说服力。毕竟,系统存在的唯一意义,就是让一线的人,少写一张纸,多修一台设备。
本文还有配套的精品资源,点击获取