
简介一套面向制造业的开源MES系统设计源码采用Java为后端核心并整合Vue、JavaScript与HTML构建前端交互可覆盖订单管理、物料跟踪、生产调度、品质管理等生产全流程适合制造企业技术人员、Java开发者以及MES系统学习者参考。压缩包共588个文件核心包含264个Java源码、81个Vue组件、62个JS脚本及31个XML配置辅以SVG图标、SQL脚本、批处理工具等结构上按核心模块与系统模块划分便于按需阅读和二次扩展。已有515人学习/下载。通过该项目使用者可获取完整的前后端分离工程结构、模块化开发思路、数据库建表脚本以及一键构建部署脚本从而快速理解MES与ERP、PLC等系统的对接逻辑积累企业级Java项目实战经验。1. 开源制造执行系统MES为什么值得用Java Web重做一遍先看它解决谁的痛点车间里最贵的不是设备是设备闲着。工单靠Excel传来传去、报工靠下班补录、物料齐不齐没人说得准——这种现场一打听商业MES报价动不动几十万项目还没启动就黄了。开源制造执行系统MES给了另一个选项用Java和Web技术自己把生产执行层搭起来。标题里的“设计源码”本质是一套可拆分、可改、可二次开发的车间管理骨架覆盖工单下达、工序报工、物料追溯、看板展示。它适合两类人一是想低成本验证MES适不适合自己车间的IT负责人二是需要一个能跑通Web项目证明业务理解的Java工程师。下文按落地顺序讲选型、数据模型、最小可运行方案、常见坑。2. 开源MES的Java技术选型和架构从若依到Spring Boot骨架怎么搭2.1 为什么Java Web组合是MES的主流车间终端的免安装优势制造业车间环境很特别终端可能是Windows工控机、触摸屏一体机甚至还有不少老旧的浏览器环境。如果MES做成C/S客户端每一台设备都要装运行时版本更新要逐台去跑维护成本直接吞掉项目预算。做成Web项目只要浏览器能打开页面就能用升级只动服务器这是Java Web在MES里长期占据主流位置的根本原因。加上Java生态里Spring Boot对Web服务、定时任务、Excel导入导出、消息推送的支持都相当成熟招Java工程师比招小众语言好招得多企业拿到一套开源源码后也敢让自有团队接手。这里还涉及一个生产场景MES不是独立系统上游要接ERP拿工单和物料需求下游要接PLC、扫码枪、电子秤。Java在这些设备对接上有大量现成案例如果换一个小众技术栈光写串口对接就可能卡住两周。我和同行聊选型时默认第一句话都是能用Java Web就先用Java Web别在底层上挑战车间。2.2 若依框架做底座权限、代码生成、数据字典带来的开发边界很多开源的Java版MES并不是从零写起的而是在若依框架RuoYi这类后台管理框架上加业务模块。若依本身不是MES它提供用户、角色、菜单、部门、数据权限、定时任务、代码生成这些通用能力。把MES业务挂在上面等于先把框架层的活外包了自己只专注生产流程。这套组合在国内非常流行“基于若依框架的mes”也是从业者搜索最多的关键词之一因为它确实能少踩基础框架的坑。常见开源MES的代码目录长这样mes-project ├── mes-admin # 启动模块打包成jar ├── mes-framework # 框架核心安全、拦截器、权限 ├── mes-system # 系统管理用户、角色、菜单 ├── mes-business # MES业务模块二次开发写在这 │ ├── controller # 接口层参数校验和权限注解 │ ├── service # 业务逻辑状态机、库存校验 │ ├── domain # 实体类 │ ├── mapper # MyBatis的Mapper接口 │ └── resources/mapper # 业务SQL映射文件 ├── mes-common # 公共工具字符串、日期处理 └── mes-ui # 前端Vue Element组件这套分层的核心逻辑是mes-business依赖mes-framework和mes-common但不反向依赖将来升级框架版本时业务模块不用动。实际二次开发时最大的风险就是手痒去改mes-framework里的公共类一旦改了今后想合并上游新版本会冲突到怀疑人生。我一般只在mes-business和mes-ui里写业务公共模块基本不开刀。技术选型上前端用Vue和Element后端用Spring Boot加MyBatis数据库选MySQL。MySQL在中小车间足够用PostgreSQL也可以但开源项目多数默认MySQL换库要改方言不是不行只是没必要。Redis用来存验证码、登录状态、看板缓存。对单个车间、几百人同时在线的场景这套单体现在就够了。不要一上来就上微服务那套全家桶MES的核心是数据准确和链路完整不是架构够不够花哨。2.3 模块拆解基础数据、执行数据、追溯数据的分层MES从业务上可以切成五块源码设计时每一块的边界要清晰否则后面每加一个功能都要改三张表。模块核心数据做什么用设计时注意基础数据物料、工序、工艺路线、BOM告诉系统“车间在做什么、怎么做”必须支持Excel导入导入错误要给出行号生产计划工单、排产告诉系统“先做哪个、做多少”工单状态机要独立不能在Service里到处改status生产执行报工、质检、不良品记录采集现场实际完成情况报工防重是底线重复提交必须挡住物料追溯批次、出入库记录出了问题能追回源头批次号编码规则要提前定上线后改不动看板展示生产看板、设备状态把数据变成车间能看懂的页面刷新用Redis或WebSocket别让看板扫数据库这五块的依赖方向是单向的基础数据 - 生产计划 - 生产执行 - 物料追溯。看板依赖下面四块的聚合数据不要单独建大宽表。很多源码翻车就翻在追溯模块想建一张“万能表”把所有字段塞进去结果数据冗余、口径不一致。设计时宁可多join两张表也别造一个谁都不敢碰的怪物表。3. 核心模块落库把车间报工、工单、物料追溯拆成可运行的源码3.1 工单、报工、批次三张表建表SQL与字段选择MES源码的设计质量一半在表结构上。工单表是执行的核心单据报工表是现场采集的记录凭证物料批次表是追溯的源头。我通常先建这三张表-- 工单表车间执行的核心单据 CREATE TABLE work_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 工单号业务查询用, product_id BIGINT NOT NULL COMMENT 产品物料ID, plan_qty INT NOT NULL COMMENT 计划数量, completed_qty INT NOT NULL DEFAULT 0 COMMENT 累计合格数, scrap_qty INT NOT NULL DEFAULT 0 COMMENT 累计报废数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待下达 10已下达 20生产中 30已完工 40已关闭, plan_start_time DATETIME NULL COMMENT 计划开工, plan_end_time DATETIME NULL COMMENT 计划完工, actual_start_time DATETIME NULL COMMENT 实际开工, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_by VARCHAR(64) NULL COMMENT 创建人, create_time DATETIME NOT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT生产工单;-- 报工表现场完工记录的原始凭证 CREATE TABLE work_report ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, work_order_id BIGINT NOT NULL COMMENT 工单主键, process_id BIGINT NOT NULL COMMENT 工序ID, batch_no VARCHAR(64) NOT NULL COMMENT 物料批次号, report_qty INT NOT NULL COMMENT 报工合格数, scrap_qty INT NOT NULL DEFAULT 0 COMMENT 报工报废数, operator_id BIGINT NOT NULL COMMENT 操作员用户ID, device_id BIGINT NULL COMMENT 设备ID, report_time DATETIME NOT NULL COMMENT 报工时间, unique_key VARCHAR(128) NOT NULL COMMENT 幂等键, remark VARCHAR(255) NULL COMMENT 备注, create_time DATETIME NOT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_report_unique (unique_key), KEY idx_order_id (work_order_id), KEY idx_batch_no (batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工序报工记录;-- 物料批次表追溯的源头 CREATE TABLE material_batch ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, batch_no VARCHAR(64) NOT NULL COMMENT 批次号建议编码规则料号生产日期流水, material_id BIGINT NOT NULL COMMENT 物料ID, qty INT NOT NULL COMMENT 当前库存数量, qc_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待检 1合格 2不合格, in_time DATETIME NOT NULL COMMENT 入库时间, supplier_id BIGINT NULL COMMENT 供应商ID, PRIMARY KEY (id), UNIQUE KEY uk_batch_no (batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT物料批次库存;字段选择有几个值得展开讲。工单号必须独立业务唯一键因为生产管理部打印出来的表上只有order_no不会给你数据库主键。completed_qty和scrap_qty不走“先查再update”而是用version做乐观锁。报工表里的unique_key是防重的最后一道保险业务防重代码就算漏了数据库唯一索引也能挡住。批次号的编码规则要提前和仓库、采购对齐否则追溯时根本无法通过条码猜出是哪天哪条线生产的。3.2 报工接口的状态机流转与防重逻辑工单状态不能各处乱改必须收敛在Service层。报工接口是MES里调用最频繁、最容易出错的地方我一般这么写Transactional(rollbackFor Exception.class) public R workReport(WorkReportDTO dto) { // 1. 先锁工单拿到当前状态避免两个流程同时改状态 WorkOrder order workOrderMapper.selectByIdForUpdate(dto.getWorkOrderId()); if (order null) { return R.error(工单不存在); } if (order.getStatus() 0 || order.getStatus() 40) { return R.error(工单未下达或已关闭不能报工); } if (order.getStatus() 30) { return R.error(工单已完工请先做超报工审批); } // 2. 业务级防重同一工单同一工序同一报工时间窗只允许一次 String uniqueKey order.getOrderNo() : dto.getProcessId() : dto.getReportTime(); if (reportMapper.countByUniqueKey(uniqueKey) 0) { return R.error(检测到重复报工请刷新后确认); } // 3. 扣减批次库存并写库存变动流水 int remain batchMapper.selectQtyByBatchNoForUpdate(dto.getBatchNo()) - dto.getReportQty(); if (remain 0) { throw new BizException(批次[ dto.getBatchNo() ]库存不足扣减后不能为负数); } batchMapper.updateQty(dto.getBatchNo(), -dto.getReportQty()); batchLogMapper.insert(new BatchLog(dto.getBatchNo(), -dto.getReportQty(), dto.getOrderNo())); // 4. 插入报工单并累加工单完工数乐观锁 workReportMapper.insert(buildReport(order, dto)); int updated workOrderMapper.increaseCompletedQty(order.getId(), dto.getReportQty(), order.getVersion()); if (updated 0) { throw new BizException(工单已被其他人更新请重试); } return R.success(报工成功); }逻辑说明第一步用selectByIdForUpdate锁工单让同一工单的报工串行化这是单机部署下最直接的办法。第二步的uniqueKey是业务防重如果车间存在同一工序多人同时报工的情况可以改成“班次ID工序ID报工人ID”。第三步扣库存一定要加锁否则并发时库存会负。第四步走乐观锁如果工单在别处被改过increaseCompletedQty返回0就整体回滚避免完成数量覆盖丢失。参数说明WorkReportDTO来自前端表单reportQty必须以设备扫码或人工确认的合格数为准不能拿“计划数减已完成数”反推那样保存时非常容易出错。BizException是若依风格的通用业务异常抛出后事务回滚前端会收到统一的错误提示。3.3 追溯查询一条SQL把单号、批次、操作员串起来追溯的核心是“由结果反查过程”。车间来了客诉客户给的是出库批次号现场能扫的是产品条码技术部拿的是工单号所以查询入口至少要支持orderNo和batchNo两个条件SELECT wo.order_no, wo.product_id, mb.batch_no, mb.qc_status, wr.report_qty, wr.report_time, u.nick_name AS operator_name FROM work_report wr JOIN work_order wo ON wr.work_order_id wo.id JOIN material_batch mb ON wr.batch_no mb.batch_no LEFT JOIN sys_user u ON wr.operator_id u.user_id WHERE wo.order_no #{orderNo} OR mb.batch_no #{batchNo} ORDER BY wr.report_time DESC LIMIT 500;LEFT JOIN sys_user而不是INNER JOIN是因为用户可能离职被禁用LEFT JOIN能保证报工明细不丢。LIMIT 500是保险阀防止车间一次导出几十万条拖垮数据库全量导出应该走异步任务而不是在线接口。不要在这里去join排产表或BOM表追溯链路越短越容易排查真需要看“这批料是谁在什么时间投到哪道工序的”再单独加一道工序投料表关联别在一张SQL里写六张表。4. 在本地把开源MES跑通环境准备、配置修改和第一个页面请求4.1 环境清单与版本匹配别让版本把源码拦在门外拿到一套开源MES源码先别急着打包先把环境对齐。很多开源项目长期停留在Spring Boot 2.x对应JDK 8新的3.x要求JDK 17。版本错配最常见的报错是编译时提示Unsupported class file major version。我一般按这张表准备环境组件推荐版本说明JDK8或17以源码pom.xml为准编译报class version错误就是版本不匹配Maven3.6配置国内仓库镜像加快依赖下载MySQL5.7或8.0源码SQL按哪个版本写就用哪个别混用Redis5.x装好后先用redis-cli ping验证Node.js16或18前端构建npm版本别太新浏览器Chrome/Edge开源MES前端基本放弃老IE兼容这里最大的坑是“新版本强迫症”JDK直接用17但项目是Boot 2.x编译时一堆javax和jakarta命名空间报错或者npm装到20前端构建报OpenSSL错误。解决办法只有一个先打开pom.xml和package.json看版本再决定装什么不要凭感觉。4.2 一键启动后端依赖、建库、改配置、打包# 1. 拉源码并初始化数据库 git clone http://your-git-server/mes-source.git cd mes-source mysql -uroot -p sql/init.sql # 2. 改数据库和Redis连接 vim mes-admin/src/main/resources/application-druid.yml # 把url、username、password改成自己本机的 # 3. 打包并启动后端 mvn clean package -Dmaven.test.skiptrue java -jar mes-admin/target/mes-admin.jar逻辑说明init.sql通常包含建库语句和演示数据如果根目录没有sql目录去mes-admin的resources目录找。改配置文件只改application-druid.yml不要动application.yml里的公共配置因为后者可能被多个模块引用。-Dmaven.test.skiptrue跳过测试编译能省几分钟如果源码里测试类有环境依赖不跳过会在打包阶段失败。配置片段spring: datasource: druid: url: jdbc:mysql://localhost:3306/mes_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 password:参数说明url里的characterEncodingutf8必须保留少了它中文就变问号serverTimezoneAsia/Shanghai避免MySQL驱动报时区错误。Redis没设密码就留空本机调试可以生产环境至少要设一个高强度密码。启动成功日志会出现Tomcat started on port(s): 8080随后浏览器访问http://localhost:8080。如果端口被占用用java -jar mes-admin.jar --server.port80临时改端口不要改配置文件里的默认端口因为前端对接地址常常写在菜单配置里。4.3 前端跑起来开发模式和打包自托管两种选择cd mes-ui # 安装依赖node_modules很大等几分钟 npm install # 开发模式启动热更新 npm run dev开发模式适合改页面但它自带一套请求转发配置把/dev-api转到后端8080。生产环境更省心的做法是把前端构建产物塞进Spring Boot的static目录同一个端口发布既没有跨域也不需要额外配置Web服务器npm run build:prod cp -r dist/* ../mes-admin/src/main/resources/static/ mvn clean package -Dmaven.test.skiptrue java -jar mes-admin/target/mes-admin.jar这种自托管方式适合六百人以下的车间规模一个Tomcat完全扛得住。以后真要多车间分布式部署再把前端单独放出去做入口最小闭环阶段不搞复杂先把页面跑通再说。生产环境的Web服务器只监听80和443端口开启HTTPS别给车间开不必要的对外端口这是最容易忽略的web服务器安全问题。4.4 第一个接口请求验证前后端链路是不是通的登录后在工单管理打开一张工单按F12看Network。如果看不到工单请求说明前端路由或登录token有问题如果请求返回了但页面没渲染多半是实体类字段和前端camelCase没配对。用curl直接验证后端更纯粹curl -H Authorization: Bearer eyJhbGciOi... \ http://localhost:8080/system/workOrder/detail?orderNoMO20250701-001返回JSON里能看到planQty和status就说明后端、数据库、Redis这一条链路已经通了。开发阶段把控制台日志级别调到debugMyBatis会把每一条SQL打印出来定位问题比看业务日志快得多。登录进去后第一件事是去用户管理改掉默认管理员密码开源项目的默认账号太容易被人猜到了。5. 开源MES上线排雷五个绕不开的坑和它们的排查方法5.1 坑一前端页面刷新就404直连部署也逃不掉现象开发模式下一切正常打包部署后点菜单进详情页没问题一按F5刷新就白屏404。原因前端路由用的是history模式浏览器刷新时把请求发到了后端路径而后端没有返回index.html兜底。解决前端用hash模式可以当场规避但URL会带个#号车间标签打印、扫码访问时容易踩坑。更好的做法是后端加一个兜底转发把前端页面路由都指向index.htmlController public class WebForwardController { // 只兜前端页面路径接口路径不要进这里 RequestMapping(value {/, /production/**, /report/**, /quality/**}) public String forwardIndex() { return forward:/index.html; } }这里的production、report、quality要按源码里的前端路由前缀调整凡是/api或/system开头的接口请求都不走这个Controller否则会把正常接口转成HTML。加完重新打包刷新404就不会再出现。5.2 坑二并发报工把批次库存打成负数现象月底对账发现某批次库存是-5但车间说“平时没人乱动”。原因报工逻辑是“先查库存、减掉、再写库”两条产线同时报工时后面的update覆盖了前面的结果负数和少账都拦不住。解决库存扣减必须加锁。单体应用用select for update即可集群再用分布式锁兜底Transactional(rollbackFor Exception.class) public R consumeStock(StockConsumeDTO dto) { MaterialBatch batch batchMapper.selectByBatchNoForUpdate(dto.getBatchNo()); if (batch.getQty() dto.getConsumeQty()) { throw new BizException(批次库存不足); } batchMapper.updateQty(dto.getBatchNo(), -dto.getConsumeQty()); return R.success(); }Transactional开事务selectByBatchNoForUpdate拿到行锁事务提交后锁才释放。注意这种写法则只对单数据库实例有效多实例必须引入分布式锁否则forUpdate落到不同库节点就各自为政。另一个兜底是库存变更日志表里对同批次建唯一约束保证相同单据不能重复扣减。5.3 坑三BOM导入中文全变问号追查半天是字符集现象Excel里物料名称正常导入后页面显示????导出报表再变一次乱码。原因MySQL连接串没有指定UTF-8字符集或者解析Excel的入口把物料名里的逗号给“吃”了导入静默错行。解决先看数据库字符集再用连接串和表结构两面夹击ALTER DATABASE mes_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE material CONVERT TO CHARACTER SET utf8mb4;url: jdbc:mysql://localhost:3306/mes_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai给开发者的建议是Excel导入用Apache POI时用WorkbookFactory.create(inputStream)按文件真实类型解析不要自己拼CSV再split逗号否则物料名里带逗号时导入会静默错行。这个坑是最典型的“数据源正常但入口解析有问题”排查时先看日志里有没有字符转换异常再看数据库字段的collation。5.4 坑四菜单隐藏了但接口还能直接访问现象给质检员配了角色菜单里看不到“工单修改”但有人把URL复制发给其他人对方直接打开了修改页。原因菜单隐藏只控制前端显示后端接口没做权限校验等于把钥匙放在门边。解决在Controller方法上加权限注解PreAuthorize(ss.hasPermi(mes:workOrder:edit)) PutMapping(/workOrder) public AjaxResult edit(RequestBody WorkOrderDTO dto) { return workOrderService.save(dto); }mes:workOrder:edit是权限标识要同步到若依的菜单管理里给角色勾上才放行。二次开发时每次新增接口都补这个注解比事后审计省事。同一集团不同厂区共用一套系统时还要在Service层加DataScope按部门过滤避免越权看到别厂数据。5.5 坑五代码生成器覆盖了手工改写的Mapper XML现象用若依代码生成器生成新模块后旧功能报SQL语法错误打开XML一看文件被恢复成初始模板。原因生成器默认按模板覆盖同名文件手工加的查询条件全没了。解决把自定义SQL单独建目录mes-business/src/main/resources/mapper/custom/ WorkOrderCustomMapper.xmlMapper的扫描配置要同时包含mapper/**/*.xml和mapper/custom/*.xml然后在Service注入WorkOrderCustomMapper。这样代码生成器再生成多少遍也不会动到custom目录。这是二次开发时最值得养成的习惯也是很多MES源码后期维护翻车的重灾区。6. 从能跑到能数上线前验证MES源码的五个检查方法和一个随手技巧6.1 五个检查方法上线前不是只看功能清单而是验证“数据到底能不能对上”。我按这个顺序过状态机验证把一张工单模拟走完“下达-开工-报工-完工-关闭”每一步检查页面和数据库的status是否一致。最常发现的问题是完工后还能报工说明Service里少了状态拦截。防重验证连续双击报工按钮或者直接调两次同一unique_key接口第二次必须失败。如果库存被扣两次防重就是摆设。追溯验证拿一张真实批次号跑一条SQL确认能查到“哪张工单、谁、什么时候、报了多少合格数”。并发验证模拟两个线程同时扣同一批次库存看最终数是否等于预估值不通过就加锁。接口安全扫描用低权限账号直接访问接口URL确认每个修改操作都有PreAuthorize。6.2 随手技巧用SQL批量造数把追溯跑冒烟-- 把日期往前推90天每天造5张工单和对应的报工记录 INSERT INTO work_report (work_order_id, process_id, batch_no, report_qty, report_time, unique_key) SELECT wo.id, p.process_id, mb.batch_no, FLOOR(10 RAND() * 30), DATE_SUB(NOW(), INTERVAL n DAY), CONCAT(wo.order_no, -, p.process_id, -, DATE_SUB(NOW(), INTERVAL n DAY)) FROM work_order wo JOIN production_process p ON p.is_first 1 JOIN material_batch mb ON mb.material_id wo.product_id JOIN (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) nums LIMIT 100;跑完SELECT COUNT(*) FROM work_report看总量然后在追溯页面翻页看响应时间如果超过2秒就要检查是否缺了work_order_id索引或者SQL里join了不必要的表。造数不是乱造要让order_no、batch_no和真实业务一样有关联才能真正验证口径。我头一回给车间上线MES时就是没做这一步上去第二天发现追溯数据差两条车间主任当着全生产线面问我“你这系统到底能不能信”场面相当难忘。从那以后我改任何MES源码都先造数、再走全链路验证确认没问题才让车间用。希望帮到你。本文还有配套的精品资源点击获取