ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot无人售货机后台管理系统:设备订单权限一体化设计

SpringBoot无人售货机后台管理系统:设备订单权限一体化设计 简介在物联网与智能零售快速发展背景下后台管理系统成为连接硬件设备与业务运营的桥梁。SpringBoot作为企业级应用开发的主流框架凭借其自动装配与生态整合能力可高效构建包含设备通信、订单流转、库存同步等复杂场景的管理平台。通过MySQL存储商品、设备、货道的结构化数据借助Redis处理设备心跳、接口幂等与缓存加速并运用JWT与RBAC模型保障后台接口安全系统得以在低耦合、可扩展的架构下稳定运行。此类设计广泛应用于无人售货机、智能货柜等无人零售场景解决从支付到出货的闭环问题。本文结合实战项目深入解析基于SpringBoot的智慧无人售货机后台管理系统的核心设计与实现细节。 前两年接了一个实际的智能零售项目要给十几台无人售货机做一套后台管理系统。当时市面上现成的SaaS后台要么按月收费、要么定制费贵得离谱最后决定基于SpringBoot自己搭一套。这套东西后来整理成了源码包含了商品管理、设备管理、订单流水、库存同步、出货指令下发、对账统计这些完整模块。如果你正好在找一套能跑起来做二次开发的智慧无人售货机后台管理系统或者想学SpringBoot企业级项目的完整套路这篇东西应该能帮你省不少时间。我会把当时从需求拆解、技术选型、数据库设计到核心代码落地的完整思路捋一遍重点讲清楚“为什么这么设计”和“哪些坑真的会踩”而不是只贴一堆截图。1. 先想清楚无人售货机的后台系统到底在管什么很多新手拿到这种项目上来就建表写接口结果做到一半发现业务根本绕不开设备状态、出货回调、库存对账这些线下逻辑。我建议你先花半天把业务链路画清楚。1.1 从一次完整的购买流程看系统职责一次标准购买是这样的用户在小程序或售货机屏幕上选商品发起支付支付成功之后后台系统要给售货机下发一个出货指令机器弹出对应货道上的商品然后机器回传一个出货结果。这个结果可能是“出货成功”也可能是“货道卡住”“库存不足”“设备离线”每一种情况后台都得接住。所以这个后台系统的核心职责不是“管商品”而是三件事第一管设备连接和状态第二管订单从创建到完成的完整生命周期第三管库存与货道的对应关系。商品信息反而最简单它就是一张基础数据表。后台的用户也不是普通消费者而是管理员。运营人员要看销售数据和商品毛利财务要对账运维人员要查看哪台设备离线了、哪个货道卡货了。这套系统里没有C端注册登录所有账号都来自后台管理员创建。1.2 业务角色与核心流程我按角色拆了一遍需求最终得出四个核心模块设备管理设备列表、在线状态、货道信息、远程操作重启/锁定/解锁、出货记录商品管理SPU/SKU维护、上下架、价格策略、货道绑定关系订单中心用户订单、退款单、异常单、对账报表系统管理管理员账号、角色权限、操作日志这四个模块对应了四类操作人员运营、财务、运维、超级管理员。权限上一定要分开比如财务只能看订单和报表不能改商品运维只能操作设备不能碰价格。源码里用的是RBAC模型后面会讲。提示设计权限时别只想着“给谁开什么功能”要想着“万一误操作会造成什么后果”。售后设备锁定、价格修改这种操作建议必须走操作日志而且日志要留到订单级别。2. 技术选型背后的真实考虑单体架构为什么够用这套源码用的是SpringBoot MyBatis-Plus MySQL Redis这套非常主流的组合。你可能觉得“这不就是最普通的后台管理系统吗”但它确实是最适合无人售货机场景的选择。2.1 这套源码用到的技术栈具体来说全套技术栈是这样的基础框架SpringBoot 2.7.x持久层MyBatis-Plus 3.5.x配合自动填充和乐观锁插件数据库MySQL 8.0InnoDB引擎utf8mb4字符集缓存Redis用于token、设备心跳缓存、订单防重接口文档Knife4jSwagger增强版权限认证JWT Spring AOP自定义注解定时任务Spring自带Scheduled 手动封装的任务调度器前端这套源码主要是后端接口前端管理页面用Vue3 Element Plus可以快速对接这里我想单独说说为什么不用Spring Cloud。如果你是个人开发者或者小团队维护一套微服务架构的成本远大于收益。无人售货机业务初期设备量就是几十台上百台单机MySQL和单应用完全扛得住真正瓶颈往往在设备离线重连和订单异常处理上不在并发。2.2 单体架构在售货机场景下的合理性我做项目的时候同行的系统有的上了Nacos、Feign、Sentinel看着很高级但真到排查问题的时候一个订单状态要跨两个服务查上下游日志对不上反而痛苦。单体应用的好处是业务边界清晰一个订单的创建、支付回调、出货指令、结果确认就在同一个事务上下文里出错容易定位。还有一点是部署成本。源码本身只依赖一个Java环境、一个MySQL、一个Redis服务器2核4G就能跑得很稳。对大多数中小运营方来说买个云服务器装个Docker直接跑起来比搞一套K8s集群实在得多。注意如果以后设备量真到了几千台这个系统也不是不能演进。先把设备通信接口和订单服务拆开就行其他模块继续留在单体里。架构演进要按业务节奏来不是一次性到位。3. 数据库建模是整个项目的关键商品、设备、订单怎么设计数据库一旦设计完再改是很痛苦的。我在第二版重构时把核心表重新设计了一遍这里贴出最关键的几张表结构并解释为什么这样设计。3.1 商品与货道的绑定关系别把货道当成普通字段售货机不是仓库它每个格子货道能放的货品有限一个货道通常只能绑定一种商品库存也是按货道维度算的。所以我建了三个表商品表product、设备表machine、货道表channel。CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 商品名称, image varchar(255) DEFAULT NULL COMMENT 商品图片, price decimal(10,2) NOT NULL COMMENT 售价, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE machine ( id bigint NOT NULL AUTO_INCREMENT, code varchar(32) NOT NULL COMMENT 设备编号, name varchar(100) DEFAULT NULL COMMENT 设备名称, address varchar(255) DEFAULT NULL COMMENT 安装位置, status tinyint NOT NULL DEFAULT 0 COMMENT 0离线 1在线 2故障 3维护, last_heartbeat_time datetime DEFAULT NULL COMMENT 最后心跳时间, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT售货机设备表; CREATE TABLE channel ( id bigint NOT NULL AUTO_INCREMENT, machine_id bigint NOT NULL COMMENT 设备ID, channel_no varchar(16) NOT NULL COMMENT 货道编号如A01, product_id bigint DEFAULT NULL COMMENT 当前商品ID, capacity int NOT NULL DEFAULT 0 COMMENT 货道容量, stock int NOT NULL DEFAULT 0 COMMENT 当前库存, status tinyint NOT NULL DEFAULT 1 COMMENT 1可用 0禁用, PRIMARY KEY (id), KEY idx_machine (machine_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT货道表;这样设计的好处是一台设备可以有多层货架每层多个货道货道可以独立控制是否可用。设备巡检时只看channel表就能知道哪个货道缺货后台补货单直接按货道汇总生成。3.2 订单表与状态机一个字段防止所有状态混乱订单表是这套系统里最核心的表没有之一。我设计的字段除了基本金额、商品信息外还加了订单状态、支付流水号、出货状态、设备编号、货道编号、回调时间、异常原因等。状态一定不能只存一个“订单状态”要分两部分支付状态和出货状态。CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, machine_id bigint NOT NULL COMMENT 设备ID, channel_id bigint NOT NULL COMMENT 货道ID, product_id bigint NOT NULL COMMENT 商品ID, product_name varchar(100) NOT NULL, price decimal(10,2) NOT NULL COMMENT 下单时价格快照, pay_status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已退款, delivery_status tinyint NOT NULL DEFAULT 0 COMMENT 0未出货 1出货中 2出货成功 3出货失败, out_trade_no varchar(64) DEFAULT NULL COMMENT 支付平台流水号, error_msg varchar(255) DEFAULT NULL COMMENT 异常信息, create_time datetime NOT NULL, pay_time datetime DEFAULT NULL, delivery_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_machine (machine_id), KEY idx_out_trade_no (out_trade_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这里有几个细节很重要。下单时价格必须“快照”到订单表不能下单前再去查商品表因为价格随时可能被运营调整。货道编号也要记录后续售后时能快速定位是哪台机器的哪个货道出了问题。订单状态机我画了一个简单的流转路径待支付 - 已支付/出货中 - 出货成功已支付/出货中 - 出货失败 - 退款中 - 已退款。所有状态变更都必须走统一的状态更新方法不允许直接update的SQL否则迟早会出现状态倒流或丢状态。4. 核心业务实现的几个硬骨头设备通信、出货指令、异常订单数据库设计好之后真正的技术难度在业务实现。无人售货机后台和普通进销存系统最大的区别就是它要跟真实世界的硬件打交道网络不稳定、机械故障、超时都会发生。4.1 设备心跳与在线状态不能只依赖最后心跳时间设备端每30秒上报一次心跳后台把心跳时间写入Redis同时更新数据库的最后心跳时间。判断设备在线不能光看数据库里的时间要结合Redis里的值和当前时间比较。这块我单独定义了一个状态枚举在线、离线、故障、维护。维护状态是运营人员手动置的如果设备正在补货或检修就不下发出货指令。离线判断是“最后心跳时间超过90秒”而不是一超过30秒就判定离线因为网络抖动很常见。public boolean isOnline(String machineCode) { String key machine:heartbeat: machineCode; String last redisTemplate.opsForValue().get(key); if (StringUtils.isBlank(last)) { return false; } return System.currentTimeMillis() - Long.parseLong(last) 90 * 1000; }Redis和数据库双写的好处是查询在线状态走Redis秒级返回数据库里的最后心跳时间用来做长期统计和离线对账。定时任务每隔一分钟扫描一次设备表把超过180秒没心跳的设备置为离线避免所有实时判断都去扫库。4.2 出货指令下发与确认接口要设计成“一次性下发循环确认”用户支付成功后系统向设备下发一个出货指令。设备执行出货后会回调一个结果。但如果设备网络不好指令下发后没收到回调怎么办所以必须有两个接口一个下发指令一个接收结果。下发指令前先把订单的delivery_status置为“出货中”再发送指令。收到设备回调时如果是成功就把状态更新为“出货成功”并扣减货道库存如果是失败就启动退款流程。这里非常关键的一点是幂等。设备可能多次回调同一个订单号所以回调接口必须按订单号去重。我当时的做法是在Redis里存一个“订单已处理”标记第一次回调执行业务后续回调直接返回成功。public Boolean confirmDelivery(String orderNo, String result, String errorMsg) { String key order:delivery:done: orderNo; Boolean first redisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.MINUTES); if (Boolean.FALSE.equals(first)) { return true; // 已经处理过直接返回成功 } // 业务处理更新订单状态、扣库存或触发退款 }4.3 库存扣减一定要用乐观锁不能先查后减货道库存的扣减发生在订单创建成功后、出货指令下发前。也就是说下单时先检查并占用库存出货成功后再真正扣减。这里容易出现超卖问题。比如货道只剩3瓶水同时来了5个订单如果程序先select stock再update stock stock - 1在并发高的时候会减出负数。解决方法是更新时加条件UPDATE channel SET stock stock - 1 WHERE id #{channelId} AND stock 0如果更新影响行数为0说明库存不足订单直接标记异常。MyBatis-Plus的乐观锁插件也是这个思路但我觉得这里用原生SQL条件更直观而且能同时拿到影响行数做判断。5. 后台管理系统接口与权限设计给前端一份能直接对接的API后台管理系统前端需要一套统一规范的接口。这里说的规范不只是RESTful风格更关键的是统一返回结构、统一异常处理、统一认证方案。5.1 统一返回体和全局异常处理所有接口返回结构一模一样前端就不用每个接口都做一层容错。我定义的返回体包含code、message、data三个字段。code为200表示成功其他为业务错误码。Data public class RT { private Integer code; private String message; private T data; public static T RT ok(T data) { RT r new R(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T RT fail(Integer code, String message) { RT r new R(); r.setCode(code); r.setMessage(message); return r; } }对应的还有一个全局异常处理器捕获业务异常和系统异常。业务异常要返回可读的提示比如“该货道库存不足”“设备已离线无法出货”系统异常返回“系统繁忙”并且打印完整堆栈到日志。5.2 JWT认证和角色权限接口层面就要防住越权后台管理系统的接口不能裸奔。我在SpringBoot里配置了一个JWT拦截器前端登录后拿token后续请求在Header里带Authorization。具体实现登录成功后签发JWTRedis里存一份token与用户信息的映射设置过期时间。拦截器里校验token存在且Redis中有记录再解析出用户ID和角色列表。这里有个容易漏的点只验证JWT能解析通过是不够的因为token无法主动失效用户被禁用后token还能用所以一定要查Redis确认token仍在有效期内。角色权限方面我用自定义注解RequirePermission标注在Controller方法上。拦截器解析完用户后检查用户角色是否包含该权限码不包含就返回403。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); }5.3 前端对接时的接口规划源码里把接口按模块拆得很清晰前端对接基本可以照着菜单来/api/product/**商品增删改查、上下架/api/machine/**设备列表、详情、心跳记录、远程操作/api/channel/**货道绑定商品、库存调整、补货记录/api/order/**订单列表、详情、退款、异常单处理/api/dashboard/**数据统计销售额、订单量、设备在线率/api/system/**管理员、角色、菜单权限做数据统计的时候我强烈建议用独立的统计SQL不要在列表接口里又count又sum数据库压力会很大。每天凌晨用定时任务把前一天的汇总数据算好放一张统计表后台报表直接查汇总表速度会快很多。6. 把源码跑起来的完整步骤与过程中踩过的坑源码拿到手之后要跑起来不只是双击一个jar那么简单。我整理了一个从零开始的部署清单照着做不会出大问题。6.1 本地环境准备首先确认Java环境是JDK 1.8或11SpringBoot 2.7.x对这两个版本支持很好。然后装MySQL 8.0和Redis。源码里有一个schema.sql直接执行建库建表不用手动建。修改application.yml里的数据库连接、Redis连接、JWT密钥和过期时间spring: datasource: url: jdbc:mysql://localhost:3306/vending_machine?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 jwt: secret: your-secret-key expire-hours: 24启动类直接运行访问http://localhost:8080/doc.html就能看到Knife4j接口文档。首次登录用admin账号密码在初始化SQL里登录后在系统管理里改掉。6.2 我实际踩过的几个坑第一个坑是MySQL时区问题。直接连MySQL 8时如果url里不加serverTimezoneAsia/Shanghai在查询时间字段时会报错或者差8个小时。这个很多人第一次跑项目都会遇到。第二个坑是Redis没启动导致登录失败。因为JWT验签会查Redis如果Redis没起来登录接口直接报连接异常。建议先把Redis跑起来再启动项目。第三个坑是Mapper扫描路径。启动类上要加MapperScan(com.example.vending.mapper)否则MyBatis-Plus找不到Mapper接口。这个在源码里已经配置好了但如果你二次开发复制了Controller包到别的路径容易漏掉。6.3 上线前一定要做的三件事第一件事是修改JWT密钥不要用源码里的默认值。第二件事是开启生产环境日志配置把日志按天滚动输出方便排查订单异常。第三件事是设置MySQL的binlog至少保留7天因为订单对账时需要回溯数据。还有一个点是接口限流。无人售货机的设备回调接口和登录接口要限流防止被刷。我用了一个简单的拦截器对同一IP每分钟超过60次请求直接拒绝够用且不复杂。7. 最后说点开发这套系统的个人体会如果让我重新做一遍这套智慧无人售货机后台管理系统我会在一开始就把“设备离线”“出货失败”“超时退款”这三个场景设计得更完整而不是先做菜单和商品管理。因为用户对系统信任度的建立靠的不是界面多好看而是异常订单能不能被自动处理干净。关于“无人”这个概念我的理解是真正让售卖机实现无人值守的不是机器本身而是后台这套订单闭环——用户付款了机器必须出货机器没出货系统必须退款系统退款失败了财务必须能看到异常单。把这三条链路做扎实了系统就稳定了一大半。你拿到源码后可以先跑起来看一遍订单从创建到出货成功的过程再模拟一个设备离线的情况看看异常单和退款流程怎么走。等这两个流程都清楚了再做二次开发改业务会顺手很多。本文还有配套的精品资源点击获取
返回列表