简介本资源是一套基于RuoYi框架SpringBootVue前后端分离开发的售货机管理系统面向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_typeSALE,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_idrelated_order_id允许事件跨实体关联如一次补货事件关联到具体设备和采购单-- ruoyi-device/src/main/resources/mapper/event/EventMapper.xml insert idinsertEvent parameterTypecom.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 idgetSalesTop10ByArea resultTypecom.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 idselectDeviceList resultTypecom.ruoyi.device.domain.Device SELECT * FROM sys_device d WHERE 11 if testparams.areaIds ! null and params.areaIds.length 0 AND d.area_id IN foreach itemareaId collectionparams.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.urljdbc:mysql://localhost:3306/ruoyi?useSSLfalseserverTimezoneAsia/Shanghai -Dflyway.userroot -Dflyway.password123456提示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.xnpm run dev会报错Cannot find module vue/cli-service。正确做法是# 进入 ruoyi-ui 目录 cd ruoyi-ui # 清理 node_modules重要 rm -rf node_modules package-lock.json # 用 npm 安装不要用 yarnyarn.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模拟设备上报事件填eventTypeDOOR_OPENcontext_json{operator:A001}GET /device/{id}/stock-snapshot看是否返回实时库存若超时检查deviceGatewayClient配置GET /report/sales-top10?days7验证报表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字段建表时未指定CHARSETutf8mb4。解决执行SQL修复注意备份ALTER TABLE sys_event MODIFY COLUMN context_json TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 同时检查连接URL是否带 charset -- jdbc:mysql://localhost:3306/ruoyi?useUnicodetruecharacterEncodingutf8mb44.3 现象补货员登录后设备列表为空但管理员能看到全部原因SysUser.responsible_areas字段为空字符串而非NULL。MyBatis 的if testparams.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 truemodule.exports { devServer: { port: 8081, historyApiFallback: true, // 关键 proxy: { /* ... */ } } }5. 二次开发实战接入新硬件只要改3个地方附完整改造清单上周帮某自动贩卖机厂商接入他们的定制设备对方用私有TCP协议心跳包是十六进制字符串0x010203...。整个过程只改了3个文件不到2小时完成联调。以下是可直接复用的改造路径5.1 协议适配层替换deviceGatewayClient实现类原项目用HTTP轮询新设备要求长连接。新建类TcpDeviceGatewayClientComponent 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} MapString, Integer stockMap new HashMap(); // ... 解析逻辑此处省略按厂商文档实现 return JSON.toJSONString(stockMap); } }关键Primary注解让Spring优先注入这个Bean无需改任何Service代码。5.2 事件解析器扩展EventType枚举和处理器厂商新增事件类型CASH_BOX_FULL钱箱满需存入sys_event。步骤在EventType.java中加枚举public enum EventType { SALE, REFILL, DOOR_OPEN, CASH_BOX_FULL; // 新增 }在EventController.java的saveEvent()方法里加分支if (CASH_BOX_FULL.equals(event.getEventType())) { // 钱箱满事件特殊处理触发工单系统 workOrderService.createCashFullTicket(event); }在workOrderService中实现创建工单逻辑调用内部工单API。5.3 前端设备卡片增加「钱箱状态」实时指示灯修改ruoyi-ui/src/views/device/device.vue在设备列表列中加一列el-table-column label钱箱状态 width100 template slot-scopescope 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:90012用TCP工具如Netcat向9001端口发010203后端日志应打印Received stock request for SN:ABC1233登录后台选一台设备点「实时库存」返回JSON含{C001:12,C002:8}无乱码4模拟上报CASH_BOX_FULL事件sys_event表新增记录且work_order表生成工单5刷新设备列表页「钱箱状态」列显示「已满」且红点亮起从那以后我每次接入新硬件都强制走一遍这个 checklist先确认协议层连通性再验证数据落库最后检查前端展示。漏掉任何一步上线后都会变成深夜告警电话。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?