用 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 实现实时数据传输,具体架构如下:

说明:
- 前端:使用 Three.js 实现 3D 仓库鸟瞰图,通过 WebSocket 接收后端推送的温湿度数据
- 后端:基于 Spring Boot 实现 WebSocket 服务器,负责采集和推送传感器数据
- 数据层:使用模拟传感器生成温湿度数据,模拟实际工业场景
- 通信:通过 WebSocket 实现前后端实时数据传输,确保数据同步
这种设计的优势在于:
- 实时性:WebSocket 长连接确保数据实时传输
- 简单性:技术栈简单,易于部署和维护
- 可扩展性:后续可轻松接入真实传感器或其他数据源
2、技术栈选择
| 层次 | 技术 | 版本 | 用途 |
|---|---|---|---|
| 后端 | Spring Boot | 3.5.10 | 提供 WebSocket 服务和传感器数据 |
| WebSocket | - | 实时数据传输 | |
| 前端 | Three.js | 0.160.0 | 3D 场景渲染和仓库模型展示 |
| JavaScript | ES6+ | 前端业务逻辑 | |
| 传感器 | 模拟传感器 | - | 生成温湿度数据 |
3、实现路径
- 数据层:模拟仓库温湿度传感器数据
- 通信层:通过 WebSocket 实时传输数据
- 可视化层:用 Three.js 渲染 3D 仓库场景
- 交互层:实现点击仓库查看详情的功能
三、后端核心实现
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 -d3、访问方式
- 前端界面: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 篇
如果你对这个系列感兴趣,记得关注我哦!