ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue+MyBatis+MySQL:车间管理系统从设计到上线的完整实践

SpringBoot+Vue+MyBatis+MySQL:车间管理系统从设计到上线的完整实践 做了几个工厂内部管理系统之后我越来越觉得SpringBootVueMyBatisMySQL这套组合在实际车间场景里是真好用。最近完整落地了一个前后端分离的工厂车间管理系统从工单下发、工序报工、设备点检到数据看板全部打通源码结构和部署文档也整理干净了。这篇文章我就把这个项目从设计到上线的全过程摊开讲重点是哪些地方容易想当然、哪些地方必须抠细节包含数据库表设计、MyBatis动态SQL、Vue权限路由、MySQL安装配置、打包部署以及我实际踩过的坑。1. 项目整体设计与架构拆解1.1 为什么选前后端分离为什么是这套技术栈车间管理系统和普通的后台管理系统不太一样使用角色很杂车间主任要排产操作工要报工质检员要录入不良品维修工要登记设备状态老板还希望办公室大屏能实时看到产量。每个角色的操作界面差异很大如果还用传统单体模板渲染大概率会变成一坨互相耦合的页面。前后端分离最大的价值是把接口和页面彻底分开。后端只用负责业务逻辑和返回JSON前端按角色拆页面、按权限控制菜单两边可以独立开发、独立部署。这样车间大屏、PDA扫码端、电脑端以后都能复用同一套API不用重写后端。代价是要处理跨域、联调、部署转发这些事但比起后续扩展省下的时间这点成本完全可以接受。技术栈选SpringBootVueMyBatisMySQL说穿了是图它稳。SpringBoot自动配置和内嵌Tomcat让后端启动非常省事Vue组件化开发特别适合车间里那些表单密集、状态切换频繁的页面MyBatis能写灵活SQL工厂里的统计报表基本都是多表关联、条件不固定用MyBatis的XML比JPA那种自动生成SQL的方式可控得多。MySQL免费部署简单中小型工厂完全够用。如果你以前接触过若依这类前后端分离脚手架会发现核心套路和这个项目是一脉相承的。1.2 功能模块拆解与数据库表设计这个车间管理系统我拆成了七个模块。系统管理用户、角色、菜单权限。这是所有系统的地基没必要重复造轮子但一定要有。基础资料产品、物料、工序、设备台账。基础数据不规范化后面所有单据都会乱。生产管理工单创建、派工、工序流转、报工。这是核心中的核心。设备管理设备点检、保养计划、维修记录。物料管理原材料出库、半成品入库、库存查询。质量检验检验任务、不合格记录、不良率统计。数据看板今日产量、设备状态、不良率趋势。数据库设计上我直接说几个关键表。工单表work_order必须有id、工单号order_no唯一索引、产品ID、计划数量、已完成数量、状态字段status0待排产、1生产中、2已完成、3已取消、计划开始时间、计划结束时间、创建人、创建时间。工序流转表process_record要记录工单ID、工序ID、设备ID、操作工ID、开始时间、结束时间、投入数量、产出数量、不良数量、备注。设备表equipment要记录设备编码、设备名称、状态0停机、1运行、2维修、所在位置、上次保养时间、下次保养时间。这里有几个数据库设计上的经验。数量字段必须用decimal(10,2)千万不要用float生产数据对精度要求高。状态字段用tinyint加注释业务枚举在Java里管理不要直接在表里存中文不然报表分组和条件筛选都会很难受。时间字段统一用datetimeJava侧用LocalDateTime接收。所有外键字段一定要加索引工单号、设备编码这些业务编号做唯一索引。数据库排序规则选择也很重要MySQL 8.0默认utf8mb4_0900_ai_ci但如果你还有5.7的环境建库建表就要统一指定utf8mb4_general_ci否则两边导数据会出现排序规则冲突。2. 后端核心实现与MyBatis实战2.1 SpringBoot工程结构、版本选择和基础配置后端工程结构我用的是最标准的Controller-Service-Mapper分层com.factory ├── config // 跨域、拦截器、分页配置 ├── controller // 接口层 ├── service // 业务层 │ └── impl ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体 ├── vo // 视图对象 ├── common // 统一返回结果、异常、枚举 └── utils版本选择上我特别想提醒一句SpringBoot版本不是越高越好。新项目如果直接上SpringBoot 3.x会遇到包名从javax换成jakarta的问题网上大量老教程和封装好的代码直接编译不过。这个项目我选的是SpringBoot 2.7.18这是2.x系列最后一个版本稳定且兼容性最好配套的MyBatis starter也要用2.3.x别乱升。如果你以后确实要升级SpringBoot 3重点检查两个地方所有javax.*改jakarta.*数据库驱动从mysql-connector-java换成com.mysql:mysql-connector-j。核心依赖片段如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml里几个配置值得关注。数据源URL一定要带useSSLfalse和serverTimezoneAsia/Shanghai否则会出现SSL连接错误和日期差8小时的问题。MyBatis开启驼峰映射和SQL日志打印spring: datasource: url: jdbc:mysql://localhost:3306/factory_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这一项不开的话order_no这种字段映射到orderNo会直接失败。log-impl用StdOutImpl调试时非常直观上线前再关掉不然日志量太大。2.2 MyBatis动态SQL如何应对车间复杂查询车间系统的查询条件几乎每个页面都是不固定的。工单列表要支持按工单号模糊查、按状态过滤、按计划开始时间范围查、按产品查甚至还要按当前所在工序查。这种场景用MyBatis动态SQL最合适也就是where加if的组合。看一段工单列表查询的Mapper XMLselect idselectOrderList resultTypecom.factory.vo.OrderVO select o.id, o.order_no, p.product_name, o.plan_qty, o.done_qty, o.status, o.plan_start_time, o.plan_end_time from work_order o left join product p on o.product_id p.id where if testorderNo ! null and orderNo ! and o.order_no like concat(%, #{orderNo}, %) /if if teststatus ! null and o.status #{status} /if if teststartTime ! null and o.plan_start_time gt; #{startTime} /if if testendTime ! null and o.plan_end_time lt; #{endTime} /if /where order by o.create_time desc /select注意XML里和不能直接写要转义成gt;和lt;或者用CDATA包起来。动态SQL的where标签会自动处理开头的and所以不需要在第一个条件后面加11那种丑写法。关于MyBatis缓存我想多说几句。一级缓存默认是开启的作用在同一个SqlSession内Spring管理下通常就是一次事务内一般问题不大。二级缓存是按namespace开启的看似能提升查询性能但车间系统数据实时性要求很高工单状态、设备状态随时在变如果开了二级缓存又没配好刷新间隔很可能出现报表数据还是几分钟前的情况。我的建议是车间管理这种写多读少、实时性敏感的业务不要开二级缓存。真到了要扛并发的时候优先考虑Redis而不是在MyBatis缓存上做文章。这其实也是面试时候我常被问到的一个点一级缓存和二级缓存的作用范围、失效时机以及为什么订单库存类系统不适合开二级缓存。事务方面有一个很隐蔽的坑。Spring的Transactional默认是私有方法不生效、同类内自调用不生效。比如某个Service里有一个方法调用了本类另一个标注了Transactional的方法事务是失效的。因为这个调用没有经过Spring代理对象。我一般把事务拆到独立方法或者用TransactionTemplate来处理宁可多写两行代码也不留一个事后才发现的数据风险。3. 前端Vue开发与交互细节3.1 Vue工程搭建、路由设计和权限控制Vue侧我用了Vue 2.7加Element UI。我知道现在新项目好多人直接上Vue3和Vite但如果你是在维护老项目或者网上找的资料大多是2.x的Vue 2.7反而是更平滑的选择。Vue版本本身不复杂真正麻烦的是环境和路由权限设计。Vue安装和环境配置有几个老生常谈的坑Node版本不要太新也不要太老我建议用16.x或18.x的LTS版本npm安装依赖慢可以换国内镜像源但这点要注意公司网络环境装依赖的时候如果报node-sass相关错误八成是Node和node-sass版本不匹配换成sass或者升级对应的包就能解决。路由设计上车间系统不同角色看到的菜单必须不同。车间主任能看到工单管理和排产操作工只有报工页面设备维修员只有设备管理。最合理的方案是登录后根据后端返回的菜单数据动态注册路由const router new VueRouter({ mode: history, routes: [ { path: /login, component: Login }, { path: /, component: Layout, children: [] } ] }) // 登录成功后根据角色菜单动态添加路由 menuList.forEach(item { router.addRoute(Layout, { path: item.path, component: () import(/views/${item.component}), meta: { title: item.title } }) })动态路由这里有个大坑() import(/views/${item.component})这种写法在打包时未必能正确解析因为Webpack需要静态分析动态导入路径。常见解决方案是用require.context把所有views下的组件都读进来或者维护一个组件映射表。我实际做的时候是用组件映射表写起来稍微啰嗦但非常稳再也不用担心部署到服务器后动态加载404。权限控制除了路由还要有按钮级的控制。比如操作工不能看到“作废工单”按钮技术人员不能看到“删除设备”按钮。我是在登录接口把当前用户的权限标识列表一起返回封装一个v-permission指令去校验没有权限的直接移除DOM。这种方式比单纯控制路由更符合车间系统的实际需求。3.2 组件化页面与Axios封装车间大屏怎么做页面组件化我觉得是这段项目里收益最明显的事。工单列表页、设备状态面板、工序进度条、报工弹窗这些组件在好几个页面都要用。我把它们全部抽成公共组件接口数据通过props传进去组件内部不直接请求后端。这样后续改样式、改逻辑只需要动一个文件。Axios封装是前后端分离里必须做的一步。我这边是统一在request.js里创建实例请求拦截器自动带Token响应拦截器统一处理后端返回的特定结构。遇到code 401就跳登录页网络超时给一个明确提示业务异常不再在每个页面写catch。车间大屏是这个项目里比较出效果的模块。我的方案是在Vue页面里挂一个定时器每30秒向/dashboard/summary请求一次数据返回今日产量、完成工单数、设备运行数、不良率然后用ECharts渲染柱状图和饼图。这里要特别提醒不要在组件销毁时不清理定时器否则页面切走之后还在请求浏览器标签开多了会卡死。大屏刷新频率也不要太激进30秒对车间管理完全够用再快的刷新很容易把数据库打慢。4. 部署上线与MySQL环境准备4.1 MySQL安装配置与连接细节部署阶段最容易出问题的往往不是Java代码而是MySQL环境。MySQL安装教程网上很多我简单说最关键的几个点。Windows上安装MySQL 8.0时安装类型选Server only字符集直接选utf8mb4。安装完成后第一步要确认服务是不是自启动第二步用root登录后创建一个专用账号不要所有应用都用root。建库和授权命令示例CREATE DATABASE factory_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER factory% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON factory_db.* TO factory%; FLUSH PRIVILEGES;这里有一个容易踩的坑JDBC连接串里的useSSLfalse。MySQL 8默认启用SSL如果Java驱动和服务器证书校验失败会报类似“SSL connection error”的问题。关掉SSL解决本地项目没必要走加密连接。另一个坑是serverTimezoneAsia/Shanghai不能漏否则Java查出来的时间比数据库实际时间晚8小时。排序规则的问题前面提过建库的时候想清楚你的生产环境MySQL版本避免开发库和生产库排序规则不一致导致数据迁移报错。如果Linux服务器上部署MySQL的安装方式又有区别推荐用官方Yum仓库或者解压安装关键是初始化后要修改root密码和开放3306端口。很多新手部署后本地连不上数据库第一反应是改防火墙实际上要先确认MySQL的bind-address是0.0.0.0而不是默认的127.0.0.1以及mysql.user表里对应账号的Host是否允许远端访问。factory%里的%就表示允许任意IP访问。还有一点关于MySQL慢查询。车间系统上线一段时间后工单表和流程记录表的数据会快速增长如果某些报表SQL没走索引查询会越来越慢。调试阶段直接打开慢查询日志定位到具体的SQL再通过EXPLAIN看执行计划。4.2 前后端打包与两种部署方案后端打包很简单在项目根目录执行mvn clean package -DskipTests然后得到factory-system.jar直接运行java -jar factory-system.jar前端打包执行npm run build生成dist目录。到这一步就有两种部署路径。第一种是分开部署前端静态文件交给Nginx后端单独跑SpringBoot。Nginx只需要做两件事location/指向dist目录并支持Vue Router的history模式刷新不404接口请求统一转到后端的8080端口。关键配置如下location / { root /opt/factory/dist; try_files $uri $uri/ /index.html; }接口转发规则我把具体配置省略掉核心思路是让所有以/api开头的请求都转发到http://127.0.0.1:8080前端开发环境里也是一样的思路。这种部署方式好处是前后端可以独立重启改前端页面不用动后端进程。第二种是合并部署把dist里的全部文件复制到SpringBoot的src/main/resources/static目录下然后重新打包成一个jar。这种方式单人维护非常方便一个jar包搞定所有特别适合车间没有专职运维的情况。界面和后端混在一起适合把Vue打包放进SpringBoot里跑。代价是以后每次改前端都得重新打包而且如果Vue路由用了history模式刷新页面时SpringBoot需要配置把非接口路径都转发到index.html不然会404。所以我更推荐生产环境独立部署但开发阶段或者给了客户一套演示环境时合并打包反而是最省心的。5. 高频问题与排查技巧实录5.1 常见问题速查表现象可能原因解决办法接口返回中文乱码数据库连接串没指定编码或者建库字符集不是utf8mb4URL加characterEncodingutf8建库用utf8mb4数据库时间比本地晚8小时JDBC连接serverTimezone未设置连接串加serverTimezoneAsia/Shanghai启动报ClassNotFoundException: javax.servletSpringBoot3.x包名变更用2.7.x版本或把javax迁移到jakartaMapper接口找不到SQLmapper-locations配置不对或XML的namespace错误检查application.yml和XML里的namespace数据库字段order_no映射到Java对象是null没开驼峰映射map-underscore-to-camel-case: true前端页面刷新后404Vue Router开启history模式但服务器没配置Nginx配置try_files $uri $uri/ /index.html;前端跨域报错前后端分离后域名或端口不一致后端允许跨域或开发环境配置接口转发工单查询明明有索引还是很慢条件字段没走索引或SQL里用了函数用EXPLAIN分析避免%开头的模糊查询报工并发数据异常两个操作工同时提交报工导致覆盖增加版本号字段update带乐观锁条件5.2 不写在文档里的私房经验整个项目做完我最大的感受是系统能不能真正用起来不在于技术多花哨而在于基础逻辑是否稳。车间报工这种操作随时有几个工人同时点提交数据库层的并发控制一定要做。我用了乐观锁在work_order表加version字段更新时UPDATE work_order SET done_qty done_qty #{addQty}, version version 1 WHERE id #{id} AND version #{oldVersion}如果更新影响行数为0说明这个工单被别人改了前端重新拉取数据再提示用户。这种做法简单可靠比锁表强太多。所有状态字段在项目里不要散落魔法数字。我统一用Java枚举类管理工单状态、设备状态数据库中只存tinyint代码里通过枚举转换描述文本。这样以后加状态、加流程判断不用满项目搜数字。数据库脚本一定要单独维护一份SQL文件不能只依赖代码初始化。车间系统上线后生产环境数据库经过多次手工调整同一个work_order表结构可能和开发库已经不完全一样了。遇到问题排查时打开两份表结构对比往往能快速定位问题。这也算是换服务器、准备演示环境时最省事的保障。最后说一个部署层面的小细节SpringBoot默认启动Banner很占日志空间但是并不碍事。如果你想给客户留下专业印象可以自定义Banner网上有不少在线生成工具可以直接用。部署时记得在启动参数里指定环境配置比如--spring.profiles.activeprod把数据库密码这些敏感信息放到配置中心或者环境变量里不要直接写在工程里。车间管理系统虽然不是什么高并发巨复杂的系统但保证稳定和可维护性才是真正能长期跑下去的关键。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进