☰
SSM+Vue3设备维修管理系统:从报修到验收全流程实战
2026/10/7 2:56:08 网站建设 项目流程

简介:本资源为基于SSM与Vue3实现的设备维修管理系统毕设项目,面向计算机相关专业毕业生及需要完整前后端分离案例的开发者。系统围绕设备维修全过程展开,涵盖设备管理、维修申请、维修审批、维修过程记录、维修验收与统计分析等模块,并为各节点加入审批功能,实现从申请到交付的全流程回溯管理。后端采用Java语言与SSM框架,前端使用Vue3配合ElementPlus,整体为B/S架构,适合作为毕设参考或课程设计模板。压缩包共约2000个文件,包含1011个js脚本、892个md说明文档、87个json配置、5个txt文本、2个html页面、1个vue组件、1个docx文档及1个sql建表脚本,整体约129.49MB,目录结构清晰,便于按模块查阅。目前已有284人学习下载,可帮助读者快速理解系统分层设计、接口组织与前端组件拆分思路,并对照文档完成环境搭建与功能复现。

1. 设备维修管理系统:从报修到验收,SSM+Vue3 怎么把流程跑通

工厂里一台注塑机半夜趴窝,值班班长在微信群里吼了三遍没人应,等第二天早班维修工到岗,翻聊天记录才发现故障描述只有一句“机器不动了”。这种场景我见过太多次,设备维修管理系统要解决的就是这件事:把报修、派工、维修、验收、归档串成一条可追溯的线,谁在什么时候接的单、换了什么件、停机多久,全部落在数据库里。标题里的 SSM 是后端骨架,Spring + SpringMVC + MyBatis 这套组合在毕设和中小型管理系统里依然是稳妥选择,Vue3 负责前端交互,Composition API 写起来比 Vue2 的 Options API 更顺手,尤其是表单联动和列表刷新这类高频操作。这套系统适合谁?做毕设的学生、刚接手设备管理模块的后端新手、想用一套完整项目练手 SSM 和 Vue3 联调的人。下面我按实际开发顺序,把选型理由、建表、接口、前端页面和踩过的坑一条条拆开讲。

2. 后端骨架怎么搭:SSM 分层与 Vue3 工程初始化

2.1 为什么还在用 SSM 而不是 SpringBoot

先说清楚一件事:SSM 不是过时技术,它只是把 SpringBoot 自动配置的那层“黑匣子”拆开了。毕设场景下,答辩老师往往会问你“IOC 容器怎么初始化的”“MyBatis 的 Mapper 代理是怎么生成的”,用 SSM 你至少能指着applicationContext.xml说清楚 Bean 的加载顺序。SpringBoot 当然更省事,但如果你连DispatcherServlet的职责都说不明白,用 SpringBoot 反而容易被问住。

SSM 的分层逻辑很直白:Controller 接请求、Service 写业务、Mapper 做持久化。设备维修管理系统的业务不复杂,但状态流转多——报修单有“待派工、维修中、待验收、已完成”四个状态,每个状态对应不同的操作权限和字段变更。用 SSM 的好处是事务边界清晰,Service 层加@Transactional就能保证“派工时同时更新维修工状态和工单状态”这种操作要么全成功要么全回滚。

Vue3 这边,我一般用 Vite 初始化项目,比 Vue CLI 快很多。npm create vite@latest repair-frontend -- --template vue这条命令跑完,项目结构就出来了。Vue3 的 Composition API 在写维修工单列表时优势明显:用ref定义响应式数据,用reactive管理表单对象,逻辑可以按功能抽成useRepairOrder这样的组合函数,而不是像 Options API 那样把data、methods、computed拆得到处都是。

2.2 数据库表设计与 MyBatis 映射

设备维修管理系统最少需要五张核心表:设备表、维修工单表、维修工表、备件表、工单备件关联表。我见过有人把维修工和用户混在一张表里,结果权限控制写得一团糟。分开建表,维修工表只存技能标签和在岗状态,用户表走独立的登录认证。

-- 设备表:记录设备基本信息和当前状态 CREATE TABLE equipment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL UNIQUE COMMENT '设备编号', name VARCHAR(64) NOT NULL COMMENT '设备名称', location VARCHAR(128) COMMENT '所在位置', status TINYINT DEFAULT 1 COMMENT '1正常 2故障 3维修中 4报废', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 维修工单表:核心流转表 CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '工单号', equipment_id BIGINT NOT NULL, reporter_id BIGINT NOT NULL COMMENT '报修人', repairer_id BIGINT COMMENT '维修工,派工后填入', fault_desc VARCHAR(512) COMMENT '故障描述', status TINYINT DEFAULT 0 COMMENT '0待派工 1维修中 2待验收 3已完成', report_time DATETIME DEFAULT CURRENT_TIMESTAMP, assign_time DATETIME, finish_time DATETIME, FOREIGN KEY (equipment_id) REFERENCES equipment(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时有两个细节容易翻车:一是order_no的生成策略,我一般用“RO+日期+四位序列”的格式,在 Service 层用 Redis 自增或者数据库序列表实现,不要用UUID因为工单号要给人看;二是状态字段用TINYINT而不是VARCHAR,查询和索引效率差很多。

MyBatis 的映射文件里,工单列表查询需要关联设备名和维修工姓名,用<resultMap>做嵌套映射比写JOIN再手动拼装更清晰。下面这个查询是工单列表页的核心 SQL:

<select id="selectOrderPage" resultMap="OrderVOMap"> SELECT o.*, e.name AS equipment_name, u.real_name AS repairer_name FROM repair_order o LEFT JOIN equipment e ON o.equipment_id = e.id LEFT JOIN sys_user u ON o.repairer_id = u.id <where> <if test="status != null">AND o.status = #{status}</if> <if test="equipmentId != null">AND o.equipment_id = #{equipmentId}</if> </where> ORDER BY o.report_time DESC LIMIT #{offset}, #{limit} </select>

<where>标签会自动处理第一个AND,这个细节新手经常忽略,导致 SQL 拼接出错。分页参数offset和limit在 Service 层根据页码计算,不要在前端传,否则容易被改参数拖库。

2.3 Vue3 项目结构与 Axios 封装

前端项目初始化后,我一般按功能模块划分目录:api放接口请求、views放页面、components放公共组件、composables放组合函数。Vue3 的setup语法糖写起来简洁,但要注意ref和reactive的使用边界:基本类型用ref,对象用reactive,别混着用导致响应式丢失。

Axios 封装是联调的第一步,请求拦截器里统一加 token,响应拦截器里统一处理错误码。下面这段代码我用了很多次,直接抄就行:

// api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截:从 localStorage 取 token 塞进 header service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截:统一处理业务错误码 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

这里有个坑:Vite 开发环境下需要配置代理解决跨域,在vite.config.js里加server.proxy,把/api转发到后端端口。生产环境用 Nginx 做反向代理,前端打包后的dist目录直接扔给 Nginx 托管。

3. 核心业务怎么落地:报修、派工、验收的接口与页面

3.1 报修单创建与工单号生成

报修单创建是整个流程的入口,前端表单要填设备、故障描述、紧急程度,后端要生成唯一工单号并写入数据库。工单号生成我试过三种方案:数据库自增主键拼日期、Redis 原子递增、雪花算法。毕设场景下 Redis 方案最稳,但如果你不想额外装 Redis,用数据库序列表也能凑合。

// Service 层:创建报修单 @Service public class RepairOrderServiceImpl implements RepairOrderService { @Autowired private RepairOrderMapper orderMapper; @Autowired private SequenceMapper sequenceMapper; @Override @Transactional(rollbackFor = Exception.class) public String createOrder(RepairOrderDTO dto) { // 生成工单号:RO + yyyyMMdd + 4位序列 String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); sequenceMapper.updateAndGet("repair_order", dateStr); Integer seq = sequenceMapper.getCurrentSeq("repair_order", dateStr); String orderNo = "RO" + dateStr + String.format("%04d", seq); RepairOrder order = new RepairOrder(); order.setOrderNo(orderNo); order.setEquipmentId(dto.getEquipmentId()); order.setReporterId(dto.getReporterId()); order.setFaultDesc(dto.getFaultDesc()); order.setStatus(0); // 待派工 orderMapper.insert(order); // 同时更新设备状态为故障 equipmentMapper.updateStatus(dto.getEquipmentId(), 2); return orderNo; } }

@Transactional的rollbackFor = Exception.class必须加,否则遇到受检异常不回滚,工单创建了但设备状态没改,数据就对不上了。序列号生成用UPDATE ... SET seq = seq + 1配合SELECT两步操作,在高并发下会有竞态问题,但毕设场景并发量低,加个synchronized或者数据库行锁就够用。

前端报修页面用 Element Plus 的el-form做校验,设备下拉框从/api/equipment/list拉数据,故障描述限制 500 字。提交成功后跳转到工单详情页,把生成的工单号展示出来。

3.2 派工逻辑与维修工状态联动

派工是管理员操作,把待派工的工单分配给具体维修工。这里有个业务规则:维修工同时最多接三单,超过三单不允许再派。这个规则在 Service 层校验,不要只靠前端限制。

@Override @Transactional(rollbackFor = Exception.class) public void assignOrder(Long orderId, Long repairerId) { // 校验维修工当前工单数 int count = orderMapper.countByRepairerAndStatus(repairerId, 1); if (count >= 3) { throw new BusinessException("该维修工当前工单已满,请选择其他人"); } RepairOrder order = orderMapper.selectById(orderId); if (order.getStatus() != 0) { throw new BusinessException("该工单已被派工或已完成"); } order.setRepairerId(repairerId); order.setStatus(1); // 维修中 order.setAssignTime(new Date()); orderMapper.updateById(order); // 更新维修工状态为忙碌 userMapper.updateWorkStatus(repairerId, 2); }

countByRepairerAndStatus这个查询走repairer_id和status的联合索引,不然工单多了之后派工页面会卡。前端派工弹窗里,维修工下拉框只显示状态为“空闲”或“忙碌但未满三单”的人,这个过滤逻辑放在后端接口里做,前端只负责展示。

3.3 验收流程与状态机收尾

维修工修完设备后,在系统里提交完工报告,工单状态变成“待验收”。报修人或者设备管理员验收通过后,工单状态变成“已完成”,设备状态恢复为“正常”。验收不通过则退回“维修中”,维修工需要重新处理。

这个状态流转用枚举管理,不要散落在各个 if-else 里:

public enum OrderStatus { PENDING_ASSIGN(0, "待派工"), REPAIRING(1, "维修中"), PENDING_ACCEPT(2, "待验收"), COMPLETED(3, "已完成"); private final int code; private final String desc; // 状态流转校验:只允许相邻状态跳转 public static boolean canTransfer(int from, int to) { if (from == 0 && to == 1) return true; if (from == 1 && to == 2) return true; if (from == 2 && to == 3) return true; if (from == 2 && to == 1) return true; // 验收不通过退回 return false; } }

验收接口里先调canTransfer校验,不合法直接抛异常。前端验收页面展示维修工提交的完工报告和更换备件列表,验收人点“通过”或“退回”按钮,调对应接口。退回时要求填写退回原因,这个原因会记录在工单日志表里,方便后续追溯。

4. 避坑与排查:SSM+Vue3 联调中最容易翻车的五个点

4.1 跨域问题:预检请求 403 的排查

现象:前端调/api/repair/order/list报 403,浏览器控制台显示Request Method: OPTIONS失败。原因:后端 SpringMVC 没有配置 CORS,或者配置了但拦截器把 OPTIONS 请求拦了。解决:在springmvc.xml里加<mvc:cors>配置,或者在 Controller 上加@CrossOrigin。如果用了拦截器做登录校验,记得在拦截器里放行 OPTIONS 请求,否则预检过不去。

<!-- springmvc.xml 中配置全局 CORS --> <mvc:cors> <mvc:mapping path="/**" allowed-origins="http://localhost:5173" allowed-methods="GET,POST,PUT,DELETE,OPTIONS" allowed-headers="*" allow-credentials="true"/> </mvc:cors>

4.2 MyBatis 驼峰映射失效:查出来字段全是 null

现象:数据库字段equipment_id查出来映射不到 Java 对象的equipmentId,返回结果里这个字段是 null。原因:MyBatis 默认不开启驼峰命名转换,需要在mybatis-config.xml里加mapUnderscoreToCamelCase=true。解决:在配置文件中开启这个选项,或者在resultMap里手动写<result column="equipment_id" property="equipmentId"/>。我一般直接开全局配置,省事。

4.3 Vue3 响应式丢失:改了数组页面不刷新

现象:在setup里用let orderList = []定义数组,调接口赋值后页面不更新。原因:let定义的普通变量不是响应式的,Vue3 追踪不到变化。解决:用const orderList = ref([]),赋值时写orderList.value = res.data。如果数组元素是对象且需要深层响应,用reactive包裹或者ref配合.value操作。Vue3 的响应式基于 Proxy,对ref数组的push、splice都能追踪,但直接替换整个数组时必须走.value。

4.4 事务不生效:Service 内部方法调用

现象:在 Service 的 A 方法里调 B 方法,B 方法加了@Transactional但回滚没生效。原因:Spring AOP 代理的是外部调用,类内部方法调用不走代理,事务注解失效。解决:把 B 方法抽到另一个 Service 里,或者用AopContext.currentProxy()获取代理对象再调。更简单的做法是调整代码结构,别在类内部调事务方法。

4.5 前端打包后接口 404:Nginx 配置漏了

现象:开发环境正常,npm run build后部署到 Nginx,调接口全部 404。原因:Vite 开发环境的proxy配置只在 dev server 生效,生产环境需要 Nginx 做反向代理。解决:在 Nginx 配置里加location /api/ { proxy_pass http://后端IP:端口/; },注意proxy_pass末尾的斜杠,带斜杠会替换掉/api前缀,不带则保留。这个细节坑过很多人,血泪经验。

5. 进阶技巧:用组合函数抽离工单逻辑与验收自查

5.1 把工单状态流转抽成 Vue3 组合函数

工单列表、详情、派工弹窗都要用到状态判断和操作按钮的显隐逻辑,如果每个页面都写一遍if (status === 0) ...,改起来会疯。我一般抽一个useOrderStatus组合函数,把状态映射、按钮权限、颜色标签都收进去。

// composables/useOrderStatus.js import { computed } from 'vue' export function useOrderStatus(orderRef) { // 状态码到文本和颜色的映射 const statusMap = { 0: { text: '待派工', color: '#E6A23C', actions: ['assign'] }, 1: { text: '维修中', color: '#409EFF', actions: ['finish'] }, 2: { text: '待验收', color: '#67C23A', actions: ['accept', 'reject'] }, 3: { text: '已完成', color: '#909399', actions: [] } } const currentStatus = computed(() => statusMap[orderRef.value?.status] || {}) // 判断当前用户是否有某个操作权限 const canAction = (action) => { return currentStatus.value.actions?.includes(action) } return { currentStatus, canAction } }

在页面里这样用:const { currentStatus, canAction } = useOrderStatus(order),模板里直接v-if="canAction('assign')"控制按钮显隐。状态映射改一处,所有页面生效。Vue3 的 Composition API 在抽离这类逻辑时比 Vue2 的 mixin 清晰得多,mixin 的命名冲突和数据来源不明确问题在这里不存在。

5.2 验收自查清单:上线前跑一遍这七项

毕设答辩前或者项目交付前,我习惯按这个清单过一遍,能挡掉大部分演示翻车:

检查项验证方式常见问题
工单号唯一性连续创建 10 条工单序列号重复或格式错乱
派工上限校验给同一维修工派第 4 单前端没拦、后端也没拦
状态流转合法性直接改数据库状态后调接口跳过中间状态没报错
分页查询性能造 1000 条工单后翻页没走索引,查询超时
事务回滚派工时故意传错维修工 ID工单状态改了但维修工没更新
前端响应式删除列表某行后页面刷新数组没重新赋值,视图不更新
生产环境接口打包后部署 Nginx 访问代理配置漏了或路径不对

这张表我每次交付前都会跑一遍,尤其是事务回滚和分页性能这两项,答辩时老师最喜欢挑这两个地方问。分页查询记得在repair_order表的status和report_time上建联合索引,ORDER BY report_time DESC配合LIMIT才不会全表扫描。

5.3 一个我踩过的坑:别在验收环节省日志

早期做这套系统时,验收退回只改了工单状态,没记录退回原因和操作人。结果维修工和报修人扯皮,谁也说不清当时为什么退回。后来加了一张repair_order_log表,每次状态变更都插一条记录,字段包括工单 ID、操作人、操作类型、备注、时间。这张表平时没人看,出问题的时候就是后悔药。写日志的代码放在 Service 层状态变更之后,和业务操作在同一个事务里,保证日志和状态一致。

// 状态变更后记录日志 private void logOrderChange(Long orderId, Long operatorId, String action, String remark) { RepairOrderLog log = new RepairOrderLog(); log.setOrderId(orderId); log.setOperatorId(operatorId); log.setAction(action); log.setRemark(remark); log.setCreateTime(new Date()); logMapper.insert(log); }

这个习惯我保持到现在,任何有状态流转的系统,日志表都是最后兜底的那道防线。设备维修管理系统看起来简单,但状态一多、人一多,没有日志就是黑匣子。希望帮到你。

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

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

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

立即咨询