☰
RuoYi IoT设备管理框架:售货机补货、故障、热力图全链路实现
2026/10/19 8:24:51 网站建设 项目流程

简介:本资源是一套基于RuoYi框架(SpringBoot+Vue前后端分离)开发的售货机管理系统,面向Java全栈初学者、Web应用开发者及后台管理平台实践者,解决智能售货设备在商品、库存、销售、设备与支付等维度的集中化、可视化管理难题,适用于校园/社区/商业场景下的轻量级物联网运营系统学习与二次开发。压缩包共541个文件,含124个Vue组件(实现管理界面交互)、94个JS逻辑脚本(支撑业务流程)、159个PNG/SVG图标资源(含logo、favicon、操作图标等)、10个SCSS样式文件(模块化主题定制),整体14.81MB,结构清晰、开箱即用。已有321人学习下载,提供完整可运行工程:从前端页面布局、API调用封装,到后端MyBatis数据操作、Spring Security权限控制,再到多环境配置(development/staging/production)与一键启停脚本(bat),覆盖项目构建、部署与维护全流程关键实践。

1. 售货机不是只能刷二维码:这个 RuoYi 项目把补货、故障上报、销量热力图全跑通了

上周帮某高校后勤处搭智能设备看板,他们提了个反直觉需求:「售货机得能当‘微型仓库’用——补货员扫一下机器码,立刻弹出缺货清单+最近3次补货记录+当前库存预警;管理员在后台点个按钮,就能按区域拉出‘哪台机器卖冰红茶最多’‘哪台连续两天没出货’的报表。」市面上多数开源售货系统只做支付对接,而这个基于 RuoYi 的项目,硬是把硬件管理逻辑揉进了标准后台框架里。它用 SpringBoot + Vue 做前后端分离,但关键不在技术栈,而在业务建模:把每台售货机抽象成「设备实体」,把每次开门、出货、报修都转为「设备事件」,再用 RuoYi 的权限模块给补货员、运维、财务分角色配操作粒度。新手照着文档能20分钟跑通本地环境,熟手改3个Mapper XML和2个Vue组件就能接入自家硬件协议。如果你正被「硬件数据进不了后台」卡住,或者想拿现成框架快速验证IoT管理流程,这个项目就是少走三个月弯路的脚手架。


2. 项目结构拆解:RuoYi 框架不是黑匣子,核心就在这4个模块

RuoYi 本身是成熟后台框架,但本项目做了针对性裁剪和增强。我下载源码后第一件事是删掉ruoyi-system里所有与「用户中心」「OA审批」相关的包——这些对售货机场景纯属冗余。真正驱动业务的是以下四个自定义模块,它们才是你二次开发时要盯死的地方:

2.1 设备管理模块(device):售货机不是“静态资产”,而是可交互节点

该模块位于ruoyi-device子工程中,核心是Device实体类及其关联表。注意它和传统资产管理的区别:

  • device_status字段不是简单存「在线/离线」,而是枚举值:ONLINE,OFFLINE,MAINTAINING,LOW_STOCK,DOOR_OPENED—— 这些状态直接触发前端告警弹窗和短信通知
  • last_heartbeat_time和last_report_time分开存储,前者由心跳包更新(每30秒),后者由设备主动上报事件时更新(如出货、故障)
  • 补货员APP扫码后调用的接口是POST /device/{id}/stock-snapshot,返回JSON含currentStock: [{skuCode:"C001", quantity:12, minThreshold:5}],前端据此高亮缺货格子
// ruoyi-device/src/main/java/com/ruoyi/device/service/impl/DeviceServiceImpl.java @Override public DeviceStockSnapshot getStockSnapshot(Long deviceId) { Device device = deviceMapper.selectById(deviceId); // 关键:这里不查数据库,而是发MQ指令给设备网关,实时拉取物理库存 String snapshotJson = deviceGatewayClient.pullRealtimeStock(device.getSn()); return JSON.parseObject(snapshotJson, DeviceStockSnapshot.class); }

提示:deviceGatewayClient是项目新增的轻量级网关适配层,支持HTTP和WebSocket两种模式,默认走HTTP。若你的设备用MQTT,只需重写MqttDeviceGatewayClient并注入Spring容器,不用动业务代码。

2.2 事件中心模块(event):所有硬件行为都必须落库,且带上下文

售货机产生的每条数据,无论大小,都必须过Event表。这不是为了日志审计,而是支撑后续分析的基础。表结构设计很务实:

  • event_type:SALE,REFILL,DOOR_OPEN,TEMP_ALARM,CASH_FULL—— 覆盖95%运维场景
  • context_json:存JSON字符串,例如{"skuCode":"C001","price":3.5,"quantity":1}或{"operatorId":"U7890","reason":"door stuck"}
  • related_device_id+related_order_id:允许事件跨实体关联(如一次补货事件关联到具体设备和采购单)
-- ruoyi-device/src/main/resources/mapper/event/EventMapper.xml <insert id="insertEvent" parameterType="com.ruoyi.event.domain.Event"> INSERT INTO sys_event ( event_type, context_json, related_device_id, related_order_id, create_time, create_by ) VALUES ( #{eventType}, #{contextJson}, #{relatedDeviceId}, #{relatedOrderId}, NOW(), #{createBy} ) </insert>

注意:context_json字段类型是TEXT而非JSON,这是刻意为之——MySQL 5.7+虽支持JSON类型,但RuoYi默认兼容MySQL 5.6,且文本字段对模糊查询更友好(比如WHERE context_json LIKE '%C001%'查某商品所有事件)。

2.3 报表服务模块(report):不用ECharts手写图表,SQL就是报表逻辑

报表不是前端拼接,而是后端SQL直出。ruoyi-report模块里没有一个Java Service类,只有ReportMapper.xml里的12个<select>标签。比如「区域销量TOP10」报表:

<!-- ruoyi-report/src/main/resources/mapper/report/ReportMapper.xml --> <select id="getSalesTop10ByArea" resultType="com.ruoyi.report.domain.SalesTop10Vo"> SELECT d.area_name AS areaName, d.device_name AS deviceName, COUNT(*) AS saleCount, SUM(CAST(JSON_EXTRACT(e.context_json, '$.price') AS DECIMAL(10,2))) AS totalAmount FROM sys_event e JOIN sys_device d ON e.related_device_id = d.device_id WHERE e.event_type = 'SALE' AND e.create_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY) GROUP BY d.area_name, d.device_name ORDER BY totalAmount DESC LIMIT 10 </select>

关键细节:JSON_EXTRACT直接从context_json里取价格,避免为每个事件建冗余字段;#{days}是MyBatis参数占位符,前端传7就查一周,传30就查一月——动态SQL比写10个固定接口更省事。

2.4 权限增强模块(auth):补货员只能看到自己负责的机器

RuoYi 原生权限是「菜单+按钮」两级,但售货机场景需要「数据级权限」。本项目在SysUser实体上加了responsible_areas字段(逗号分隔的区域ID列表),并在所有设备相关Mapper里加了数据过滤:

<!-- ruoyi-device/src/main/resources/mapper/device/DeviceMapper.xml --> <select id="selectDeviceList" resultType="com.ruoyi.device.domain.Device"> SELECT * FROM sys_device d WHERE 1=1 <if test="params.areaIds != null and params.areaIds.length > 0"> AND d.area_id IN <foreach item="areaId" collection="params.areaIds" open="(" separator="," close=")"> #{areaId} </foreach> </if> </select>

补货员登录后,ShiroRealm会把responsible_areas注入到LoginUser对象,前端请求时自动带上areaIds参数。这种设计比RBAC模型更轻量,也更贴合现场管理逻辑。


3. 启动与调试:三步跑通,但第2步最容易翻车

项目用 Maven 多模块管理,启动顺序和配置有强依赖。别急着mvn spring-boot:run,先确认这三件事:

3.1 数据库初始化:别信SQL文件,用Flyway自动迁移

项目根目录下sql/文件夹里的init.sql是初始结构,但实际开发中你一定会改表。本项目已集成 Flyway,所有变更必须走src/main/resources/db/migration/下的V版本SQL文件:

  • V1.0__init_device_table.sql:建设备表
  • V1.1__add_event_context_json.sql:给事件表加JSON字段
  • V1.2__index_for_device_area.sql:给区域ID加索引
# 启动前先执行迁移(确保MySQL已运行) cd ruoyi-admin mvn flyway:migrate -Dflyway.url=jdbc:mysql://localhost:3306/ruoyi?useSSL=false&serverTimezone=Asia/Shanghai -Dflyway.user=root -Dflyway.password=123456

提示:Flyway 会自动在库中建flyway_schema_history表记录版本,下次启动时若发现新SQL文件,会自动执行。这比手动执行SQL文件可靠得多——尤其当你和同事协同开发时。

3.2 前端启动:Vue CLI 4.5 是唯一兼容版本

ruoyi-ui是标准 Vue CLI 项目,但注意package.json中锁定了vue-cli-service版本:

"devDependencies": { "@vue/cli-service": "4.5.15", "vue-template-compiler": "2.6.14" }

若你全局装了 Vue CLI 5.x,npm run dev会报错Cannot find module '@vue/cli-service'。正确做法是:

# 进入 ruoyi-ui 目录 cd ruoyi-ui # 清理 node_modules(重要!) rm -rf node_modules package-lock.json # 用 npm 安装(不要用 yarn,yarn.lock 未提交) npm install # 启动(自动代理到后端) npm run dev

翻车点:npm run dev默认启动http://localhost:80,但浏览器常被占用。解决方案是在vue.config.js里改端口:

module.exports = { devServer: { port: 8081, // 改成8081 proxy: { '/prod-api': { // 注意这里是 /prod-api,不是 /api target: 'http://localhost:8080', changeOrigin: true } } } }

3.3 后端联调:Swagger 不是摆设,它是硬件协议调试器

启动ruoyi-admin后,访问http://localhost:8080/swagger-ui.html。重点测试三个接口:

  • POST /device/{id}/event:模拟设备上报事件(填eventType=DOOR_OPEN,context_json={"operator":"A001"})
  • GET /device/{id}/stock-snapshot:看是否返回实时库存(若超时,检查deviceGatewayClient配置)
  • GET /report/sales-top10?days=7:验证报表SQL是否生效

注意:Swagger 测试时,Header 必须带Authorization: Bearer xxx。Token 从登录接口获取,账号密码默认是admin/admin123。若提示401 Unauthorized,说明ShiroConfig中的jwtFilter拦截了,此时先用curl绕过:

curl -X POST "http://localhost:8080/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}'

4. 常见问题排查:这5个坑我替你踩过了

部署或调试时,以下问题出现频率极高。别搜Stack Overflow,答案就在这里:

4.1 现象:前端登录成功,但点击「设备管理」菜单空白,控制台报TypeError: Cannot read property 'length' of undefined

原因:ruoyi-ui/src/api/device.js中listDevice()接口返回的数据结构变了,但Device.vue的handleDeviceList方法还按老格式解析。RuoYi 3.8.0 升级后,分页返回对象从{rows: [], total: 100}改为{records: [], total: 100}。
解决:打开ruoyi-ui/src/views/device/device.vue,找到第127行:

// 错误写法(旧版) this.deviceList = res.rows || [] // 正确写法(新版) this.deviceList = res.records || []

4.2 现象:设备事件上报后,sys_event表里context_json字段全是乱码(如{"skuCode":"C001"})

原因:MySQL 表字符集是latin1,但应用层用UTF-8写入JSON。context_json字段建表时未指定CHARSET=utf8mb4。
解决:执行SQL修复(注意备份):

ALTER TABLE sys_event MODIFY COLUMN context_json TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 同时检查连接URL是否带 charset -- jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8mb4

4.3 现象:补货员登录后,设备列表为空,但管理员能看到全部

原因:SysUser.responsible_areas字段为空字符串"",而非NULL。MyBatis 的<if test="params.areaIds != null">判断失败,导致SQL没加WHERE d.area_id IN (...)条件,但因responsible_areas为空,最终查不到数据。
解决:在SysUser实体的getResponsibleAreas()方法里加空值处理:

public String getResponsibleAreas() { return StringUtils.isEmpty(responsibleAreas) ? null : responsibleAreas; }

4.4 现象:报表页面加载慢,EXPLAIN显示sys_event表全表扫描

原因:event_type和create_time两个字段没联合索引,而报表查询必带这两个条件。
解决:建复合索引(执行一次即可):

CREATE INDEX idx_event_type_time ON sys_event (event_type, create_time);

4.5 现象:npm run dev启动后,访问http://localhost:8081显示Cannot GET /

原因:Vue Router 是 history 模式,但开发服务器没配置 fallback。Webpack Dev Server 默认不处理 HTML5 History API 的路由。
解决:修改vue.config.js,加configureWebpack.devServer.historyApiFallback = true:

module.exports = { devServer: { port: 8081, historyApiFallback: true, // 关键! proxy: { /* ... */ } } }

5. 二次开发实战:接入新硬件只要改3个地方,附完整改造清单

上周帮某自动贩卖机厂商接入他们的定制设备,对方用私有TCP协议,心跳包是十六进制字符串0x010203...。整个过程只改了3个文件,不到2小时完成联调。以下是可直接复用的改造路径:

5.1 协议适配层:替换deviceGatewayClient实现类

原项目用HTTP轮询,新设备要求长连接。新建类TcpDeviceGatewayClient:

@Component public class TcpDeviceGatewayClient implements DeviceGatewayClient { @Value("${device.gateway.tcp.host:127.0.0.1}") private String host; @Value("${device.gateway.tcp.port:9001}") private int port; @Override public String pullRealtimeStock(String sn) { // 1. 构造TCP请求:SN+命令码0x01 byte[] request = buildTcpRequest(sn, (byte) 0x01); // 2. 发送并接收16进制响应 String hexResponse = tcpClient.sendAndReceive(host, port, request); // 3. 解析十六进制为JSON(厂商提供解析规则文档) return hexToStockJson(hexResponse); } private String hexToStockJson(String hex) { // 示例:将 "01020304" → {"C001":12,"C002":8} Map<String, Integer> stockMap = new HashMap<>(); // ... 解析逻辑(此处省略,按厂商文档实现) return JSON.toJSONString(stockMap); } }

关键:@Primary注解让Spring优先注入这个Bean,无需改任何Service代码。

5.2 事件解析器:扩展EventType枚举和处理器

厂商新增事件类型CASH_BOX_FULL(钱箱满),需存入sys_event。步骤:

  1. 在EventType.java中加枚举:
public enum EventType { SALE, REFILL, DOOR_OPEN, CASH_BOX_FULL; // 新增 }
  1. 在EventController.java的saveEvent()方法里加分支:
if ("CASH_BOX_FULL".equals(event.getEventType())) { // 钱箱满事件特殊处理:触发工单系统 workOrderService.createCashFullTicket(event); }
  1. 在workOrderService中实现创建工单逻辑(调用内部工单API)。

5.3 前端设备卡片:增加「钱箱状态」实时指示灯

修改ruoyi-ui/src/views/device/device.vue:

  • 在设备列表列中加一列:
<el-table-column label="钱箱状态" width="100"> <template slot-scope="scope"> <span :class="['status-dot', scope.row.cashBoxStatus === 'FULL' ? 'red' : 'green']"></span> {{ scope.row.cashBoxStatus === 'FULL' ? '已满' : '正常' }} </template> </el-table-column>
  • 在CSS中加指示灯样式:
.status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; } .status-dot.red { background-color: #f56c6c; } .status-dot.green { background-color: #67c23a; }
  • 在getDeviceList()成功回调里,为每台设备加cashBoxStatus字段(从最新CASH_BOX_FULL事件推断)。

5.4 改造验证 checklist(必须逐项执行)

步骤操作验证方式
1启动TcpDeviceGatewayClient查看日志是否有Connected to 127.0.0.1:9001
2用TCP工具(如Netcat)向9001端口发010203后端日志应打印Received stock request for SN:ABC123
3登录后台,选一台设备点「实时库存」返回JSON含{"C001":12,"C002":8},无乱码
4模拟上报CASH_BOX_FULL事件sys_event表新增记录,且work_order表生成工单
5刷新设备列表页「钱箱状态」列显示「已满」,且红点亮起

从那以后我每次接入新硬件,都强制走一遍这个 checklist:先确认协议层连通性,再验证数据落库,最后检查前端展示。漏掉任何一步,上线后都会变成深夜告警电话。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询