ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的工厂车间管理系统设计与部署实践

基于SpringBoot+Vue的工厂车间管理系统设计与部署实践 说起来这个项目最早是给一家电机装配车间做的内部工具。车间里排产、报工、设备点检、质量记录全靠纸单和Excel来回倒腾月底统计工时和良品率的时候几个班长拿着台账一张一张翻效率低也容易出错。我用SpringBootVueMyBatisMySQL这套前后端分离的技术栈从零落地了一套工厂车间管理系统覆盖了工单派发、报工、质检、设备点检、物料出入库这些核心场景前后大概花了三周时间从设计到部署上线。这篇文章把这些真正有用的东西拆开讲业务模块怎么划分、数据库表怎么设计、后端接口怎么约定、前端页面怎么联调以及部署上线时最容易踩的几个坑。适合正在用前后端分离技术栈做毕设、或者小团队想搞一套轻量级车间管理系统的朋友参考代码结构是完整的照着部署教程也能跑起来。1. 先想清楚业务边界车间系统应该拆成哪些模块很多做管理系统的人一上来先建表、写接口等到页面做完了才发现逻辑拧成一团。车间系统比普通的后台管理系统复杂一点它不光管“数据”还管“流转”一个工单要从创建走到派工、报工、检验、入库每个环节的状态变化必须清晰。所以第一步不是写代码而是把业务边界划清。1.1 为什么选前后端分离而不是以前的单体JSP方案如果是那种几十个人的小工厂搞一整套微服务完全是给自己找罪受但如果继续用传统的单体模板渲染比如JSP、Thymeleaf直接包页面后期加新页面、改布局又特别痛苦。前后端分离的好处在于后端只关注业务逻辑和接口前端只关注页面交互两边通过JSON通信职责边界非常清楚。这个项目里选型的逻辑很直接——后端SpringBoot负责接口和事务Vue负责页面和交互MyBatis做数据访问MySQL存数据。这套组合在中小型管理系统里算是最成熟、成本最低的组合之一。SpringBoot的starter机制能把配置量压得很低Vue的组件化让车间里的工单台、看板这类反复使用的UI能做成独立组件MyBatis能把复杂的多表联查SQL写得很直白比JPA那种自动生成SQL的方式更好控制。说白了这不是炫技而是车间管理系统确实需要看得懂、改得动、出了问题能快速定位的技术栈。1.2 六个核心模块的边界划分我没有参考那些大型MES系统的做法那种动不动就十几个模块的设计在中小车间根本用不起来。最后落到项目里的是六个模块基础数据员工、班组、产品型号、工序、工艺路线这些是其他模块的“字典数据”。生产工单从销售订单或计划拆出的生产任务包含数量、交期、优先级。报工管理工人对每个工序进行完工数量上报系统记录工时。质量检验按工序或工单批次进行检验登记生成合格/不合格记录。设备管理设备台账、点检记录、维修登记。这是很多车间系统容易漏的但现场很需要。物料管理领料、退料、入库至少要把生产过程中的物料流向串起来。这六个模块其实对应着车间的一个日常闭环接到工单 → 排到班组 → 工人施工并报工 → 质检确认 → 合格入库 → 设备状态保障生产。如果你打算参考这个项目模块可以增减但这个闭环最好不要断。少了任何一个环节数据都会在生产流转中“卡住”。1.3 技术选型里的小心思SpringBoot选2.7.x还是3.x我在实际部署时吃过springboot版本太高的亏3.x强制JDK17很多老环境还在JDK8上跑生产环境的Java版本不一定由你说了算。如果只是做内部管理系统我建议SpringBoot 2.7.18 JDK8 mybatis-spring-boot-starter 2.3.x这套在兼容性上相当稳。Vue用2还是3这个项目用的是Vue3但目前很多教材和现成组件还是Vue2的。如果你更熟悉Vue2用2.x跑这套结构也完全没问题核心的组件化和axios调用方式差别不大。MySQL版本建议用5.7以上8.x也没问题但要特别注意驱动和时区配置。2. 数据库的表关系和状态字段设计车间系统一旦上线数据会持续累积表结构改起来代价很大。所以建表阶段宁可多花一天讨论也不要写到一半再回去改字段。2.1 核心表设计思路我把表分成三类基础表、流程表、记录表。基础表包括employee员工、product产品、process工序、device设备这类它们的特点是字段稳定很少发生状态流转。流程表就是work_order工单和work_order_task工序任务这类表最核心的特征是有一个“状态”字段并且状态会按固定方向流转。记录表则是report_record报工、quality_check质检、device_check点检、material_record物料记录它们的特征是只增不改主要用于统计查询。下面是工单和工序任务的两张核心表我做了字段级别的简化表名核心字段说明work_orderorder_no, product_id, quantity, plan_start_time, plan_end_time, priority, status, create_byorder_no设计成唯一编号比如WO日期流水号业务人员认这个work_order_taskorder_id, process_id, assign_employee, completed_quantity, status, report_time一个工单拆成多道工序每道工序独立派工和报工report_recordtask_id, employee_id, quantity, work_hours, report_time报工记录只增不改后续算工资和工时都靠它quality_checktask_id, inspector, qualified_quantity, defect_quantity, remark质检记录跟工单任务关联device_checkdevice_id, check_date, checker, status, note每台设备每天一条或者按班次一条2.2 工单状态字段的“状态机”设计工单状态我定义成四个值PENDING待派工、PROCESSING生产中、COMPLETED已完成、CLOSED已关闭。看着简单但里面有几个细节很容易写崩第一状态流转必须有方向。不允许从PENDING直接跳到COMPLETED中间必须经过报工和质检逻辑。在后端接口上用枚举校验而不是只靠前端按钮来控制。第二并发问题。车间里多个人可能对同一张工单操作比如班组组长在派工质检员在检验仓库在准备物料。如果两个接口同时更新一条工单记录后提交的请求很可能会把前一个请求的修改覆盖掉。我用的是乐观锁work_order表加一个version字段每次更新都带上WHERE version ?更新成功后version 1。这个方案在低并发情况下足够用也避免了强行上Redis分布式锁带来的复杂度。第三工单创建时间、完成时间这些字段必须用后端统一生成不能相信前端传过来的时间。铝厂、玻璃厂、机械加工车间的工人都知道不同电脑的系统时间能差出好几分钟统一由后端生成才能保证数据可信。2.3 建表时容易忽略的细节关于数据库时间字段建议所有时间字段都用DATETIME而不是TIMESTAMPTIMESTAMP在2038年会溢出虽然看着遥远但一些老系统的坑已经提前暴露了。另外在MySQL配置连接串时注意加上serverTimezoneAsia/Shanghai否则运行时会发现数据库时间和Java时间对不上傻乎乎差8个小时。数量字段统一用INT涉及金额的字段用DECIMAL(10,2)物料数量绝不能用FLOAT或DOUBLE。浮点数的精度问题在这类系统里非常坑一批物料入库100个出库99个因为浮点误差会显示成0.9999999个算是这个圈子里真实会碰到的笑话。库存扣减不要写UPDATE stock SET quantity quantity - #{num}这种裸SQL一定要带上条件判断WHERE quantity #{num}并且用事务包裹否则并发领料时很容易把库存扣成负数。2.4 索引怎么加才合理外键字段必须建索引work_order_task.order_id、report_record.task_id这些字段是高频查询的关联条件。状态字段也要索引车间列表页默认就是按状态筛选比如查看所有“生产中”的工单用状态字段建索引能避免全表扫描。时间字段按需索引工单列表如果经常按月份查给plan_start_time建索引但索引不是越多越好写多读少的记录表比如报工记录就别加太多索引。组合索引优于多个单列索引比如查询条件是status priority建一个(status, priority)的组合索引比单独两个索引更高效。到了这一步数据库的基本骨架就算立住了。接下来要做的就是把后端的接口逻辑跟这些表对应起来。3. SpringBoot后端落地接口约定、事务控制与MyBatis细节很多初学者问我为什么我的SpringBoot项目写完接口一翻代码就乱七八糟其实就是分层和约定没搞好。3.1 项目分包逻辑我建议后端按“表现层-业务层-持久层”分包不按“按功能建包”。如果你是按功能分包比如controller/OrderController、service/OrderService、mapper/OrderMapper一个功能跑通所有层看着清爽但多个功能之间一旦有交叉调用代码会乱成一锅粥。这个项目我使用传统分层com.factory.mes ├── controller // 只做参数接收和结果封装 ├── service // 业务逻辑事务都写在这层 ├── mapper // MyBatis持久层接口 ├── entity // 数据库实体 ├── dto // 传输对象不对应数据库表 ├── common // 通用返回体、异常、工具类 └── config // 自定义配置跨域、拦截器、事务一个很重要的约定controller不写业务代码service不出现SQL语句。如果看到controller里又拼接逻辑、又做参数校验后面改需求的时候一定头大。3.2 统一返回体与全局异常前后端分离牵扯到对接口的理解所以接口的返回结构必须统一。我的统一返回体是ResultTpublic class ResultT { private Integer code; // 200成功500失败401未登录 private String msg; private T data; public static T ResultT ok(T data) { return new Result(200, success, data); } public static T ResultT error(String msg) { return new Result(500, msg, null); } }这里最重要的一条经验业务异常不能靠抛RuntimeException让框架兜底。比如库存不足、工单状态不允许变更这类属于正常业务分支必须显式处理。用全局异常拦截器统一捕获自定义异常再转成Result返回。为什么要这样因为前端好处理。不管接口成功还是失败前端只需要检查code字段即可。如果错误状态乱糟糟的有的返回500、有的返回200但data是null、有的直接抛出一堆异常堆栈前端联调就会持续在“黑盒猜谜”效率极低。3.3 MyBatis用法XML还是注解MyBatis有三种用法注解SQL、XML映射、MyBatis-Plus直接CRUD。车间管理系统里简单的单表操作我用MyBatis-Plus的LambdaQueryWrapper比如查询设备列表按状态筛选复杂多表联查比如“工单列表页需要展示产品名称 负责人姓名 当前进度”就写成XML里的SQL。举个例子工单列表这个查询要关联两张表用注解写会很挤放XML更直观select idselectWorkOrderPage resultTypecom.factory.mes.dto.WorkOrderDTO SELECT wo.id, wo.order_no, p.product_name, e.emp_name AS assign_name, wo.quantity, wo.status, wo.plan_end_time FROM work_order wo LEFT JOIN product p ON wo.product_id p.id LEFT JOIN employee e ON wo.assign_employee_id e.id WHERE 11 if teststatus ! null and status ! AND wo.status #{status} /if if testkeyword ! null and keyword ! AND (wo.order_no LIKE CONCAT(%, #{keyword}, %) OR p.product_name LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY wo.plan_start_time DESC /selectXML里几个细节提醒一下用if标签做条件拼接比用字符串拼接多个where子句干净很多但注意WHERE 11这种用法在数据量大时会让优化器放弃部分索引条件多的时候配合where标签更合适。LIKE查询尽量配合索引或者别对没建索引的字段做模糊匹配。分页查询不要写死LIMIT用MyBatis-Plus的分页插件PaginationInnerInterceptor插件会自动把传入的pageNum和pageSize拼成LIMIT ?语句省心还安全。3.4 事务边界怎么划车间系统里最容易出事故的是“报工更新工单进度”和“领料扣库存”这两类操作它们天然要放在同一个事务里。我用Spring的Transactional注解时一开始是按整个方法加事务后来发现粒度太粗。比如报工接口里既有业务校验又有库存更新如果校验失败直接抛异常事务回滚会把前面的库存扣减也卷进去。后来我改成按“最小事务单元”拆校验逻辑不放在事务方法里真正要落库的更新操作单独放在一个Transactional方法里。另外要提醒一个情况自调用事务失效。在同一个Service类里一个方法调用另一个带有Transactional的方法第二个方法的事务是不生效的因为Spring事务基于AOP代理自调用走的是this调用而不是代理对象。我当时排查报工数据不一致的问题就卡在这里很久最后把事务方法挪到不同Service里或者用AopContext.currentProxy()绕开才彻底解决。3.5 登录鉴权轻量方案够用车间系统的用户量通常只有几十到几百人用Spring Security OAuth2那套东西明显偏重。我用的是JWT 自定义拦截器的方式登录接口校验用户名密码密码加盐后BCrypt加密存储成功后生成JWT Token。前端在请求头里带Authorization: Bearer token。后端写一个拦截器在HandlerInterceptor里解析Token并把用户信息放到ThreadLocal里后续业务代码直接取即可。权限方面简单用角色字段控制管理员、班组长、工人三种角色。对前端返回菜单列表时直接按角色过滤后端接口也可以加RequiresRole注解做二次校验。工业现场数据没那么敏感但“未登录不能访问接口”这条底线必须有。4. Vue前端的页面组织与接口联调细节后端接口调通了前端才算真正开始。4.1 Vue工程初始化时的环境配置Vue脚手架创建项目时会问一堆问题我在实际操作中建议用Vite或者Vue CLI都行但Node版本尽量用16或18以上版本。不少部署失败场景不是代码问题而是vue安装及环境配置不一致导致的。如果你拿到源码发现npm install报了一堆错先查两件事Node版本和npm镜像源。node -v npm config get registry # 如果不是国内镜像可以临时用 npm install --registryhttps://registry.npmmirror.com前端工程目录我按视图模块划分src ├── api // 接口请求封装按模块分文件 ├── components // 公用组件表格、弹窗、文件上传 ├── router // 路由配置 ├── store // 状态管理 ├── views │ ├── workOrder // 工单管理页 │ ├── report // 报工页 │ ├── quality // 质检页 │ ├── device // 设备管理页 │ └── dashboard // 看板页 └── utils // 请求拦截器、工具函数4.2 API模块封装与跨域代理管理后台的重复工作就是调接口、拿数据、渲染表格。所以axios请求一定要封装否则几十个页面每个都写一遍axios.get(url, { params })后面改token逻辑或者加loading提示会抓狂。我封装了一个request.js核心是请求拦截器和响应拦截器import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器统一加token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截器统一处理code request.interceptors.response.use(res { const { code, data, msg } res.data if (code 200) { return data } // 401跳登录 if (code 401) { router.push(/login) } return Promise.reject(new Error(msg)) })开发环境跨域用Vite或Vue CLI的代理解决。生产环境则借助Nginx反向代理。不要把baseURL直接写成后端地址否则需要手动开启跨域模式。我常见到初学者在后端配置CrossOrigin、前端也改地址搞了一堆CORS配置最后还得靠Nginx转发这就绕远了。4.3 工单管理页的核心逻辑工单管理页是车间用得最多的页面看起来是个表格实际上里面藏着整个状态机。我的实现逻辑是列表页从后端拉取工单数据根据状态字段显示不同颜色的标签。每个工单行提供不同的操作按钮。查看按钮所有人可点派工按钮只有班组长及以上角色能看报工按钮只有该工单负责人能看。前端按钮的显隐不涉及安全问题只为了操作体验。真正的后台接口必须再次校验权限不能相信前端隐藏按钮就代表了安全。报工操作我单独弹窗处理工人只需要输入完工数量工时和报工ID都由后端根据登录用户自动生成。这里有个交互细节前端点“提交”的时候要加防重复提交逻辑否则工人手快点了两下就会产生两条重复报工记录月底对账时非常痛苦。我用按钮加loading状态点击后置灰请求完成后解除。4.4 车间看板页面数据可视化看板是给车间主任和老板看的我用ECharts在大屏上展示了三个核心指标今日工单完成率、各班组工时分布、设备运行状态。看板页有一个特殊注意点数据要定时刷新但不要整页刷新那样表格会跳动、图表会闪。我使用setInterval定时拉取接口更新图表数据时用myChart.setOption(option, true)而不是notMerge参数直接替换整个配置。实测下来每30秒刷新一次就够了太频繁反而给后端数据库造成压力。另一个容易踩的坑是ECharts在Vue组件里用不好会出现“TypeError: Cannot read properties of undefined”基本都是因为容器div的宽高没有初始化。看板页的容器必须先有明确的宽度高度再去初始化图表。4.5 前端状态管理与跨页面通信车间系统的前端状态其实不算复杂全局要共享的只有“当前登录用户信息”和“消息通知未读数”。我用Pinia或Vuex看你想用哪个管理这两个数据页面间传参尽量通过路由query或者刷新后重新拉取接口避免为了传一个id在store里塞一堆临时变量。有一个经验可以分享不要什么数据都往store里塞。store是有状态的刷新页面就没了且多一层数据对应关系排错时要到处找。我的原则是接口能拿到的数据就不要存store页面生命周期内的数据用组件的局部data就够了。5. 部署全流程与实测避坑代码写完了部署上线才是真正考验人的地方。5.1 环境准备JDK与MySQL部署的第一步是环境。JDK版本跟SpringBoot版本要对齐我用2.7.x JDK8。MySQL安装时有两个地方容易翻车一是下载地址和版本选择不对建议直接用官方MySQL Community Server 5.7或8.0版本不要去那种第三方优化版二是在Windows上初始化MySQL时容易忘记执行mysqld --initialize-insecure导致root密码一直不对这时候要么用临时随机密码要么直接重置初始化。初始化成功后记得创建数据库并设置字符集CREATE DATABASE mes DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;工单、报工这些记录里可能会输入中文如果数据库字符集是latin1就会出现经典的“”乱码。UTF8MB4是标准答案别用utf8了。5.2 前端打包与后端构建后端构建用Maven打包跳过单元测试mvn clean package -DskipTests打完的jar在target/目录下启动命令java -jar mes-backend-1.0.0.jar --spring.profiles.activeprod前端打包要注意接口地址和Nginx转发的关系。我的前端代码走/api前缀那在nginx.conf里就要把/api转发到后端端口server { listen 80; server_name your-domain-or-ip; location / { root /opt/mes/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有几个细节前端路由用history模式时刷新页面会404必须配try_files把请求都指向index.html。proxy_pass末尾的/很关键。如果写proxy_pass http://127.0.0.1:8080;不带斜杠Nginx会把整个/api/xxx原样传给后端如果带斜杠会把/api前缀去掉再接上后面的路径。我的后端Controller是RequestMapping(/api/order)所以这里要带斜杠、去掉前缀。5.3 两种部署方案独立部署与合并部署方案一前端跑Nginx后端跑Java进程互不干扰。这是推荐的生产部署方式方便各自扩容、更新、查日志。方案二把前端打好的dist目录里的静态文件复制到SpringBoot的src/main/resources/static下再重新打包成一个jar。这个方案适合小项目或者临时演示环境好处是一只手就能启动缺点是前后端耦合了每次更新前端都还要重新打一次jar包。如果你看到有人在网上提“vue打包放进springboot中”说的就是这个方案。我只有在客户服务器资源紧张时才会这样搞正常生产环境用方案一。5.4 实际部署中遇到的几个高频报错排除网络波动、端口被占这类小问题我遇到最多的有四类第一MySQL驱动版本不匹配。SpringBoot 2.7.x自动装配的是MySQL Connector/J 8.x如果你连接的是MySQL 5.7驱动类名要写com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver否则启动时会报ClassNotFoundException。第二Docker安装MySQL失败。不少人图省事用容器装数据库结果要么是时区不对要么是容器内部权限和本机映射目录冲突。遇到过最典型的报错是MySQL容器启动后立刻退出查日志发现数据目录权限不对。这时候用docker logs mysql看错误信息加上-v映射时指定正确路径通常能解决。我的建议是如果就是生产环境用别费劲折腾Docker映射权限直接在本机装MySQL更省事。第三SpringBoot版本太高导致的兼容性问题。我试过SpringBoot 3.2确实启动快、性能好但MyBatis的starter、一些老掉牙的工具包在Jakarta命名空间下全得改。如果是一套新的微服务可以上3.x但做管理系统特别是要跑在老服务器上的系统SpringBoot 2.7.18仍然是更稳妥的选择。第四跨域问题在部署后显现。开发环境有代理生产环境换成了Nginx有人把前端请求地址写成了localhost:8080/api/xxx浏览器就会报跨域错误。统一走/api前缀、由Nginx转发是最干净的方案。5.5 备份与日志系统上线不是终点。我给这套系统配置了两件事数据库定时备份。用crontab每天凌晨跑一次mysqldump备份文件按日期保留最近7天。日志按天滚动。SpringBoot默认日志只输出到控制台我加了logback-spring.xml把INFO级别日志写到/var/log/mes/目录下按天分割保留30天。然后配合grep查问题比如grep ERROR /var/log/mes/backend-2025-07-15.log车间系统的故障排查百分之八十靠日志。日志配置得规范能少掉很多半夜起来救火的机会。6. 关于这套系统我最后想说的几条实在话第一次做前后端分离项目的人往往会把精力全放在“跑通代码”上觉得页面能打开、接口能返回数据就算大功告成。但真正上线跑过之后才会明白踏实的系统赢在规范上接口返回格式统一前端联调就快了数据库字段设计合理统计报表就好做了日志齐全线上问题就能被及时定位。这中间有个小细节拿工单编号来说我一开始用的是数据库自增ID后来跟车间班组长沟通才知道现场口头沟通根本不会说“第42号工单”而是“WO20250715001这种单子”。于是我把编号改成了“WO 日期 三位流水号”还去掉了容易混淆的字母。这种细节数据库表设计阶段就要和实际使用者对齐否则用户只是嘴上用软件手上永远在用Excel。如果后续想在这个项目上继续扩展我建议两个方向一是把看板数据做成实时推送可以往SpringBoot整合Flink或消息队列的方向深挖二是把质检数据的分析功能加强给管理层提供按班组、按故障类型的统计图表。生产管理的核心就八个字过程可查结果可溯。最后说一句实际操作中的感受这套系统的难点不在某个单独的技术点而在于把车间现场的碎片化操作整理成一条完整、清晰、可追踪的数据流。能做到这一点代码层面的问题反而是最简单的。
返回列表