用 Spring Boot + Three.js + Vue3 构建简单的仓库数字孪生系统

用 Spring Boot + Three.js + Vue3 构建简单的仓库数字孪生系统
🧭 本文属于专栏《Java × 工业智能》第 12 篇 | GitHub 源码:github.com/iweidujiang/java-industrial-smart

在工业场景中,仓库环境的监控是确保存储安全的关键:

  • 粮食仓库:如何实时监控不同仓的温湿度变化?
  • 药品仓库:如何确保存储环境符合标准?
  • 物流仓库:如何快速了解各区域的环境状态?

本篇将用 Spring Boot + Three.js 实现一个简单的仓库数字孪生系统,模拟实际工业智能场景的解决方案,实现:

  • 仓库鸟瞰图的 3D 可视化
  • 点击仓库显示实时温湿度信息
  • 基于 WebSocket 的数据实时传输
  • 支持场景旋转查看

一、简单介绍

1、核心特征

特征说明反例
直观可视化用 3D 场景直观展示物理空间仅数字表格,无空间展示
实时数据物理环境数据与数字镜像实时同步静态展示,无数据更新
交互性支持用户与数字孪生的简单交互(如点击查看)仅被动展示,无用户交互
易于部署技术栈简单,部署便捷复杂依赖,部署困难

简单说:本系统 = 3D 仓库鸟瞰图 + 实时温湿度数据 + 点击交互

2、为什么需要数字孪生系统?

在仓库管理中,传统的温湿度监控存在以下问题:

  • 分散:温湿度数据散落在不同的仪表或系统中
  • 抽象:数字表格难以直观理解环境状态
  • 滞后:无法实时反映环境变化
  • 复杂:大型系统部署成本高,维护困难

数字孪生系统,让仓库监控变得一目了然。

二、技术架构与实现方法

1、系统结构

本系统采用前后端分离的架构,通过 WebSocket 实现实时数据传输,具体架构如下:

说明

  1. 前端:使用 Three.js 实现 3D 仓库鸟瞰图,通过 WebSocket 接收后端推送的温湿度数据
  2. 后端:基于 Spring Boot 实现 WebSocket 服务器,负责采集和推送传感器数据
  3. 数据层:使用模拟传感器生成温湿度数据,模拟实际工业场景
  4. 通信:通过 WebSocket 实现前后端实时数据传输,确保数据同步

这种设计的优势在于:

  • 实时性:WebSocket 长连接确保数据实时传输
  • 简单性:技术栈简单,易于部署和维护
  • 可扩展性:后续可轻松接入真实传感器或其他数据源

2、技术栈选择

层次技术版本用途
后端Spring Boot3.5.10提供 WebSocket 服务和传感器数据
WebSocket-实时数据传输
前端Three.js0.160.03D 场景渲染和仓库模型展示
JavaScriptES6+前端业务逻辑
传感器模拟传感器-生成温湿度数据

3、实现路径

  1. 数据层:模拟仓库温湿度传感器数据
  2. 通信层:通过 WebSocket 实时传输数据
  3. 可视化层:用 Three.js 渲染 3D 仓库场景
  4. 交互层:实现点击仓库查看详情的功能

三、后端核心实现

1、传感器模拟:为每个仓库生成温湿度数据

我们通过 SensorManager 为每个仓库创建温湿度传感器:

/** * 传感器管理器 * 为每个仓库初始化温湿度传感器 */@ServicepublicclassSensorManager{privatefinalMap<String,Sensor> sensors;// 传感器ID -> 传感器实例/** * 初始化默认传感器 */privatevoidinitializeDefaultSensors(){// 1号仓传感器registerSensor(newTemperatureSensor("temp-001","1号仓",newdouble[]{-100,0,1}));registerSensor(newHumiditySensor("humid-001","1号仓",newdouble[]{-100,0,1}));// 2号仓传感器registerSensor(newTemperatureSensor("temp-002","2号仓",newdouble[]{-30,0,1}));registerSensor(newHumiditySensor("humid-002","2号仓",newdouble[]{-30,0,1}));// 3号仓传感器registerSensor(newTemperatureSensor("temp-003","3号仓",newdouble[]{40,0,1}));registerSensor(newHumiditySensor("humid-003","3号仓",newdouble[]{40,0,1}));// 4号仓传感器registerSensor(newTemperatureSensor("temp-004","4号仓",newdouble[]{110,0,1}));registerSensor(newHumiditySensor("humid-004","4号仓",newdouble[]{110,0,1}));// 5号仓传感器registerSensor(newTemperatureSensor("temp-005","5号仓",newdouble[]{-100,80,1}));registerSensor(newHumiditySensor("humid-005","5号仓",newdouble[]{-100,80,1}));// 6号仓传感器registerSensor(newTemperatureSensor("temp-006","6号仓",newdouble[]{-30,80,1}));registerSensor(newHumiditySensor("humid-006","6号仓",newdouble[]{-30,80,1}));// 7号仓传感器registerSensor(newTemperatureSensor("temp-007","7号仓",newdouble[]{40,80,1}));registerSensor(newHumiditySensor("humid-007","7号仓",newdouble[]{40,80,1}));// 8号仓传感器registerSensor(newTemperatureSensor("temp-008","8号仓",newdouble[]{110,80,1}));registerSensor(newHumiditySensor("humid-008","8号仓",newdouble[]{110,80,1}));}/** * 采集所有传感器数据 * @return 传感器数据列表 */publicList<SensorData>collectAllSensorData(){List<SensorData> dataList =newArrayList<>();for(Sensor sensor : sensors.values()){ dataList.add(sensor.collectData());}return dataList;}}

2、WebSocket 实时传输:让数据流动起来

为实现数字孪生的实时性,我使用了 WebSocket 推送仓库数据:

/** * 传感器WebSocket处理器 * 实时传输仓库温湿度数据到前端 */@ComponentpublicclassSensorWebSocketHandlerextendsTextWebSocketHandler{/** * 推送传感器数据 * @throws Exception 异常 */privatevoidpushSensorData()throwsException{// 采集所有传感器数据List<SensorData> sensorDataList = sensorManager.collectAllSensorData();// 按仓库分组处理数据Map<String,Map<String,Object>> warehouseData =newHashMap<>();for(SensorData data : sensorDataList){String warehouseName = data.getArea();String sensorType = data.getSensorType(); warehouseData.computeIfAbsent(warehouseName, k ->{Map<String,Object> info =newHashMap<>(); info.put("name", warehouseName); info.put("status","正常");// 根据仓库名称设置粮种switch(warehouseName){case"1号仓": info.put("grainType","小麦");break;case"2号仓": info.put("grainType","玉米");break;case"3号仓": info.put("grainType","大豆");break;case"4号仓": info.put("grainType","水稻");break;case"5号仓": info.put("grainType","高粱");break;case"6号仓": info.put("grainType","大麦");break;case"7号仓": info.put("grainType","燕麦");break;case"8号仓": info.put("grainType","荞麦");break;default: info.put("grainType","未知");}return info;});if(sensorType.equals("temperature")){ warehouseData.get(warehouseName).put("temperature", data.getValue());}elseif(sensorType.equals("humidity")){ warehouseData.get(warehouseName).put("humidity", data.getValue());}}// 构建推送数据Map<String,Object> data =newjava.util.HashMap<>(); data.put("warehouseData", warehouseData);// 序列化数据String jsonData = objectMapper.writeValueAsString(data);// 推送到所有连接的客户端for(WebSocketSession session : sessions){if(session.isOpen()){ session.sendMessage(newTextMessage(jsonData));}}}}

四、前端核心实现

1、3D 仓库场景构建:创建数字孪生的视觉基础

使用 Three.js 构建仓库建筑群:

// 创建仓库区域functioncreateWarehouseArea(){// 创建地面(草地)const groundGeometry =newTHREE.PlaneGeometry(800,800);const groundMaterial =newTHREE.MeshLambertMaterial({color:0x228B22,side:THREE.DoubleSide });const ground =newTHREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = Math.PI/2; ground.position.y =-0.1; ground.receiveShadow =true; scene.add(ground);// 创建仓库建筑群// 第一排仓库createWarehouse(-100,0,-50,'1号仓');createWarehouse(-30,0,-50,'2号仓');createWarehouse(40,0,-50,'3号仓');createWarehouse(110,0,-50,'4号仓');// 第二排仓库createWarehouse(-100,0,30,'5号仓');createWarehouse(-30,0,30,'6号仓');createWarehouse(40,0,30,'7号仓');createWarehouse(110,0,30,'8号仓');// 创建办公楼createOfficeBuilding(-150,0,0,'办公楼');// 创建道路createRoads();// 创建树木createTrees();// 创建蓝色光环createBlueRings();}// 创建仓库functioncreateWarehouse(x, y, z, name){// 仓库主体(长方体)const warehouseGeometry =newTHREE.BoxGeometry(40,10,25);const warehouseMaterial =newTHREE.MeshLambertMaterial({color:0xcccccc});const warehouse =newTHREE.Mesh(warehouseGeometry, warehouseMaterial); warehouse.position.set(x,5, z); warehouse.castShadow =true; warehouse.receiveShadow =true; warehouse.userData ={name: name,type:'warehouse'}; scene.add(warehouse);// 仓库屋顶(斜坡顶)const roofGeometry =newTHREE.BoxGeometry(42,3,27);const roofMaterial =newTHREE.MeshLambertMaterial({color:0x444444});const roof =newTHREE.Mesh(roofGeometry, roofMaterial); roof.position.set(x,11.5, z); roof.rotation.x = Math.PI*0.05;// 轻微倾斜 roof.castShadow =true; roof.receiveShadow =true; scene.add(roof);// 仓库门const doorGeometry =newTHREE.BoxGeometry(3,6,0.2);const doorMaterial =newTHREE.MeshLambertMaterial({color:0x888888});const door =newTHREE.Mesh(doorGeometry, doorMaterial); door.position.set(x,3, z -12.5); scene.add(door);// 仓库窗户for(let i =-15; i <=15; i +=10){const windowGeometry =newTHREE.BoxGeometry(2,3,0.2);const windowMaterial =newTHREE.MeshLambertMaterial({color:0x409eff});const window =newTHREE.Mesh(windowGeometry, windowMaterial); window.position.set(x + i,5, z +12.6); scene.add(window);}// 添加仓库编号标签addLabel(x,15, z, name);}

2、交互功能实现:点击仓库查看详情

// 鼠标点击事件 document.getElementById('map-container').addEventListener('click',function(event){// 计算鼠标在归一化设备坐标中的位置const mapContainer = document.getElementById('map-container');const rect = mapContainer.getBoundingClientRect(); mouse.x =((event.clientX - rect.left)/ mapContainer.clientWidth)*2-1; mouse.y =-((event.clientY - rect.top)/ mapContainer.clientHeight)*2+1;// 更新raycaster raycaster.setFromCamera(mouse, camera);// 获取所有可交互的物体const objects =[]; scene.traverse(function(object){if(object.userData && object.userData.type ==='warehouse'){ objects.push(object);}});// 检测碰撞const intersects = raycaster.intersectObjects(objects);if(intersects.length >0){const clickedObject = intersects[0].object;updateInfoPanel(clickedObject);}});// 更新信息面板functionupdateInfoPanel(object){if(object.userData && object.userData.type ==='warehouse'){const name = object.userData.name;const data = warehouseData[name];if(data){// 更新右侧信息面板 document.getElementById('warehouse-name').textContent = name; document.getElementById('warehouse-temperature').textContent = data.temperature ?(data.temperature +'°C'):'--°C'; document.getElementById('warehouse-humidity').textContent = data.humidity ?(data.humidity +'%'):'--%'; document.getElementById('warehouse-status').textContent = data.status ||'--'; document.getElementById('warehouse-grain-type').textContent = data.grainType ||'--';}else{// 如果没有后端数据,使用默认值 document.getElementById('warehouse-name').textContent = name; document.getElementById('warehouse-temperature').textContent ='--°C'; document.getElementById('warehouse-humidity').textContent ='--%'; document.getElementById('warehouse-status').textContent ='正常'; document.getElementById('warehouse-grain-type').textContent ='--';}}}

3、场景旋转功能:自由查看仓库布局

// 鼠标按下事件 document.getElementById('map-container').addEventListener('mousedown',function(event){ isDragging =true; previousMousePosition ={x: event.clientX,y: event.clientY };});// 鼠标移动事件 document.getElementById('map-container').addEventListener('mousemove',function(event){// 计算鼠标在归一化设备坐标中的位置const mapContainer = document.getElementById('map-container');const rect = mapContainer.getBoundingClientRect(); mouse.x =((event.clientX - rect.left)/ mapContainer.clientWidth)*2-1; mouse.y =-((event.clientY - rect.top)/ mapContainer.clientHeight)*2+1;// 处理拖拽旋转if(isDragging){const deltaMove ={x: event.clientX - previousMousePosition.x,y: event.clientY - previousMousePosition.y };// 更新相机旋转 cameraRotation.y += deltaMove.x *0.01; cameraRotation.x += deltaMove.y *0.01;// 限制垂直旋转角度 cameraRotation.x = Math.max(-Math.PI/3, Math.min(Math.PI/3, cameraRotation.x)); previousMousePosition ={x: event.clientX,y: event.clientY };}});// 更新相机旋转functionupdateCameraRotation(){// 计算新的相机位置const radius =150;const theta = cameraRotation.y;const phi = Math.PI/2- cameraRotation.x; camera.position.x = radius * Math.sin(phi)* Math.cos(theta); camera.position.y = radius * Math.cos(phi); camera.position.z = radius * Math.sin(phi)* Math.sin(theta); camera.lookAt(0,0,0);}

4、WebSocket 数据接收:实时响应环境变化

// 初始化WebSocket连接functioninitWebSocket(){try{const ws =newWebSocket('ws://localhost/ws/sensor'); ws.onopen=function(){ console.log('WebSocket连接已打开');}; ws.onmessage=function(event){const data =JSON.parse(event.data); console.log('收到WebSocket数据:', data);// 更新仓库数据if(data.warehouseData){ warehouseData = data.warehouseData;}}; ws.onclose=function(){ console.log('WebSocket连接已关闭');}; ws.onerror=function(error){ console.error('WebSocket错误:', error);};}catch(error){ console.error('WebSocket初始化失败:', error);}}

五、效果展示

六、部署与运行

1、Docker Compose 配置

version:'3.8'services:redis:image: redis:7-alpine container_name: industrial-redis-12ports:-"6379:6379"volumes:- E:/docker_service_data/java_industrial_redis_12:/data command:-"redis-server"-"--requirepass"-"devRed1s"-"--appendonly"-"yes"restart: unless-stopped backend:image: indoor-digital-twin-map-backend:local container_name: indoor-digital-twin-map-backend depends_on:- redis environment:- REDIS_HOST=redis restart: always frontend:image: indoor-digital-twin-map-frontend:local container_name: indoor-digital-twin-map-frontend depends_on:- backend ports:-"80:80"restart: always 

注意:要先 docker build 构建前后端镜像。具体的 Dockerfile 文件已在源代码中,有兴趣的童鞋可以去查看:

https://github.com/iweidujiang/java-industrial-smart/tree/main/12-indoor-digital-twin-map

2、启动命令

12-indoor-digital-twin-map 目录下执行:

docker-compose up -d

3、访问方式

  • 前端界面:http://localhost
  • 后端 API:http://localhost:8080

七、核心技术点总结

1、3D 场景构建的关键

  • 模型简化:使用基本几何体构建仓库模型,平衡视觉效果和性能
  • 场景布局:合理规划仓库、道路、树木等元素的位置,形成完整的园区布局
  • 光照设置:添加环境光和定向光,增强场景的立体感
  • 标签系统:为每个仓库添加编号标签,提高可识别性

2、实时数据传输的优化

  • WebSocket 长连接:建立持久连接,减少连接开销
  • 数据分组:按仓库分组传输数据,方便前端处理
  • JSON 序列化:使用 Jackson 库高效序列化数据
  • 定时推送:设置合理的推送间隔,平衡实时性和服务器负载

3、用户交互的设计

  • 射线检测:使用 Three.js 的 Raycaster 实现鼠标点击检测
  • 拖拽旋转:通过鼠标事件实现场景的自由旋转
  • 信息面板:点击仓库时在右侧显示详细信息
  • 视觉反馈:鼠标悬停时显示指针变化,增强交互感

八、小结

通过本篇,实现了一个简单的仓库数字孪生系统,它具备:

  • 直观的视觉效果:3D 仓库鸟瞰图,一目了然
  • 实时的数据分析:温湿度数据实时更新
  • 便捷的用户交互:点击查看详情,拖拽旋转场景
  • 简单的部署方式:Docker Compose 一键启动

本文实现的仓库数字孪生系统,虽然简单,但已经展现了数字孪生的核心价值:将物理世界映射到数字空间,让管理变得更直观、更高效。


最后,按惯例提醒:

完整源码已开源
📁 模块路径:12-indoor-digital-twin-map

🔗 仓库地址:github.com/iweidujiang/java-industrial-smart

欢迎 Star & 提 Issue!


本文属于专栏 《Java × 工业智能》第 12 篇

如果你对这个系列感兴趣,记得关注我哦!

Read more

Flutter 组件 http_retry 的适配 鸿蒙Harmony 深度进阶 - 驾驭分布式负载感知重试、实现鸿蒙端高可靠通讯与协议幂等性审计方案

Flutter 组件 http_retry 的适配 鸿蒙Harmony 深度进阶 - 驾驭分布式负载感知重试、实现鸿蒙端高可靠通讯与协议幂等性审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 http_retry 的适配 鸿蒙Harmony 深度进阶 - 驾驭分布式负载感知重试、实现鸿蒙端高可靠通讯与协议幂等性审计方案 前言 在前文中,我们探讨了 http_retry 在鸿蒙(OpenHarmony)生态中解决单一移动终端弱网重试的基础实战。但在真正的“分布式工业物联网集成”、“跨设备协同办公资产同步”以及“需要对接具备动态压力管控的超大规模云原生后端”场景中。简单的指数退避往往难以应对复杂的网络分位震荡。面对一个需要在鸿蒙手机、智能穿戴设备与边缘网关之间,根据当前全网的平均负载压力(Load Pressure)动态调节重试节奏,并且要求在执行涉及核心资产变更(如:支付订单、库存锁定)的重试时执行绝对严密的协议幂等性(Idempotency)校验的高阶需求。如果缺乏一套具备分布式感知的重试调度模型。不仅会导致后端服务在故障恢复瞬间遭遇“重试波峰”引发再次崩溃,更会因为对非幂等操作的盲目重试。引发严重的业务资产错乱。 我们需要

By Ne0inhk
Flutter 三方库 brick_offline_first_with_supabase 深度鸿蒙离线缓存架构适配解析:极速搭建边缘物理存储与高可用同步中枢-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 brick_offline_first_with_supabase 深度鸿蒙离线缓存架构适配解析:极速搭建边缘物理存储与高可用同步中枢-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 brick_offline_first_with_supabase 深度鸿蒙离线缓存架构适配解析:极速搭建边缘物理存储与高可用同步中枢协同引擎 在开发需要高可用性和强离线能力的鸿蒙应用时,如何平衡云端实时性与本地响应速度是核心挑战。brick_offline_first_with_supabase 是一套成熟的离线优先(Offline First)数据流方案。本文将探讨该库在 OpenHarmony 环境下的深度集成与适配。 前言 什么是离线优先?它意味着应用的所有读写操作首选本地数据库,并由系统在后台自动处理与云端(Supabase)的增量同步。在鸿蒙这个强调无缝连接和万物互联的系统中,确保应用在电梯、地库等弱网环境下依然“丝滑”可用,是构建精品级鸿蒙应用的必修课。brick 库通过自动化的代码生成,极大地降低了这一架构的开发门槛。 一、原理解析 1.1 基础概念 该库主要集成了三层架构: * SqliteProvide

By Ne0inhk
你真的会打印日志吗?基于 Spring Boot 的全方位日志指南

你真的会打印日志吗?基于 Spring Boot 的全方位日志指南

—JavaEE专栏— 目录 * 一、日志概述:为什么它比 System.out.println 更重要? * 1.1 日志的核心用途 * 1.2 为什么弃用标准输出? * 二、日志框架体系:门面模式的深度解析 * 2.1 门面模式 (Facade Pattern) * 2.2 常见框架对比 * 三、实战:Spring Boot 日志的基本使用 * 3.1 传统方式获取日志对象 * 3.2 进阶方式:使用 Lombok (@Slf4j) * 四、深入理解日志级别 * 五、日志的高级配置 (application.yml) * 5.1 修改日志级别 * 5.

By Ne0inhk
【金仓数据库】ksql 指南(二) —— 创建与管理本地数据库

【金仓数据库】ksql 指南(二) —— 创建与管理本地数据库

引言 掌握了 ksql 对接本地 KingbaseES 数据库的基本操作之后,接下来要学习的是“数据库自身的运作”,数据库是数据存储的顶级容器,所有的表,视图等对象均依托数据库而存在,本文将会细致阐述怎样经由 ksql 命令行来完成本地数据库从“创建,查看,切换到删除”的全部操作,各个步骤均配有具体的实例以及需要注意的地方,从而保证初学者能够顺利实施。 文章目录 * 引言 * 一、前置条件:明确 “在哪操作”—— 权限库与连接状态 * 1.1 确认连接到 “权限库” * 1.2 确认当前用户权限(避免 “权限不足” 报错) * 二、创建本地数据库:两种核心方式(语句 / 工具) * 2.1 方式一:采用 CREATE DATABASE 语句来创建数据库(此方法较为推荐,

By Ne0inhk