news 2026/9/28 5:39:43

工厂设备报修管理系统:Node.js+Vue全栈开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
工厂设备报修管理系统:Node.js+Vue全栈开发实战

1. 项目背景与核心需求

做工厂设备维护报修管理系统,是我近几年接过比较典型的企业内部工具类项目。这类系统单看技术含量不算顶尖,但真要落地好用,涉及的业务细节一点都不少。这个项目用 Node.js + Vue 实现了生产设备的台账管理、故障报修、维保计划、工单流转和统计报表,覆盖了工厂设备科日常最头疼的那几件事:设备坏了找谁修、修到什么程度、备件够不够、哪些设备该保养了。

先说为什么需要这么一套系统。很多工厂的设备管理还停留在微信群里报修、Excel 记台账、纸质工单签字的阶段。设备一多,问题就来了:报修消息被聊天记录淹没,维修工不知道优先级;设备的历史故障记录散落各处,下次出问题还得翻聊天记录;维保计划全凭老师傅脑子记,漏保养是常态。这些问题叠加起来,轻则影响生产效率,重则造成设备损坏甚至安全事故。所以工厂需要的不只是一个"报修小程序",而是一套能管设备全生命周期的系统,从建档、保养、报修、派单、维修到统计复盘,形成闭环。

这套系统适合谁来参考?如果你是刚入行的全栈开发者,想找一个既有业务深度又有技术覆盖面的练手项目,这个选题非常合适。它不像电商系统那样业务庞杂,但包含用户权限、工作流状态机、文件上传、数据可视化、前后端交互等常见模块,麻雀虽小五脏俱全。如果你正好在工厂或制造企业做信息化,这套系统的设计思路也可以直接迁移。

开发这套系统的过程中,我最大的体会有两点:第一,设备管理系统的核心不在技术炫技,而在工单流转是否顺畅、数据是否准确;第二,前后端分离的 Node.js + Vue 组合,在这类内部管理系统里确实省心,开发效率高,后期维护也容易找人接手。

2. 技术选型与整体架构

2.1 为什么选 Node.js + Vue 这个技术组合

选技术栈之前,我先考虑了工厂信息化的实际环境:这类系统往往部署在企业内网服务器上,访问量不大但并发提交可能集中(比如上班后半小时内密集报修),对并发处理的要求不算极端,但对开发迭代速度要求高。Node.js 的异步非阻塞模型处理这类 I/O 密集型请求很合适,同时 JavaScript 全栈可以复用很多代码逻辑,比如枚举定义、工具函数。

Vue 这边我选了 Vue 3 组合式 API 写法,配合 Vite 构建工具。相比 Vue 2 的 Options API,组合式 API 在管理复杂状态逻辑时更清晰。工厂设备管理页面有大量表格、表单、弹窗交互,组件之间需要共享设备状态、用户信息,用 Pinia 做状态管理比 Vuex 更简洁。

组件库我用的 Element Plus,这是 Vue 3 生态最成熟的中后台组件库,表格、表单、日期选择器、步骤条这些组件开箱即用。设备维修记录里经常要展示时间线,Element Plus 的 Timeline 组件能省不少事。

2.2 整体架构设计

系统采用经典的前后端分离架构,大致分层如下:

  • 前端:Vue 3 + Vite + Pinia + Vue Router + Element Plus
  • 后端:Node.js + Express(也可以换 Koa,但 Express 中间件生态更全)
  • 数据库:MySQL 8.0(工厂内部系统通常已有 MySQL 环境,复用成本低)
  • 认证方案:JWT(无状态,便于水平扩展)
  • 接口风格:RESTful API

前后端通过 HTTP 接口通信,前端开发时用 Vite 的 proxy 代理解决本地跨域,生产环境用 Nginx 反向代理。这种模式下,前端只负责渲染和交互,后端统一处理业务逻辑和数据校验,职责清晰。

2.3 数据库设计:设备管理系统的关键表

数据库设计是这类系统的地基,我踩过不少坑,最重要的心得是:宁可一开始多花时间设计表结构,也不要等上线后频繁改表。下面是核心表的设计思路。

用户表(sys_user)

存储系统用户,包括管理员、设备科人员、维修工、车间操作工等角色。字段包括 id、用户名、密码哈希、姓名、手机号、角色类型、部门、创建时间。密码不要存明文,用 bcrypt 加密。

设备表(device)

这是系统的核心主数据。字段包括 id、设备编号(唯一)、设备名称、型号规格、所属车间、安装位置、设备状态(运行/停机/维修中/报废)、购置日期、保修截止日期、维保周期类型、维保周期天数、备注。

这里有个容易忽略的细节:设备状态和报修工单状态要联动。设备在维修中时,应该禁止重复提交报修或至少给出提示,避免同一台设备被多人重复报修。

报修工单表(repair_order)

这是流转最复杂的表。字段包括 id、工单编号、设备 id、报修人 id、故障描述、故障图片、紧急程度(普通/紧急/非常紧急)、工单状态、指派人 id、处理结果、完成时间、评价等级、评价内容。

工单状态我用的是:待派单、处理中、已完成、已驳回、已取消。状态变更要记录到一张工单操作日志表里,方便追溯。

维保计划表(maintenance_plan)

字段包括 id、设备 id、计划维保日期、实际维保日期、维保类型(日常保养/一级保养/二级保养)、执行人 id、维保内容、使用备件、工时、费用。维保计划要支持按周期自动生成待办提醒,这需要在后端做定时任务扫描。

备件表(spare_part)

字段包括 id、备件编码、备件名称、规格型号、库存数量、安全库存、所在仓库位置、供应商。维修工单挂接备件时,要联动扣减库存,库存不足时要提示。

这些表之间的关联关系,归纳起来就是:用户创建设备,设备产生报修工单和维保计划,维修工处理工单时消耗备件。工单和备件之间我用一个关联表 repair_order_part 来记录"哪个工单用了哪些备件、用了多少",这样成本核算和库存追溯都有据可查。

3. Node.js 后端核心实现

3.1 项目初始化与目录结构

后端我用 Express 搭建,项目初始化步骤如下:

  1. 创建项目目录并进入
  2. 使用 npm init -y 初始化 package.json
  3. 安装依赖:express、mysql2、jsonwebtoken、bcryptjs、multer(文件上传)、dotenv、cors
  4. 安装 nodemon 作为开发依赖,实现代码热重载

项目结构我习惯按功能模块划分,而不是按文件类型(controller/service 混着摆),这样每个业务模块自成一体,改动时不用跨目录找文件。

server/ ├─ app.js ├─ config/ │ └─ db.js ├─ middleware/ │ ├─ auth.js │ └─ upload.js ├─ modules/ │ ├─ user/ │ │ ├─ user.controller.js │ │ ├─ user.service.js │ │ └─ user.route.js │ ├─ device/ │ │ ├─ device.controller.js │ │ ├─ device.service.js │ │ └─ device.route.js │ └─ repair/ │ ├─ repair.controller.js │ ├─ repair.service.js │ └─ repair.route.js ├─ utils/ │ └─ response.js └─ .env

这种按业务模块组织的目录,在项目膨胀之后优势会非常明显。我见过很多 Node 项目把所有 controller 塞一个文件夹、所有 service 塞一个文件夹,结果改一个报修功能要在几十个文件里跳来跳去。

3.2 基于 Express 实现报修工单接口

先看入口文件 app.js 的核心部分:

const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); dotenv.config(); const userRoute = require('./modules/user/user.route'); const deviceRoute = require('./modules/device/device.route'); const repairRoute = require('./modules/repair/repair.route'); const app = express(); app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:5173', credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 静态资源,存放上传的故障图片 app.use('/uploads', express.static('uploads')); // 路由挂载 app.use('/api/user', userRoute); app.use('/api/device', deviceRoute); app.use('/api/repair', repairRoute); // 统一错误处理中间件 app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ code: 1, message: err.message || '服务器内部错误' }); }); app.listen(process.env.PORT || 3000, () => { console.log(`Server running on port ${process.env.PORT || 3000}`); });

有一个细节必须注意:CORS 的 origin 不要简单设置成*。虽然开发时方便,但生产环境如果带 cookie 会话,*会导致跨域报错。这里推荐显式指定前端地址,或者用数组形式支持多个可信来源。

下面是报修工单新增接口的实现,我重点解释几个关键点:

// repair.controller.js const repairService = require('./repair.service'); // 创建报修工单 async function createRepair(req, res, next) { try { const { deviceId, description, urgency, imageUrls } = req.body; const reporterId = req.user.id; // 来自 JWT 中间件 // 参数校验 if (!deviceId || !description) { return res.status(400).json({ code: 1, message: '设备ID和故障描述不能为空' }); } // 调用服务层 const orderNo = await repairService.createRepair({ deviceId, description, urgency: urgency || '普通', imageUrls: imageUrls || [], reporterId }); res.status(201).json({ code: 0, message: '报修提交成功', data: { orderNo } }); } catch (err) { next(err); } } module.exports = { createRepair };

这里有个设计细节:工单编号不要用自增 id 直接暴露给业务人员,一是容易猜测总量,二是业务侧希望工单号有可读性。我在 service 层生成编号时用了日期 + 设备编号 + 随机数的组合:BX202501011234-DEV001-82。这样维修工在群里汇报时,其他人一眼能看出是哪天的哪个设备。

JWT 认证中间件也是核心,它负责两件事:校验 token 是否有效、把用户信息挂到 req 对象上。代码实现如下:

const jwt = require('jsonwebtoken'); module.exports = function auth(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ code: 1, message: '未登录或登录已过期' }); } try { const token = authHeader.split(' ')[1]; const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { return res.status(401).json({ code: 1, message: 'token 无效或已过期' }); } };

注意,这里有个安全细节:JWT_SECRET 绝对不能写在代码里,一定要从环境变量读取,并且要使用足够长的随机字符串。我之前见过有人把 secret 写成'123456',结果被脚本扫描到,服务器直接被拖库。

3.3 报修工单状态机流转设计

工单状态是这个系统最核心的业务逻辑,一开始我直接硬编码 if/else,后来状态多了根本维护不了。第二次重构时我改成了状态机配置,代码清晰很多。

// repair/repairStates.js const REPAIR_STATES = { PENDING: '待派单', PROCESSING: '处理中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELED: '已取消' }; // 允许的状态流转 const TRANSITIONS = { [REPAIR_STATES.PENDING]: [REPAIR_STATES.PROCESSING, REPAIR_STATES.CANCELED], [REPAIR_STATES.PROCESSING]: [REPAIR_STATES.COMPLETED, REPAIR_STATES.REJECTED], [REPAIR_STATES.COMPLETED]: [], [REPAIR_STATES.REJECTED]: [REPAIR_STATES.PENDING], // 驳回后可重新派单 [REPAIR_STATES.CANCELED]: [] };

状态变更接口统一走一个方法,变更前先查状态机,不允许的流转直接抛异常。这样设计的好处是:以后增加状态(比如"待验收")时,只需要改配置,不用在多个接口里挖逻辑。

工单流转还涉及到操作日志,我用一张 repair_log 表记录每次状态变更的时间、操作人、处理内容。这样设备科在复盘时能清楚地看到完整链路:谁报修的、谁派的单、维修工几点接单、几点完成。

4. Vue 前端核心实现

4.1 前端项目初始化与路由权限控制

前端我用 Vite 创建 Vue 3 项目:

npm create vite@latest frontend -- --template vue cd frontend npm install npm install vue-router@4 pinia element-plus axios dayjs

路由配置里最需要用心的是权限控制。系统有四种角色:管理员(能管所有配置)、设备科(能派单、查看统计)、维修工(能接单、更新进度)、普通操作工(只能发起报修)。我用路由守卫检查用户角色来决定能否访问某个页面。

// router/index.js import { createRouter, createWebHistory } from 'vue-router'; import { useUserStore } from '../stores/user'; const routes = [ { path: '/login', component: () => import('../views/Login.vue') }, { path: '/', component: () => import('../layouts/MainLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('../views/Dashboard.vue'), meta: { title: '工作台' } }, { path: 'device', component: () => import('../views/DeviceList.vue'), meta: { title: '设备台账', roles: ['admin', 'maintenance_admin'] } }, { path: 'repair', component: () => import('../views/RepairList.vue'), meta: { title: '报修管理' } }, { path: 'maintenance', component: () => import('../views/MaintenancePlan.vue'), meta: { title: '维保计划', roles: ['admin', 'maintenance_admin'] } } ] } ]; router.beforeEach((to, from, next) => { const userStore = useUserStore(); if (to.path === '/login') return next(); if (!userStore.token) return next('/login'); const roles = userStore.roles || []; if (to.meta.roles && !to.meta.roles.some(r => roles.includes(r))) { return next('/dashboard'); } next(); });

路由守卫的加载顺序需要注意:Pinia 必须先安装再调用路由守卫,否则 store 还没初始化就读取会报错。我在 main.js 里是先app.use(pinia),再app.use(router),顺序反了会出现诡异的 undefined 错误。

4.2 设备管理页面实现

设备列表页面是整个系统最常用的界面,核心需求是:表格展示、按条件搜索、分页、新增和编辑设备。我用 Element Plus 的 el-table 渲染,配合 el-pagination 做分页。

下面这段代码是设备列表的核心逻辑:

<template> <div class="device-container"> <el-card> <el-form :inline="true" @submit.prevent> <el-form-item label="设备名称"> <el-input v-model="queryParams.keyword" placeholder="输入设备名称或编号" clearable /> </el-form-item> <el-form-item label="设备状态"> <el-select v-model="queryParams.status" placeholder="全部状态" clearable> <el-option label="运行中" value="running" /> <el-option label="停机" value="stopped" /> <el-option label="维修中" value="repairing" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <el-button type="success" @click="openDialog()">新增设备</el-button> <el-table :data="deviceList" border stripe> <el-table-column prop="deviceCode" label="设备编号" width="120" /> <el-table-column prop="deviceName" label="设备名称" min-width="150" /> <el-table-column prop="workshop" label="所属车间" width="120" /> <el-table-column prop="status" label="状态" width="90"> <template #default="{ row }"> <el-tag :type="statusTagMap[row.status]">{{ statusTextMap[row.status] }}</el-tag> </template> </el-table-column> <el-table-column prop="lastMaintenanceDate" label="最近维保时间" width="130" /> <el-table-column label="操作" width="140" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openDialog(row)">编辑</el-button> <el-button link type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @change="fetchDeviceList" /> </el-card> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { getDeviceList, deleteDevice } from '@/api/device'; const deviceList = ref([]); const total = ref(0); const queryParams = reactive({ keyword: '', status: '', page: 1, pageSize: 10 }); const statusTagMap = { running: 'success', stopped: 'info', repairing: 'danger' }; const statusTextMap = { running: '运行中', stopped: '停机', repairing: '维修中' }; async function fetchDeviceList() { const { data } = await getDeviceList(queryParams); deviceList.value = data.records; total.value = data.total; } function handleQuery() { queryParams.page = 1; fetchDeviceList(); } function handleReset() { queryParams.keyword = ''; queryParams.status = ''; queryParams.page = 1; fetchDeviceList(); } async function handleDelete(row) { await ElMessageBox.confirm(`确认删除设备「${row.deviceName}」吗?`, '删除确认', { type: 'warning' }); await deleteDevice(row.id); ElMessage.success('删除成功'); fetchDeviceList(); } onMounted(fetchDeviceList); </script>

写这段代码时我特别注意了操作列的宽度。很多新手把表格操作列宽度忽略不设,结果在窄屏上按钮换行,非常丑。fixed="right" 这个属性也建议加上,当设备字段多、表格横向滚动时,操作按钮始终可见,对经常翻页的维修工来说体验提升明显。

4.3 报修工单流程页面

报修工单页面是这个系统的重头戏,它串联了三种角色:普通操作工发起报修,设备科派单,维修工处理和验收。前端我用标签页方式区分不同角色看到的视图。

发起报修的表单比较简单,核心是设备选择、故障描述、紧急程度和图片上传。图片上传部分我封装了 Element Plus 的 el-upload,注意要设置 headers 带上 JWT token,否则上传接口会被认证中间件拦下。

工单详情页我用"步骤条 + 时间线"的方式展示处理进度,效果直观:

<el-steps :active="currentStep" align-center finish-status="success"> <el-step title="提交报修" description="操作工提交故障信息" /> <el-step title="设备科派单" description="指派维修工" /> <el-step title="维修处理" description="维修工更新进度" /> <el-step title="验收完成" description="设备科确认结果" /> </el-steps> <el-timeline> <el-timeline-item v-for="(log, index) in repairLogs" :key="index" :timestamp="log.createdAt" :type="index === 0 ? 'primary' : 'info'" > {{ log.operatorName }} 于 {{ log.action }}:{{ log.content }} </el-timeline-item> </el-timeline>

这里有个容易踩的坑:后端返回的时间戳如果是 ISO 字符串(比如 2025-01-01T08:30:00Z),浏览器里显示时间会因为时区偏差跟本地时间不一样。解决方案是后端统一返回时间戳(毫秒数),前端用 dayjs 格式化。我前端专门封装了一个src/utils/date.js,统一导出格式化函数,避免每个人格式不一致。

4.4 axios 请求封装与 token 拦截器

前后端交互质量直接影响系统体验,axios 封装是我每次必做的。核心是请求拦截器和响应拦截器。

// src/api/request.js import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '../router'; import { useUserStore } from '../stores/user'; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 15000 }); // 请求拦截器:附带 token request.interceptors.request.use(config => { const userStore = useUserStore(); if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}`; } return config; }); // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data; // 后端统一返回 { code, message, data } if (res.code !== 0) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { const userStore = useUserStore(); userStore.logout(); router.push('/login'); ElMessage.error('登录已过期,请重新登录'); } else { ElMessage.error(error.response?.data?.message || '网络异常,请稍后重试'); } return Promise.reject(error); } ); export default request;

响应拦截器里统一处理 401 是一个很省心的做法。后续如果要扩展"刷新 token"机制,也只需要在这个拦截器里加请求队列重放逻辑,不用每个接口单独写。

5. 环境搭建常见问题与排查

Node.js 相关的开发环境问题,几乎每个新手都会遇到,我系统整理一下常见问题和解决办法。

5.1 npm 无法加载文件 npm.ps1,禁止运行脚本

微信群里经常有人发这个错误:npm : 无法加载文件 d:\program files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这个问题的本质是 Windows PowerShell 的执行策略默认限制了 .ps1 脚本运行。

解决方法很简单,用管理员身份打开 PowerShell,执行:

Set-ExecutionPolicy RemoteSigned

输入 Y 确认即可。RemoteSigned的意思是:本地下载的脚本可以运行,从网络下载的脚本必须有可信签名。这个设置足够日常开发使用。

这里叮嘱一句:网上有些教程让你执行Set-ExecutionPolicy Bypass,虽然也能解决,但会把本机的执行策略降到底,不建议长期这么做。尤其公司电脑有安全基线要求的话,这条策略会触发告警。

5.2 Node.js 安装和环境变量配置

下载 Node.js 时认准官网,尽量下载 LTS 版本,别下载 Current 尝鲜版。LTS 版本稳定性经过社区大量验证,项目开发最怕依赖环境出幺蛾子。

安装注意几点:

  • 安装路径不要带中文和空格,否则后续全局安装的模块路径解析容易出问题
  • 安装向导里的"Add to PATH"选项必须勾选
  • 如果电脑里同时有多个 Node 项目,强烈建议装 nvm-windows 管理多版本。nvm 让开发环境可以随时切换 Node 版本,比如老项目用 Node 16,新项目用 Node 20,这样不会再因为版本差异踩坑

检查环境变量是否正确配置,在命令行执行:

node -v npm -v

如果提示"无法识别 node 命令",说明 PATH 没配好。需要检查系统环境变量里是否有C:\Program Files\nodejs\这个路径。

5.3 前后端跨域问题

前端地址是 http://localhost:5173,后端接口是 http://localhost:3000,浏览器默认会拦截跨域请求。解决办法有两种:

第一种是后端开启 CORS,我上面代码里已经写过。适合前后端独立部署的场景。

第二种是前端通过 Vite 代理转发,适合开发环境。在 vite.config.js 里配置:

export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } });

配置之后,前端请求/api/xxx会被 Vite 启动的开发服务器代理到http://localhost:3000/api/xxx,浏览器端视角是同源请求,不会触发跨域。这两种方案二选一即可,同时配上也不冲突。生产环境用 Nginx 做反向代理时,跨域问题自然消解。

5.4 MySQL 连接超时与连接池配置

Node.js 连接 MySQL 时,如果不配置连接池,频繁创建销毁数据库连接会导致握手开销大,高并发时出现ETIMEDOUT错误。我用 mysql2 的 createPool 替代 createConnection:

const mysql = require('mysql2'); const pool = mysql.createPool({ host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 3306, user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'factory_mms', waitForConnections: true, connectionLimit: 10, queueLimit: 0, enableKeepAlive: true, keepAliveInitialDelay: 0 }); module.exports = pool.promise();

连接数设置在 10 左右对这类内部系统足够,同时注意 MySQL 服务端max_connections默认是 151,别把连接池上限设太大,否则会反过来把数据库挤垮。

5.5 vue devtools 调试技巧

调试 Vue 项目时,浏览器装一个 Vue Devtools 插件(Vue 3 对应版本)几乎是必备。它能在控制台直接查看组件树、props、Pinia 状态,排查数据传递问题效率极高。

我调试时最常用的一个功能是检查响应式数据有没有按预期更新。如果页面数据没变,先在 Devtools 里看组件绑定的数据源,再去看网络请求是否成功返回,基本能定位是前端渲染问题还是接口数据问题。

6. 部署与运维

开发完不是终点,把系统稳定跑起来才是真正的考验。工厂内部系统部署环境相对受限,我一般按后端、前端、数据库三步走。

6.1 后端部署:PM2 进程管理

后端是 Node.js 服务,不能像 Java 那样打一个 war 包丢进 Tomcat 就跑。我推荐用 PM2 做进程守护,它能在进程崩溃时自动重启,还能查看日志、监控资源。

部署步骤:

npm install -g pm2 pm2 start app.js --name factory-mms pm2 save pm2 startup # 设置开机自启

常用的 PM2 命令:

pm2 logs factory-mms # 查看实时日志 pm2 restart factory-mms # 重启应用 pm2 monit # 查看CPU/内存占用

生产环境的 .env 文件一定要单独配置,不能复用开发环境的配置。数据库密码、JWT_SECRET 这些敏感信息通过.env文件管理,并且把.env加进 .gitignore,防止误传仓库。

6.2 前端构建与 Nginx 部署

前端构建很简单:

npm run build

构建完成后,dist 目录打包拷贝到服务器。我用 Nginx 来托管静态文件,同时反向代理后端 API。一个典型配置如下:

server { listen 80; server_name factory.example.com; # 前端静态资源 root /var/www/factory-mms/dist; index index.html; # 单页应用路由,防止刷新 404 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 上传文件访问 location /uploads/ { alias /var/www/factory-mms/uploads/; } }

try_files $uri $uri/ /index.html这一行是 Vue Router history 模式的关键。如果没有它,刷新页面时 Nginx 会尝试找真实文件,找不到就会 404。

6.3 数据备份策略

工厂设备维修记录是重要资产,备份策略一定要提前定好。我每天凌晨通过 crontab 执行 MySQL 备份:

0 2 * * * mysqldump -uroot -p密码 factory_mms > /backup/factory_mms_$(date +%Y%m%d).sql --single-transaction --quick

备份保留 30 天,定期清理旧文件。这个操作虽然简单,但能救命。我曾经遇到过数据库误删事故,多亏有备份才能在 20 分钟内恢复。

7. 实操经验与避坑记录

这部分我把自己开发过程中的经验和踩过的坑整理一下,希望能帮读者少走弯路。

7.1 时间字段和时区统一

工厂用户都在国内,时间处理相对简单,但项目里如果出现了海外服务器或者用户在不同时区,就会很麻烦。我的原则是:数据库统一存 UTC 时间,后端接口返回时间戳,前端展示时再转本地时区。

这个坑我踩过。有一版后端直接返回了本地时间字符串,前端在浏览器直接展示,结果换了一个用户在其他时区访问,看到的维保提醒时间差了几个小时,被设备科投诉。后来全部改成时间戳传递,问题彻底解决。

7.2 权限控制要前后端同步

前端隐藏了报修管理菜单不代表后端接口也安全。有些新手爬虫工程师会直接 POST 请求绕过前端调用接口。所以后端每个接口都要有对应的权限校验,重点接口尤其如此。

我的做法是写一个requireRole中间件,配合 JWT 中的角色信息做二次校验。

module.exports = function requireRole(...roles) { return (req, res, next) => { if (!roles.includes(req.user.role)) { return res.status(403).json({ code: 1, message: '没有权限执行操作' }); } next(); }; };

使用示例:

router.post('/api/repair', auth, requireRole('admin', 'maintenance_admin'), repairController.createRepair);

7.3 设备状态与工单状态的联动

设备状态不能只靠人工修改,应该由工单流转自动驱动。比如维修工接单后,设备状态自动从"运行中"变成"维修中";工单完成后,设备状态恢复为"运行中"。这个联动如果放在前端做,用户关掉页面就失效了,一定要放在后端事务里处理。

实现时我在一个事务里同时更新设备状态和工单状态,保证一致性:

// repair.service.js const connection = await pool.getConnection(); try { await connection.beginTransaction(); await connection.execute( 'UPDATE device SET status = ? WHERE id = ?', ['repairing', deviceId] ); await connection.execute( 'UPDATE repair_order SET status = ? WHERE id = ?', ['处理中', orderId] ); await connection.execute( 'INSERT INTO repair_log (order_id, operator_name, action, content) VALUES (?, ?, ?, ?)', [orderId, operatorName, '派单', `维修工 ${workerName} 接单`] ); await connection.commit(); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); }

7.4 表单校验前后端都要做

只做前端校验的表单等于没校验,直接 POST 请求就能绕过去。比如工单的紧急程度字段,前端用下拉框限制了值,但后端也必须校验传入值是否在['普通', '紧急', '非常紧急']数组里。我用的校验方式很简单:

const allowedUrgency = ['普通', '紧急', '非常紧急']; if (!allowedUrgency.includes(urgency)) { return res.status(400).json({ code: 1, message: '紧急程度不合法' }); }

同理,设备编号、手机号、金额这类字段,前端正则校验了,后端也要再校验一遍。宁可多写几行代码,也不能让脏数据进表。

7.5 工单并发提交的重复问题

车间操作工经常会有"设备坏了很着急,连续点了三下提交"的操作。如果不做处理,同一台设备会生成 3 张重复工单。解决办法有几种:

一是前端在提交按钮上做 loading 禁止重复点击,这个只管住正常用户。

二是后端做防重校验:在同一台设备已有"待派单"或"处理中"状态的工单时,禁止创建新的报修工单。我在 service 层加了判断:

const existing = await pool.execute( `SELECT id FROM repair_order WHERE device_id = ? AND status IN ('待派单', '处理中') LIMIT 1`, [deviceId] ); if (existing[0].length > 0) { throw new Error('该设备已有未完成的报修工单,请勿重复提交'); }

这道防线能拦截绝大多数的重复提交。

7.6 枚举值的数据库设计

设备状态、工单状态、紧急程度、角色类型这些枚举值,在数据库里我建议直接存中文文本,而不是存 0/1/2 数字。虽然数字更节省存储,但排查问题时你看status = 1不知道自己看到的是什么,还得翻代码。存中文文本对后期维护和写报表 SQL 都更友好,代价是多了几个字节的存储空间,对内部系统来说完全可接受。

如果项目规模大、业务方想改枚举名称,可以在后端维护一个常量配置表,通过接口提供给前端渲染,避免硬编码。

8. 扩展方向与个人体会

系统做到这里已经能支撑工厂日常运维了,但说实话,内部系统永远有做不完的优化。我自己后续准备往这几个方向扩展。

最想做的第一个扩展是接入设备传感器数据。现在很多工厂设备本身带 PLC 或传感器,可以通过 Modbus 协议把运行状态、温度、电流等数据接入系统。一旦检测到异常参数,系统自动生成报警工单并推送给维修工,从"人报修"变成"设备自己报修"。这个能力需要硬件配合,但技术上 Node.js 社区有不少工业协议库可以调研。

第二个扩展是维修知识库。维修工在处理完故障后,把故障现象、原因、解决方案沉淀成知识文章,下次遇到相似问题可以直接搜索参考。这对老师傅经验传承非常有用,能让维修团队整体水平快速提升。

第三个扩展是智能派单。根据维修工的历史维修记录、技能标签、当前未完成工单数量,自动推荐最合适的维修工,替代现在设备科手动分配的方式。这个功能用简单的评分排序就能实现,不一定需要多复杂的算法。

最后再说两句。这段时间开发这个系统的感受是:管理系统的价值不在技术多新,而在流程能否真正落地。选择一个生产车间作为试点、跟设备科的老师傅反复确认操作习惯,比闷头写代码重要得多。用户操作路径越顺畅,系统数据越完整,统计报表越可信,整个设备管理体系才能良性循环。数据质量是这套系统的生命线,而数据质量靠的是业务流程设计得足够贴合实际。希望这篇分享能给正在做类似项目的读者一点参考。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 5:39:36

Wi-Fi 7部署避坑指南:从MLO配置到PoE供电的十大高频问题

1. 选型期的三个坑&#xff1a;别把Wi-Fi 7当万能药去年年底第一次给客户部署Wi-Fi 7 AP&#xff0c;我原本以为只是把设备从Wi-Fi 6换到Wi-Fi 7&#xff0c;插上PoE网线、更新一下后台模板就能收工。结果从第一天起就不断踩坑&#xff1a;客户会议室里明明显示连接速率1882Mbp…

作者头像 李华
网站建设 2026/9/28 5:38:49

Flutter跨平台鸿蒙开发:if-else条件决策逻辑深度解析

Flutter 框架跨平台鸿蒙开发 —— 基础&#xff1a;条件决策逻辑 if-else 深度解析与实战从我自己踩坑说起。去年我接手一个已经跑在 Android 和 iOS 上的 Flutter 项目&#xff0c;突然要适配鸿蒙终端。项目本身不大&#xff0c;但代码里到处是平台判断、状态判断、权限判断。…

作者头像 李华
网站建设 2026/9/28 5:38:28

Flutter物理量库quantity鸿蒙移植实战:从依赖替换到编译验证

做Flutter开发这些年&#xff0c;有一个体会越来越深&#xff1a;把一个你天天在用的三方库体系搬到另一个平台上&#xff0c;才是对“跨端”二字的极限测试。今天想聊的就是这个——我把pub.dev上非常常用的物理量与单位计算库quantity&#xff0c;完整移植到了鸿蒙系统的Flut…

作者头像 李华
网站建设 2026/9/28 5:37:53

YOLOv8+Streamlit足球分析:从目标检测到战术地图的完整实战

简介&#xff1a;基于YOLOv8与Streamlit构建的足球检测与跟踪项目&#xff0c;面向具备一定Python与深度学习基础的计算机视觉学习者、体育数据分析爱好者及目标检测课程设计者。资源集成完整源码、预训练权重、数据集配置与演示视频&#xff0c;覆盖球员、裁判、足球的实时检测…

作者头像 李华
网站建设 2026/9/28 5:37:46

Docker 快速部署 Oracle 19c:镜像选型、参数配置与常见坑全解析

“docker 安装oracle19C”这句话&#xff0c;最近在我这边的开发群里出现的频率非常高。原因也很好理解&#xff1a;想用 Oracle 19c&#xff0c;但不想在本地或者服务器上搞一套完整的 Oracle 环境——安装界面繁琐、系统参数要求多、配置错一步就是半天&#xff1b;装完之后想…

作者头像 李华
网站建设 2026/9/28 5:37:32

ESP32迷你MP3播放器实战:Helix解码+I2S输出全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华