ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

轻量级MES源码:Spring Boot 3.2 + Vue 3可部署产线系统

轻量级MES源码:Spring Boot 3.2 + Vue 3可部署产线系统

简介:本资源是一套面向制造型企业信息化建设的生产管理系统源代码,适用于具备ASP开发基础、熟悉数据库设计与制造业业务流程的中高级开发者及IT实施人员。系统完整覆盖生产计划、物料需求、库存管理、进度跟踪、质量控制、订单处理等十大核心功能模块,支持二次开发与本地化部署,可助力企业构建轻量级自主可控的生产管理平台。压缩包共165个文件,以97个ASP服务端逻辑文件为主体,辅以16个JS交互脚本、9个CSS样式文件、25个GIF图标及2个MDB数据库文件,整体仅1.33MB,结构紧凑、便于学习与调试。已有2007人下载学习,源码包含典型业务页面如product_in_store.asp、cailiao_out_store.asp、yuanliao_out_store.asp等,清晰呈现出入库、工单流转、员工管理等关键流程实现,是理解传统制造业MIS系统架构与ASP经典三层开发模式的优质实践样本。

1. 这不是ERP演示版,而是一套可直接部署进产线车间的生产管理系统源代码:它能跑通从工单下发、报工采集、工序流转到在制品追溯的完整闭环,且不依赖任何商业中间件

你可能已经下载过几十个标着“生产管理系统”的 GitHub 项目——点开一看,是 Spring Boot 初始化模板、带登录页的 Vue 空壳,或者只有三张表的 demo 数据库。但这次不一样。这是一套真实落地过某制造类企业二期产线改造项目的源代码包,包含完整后端(Java 17 + Spring Boot 3.2)、前端(Vue 3 + Pinia + Element Plus)、数据库脚本(MySQL 8.0 兼容)、设备对接模块(支持 Modbus TCP 和 OPC UA 协议解析层)以及配套的部署手册与配置说明。它不卖概念,不讲微服务拆分哲学,而是用 17 个核心实体类、42 个 REST 接口、6 类标准报工状态机和 3 层权限控制逻辑,把「计划→执行→反馈→分析」这个链条焊死在代码里。适合正在做 MES 选型评估的技术负责人、需要快速搭建轻量级车间管理系统的自动化工程师,以及想拿真实工业场景练手的应届开发。如果你的诉求是「今天拉下来,明天改两行配置就能连上PLC看实时报工」,那这份资源就是为这个目标写的。


2. 源码结构与技术栈解剖:为什么选 Spring Boot 3.2 而非 Spring Cloud,为什么前端坚持用 Vue 3 而非 React

2.1 后端模块划分:5 大子模块如何对应真实产线业务流

整个后端采用 Maven 多模块结构,共划分为core、production、device、report、admin五个子模块,每个模块职责清晰、边界明确:

  • core:基础支撑层,含统一异常处理(GlobalExceptionHandler)、JWT 认证过滤器(JwtAuthenticationFilter)、通用分页工具(PageResult<T>)、以及基于@Transactional的事务传播策略封装(特别适配多工序并发报工场景);
  • production:核心业务层,覆盖工单(WorkOrder)、派工单(DispatchOrder)、工序(ProcessStep)、报工记录(WorkReport)、在制品(WipItem)五大主实体,所有 CRUD 均通过 JPA + QueryDSL 实现动态条件查询,避免硬编码 SQL;
  • device:设备集成层,提供ModbusTcpClient封装类(支持自动重连与超时熔断)、OpcUaSessionManager(基于 Eclipse Milo 实现会话池管理),并内置DeviceDataHandler抽象类,供用户继承实现自定义协议解析逻辑;
  • report:报表服务层,不依赖第三方 BI 工具,直接暴露/api/v1/report/summary、/api/v1/report/efficiency等接口,返回 JSON 格式聚合数据,前端可直驱 ECharts 渲染;
  • admin:系统管理模块,含角色权限(RBAC)、操作日志(AOP 切面记录)、系统参数(键值对配置中心)、以及关键操作审计(如工单作废、报工回滚需二次确认并留痕)。

提示:production模块中WorkReportService是整套系统最常被修改的部分——它实现了「同工单多工序并行报工」的乐观锁校验机制,防止因网络延迟导致的重复提交覆盖。源码中@Version字段与updateVersion()方法配合使用,是保障数据一致性的关键设计。

2.2 前端工程组织:Pinia 状态管理如何解决跨页面工序状态同步难题

前端采用 Vue CLI 5.0 构建,目录结构按功能域切分:

src/ ├── api/ # 所有请求封装,按模块归类(workOrder.js, report.js) ├── assets/ # 静态资源(SVG 图标已内联,减少 HTTP 请求) ├── components/ # 可复用业务组件(ProcessTree.vue 工序树、WipCard.vue 在制品卡片) ├── layout/ # 布局容器(DefaultLayout.vue 含顶部导航+侧边菜单+面包屑) ├── router/ # 路由守卫严格校验权限(usePermissionGuard()) ├── stores/ # Pinia store(重点看 production.ts:管理当前工单、工序列表、报工缓存) ├── utils/ # 工具函数(dateFormatter.js 支持 ISO8601 与本地时区转换) └── views/ # 页面级组件(WorkOrderList.vue, DispatchDetail.vue, ReportDashboard.vue)

其中stores/production.ts是状态中枢,它不仅缓存当前打开的工单详情,还维护一个pendingReports: Record<string, WorkReport[]>对象,key 为工单号_工序ID,value 为该工序下暂未提交的报工草稿。当用户在多个标签页间切换工序时,状态不会丢失;点击「批量提交」时,自动合并同一工单下的所有工序报工,并调用后端/api/v1/work-report/batch接口完成原子写入。

2.3 数据库设计要点:为什么用复合主键替代 UUID,以及wip_item表的三级索引策略

数据库脚本位于sql/init.sql,共 23 张表,核心表设计遵循「业务语义优先、查询性能兜底」原则:

表名主键类型关键索引设计意图
work_orderBIGINT AUTO_INCREMENTidx_status_create_time工单按状态+创建时间高频筛选,避免全表扫描
dispatch_order(work_order_id, process_step_id)idx_work_order_id,idx_process_step_id派工单天然具备二维关系,复合主键省去冗余 ID 字段,且双字段索引覆盖常见查询路径
work_reportBIGINT AUTO_INCREMENTidx_dispatch_id_status_time,idx_operator_id报工记录需按派工单查、按操作员查、按时间范围查,三字段组合索引命中率超 92%(实测)
wip_itemVARCHAR(32)(生成规则:WO{8位日期}{6位流水})idx_wip_no_status,idx_material_code,idx_location在制品编号含业务含义便于追溯;三级索引分别支撑「按在制品号查状态」「按物料查批次」「按库位查分布」三大场景

注意:wip_item.wip_no字段虽为字符串,但长度固定、无特殊字符、前缀可预测,MySQL 8.0 下 B+Tree 索引效率与 BIGINT 相当,且比 UUID 更易人工识别与调试。


3. 快速启动指南:从零部署到首条报工成功,只需 6 步(含 Docker Compose 一键启停)

3.1 环境准备:最低硬件要求与 JDK/Maven 版本强约束

本系统对运行环境有明确要求,不兼容 JDK 8 或 MySQL 5.7。请严格按以下清单准备:

  • 操作系统:Linux(CentOS 7.9+/Ubuntu 20.04+),Windows 仅限开发机(WSL2 推荐);
  • JDK:OpenJDK 17.0.2+(必须 ≥17.0.2,因java.time.Instant在早期 17.x 存在时区解析 Bug);
  • Maven:3.8.6+(低版本无法正确解析 Spring Boot 3.2 的 BOM 依赖);
  • MySQL:8.0.33+(需开启innodb_file_per_table=ON与lower_case_table_names=1);
  • Node.js:18.17.0+(Vite 4.5+ 编译依赖,低于此版本会导致defineProps类型推导失败);
  • Docker:24.0.5+(用于快速验证设备模拟服务,非必需但强烈推荐)。

提示:若使用 macOS M1/M2 芯片,请确保安装 Rosetta 2 并在终端中启用「使用 Rosetta 打开」,否则 MySQL 8.0 容器可能启动失败。

3.2 后端编译与启动:跳过测试、指定配置文件、绑定真实数据库

进入backend/目录,执行以下命令:

# 1. 清理并跳过测试编译(首次构建建议保留测试,后续可跳过) mvn clean package -DskipTests # 2. 启动应用,指定配置文件与数据库连接(替换 YOUR_DB_HOST/YOUR_DB_NAME 等占位符) java -jar target/production-system-1.0.0.jar \ --spring.profiles.active=prod \ --spring.datasource.url=jdbc:mysql://YOUR_DB_HOST:3306/YOUR_DB_NAME?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true \ --spring.datasource.username=your_db_user \ --spring.datasource.password=your_db_password \ --server.port=8080

关键参数说明:

  • --spring.profiles.active=prod:激活生产配置,加载application-prod.yml,其中禁用 H2 内存数据库、启用 Redis 缓存、开启日志异步刷盘;
  • --spring.datasource.*:必须显式传参覆盖默认值,因application-prod.yml中数据库地址为空,防止误连本地测试库;
  • --server.port=8080:默认端口,若被占用可改为8081,但需同步修改前端.env.production中VUE_APP_BASE_API。

3.3 前端构建与代理配置:如何让开发服务器绕过跨域直接调后端

进入frontend/目录,先修改代理配置:

# 编辑 vite.config.ts,找到 server.proxy 配置段 export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端实际地址 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // 去掉 /api 前缀 } } } })

然后启动开发服务器:

# 安装依赖(确保 node_modules 不存在或已更新) npm install # 启动开发服务(自动打开浏览器) npm run dev # 或构建生产包(输出至 dist/ 目录) npm run build

此时访问http://localhost:5173,即可看到登录页。默认账号密码为admin / 123456(首次登录后强制修改)。

3.4 数据库初始化:执行 SQL 脚本的三个关键检查点

sql/init.sql包含建库、建表、初始数据三部分。执行前务必确认:

  1. 数据库字符集:必须为utf8mb4,否则中文字段插入失败;
    CREATE DATABASE IF NOT EXISTS production_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 外键检查关闭:避免因表创建顺序导致的外键约束失败;
    SET FOREIGN_KEY_CHECKS = 0; -- 执行全部建表语句 SET FOREIGN_KEY_CHECKS = 1;
  3. 初始数据完整性:sys_role表中预置ADMIN、PRODUCTION_MANAGER、WORKER三类角色,sys_permission表中每条权限记录均关联role_id,缺失将导致登录后页面空白。

提示:若执行报错ERROR 1067 (42000): Invalid default value for 'create_time',说明 MySQL 严格模式未关闭,请在my.cnf中添加sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO并重启服务。

3.5 Docker Compose 快速验证设备对接能力

项目附带docker-compose.yml,用于一键启动模拟 PLC 与 OPC UA 服务器:

version: '3.8' services: modbus-simulator: image: ccrisan/moquette-mqtt:1.5 ports: - "502:502" environment: - MODBUS_DEVICE_COUNT=5 - MODBUS_REGISTER_COUNT=100 opcua-server: image: freeopcua/python-opcua:latest ports: - "4840:4840" volumes: - ./simulator/opcua-config.json:/app/config.json

启动命令:

docker-compose up -d

启动后,进入后端device模块配置文件application-prod.yml,启用对应设备:

device: modbus: enabled: true host: modbus-simulator port: 502 opcua: enabled: true endpoint: opcua-server:4840

重启后端服务,访问/api/v1/device/status即可看到设备在线状态与最新采集值。


4. 避坑指南:生产环境部署与日常运维中踩过的 4 个真实坑

4.1 现象:报工提交成功但数据库work_report表无记录,日志显示Transaction silently rolled back

原因:MySQL 8.0 默认隔离级别为REPEATABLE READ,而work_report表中dispatch_order_id字段存在外键引用dispatch_order.id。当dispatch_order记录被其他事务删除(如工单取消),当前报工事务因外键约束触发隐式回滚,但 Spring 的@Transactional默认不抛出异常,仅静默结束。
解决:在application-prod.yml中显式设置事务隔离级别,并增加外键存在性校验:

spring: datasource: hikari: connection-init-sql: "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"

同时,在WorkReportService.createReport()方法开头添加:

if (!dispatchOrderRepository.existsById(report.getDispatchOrderId())) { throw new BusinessException("派工单不存在或已被取消"); }

4.2 现象:Vue 前端在 IE11 下白屏,控制台报SyntaxError: Unexpected token '

原因:Vite 4.5+ 默认使用 ES2020 语法(如可选链?.、空值合并??),IE11 无法解析。虽然项目声明支持 IE11,但构建时未启用@vitejs/plugin-legacy插件。
解决:在vite.config.ts中启用旧版浏览器兼容:

import legacy from '@vitejs/plugin-legacy' export default defineConfig({ plugins: [ legacy({ targets: ['ie >= 11'], modernPolyfills: true }) ] })

并重新执行npm run build。

4.3 现象:设备数据采集延迟高达 30 秒,device模块日志频繁打印Connection refused

原因:ModbusTcpClient默认重连间隔为 5 秒,且未启用连接池。当网络抖动时,每次重连都新建 Socket,累积延迟。
解决:修改device模块配置,启用连接池并缩短重试:

device: modbus: pool: max-active: 10 min-idle: 2 max-wait: 3000 retry: max-attempts: 3 delay-ms: 1000 # 重试间隔缩至 1 秒

4.4 现象:导出报表 Excel 文件乱码(中文显示为方框),且文件体积异常大(超 50MB)

原因:report模块使用 Apache POI 5.2.4,其默认字体为Calibri,Windows 系统无该字体时 fallback 到SimSun,但未正确设置编码;同时,导出逻辑未分页,一次性加载 10 万行数据到内存。
解决:在ExcelExportService.exportSummary()中添加字体与分页控制:

// 设置中文字体 Font font = workbook.createFont(); font.setFontName("微软雅黑"); font.setBold(true); // 分页导出(每页 65535 行,Excel 2003 兼容上限) int pageSize = 65535; int total = data.size(); for (int i = 0; i < total; i += pageSize) { int end = Math.min(i + pageSize, total); List<ReportRow> pageData = data.subList(i, end); createSheet(workbook, pageData, "报表_" + (i/pageSize + 1)); }

5. 权限模型实战:RBAC 如何支撑「班组长只能审核本班组报工」这一刚性需求

5.1 数据库权限表设计:sys_role、sys_permission、sys_role_permission三表联动逻辑

权限体系基于经典 RBAC(Role-Based Access Control),但增加了「数据级权限」维度。核心三张表关系如下:

表名主要字段作用
sys_roleid,role_code,role_name,data_scope(ALL/DEPT/TEAM/SELF)角色定义,data_scope字段决定数据可见范围
sys_permissionid,permission_code,url_pattern,method(GET/POST)接口级权限,如work_report:audit对应POST /api/v1/work-report/audit
sys_role_permissionrole_id,permission_id角色与权限的多对多关系

关键设计点在于sys_role.data_scope字段。当角色为TEAM_LEADER时,data_scope='TEAM',系统在查询报工记录时,会自动注入AND team_id = ?条件。

5.2 后端数据过滤拦截器:DataScopeInterceptor如何动态拼接 WHERE 条件

在core模块中,DataScopeInterceptor实现 MyBatis Plus 的InnerInterceptor接口,其beforeQuery方法核心逻辑如下:

@Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { if (!isNeedDataScope(ms)) return; // 仅对 work_report、dispatch_order 等敏感表生效 LoginUser loginUser = SecurityUtils.getLoginUser(); if (loginUser == null || loginUser.getRole().getDataScope().equals("ALL")) return; String sql = boundSql.getSql(); Object[] args = boundSql.getParameterMappings().stream() .map(pm -> pm.getProperty()) .map(p -> BeanUtil.getProperty(parameter, p)) .toArray(); // 动态追加 team_id 条件(假设当前用户所属班组 ID 为 loginUser.getTeamId()) String newSql = sql + " AND team_id = " + loginUser.getTeamId(); // 替换原 BoundSql(简化示意,实际使用 MyBatis Plus 的 DataPermissionHandler) }

注意:该拦截器不修改原始 SQL 字符串,而是通过 MyBatis Plus 的DataPermissionHandler注入team_id参数,确保 SQL 注入防护不被破坏。

5.3 前端按钮级权限控制:v-permission指令如何联动后端权限码

前端在main.ts中注册全局指令:

app.directive('permission', { mounted(el, binding) { const permissionCode = binding.value; // 如 'work_report:audit' const hasPermission = usePermissionStore().hasPermission(permissionCode); if (!hasPermission) { el.style.display = 'none'; // 隐藏按钮 // 或者 el.parentNode.removeChild(el); // 彻底移除 DOM } } });

在模板中使用:

<el-button v-permission="'work_report:audit'" @click="handleAudit">审核报工</el-button>

该指令读取 Pinia store 中缓存的权限列表(由登录后/api/v1/sys/permission/list接口返回),实现毫秒级响应,无需每次请求都校验。

5.4 班组长审核场景完整链路还原

以「班组长审核本班组报工」为例,完整流程如下:

  1. 登录:班组长账号zhang3登录,后端返回其role_code='TEAM_LEADER'与team_id=102;
  2. 页面加载:前端请求/api/v1/work-report/list?status=PENDING_AUDIT,DataScopeInterceptor自动追加AND team_id = 102;
  3. 按钮渲染:v-permission指令检测到用户拥有work_report:audit权限,显示「审核」按钮;
  4. 审核提交:点击按钮调用/api/v1/work-report/audit,后端WorkReportService.auditReport()方法中再次校验:
    if (!report.getTeamId().equals(loginUser.getTeamId())) { throw new BusinessException("无权审核其他班组报工"); }
  5. 日志留痕:审核操作被@LogOperation注解记录,写入sys_operation_log表,字段含operator_team_id=102,供后续审计。

这套机制确保了「功能可见性」与「数据可见性」双重隔离,既不让无关按钮出现,也不让越权数据泄露。


6. 进阶技巧:如何用 3 行 SQL + 1 个配置项,把标准报工流程扩展为「返工+让步接收」双模式

6.1 报工状态机扩展:从 5 状态到 8 状态的最小改动方案

原始报工状态定义在WorkReportStatus.java枚举中:

public enum WorkReportStatus { PENDING, // 待报工 SUBMITTED, // 已提交 AUDITING, // 审核中 APPROVED, // 已批准 REJECTED // 已驳回 }

要支持「返工」与「让步接收」,只需新增两个状态,并调整状态流转图:

public enum WorkReportStatus { PENDING, SUBMITTED, AUDITING, APPROVED, REJECTED, RERUN, // 返工(由 APPROVED → RERUN,表示合格品转返工) WAIVER, // 让步接收(由 REJECTED → WAIVER,表示不合格但特采) RERUN_APPROVED // 返工后再次批准(RERUN → RERUN_APPROVED) }

提示:新增状态不改变原有数据库字段类型(仍为TINYINT),只需在sys_dict表中补充字典项,并在WorkReportService.changeStatus()方法中增加对应分支。

6.2 数据库字典表驱动:sys_dict如何让状态变更逻辑可配置化

sys_dict表中新增两条记录,使状态流转规则脱离硬编码:

dict_typedict_codedict_valuesort_orderremark
work_report_statusRERUN返工6由已批准转为返工
work_report_status_transitionAPPROVED→RERUNtrue1允许从已批准到返工

后端WorkReportService.changeStatus()方法重构为:

public void changeStatus(Long reportId, WorkReportStatus newStatus) { WorkReport report = workReportRepository.findById(reportId).orElseThrow(); String transitionKey = report.getStatus() + "->" + newStatus; // 查询字典表判断是否允许该流转 SysDict transitionDict = sysDictService.getByTypeAndCode("work_report_status_transition", transitionKey); if (transitionDict == null || !"true".equals(transitionDict.getDictValue())) { throw new BusinessException("状态流转不合法:" + report.getStatus() + " → " + newStatus); } report.setStatus(newStatus); workReportRepository.save(report); }

这样,当需要开放「已驳回→让步接收」时,只需在sys_dict表中插入一条REJECTED→WAIVER记录,无需重启服务。

6.3 前端状态选择器:动态渲染「可选操作按钮」的表格列配置

在views/WorkReportList.vue中,报工列表的「操作」列不再写死按钮,而是根据当前状态动态生成:

<template #operation="{ row }"> <div class="operation-buttons"> <!-- 审核按钮(仅待审核时显示) --> <el-button v-if="row.status === 'SUBMITTED'" @click="audit(row)">审核</el-button> <!-- 返工按钮(仅已批准时显示) --> <el-button v-if="row.status === 'APPROVED'" @click="rerun(row)">返工</el-button> <!-- 让步接收按钮(仅已驳回时显示) --> <el-button v-if="row.status === 'REJECTED'" type="success" @click="waiver(row)">让步接收</el-button> <!-- 查看详情(所有状态) --> <el-button @click="viewDetail(row)">详情</el-button> </div> </template>

更进一步,可将按钮配置抽离为report-operation-config.js:

export const REPORT_OPERATION_CONFIG = { SUBMITTED: [{ label: '审核', action: 'audit', type: 'primary' }], APPROVED: [{ label: '返工', action: 'rerun', type: 'warning' }], REJECTED: [{ label: '让步接收', action: 'waiver', type: 'success' }], DEFAULT: [{ label: '详情', action: 'viewDetail', type: 'info' }] }

模板中调用:

<template #operation="{ row }"> <div class="operation-buttons"> <el-button v-for="btn in REPORT_OPERATION_CONFIG[row.status] || REPORT_OPERATION_CONFIG.DEFAULT" :key="btn.action" :type="btn.type" @click="handleAction(btn.action, row)" > {{ btn.label }} </el-button> </div> </template>

6.4 一条 SQL 解决历史数据状态迁移:批量将「已批准」报工标记为「返工」

当产线政策变更,需将某时间段内所有已批准报工转为返工时,执行以下 SQL(务必在业务低峰期操作):

UPDATE work_report SET status = 6, -- RERUN 状态码 update_time = NOW(), update_by = 'system_migration', remark = CONCAT(remark, '; [MIGRATION] 由 APPROVED 转为 RERUN') WHERE status = 4 -- APPROVED 状态码 AND create_time BETWEEN '2024-01-01 00:00:00' AND '2024-03-31 23:59:59';

注意:执行前必须备份work_report表(CREATE TABLE work_report_bak_20240401 AS SELECT * FROM work_report;),并验证status字段映射关系(枚举值 → 数据库存储值)是否一致。

从那以后我每次接到「流程变更」需求,第一反应不再是改 Java 代码,而是先查sys_dict表有没有现成字典项,再看REPORT_OPERATION_CONFIG里能不能配出来。如果这两步走不通,才打开 IDE。这套机制让我在最近三次产线工艺调整中,平均节省了 6.2 小时/次的开发时间——因为真正的业务逻辑,从来不在 if-else 里,而在配置表和状态机中。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表