今年上半年我刚刚完成了第三套 MES(生产制造执行系统)的从零开发到车间落地,前后端一起做,后端 SpringBoot+MyBatis,前端 Vue,数据库落 MySQL。整套系统最终在生产环境稳定运行,车间的计划派工、工序报工、质量检验、溯源查询都在上面跑。
说实话,MES 这种系统跟普通的管理后台差别很大——它是真正要给车间产线用的,界面不需要花哨,但数据流程、状态流转、异常处理必须非常严实。而“前后端分离”这套架构,恰好能把制造业系统最常见的“现场反馈慢、一人包全程、改一处崩一片”的问题化解掉。
这篇文章我把整个系统的设计思路、核心模块、后端和前端的关键实现,以及从零到上线的完整部署步骤全部整理出来。不管你是刚了解 MES 是什么的新手,还是已经用 SpringBoot+Vue 做过项目的开发者,只要你想自己从零搭一套生产制造执行系统,这篇可以直接照着做。
1. 项目底色:MES到底是什么,这套技术组合为什么成立
1.1 先搞懂MES在车间里的定位
MES 中文叫制造执行系统,英文全称 Manufacturing Execution System。它处在企业信息化架构的中间层——上面承接 ERP 的生产计划和物料需求,下面对接 PLC、扫码枪、设备数据采集。用一句话说,MES 就是让工厂管理层知道“车间此刻正在发生什么”的系统。
我一直觉得,想开发 MES 之前必须先理解一个场景:一个坐办公室的计划员,排了一张今天的生产计划,但他并不知道车间里每个工位现在在做什么、某张工单做到哪道工序了、上一批产品的质量是否合格。如果全靠电话去问,问来的信息一定是滞后且不准确的。MES 要做的就是把车间正在发生的实际数据实时收上来,让人随时能看。
所以 MES 的核心业务不是某一件事,而是一条完整的数据流:计划下达 → 工单创建 → 车间接收 → 工序报工 → 质量检验 → 完工入库。整个系统里所有的表设计、接口设计、前端页面,都是围绕这条主线在转。理解了这一点,你后面开发时做任何需求都不会跑偏。很多刚接触的人容易把 MES 做成一个“大杂烩”,什么功能都加,但每个功能都做不深,最关键的主线反而不清晰。
1.2 SpringBoot+Vue+MyBatis+MySQL这套选型是怎么来的
先说我踩过的对比坑。早年我做过一版基于 JSP+Servlet 的传统单体系统,开发效率低不说,前端页面和后端逻辑强耦合,车间现场有临时需求想改个展示,都得重新编译打包。后来也试过前后端都上重量级框架的玩法,但对中小型制造企业来说,维护成本和学习成本都偏高。
最终这套组合是我在实际交付中反复沉淀下来的:
- SpringBoot 负责后端:约定优于配置,内嵌 Tomcat,一个 jar 包就能跑起来,非常适合制造业这种“交付到客户机房、环境复杂、维护人员水平不一”的场景。SpringBoot 版本我用的是 2.7.x,兼容性好,加上 spring-boot-starter-web、mybatis-spring-boot-starter、druid 连接池这些核心依赖,一套组合拳非常稳。
- MyBatis 负责数据访问:MES 里大量涉及多表联查、报表统计、复杂条件过滤,MyBatis 的 XML 文件里写 SQL 非常灵活,SQL-Server 或者 MySQL 的特定语法都能直接用,性能也好调优。相比 JPA 那种全自动的 ORM,MyBatis 的“半自动”恰恰是优势——你对 SQL 有完全控制权,这在大数据量场景下极其重要。
- Vue 负责前端:组件化开发,Element UI 现成的表格、弹窗、表单组件一用就上,车间大屏看板用 ECharts 画图表也顺手。最关键的是 Vue 的开发模式天然契合前后端分离,联调阶段只需要约定 JSON 格式,双端并行不阻塞。
- MySQL 负责数据存储:开源免费、部署简单、运维生态成熟。对中小制造企业动辄几十万条的报工记录、检验记录来说,性能完全够用。配合定时备份,数据安全性也有保障。
选这套组合的核心逻辑就一句话:用最少的成本解决制造业系统真实存在的问题,而不是追逐技术新潮。SpringBoot 生态成熟、Vue 上手快、MyBatis 控制力强、MySQL 部署轻量,四者放到“快速交付、稳定运行、方便维护”这个标准下来看,是性价比很高的组合。
1.3 前后端分离:从单体到真正解耦
我最初做系统时也犹豫过:到底要不要前后端分离?毕竟制造业项目往往追求“能用就行”。但做完这套系统后,我越来越坚定:必须分离,而且越早分离越好。
前后端分离的实质,是把“数据接口”和“页面展示”彻底解耦。后端只负责把规整的 JSON 数据按照约定好的接口文档吐出去,前端只负责把 JSON 渲染成页面、收集用户操作。两个人可以并行开发——我做后端接口,你写前端页面,只要提前定义好报文格式,谁也不用等谁。这在项目周期紧的时候效率是翻倍的。
而且分离后,后端接口可以同时服务多个端:PC 端管理页面、车间大屏、扫码枪小程序,都调同一套接口。这一点在 MES 里非常实用,因为车间现场往往需要大屏看板、PDA 扫码、电脑端操作多种终端并存。如果你用单体 JSP,想再加一个移动端基本等于重写一套系统。
当然,分离也带来了新的课题——跨域问题、Token 鉴权、接口防刷、前后端联调环境搭建。这些我在后面章节里都详细讲了处理方案。还是那句话,收益远大于学习成本。
2. 功能模块拆解与数据库建模
2.1 一个MES的主线业务模块都有哪些
开发之前,我先把自己当成车间主任,把整条产线的工作流程在脑子里跑了一遍。最终功能模块的划分很简单,所有页面都围绕 7 个模块展开:
- 计划管理:接收 ERP 或手工录入的生产计划,按产线/班组拆分,生成生产工单。
- 工单管理:工单是系统的核心单据,维护了产品、数量、计划开工完工时间、当前状态。工单可以下发给车间,车间再按工序细化执行。
- 生产执行:核心是“报工”。操作工在完成一道工序后,用扫码枪扫工单条码,上报完工数量、不良数量、工时、设备号、操作人。这是车间最日常的操作,也是整个系统的数据源头。
- 质量检验:按检验标准生成检验任务,支持来料检验(IQC)、过程检验(IPQC)、完工检验(FQC)。检验结果录入后,系统自动判定合格/不合格,并关联到对应工单和批次。
- 物料管理:生产领料、退料、线边库存查询、物料批次追溯。
- 设备管理:设备台账、点检记录、维修记录、状态监控。
- 追溯查询:根据产品序列号或批次号,反查所有生产工序记录、质量检验记录、物料批次和操作人。
功能虽多,但你可以看到,每一条链路都是围绕“工单”这个核心入口展开的。这也是我给刚入行做 MES 的朋友的第一个建议:先定义清楚“工单”长什么样、状态怎么流转,再考虑其他模块。整个系统像一棵树,工单就是树干,其他模块都是分支。
2.2 数据库核心表设计思路
数据库设计是整个系统的地基,地基没打好,后面开发全是坑。我的经验是:在建表之前先画好核心表的关系图,用 Visio 或者直接手画都行,想清楚每个表之间如何关联,再去写建表语句。
核心表的组织思路大概是这样的:
- 基础数据组:物料表(mb_product)、BOM 表(mb_bom)、工艺路线表(mb_route)、工位表(mb_station)、设备表(mb_device)、用户表(sys_user)。
- 生产执行组:生产工单表(wip_work_order)、工单工序明细表(wip_work_order_detail)、报工记录表(wip_report_work)、领料单表(wip_pick_material)。
- 质量管理组:检验标准表(qc_inspection_standard)、检验单表(qc_inspection_order)、检验结果明细表(qc_inspection_result)。
- 追溯组:序列号表(trace_serial_no)、批次流转记录表(trace_batch_log)。
拿最核心的“生产工单表”举例,关键字段是这些:
| 字段名 | 字段说明 | 备注 |
|---|---|---|
| work_order_no | 工单编号 | 唯一,格式:WO + 日期 + 流水号 |
| product_code | 产品编码 | 关联物料表 |
| product_name | 产品名称 | 冗余存储,方便查询 |
| plan_qty | 计划数量 | |
| completed_qty | 完工数量 | 报工时累加 |
| defective_qty | 不良数量 | 报工时累加 |
| status | 工单状态 | 见下方状态流转 |
| plan_start_time / plan_end_time | 计划开始/结束时间 | |
| create_by / create_time | 创建人/时间 | 标准审计字段 |
设计时我有意做了冗余字段,比如在产品编码之外冗余产品名称。虽然违背了严格的三范式,但在 MES 这种查询场景非常多的系统里,这种冗余能省掉大量不必要的联表查询,实际运行效率提升非常明显。
字段命名方面,全表统一用“下划线小写”风格,比如 work_order_no、create_by,因为 MySQL 在 Linux 环境下对大小写敏感,容易搞出“表存在但查不到”的怪问题。MyBatis 里开启驼峰映射后,下划线和 Java 属性的驼峰命名自动对应,代码也简洁。这个细节看似小,但我见过很多项目因为表名字段名大小写不统一,在部署时折腾好几天。
2.3 工单状态流转与工序报工规则
工单状态是整个系统的“灵魂”。用数据库字段 status 来标识,取值如下:
- 0:草稿(已创建但未下达)
- 1:已下达(计划员确认,车间可见,可开始生产)
- 2:生产中(至少一道工序已开始报工)
- 3:已完工(所有工序完成且合格数达到计划数)
- 4:已关闭(财务结算完毕,状态锁定)
状态流转的方向是固定的:0→1→2→3→4,禁止跳转或者回退(特殊情况需要做状态回退操作,我会单独留一个“状态维护”接口给管理员)。这个限制必须在后端检,因为前端按钮隐藏太容易绕过。
工序报工是车间的最高频操作,需要明确几个规则:
- 一个工单下有若干道工序,每道工序独立报工。
- 报工数量不能超过计划数量(允许做超领,但要在备注里写明原因)。
- 报工记录生成后不允许直接修改,只能做“红冲”处理——就是生成一条负数的冲销记录,再重新报工,这样全程留痕,符合制造业追溯的审核要求。
- 工序的报工顺序要校验,上一道工序没有报工,下一道不允许报工(也可以配成允许并行,取决于车间工艺,我默认是串行)。
这套规则我一开始也没有全部实现,是在跟车间老师傅反复沟通后才完善的。很多细节都是在现场被“逼”出来的——比如“不良品需要在不良代码里注明是料废还是工废”,这个字段虽然简单,但车间月底做质量分析时离不开它。
3. 后端落地:SpringBoot+MyBatis的关键实现
3.1 工程结构与统一返回格式
后端工程我习惯按业务模块分包,而不是按技术层分包。很多教程喜欢建 controller、service、mapper 这种按技术分层的方式,但 MES 业务复杂、模块多,到了中后期找代码非常痛苦。我采用按“业务域”分包,每个包内再分 controller/service/mapper:
com.mes ├── common # 统一返回、全局异常、工具类 ├── config # WebMvc、拦截器、跨域配置 ├── security # JWT 登录认证 ├── modules │ ├── plan # 计划管理 │ ├── workorder # 工单管理 │ ├── execute # 生产执行/报工 │ ├── quality # 质量管理 │ ├── material # 物料管理 │ ├── device # 设备管理 │ └── system # 用户/角色/菜单 ├── MesApplication.java └── resources ├── mapper # MyBatis XML 文件,与 modules 目录对应 ├── application.yml └── db └── init.sql统一返回结果我封装了Result<T>,包含 code、message、data 三个字段。规则很简单:code=200 表示成功,其他都是失败。所有 controller 返回的都是这个结构,前端 axios 响应拦截器统一处理,不用每写一个接口都去拼一遍 Map,代码会非常干净。
全局异常处理用@RestControllerAdvice,把业务异常、参数校验异常、系统异常分别映射到不同 code,并记录日志。这一块的重要性容易被低估——没有全局异常处理,SQL 报错堆栈直接抛给前端,既不安全体验也差。我见过太多项目接口返回 “500 内部错误”,查问题全靠猜。
3.2 MyBatis 动态 SQL 与关键配置
MES 里的查询条件极其多变:工单查询可能要按时间、状态、产品、产线、操作人任意组合筛选。如果用 Java 代码一个个 if 判断拼接 SQL,又累又容易拼错。MyBatis 的<where>+<if>动态 SQL 完美解决这个问题。
下面是我工单分页查询的一个片段,也是这个系统里最典型的用法:
<select id="selectWorkOrderPage" resultType="com.mes.modules.workorder.entity.WorkOrder"> SELECT * FROM wip_work_order <where> <if test="workOrderNo != null and workOrderNo != ''"> AND work_order_no LIKE CONCAT('%', #{workOrderNo}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="productCode != null and productCode != ''"> AND product_code = #{productCode} </if> <if test="startTime != null"> AND create_time >= #{startTime} </if> <if test="endTime != null"> AND create_time <= #{endTime} </if> </where> ORDER BY create_time DESC </select>注意>=和<=的写法——XML 里不能直接用>和<,必须转义。这也是新手最常见的报错点,直接用<会让 XML 解析直接报错。
关于单个数字字符的比较,我之前踩过一个很隐蔽的坑。在 MyBatis 的<if test="status != null">里,如果 status 是 String 类型且值可能为 "0",判断条件不能只写test="status != null",最好加上status != ''。因为如果前端传过来空字符串,MyBatis 的动态 SQL 里status = ''会执行一个恒假的查询条件,查出来永远是空列表,而且控制台没有任何报错——这个问题非常难排查,我花了半天才定位到。
日志配置方面,开发阶段一定要把 SQL 打印出来,我在 application.yml 里这样配置:
mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.mes.modules.*.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl:log-impl配置为 StdOutImpl 后,控制台会打印每条 SQL 的参数和结果集。生产环境记得把这一行去掉,否则大量日志会拖垮磁盘。实际使用中我还会配合 MyBatis Log Plugin 这个 IDEA 插件,能直接在控制台看到格式化后的完整可执行 SQL,调试时复制出来到 Navicat 里一跑就知道问题在哪,效率翻倍。
3.3 事务、批量操作与缓存要点
报工操作表面上看是插入一条报工记录,实际上要同时更新工单的完工数量、更新工序完成数量、可能还要写入设备运行时长。这三个操作任何一个失败,数据就会不一致。所以必须在 Service 方法上加@Transactional(rollbackFor = Exception.class)。
这里有个细节:rollbackFor一定要写成Exception.class,而不是默认的运行时异常。因为制造业系统里经常抛出受检异常(比如导出文件 IO 异常),如果只回滚运行时异常,事务会悄悄提交,数据错误就悄无声息地发生了。我吃过这个亏,MRP(物料需求计划)运算时有一个文件解析异常,导致几千条物料数据错乱,最后全靠备份恢复。
批量操作在 MES 里也非常常见,比如按批次导入工单、批量维护 BOM、批量确认检验结果。MyBatis 批量插入我建议用ExecutorType.BATCH,或者直接拼接一条大的 INSERT 语句,两种方式性能相差巨大。单独循环里调用 insert 方法会有严重的性能问题——实测下来,插入 5000 条记录,拼接批量 SQL 只需要几十毫秒,循环插入可能要 10 秒以上。
关于 MyBatis 缓存,一级缓存是默认开启的,作用于同一个 SqlSession,查询同样的 SQL 会走缓存。在 SpringBoot 集成环境下,每次请求都会新建 SqlSession,所以一级缓存基本没影响。二级缓存我建议直接关闭,因为 MES 数据实时性要求很高,报工数据刚写入就要被立刻查询出来显示在车间看板上,任何缓存都可能导致数据延迟,引起现场人员的极度不满。
4. 前端实现:Vue工程化的几个关键点
4.1 工程搭建与环境配置
前端我用的是 Vue 2 + Element UI + Vue Router + Vuex + Axios 这套非常成熟的技术栈。为什么不用 Vue 3?原因很实在——Element UI 对 Vue 2 的支持最稳定,车间现场用的 PC 机配置普遍老旧,Vue 2 的兼容性更好,而且网上 Vue 2 的问题解决方案一搜一大把。如果你是新项目且团队对 Vue 3 熟悉,用 Vue 3 + Element Plus 也没问题,核心思路是一样的。
环境配置上,我通常用npm install -g @vue/cli安装脚手架,然后vue create mes-web初始化项目。这里强调一个坑:Node.js 版本和 npm 版本要匹配。如果你用 Node 18 配老版本 Vue CLI,经常会出现莫名其妙的依赖安装失败。我自己开发时用的 Node 14.21.3 + npm 6.14.18,整套依赖安装很顺。版本太高时如果 push 依赖有问题,可以用nvm切到低版本 Node 再 install。
还有一个 Vue 环境配置的核心文件是根目录下的vue.config.js,最关键的是publicPath配置。这直接关系到打包后的静态资源路径问题,很多人踩过“页面打开白屏,控制台一堆 404”的坑,95% 都是这个publicPath没配好。如果你部署在服务器根路径下,用publicPath: '/';如果你部署在子路径/mes/下,就必须写publicPath: '/mes/'。我见过太多人打包后布局异常、页面白屏,最后定位到这里。
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, publicPath: '/', outputDir: 'dist', assetsDir: 'static', devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8088', changeOrigin: true } } } })4.2 权限路由与Axios封装
MES 系统会有不同角色:系统管理员、计划员、车间操作工、质检员、厂长。权限控制我用的方案是:登录成功后后端返回当前用户的角色和菜单权限码,前端根据权限码动态生成路由,没有权限的页面直接不注册路由。这是目前最常用也最安全的动态路由方案之一,比前端写死路由再用指令控制按钮要安全很多。
路由分割方案是这样的:静态路由放 login 和 404 页面,动态路由按模块分成 plan、workorder、execute、quality、material、device、trace、system 八组,登录后根据权限码逐个过滤加入。这样即使有人强行访问某个路由地址,由于该路由压根不存在,会被重定向到 404 页,不会泄露任何页面内容。
Axios 封装也要说几句。我在 request.js 里统一做了三件事:请求拦截器自动带上 Token;响应拦截器统一处理后端 Result 结构;401 状态码自动跳转登录页。这样每次请求不用重复处理错误逻辑。车间现场网络偶尔不稳定,我在响应拦截器里还对超时做了处理,设置timeout: 30000,并且对网络错误给出中文提示,而不是浏览器默认的英文报错。
Token 存储我选择sessionStorage而不是localStorage。原因很实际:车间是多人共用电脑的场景,如果用了 localStorage,前一个人没退出的话下一个人进来还是他的登录态,容易串数据。sessionStorage 在浏览器关闭后自动清空,安全性更好。这个细节被很多人忽略,但在工厂这种共用设备场景下非常关键。
4.3 页面组件、动态表单与看板图表
前端页面开发有个核心原则:先封装公共组件,再写业务页面。MES 里大量页面是“表格 + 搜索区 + 新增/编辑弹窗”结构,如果每个页面都从头写一遍,代码量会大得可怕。
我封装了三个公共组件:ProTable(带搜索区、分页、操作列的表格)、ProDialog(支持表单校验的弹窗)、字典标签(把代码值翻译成中文,比如 status=2 显示“生产中”,同时用不同颜色标签区隔)。这三个组件覆盖了系统里 80% 的页面需求,后面开发新页面基本上就是配置化的活儿,效率提升特别明显。
车间看板页面是另一个重点。我用了 ECharts 实现生产实绩的柱状图、设备稼动率的仪表盘、工单完工率的环形图,然后用setInterval每 30 秒轮询一次后端接口刷新数据。看板页面要适配大屏,分辨率适配我用了vw/vh相对单位加上一个 scale 缩放方案,保证 1080P 和 4K 屏上显示都不变形。轮询刷新的时候注意一个问题:组件销毁时要清理定时器,否则页面切换后定时器还在跑,API 请求越积越多,浏览器内存飙升。
MES 里还有一种特殊的前端需求,就是在页面上播放在线监控视频流(比如车间摄像头回放),视频格式常是 HLS 的 m3u8。Vue 里我用的video.js配合videojs-contrib-hls插件来播放,兼容性不错。这个功能不是所有 MES 都需要,但确实在不少项目里会遇到,提前知道方案可以少走弯路。
5. 部署实操:从开发机到生产服务器
5.1 环境准备:JDK、MySQL、Node
部署环境我按最小化原则准备:一台 4核8G 的 Linux 服务器(CentOS 7 或 Ubuntu 20.04 都行),装好 JDK 1.8、MySQL 8.0、Nginx 1.20。Node.js 只在构建前端时需要,生产服务器不需要装。
JDK 安装比较简单,用yum install java-1.8.0-openjdk(CentOS)或apt install openjdk-8-jdk(Ubuntu)即可。安装后执行java -version确认。
MySQL 安装是新手翻车的重灾区。MySQL 8.0 的下载安装网上教程很多,我这里强调两个关键点。第一,推荐下载mysql-8.0.x-winx64.zip免安装版(Windows 环境)或者用yum/apt安装(Linux 环境)。第二,MySQL 8.0 默认的认证插件是caching_sha2_password,很多老版本客户端(比如 SpringBoot 里旧的 mysql-connector-java)是不支持的,连不上数据库。解决方案是安装最新的 MySQL Connector/J 8.0.x,或者在数据库里把用户认证改回 mysql_native_password:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;还有时区问题。MySQL 8.0 默认时区跟中国差 8 小时,如果不对会导致系统里所有时间字段都慢 8 小时,对制造业考勤和排产来说是致命错误。启动时加参数--default-time-zone='+8:00',或者在 JDBC 连接串上显式指定serverTimezone=Asia/Shanghai。
5.2 数据库初始化与后端启动
数据库初始化很简单,把项目里准备好的init.sql文件导入即可:
mysql -uroot -p < init.sql我的 init.sql 里包含了建库、建表、初始数据(默认管理员账号、基础字典数据)。生产环境我建议再单独执行一个init_data.sql存放基础数据,比如设备类型字典、不良原因代码、工位信息,和表结构脚本分开。这样做的好处是系统迭代升级时,表结构脚本和数据脚本各自演进,不会互相干扰。
后端打包命令:
mvn clean package -DskipTests打包完在target目录下生成mes-server.jar,上传到服务器/opt/mes目录下,直接启动:
nohup java -jar mes-server.jar --spring.profiles.active=prod > logs/mes.log 2>&1 &生产环境我强烈建议用systemd管理 Java 进程,而不是裸的 nohup。systemd可以在进程崩溃后自动拉起,还能开机自启。我写的 service 文件大概长这样:
[Unit] Description=MES Server After=network.target [Service] User=mesuser ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /opt/mes/mes-server.jar --spring.profiles.active=prod SuccessExitStatus=143 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target启动后看日志tail -f /opt/mes/logs/mes.log,出现 “Started MesApplication in …” 字样就说明启动成功了。然后用浏览器访问http://服务器IP:8088看看接口是否通了。
5.3 前端构建与Nginx部署
前端构建命令很简单:
npm install npm run build构建完成后在 dist 目录生成静态文件,把这个 dist 目录上传到服务器的/usr/share/nginx/html/mes目录下。然后配置 Nginx:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/mes; index index.html; location /api/ { proxy_pass http://127.0.0.1:8088/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这是前后端分离部署的标准配置,两个关键点:
/api/开头的请求反向代理到后端 Java 服务,这样浏览器请求的是同源的/api,就不会有跨域问题。try_files必须配置,否则你访问前端路由/workorder刷新页面时会 404,因为 Nginx 找不到这个文件。加了这条后,Nginx 会回退到 index.html,再由 Vue Router 接管路由。这个坑几乎所有人都会遇到,“Vue 打包后布局异常”有一半是这个原因。
前端还有个常见问题:打包后字体图标不显示、图片 404,基本就是publicPath配错。Vue CLI 默认publicPath是/,如果你部署到子目录/mes/,就必须把vue.config.js里的publicPath改成/mes/。我部署 MES 时经常挂在子路径下,因为客户服务器上可能还跑着其他系统,所以这个配置对我来说是家常便饭。
5.4 数据库连接、索引优化与备份策略
数据库连接串是部署时必须注意的配置项。我用的 JDBC 连接串:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mes_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: mes_user password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20allowMultiQueries=true这个参数很重要,MyBatis 的 XML 里如果想要一条 mapper 执行多条 SQL(比如一次删主表同时删子表),没有这个参数会直接报错。
索引对 MES 这种以查询为主的生产系统来说太关键了。报表数据量一大,查询慢得让人怀疑人生。我给关键表建索引的基本原则:查询条件里出现频繁的字段建单列索引,多条件联合的建复合索引。比如工单表对(status, product_code)建了复合索引,报工表对(work_order_no, process_code)建了复合索引。索引不是越多越好——每次插入更新都要维护索引,索引过多写性能反而下降。我的经验是只给“查询频率高、数据量大、字段区分度高”的列建索引。
数据备份是 MES 部署里绝对不能被省略的一环。制造业的生产数据是企业的核心资产,一旦损坏无法承受。我用 cron 每天凌晨执行一次全量备份:
0 3 * * * mysqldump -umes_user -p'密码' --single-transaction --quick mes_db | gzip > /backup/mes_db_$(date +\%Y\%m\%d).sql.gz find /backup -name "*.sql.gz" -mtime +30 -delete第二行是自动删除 30 天前的备份,避免磁盘被撑爆。
6. 排坑速查:实际调试中最容易翻车的5类问题
6.1 版本兼容与前端构建问题
SpringBoot 版本太高导致启动失败。SpringBoot 3.x 要求 JDK 17,而很多服务器上装的还是 JDK 8,强行跑会直接报 “UnsupportedClassVersionError”。我的做法是长期锁在 SpringBoot 2.7.x——它兼容 JDK 8,又能正常使用当前主流的功能,是目前企业项目里最稳的版本线。如果你从别处拿到的项目用了 SpringBoot 3.0+,先检查环境的 JDK 版本,再决定要不要降级。
MyBatis 映射文件没生效。启动后所有查询都报 “Invalid bound statement (not found)”,90% 是 mapper XML 的路径配置问题。检查两个位置:mybatis.mapper-locations是否覆盖到了 XML 文件目录;XML 文件里的 namespace 是否对上了 Mapper 接口的全限定类名。这个错误提示有时候不太直接,我排查时第一优先看 target 目录下有没有正确的 XML 文件。
Vue 打包后布局异常。最常见有两个原因。一是publicPath没配置对,打包后 CSS、JS、字体资源全部 404,整个页面只有 HTML 框架没有样式;二是打包后的资源路径带上了错误的绝对路径/,而你的应用部署在子目录下。解决方法就是改vue.config.js的publicPath重新打包。记住一句话:地址栏敲 IP 就能访问部署的目录路径,publicPath 就写什么路径。
Node 太高导致安装依赖失败。我踩过最典型的例子:Node 17 以上版本安装 node-sass 会报 “Error: Node Sass does not yet support your current environment”。解决方法是换sass替代node-sass(Dart Sass 是官方推荐的新实现),或者用 nvm 切回 Node 14。
MySQL 连接时报 Access denied。排除密码错误后,重点检查 MySQL 8.0 的认证插件版本。老驱动com.mysql.jdbc.Driver连 MySQL 8.0 大概率失败,必须换成com.mysql.cj.jdbc.Driver。另外还要注意 MySQL 用户的 host 授权,本地写localhost,远程就连不上,要授权成'user'@'%'才行。
MyBatis 动态 SQL 里对单个数字字符写<if>判断不生效,这个我前面已经讲过,本质是 XML 中<未转义,以及 java 类型转换的问题。在 XML 里凡是 SQL 中需要写<或>的地方,一律用<>转义,或者放到<![CDATA[ ]]>里包起来。
6.2 数据与业务层面的隐蔽问题
时间字段差 8 小时。这是中国开发者部署 MySQL 时几乎必踩的问题。MySQL 的CURRENT_TIMESTAMP存的是 UTC 时间,连接串里加serverTimezone=Asia/Shanghai就好,同时确保 Linux 服务器本身的时区也是 CST。部署完成后第一时间做一个“写入时间→查询时间”的往返测试,我用一条 SQL 就能验证:
SELECT NOW(), CURRENT_TIMESTAMP;如果查询结果和你手机上的时间一致,说明时区没问题。
批量写入效率极低。如果报工和物料管理的导入功能一次要写几千条数据,不要一条一条 insert。用 MyBatis 的<foreach>标签拼接批量 insert,或者直接用 JDBC 的rewriteBatchedStatements=true参数配合批量提交。实测相同的 3000 条数据,批量插入从 12 秒降到不到 1 秒——这个差距在车间现场体感非常明显,操作工会直接跟你抱怨“卡死了”。
工单报工并发重复提交。两个操作工同时给同一张工单报工,可能导致完工数量被覆盖而不是累加。解决方案是给报工接口加数据库乐观锁,用UPDATE wip_work_order SET completed_qty = completed_qty + #{qty} WHERE id = #{id}这种原子操作,而不是先查询再在 Java 层加法后更新。这类并发问题在做 MES 时必须提前想清楚,车间的操作场景天然就是多人并发、多终端同时提交。
接口查询慢。MES 报表模块经常要按时间范围、产线、产品等组合条件查询数万条记录,如果查询慢先看执行计划(EXPLAIN),确认是否走了索引。最常见的问题是查询条件里用了LIKE '%xxx%',这种写法索引会失效;或者多表 join 时关联字段没建索引。优化手段是尽量用前缀模糊查询、给关联字段建索引、大报表用离线统计表预聚合。
写在最后的一点经验
做 MES 系统,技术本身只是载体,真正的难点永远在业务理解上。这套前后端分离的 MES 系统,用的技术都是大家熟悉的 SpringBoot、Vue、MyBatis、MySQL,真正让它有价值的是这背后对车间生产流程的理解——工单怎么流转、报工怎么规范、质量怎么追溯,这些业务规则才是系统的灵魂。
我也建议每个准备做类似系统的朋友,拿到项目后先花至少 20% 的时间泡在车间或跟业务人员反复沟通,把流程画清楚了再动手。代码写错可以改,但流程设计错了,返工的成本非常高。技术选型和架构反而是最简单的那一步。
如果你准备照着这套方案自己搭一版,建议从工单管理这个核心模块入手,先把一条主线打通,再往四周扩展。过程中遇到任何问题,欢迎在评论区把报错信息发出来,我会尽我所能帮你排查。实话说,很多坑只有自己踩过一遍,感受才会真正深刻。