SpringBoot + MQTT實(shí)現(xiàn)取貨就走的智能售貨柜系統(tǒng)完整流程
大家好,我是大華。昨天在辦公樓底下,我用了一下那種開門拿貨,關(guān)門自動(dòng)扣費(fèi)的智能售貨柜,真挺方便的。
其實(shí)這種售貨柜并不少見,很多無人售貨店、地鐵站和景區(qū)都能經(jīng)常看懂。
那這種流程是怎么實(shí)現(xiàn)的呢?下面我們來分析一下整個(gè)實(shí)現(xiàn)的流程。
場(chǎng)景:
- 你用微信掃描售貨柜上的二維碼
- 柜門咔嚓一聲自動(dòng)打開
- 你拿出想要的商品,同時(shí)可以隨意更換
- 關(guān)上柜門,然后手機(jī)自動(dòng)收到扣款通知
- 整個(gè)過程中無需任何額外操作
核心技術(shù)棧(Spingboot)
| 技術(shù)組件 | 作用 |
|---|---|
| Spring Boot | 后端主框架 |
| Redis | 高速緩存 |
| MQTT | 物聯(lián)網(wǎng)通信協(xié)議 |
| MySQL | 關(guān)系型數(shù)據(jù)庫 |
| 消息隊(duì)列 | 異步任務(wù)處理 |
| 計(jì)算機(jī)視覺 | 拍攝商品識(shí)別 |
完整技術(shù)流程詳解
第一階段:掃碼開門(身份驗(yàn)證與初始化)
用戶動(dòng)作:微信掃碼 → 授權(quán) → 柜門打開
后臺(tái)流程:
- 身份認(rèn)證:驗(yàn)證微信賬號(hào)的合法性
- 設(shè)備狀態(tài)檢查:確認(rèn)售貨柜是否可用
- 創(chuàng)建會(huì)話:在Redis中建立臨時(shí)購物車
- 數(shù)據(jù)采集:拍攝貨架初始照片,記錄傳感器數(shù)據(jù)
- 開門指令:通過MQTT協(xié)議發(fā)送開門命令
技術(shù)要點(diǎn):
- 使用Redis存儲(chǔ)臨時(shí)會(huì)話,讀寫速度達(dá)到微秒級(jí)
- MQTT協(xié)議專為物聯(lián)網(wǎng)設(shè)計(jì),低功耗、高可靠
- 初始快照為后續(xù)對(duì)比提供基準(zhǔn)數(shù)據(jù)
第二階段:自由選購(實(shí)時(shí)事件追蹤)
用戶動(dòng)作:拿取商品 → 可能更換 → 繼續(xù)選購
系統(tǒng)監(jiān)控:
- 視覺追蹤:攝像頭實(shí)時(shí)識(shí)別手部動(dòng)作和商品變化
- 重量感應(yīng):每個(gè)貨道的傳感器監(jiān)測(cè)重量變化
- 事件上報(bào):實(shí)時(shí)將"拿取/放回"動(dòng)作發(fā)送到后臺(tái)
- 實(shí)時(shí)記錄:在Redis中更新購物車狀態(tài)
技術(shù)難點(diǎn)突破:
- 實(shí)時(shí)視頻流處理,延遲控制在100ms以內(nèi)
- 多傳感器數(shù)據(jù)融合,提高識(shí)別準(zhǔn)確率
- 高并發(fā)事件處理,支持多用戶同時(shí)購物
第三階段:關(guān)門結(jié)算(異步清算流程)
用戶動(dòng)作:關(guān)閉柜門 → 自動(dòng)觸發(fā)結(jié)算
核心清算流程:
關(guān)門信號(hào) → 啟動(dòng)異步任務(wù) → 數(shù)據(jù)收集 → 三重校驗(yàn) → 支付扣款 → 狀態(tài)更新
詳細(xì)步驟:
- 觸發(fā)結(jié)算:門磁傳感器檢測(cè)到關(guān)門動(dòng)作
- 異步處理:避免用戶等待,另起線程處理復(fù)雜計(jì)算
- 數(shù)據(jù)收集:獲取關(guān)門快照和最終傳感器數(shù)據(jù)
- 三重校驗(yàn):
- 視覺對(duì)比:開門vs關(guān)門圖片差異分析
- 重量分析:各貨道重量變化計(jì)算
- 事件復(fù)核:核對(duì)實(shí)時(shí)記錄的事件序列
- 沖突解決:當(dāng)三種方式結(jié)果不一致時(shí)的智能決策
- 支付執(zhí)行:調(diào)用支付接口完成扣款
- 狀態(tài)更新:標(biāo)記訂單完成,清理緩存
示例代碼實(shí)現(xiàn)
1. 核心數(shù)據(jù)模型
// 訂單實(shí)體
@Entity
@Table(name = "vending_orders")
@Data
public class VendingOrder {
@Id
private String orderId;
private String userId; // 用戶ID
private String deviceId; // 設(shè)備ID
private String status; // 狀態(tài): OPEN/CLOSED/PAID/FAILED
private BigDecimal amount; // 訂單金額
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// Redis緩存中的設(shè)備會(huì)話
@Data
public class DeviceSession {
private String sessionId;
private String deviceId;
private String orderId;
private String userId;
private String status; // 會(huì)話狀態(tài)
private LocalDateTime startTime;
private List<DeviceEvent> events; // 購物事件記錄
}
// 設(shè)備事件
@Data
public class DeviceEvent {
private String eventId;
private String deviceId;
private String orderId;
private String type; // PICK/PUT_BACK
private String productId;
private LocalDateTime eventTime;
private String position; // 貨道位置
}2. 控制器層 - 處理HTTP請(qǐng)求
@RestController
@RequestMapping("/api/vending")
@Slf4j
public class VendingController {
@Autowired
private VendingService vendingService;
@Autowired
private SettlementService settlementService;
/**
* 掃碼開門接口
*/
@PostMapping("/open")
public ResponseEntity<ApiResponse> openDevice(
@RequestParam String deviceId,
@RequestParam String authToken) {
log.info("收到開門請(qǐng)求: deviceId={}", deviceId);
try {
OpenResult result = vendingService.processOpenDevice(deviceId, authToken);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (BusinessException e) {
log.warn("開門業(yè)務(wù)異常: {}", e.getMessage());
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
}
}
/**
* 查詢訂單狀態(tài)
*/
@GetMapping("/order/{orderId}")
public ResponseEntity<ApiResponse> getOrderStatus(@PathVariable String orderId) {
VendingOrder order = vendingService.getOrderById(orderId);
return ResponseEntity.ok(ApiResponse.success(order));
}
}
// 統(tǒng)一API響應(yīng)格式
@Data
class ApiResponse {
private boolean success;
private String message;
private Object data;
public static ApiResponse success(Object data) {
ApiResponse response = new ApiResponse();
response.setSuccess(true);
response.setData(data);
return response;
}
public static ApiResponse error(String message) {
ApiResponse response = new ApiResponse();
response.setSuccess(false);
response.setMessage(message);
return response;
}
}3. 核心服務(wù)層 - 開門處理
@Service
@Slf4j
public class VendingService {
@Autowired
private UserAuthService authService;
@Autowired
private DeviceService deviceService;
@Autowired
private OrderService orderService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private MqttService mqttService;
/**
* 處理開門請(qǐng)求的核心邏輯
*/
public OpenResult processOpenDevice(String deviceId, String authToken) {
// 1. 用戶身份驗(yàn)證
String userId = authService.verifyWechatToken(authToken);
if (userId == null) {
throw new BusinessException("用戶身份驗(yàn)證失敗");
}
// 2. 檢查設(shè)備狀態(tài)
DeviceStatus deviceStatus = deviceService.getDeviceStatus(deviceId);
if (!deviceStatus.isAvailable()) {
throw new BusinessException("設(shè)備暫不可用: " + deviceStatus.getStatus());
}
// 3. 創(chuàng)建訂單
VendingOrder order = orderService.createOrder(userId, deviceId);
log.info("創(chuàng)建訂單成功: orderId={}, userId={}, deviceId={}",
order.getOrderId(), userId, deviceId);
// 4. 創(chuàng)建設(shè)備會(huì)話并緩存
DeviceSession session = createDeviceSession(deviceId, order);
cacheDeviceSession(session);
// 5. 鎖定設(shè)備,避免重復(fù)開門
deviceService.lockDevice(deviceId, order.getOrderId());
// 6. 發(fā)送開門指令
mqttService.sendOpenCommand(deviceId);
// 7. 請(qǐng)求設(shè)備上報(bào)初始狀態(tài)
mqttService.requestInitialSnapshot(deviceId);
return new OpenResult(order.getOrderId(), deviceId, "開門指令已發(fā)送");
}
private DeviceSession createDeviceSession(String deviceId, VendingOrder order) {
DeviceSession session = new DeviceSession();
session.setSessionId(UUID.randomUUID().toString());
session.setDeviceId(deviceId);
session.setOrderId(order.getOrderId());
session.setUserId(order.getUserId());
session.setStatus("OPEN");
session.setStartTime(LocalDateTime.now());
session.setEvents(new ArrayList<>());
return session;
}
private void cacheDeviceSession(DeviceSession session) {
String key = buildSessionKey(session.getDeviceId());
redisTemplate.opsForValue().set(key, session, Duration.ofMinutes(10));
log.debug("設(shè)備會(huì)話已緩存: key={}", key);
}
private String buildSessionKey(String deviceId) {
return "vending:session:" + deviceId;
}
}4. MQTT消息處理 - 設(shè)備通信
@Component
@Slf4j
public class MqttMessageHandler {
@Autowired
private VendingService vendingService;
@Autowired
private SettlementService settlementService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 處理關(guān)門事件 - 觸發(fā)結(jié)算流程
*/
@MqttListener(topics = "device/${spring.application.env}/+/event/door_close")
public void handleDoorClose(String message) {
try {
DoorCloseEvent event = JSON.parseObject(message, DoorCloseEvent.class);
log.info("收到關(guān)門事件: deviceId={}", event.getDeviceId());
// 異步處理結(jié)算,不阻塞MQTT線程
CompletableFuture.runAsync(() -> {
settlementService.startSettlementProcess(event.getDeviceId());
});
} catch (Exception e) {
log.error("處理關(guān)門事件失敗: message={}", message, e);
}
}
/**
* 處理商品拿取/放回事件
*/
@MqttListener(topics = "device/${spring.application.env}/+/event/product")
public void handleProductEvent(String message) {
try {
ProductEvent event = JSON.parseObject(message, ProductEvent.class);
log.debug("處理商品事件: deviceId={}, type={}, product={}",
event.getDeviceId(), event.getEventType(), event.getProductId());
// 記錄到Redis緩存
recordProductEvent(event);
} catch (Exception e) {
log.error("處理商品事件失敗: message={}", message, e);
}
}
/**
* 處理設(shè)備上報(bào)的初始/最終快照
*/
@MqttListener(topics = "device/${spring.application.env}/+/snapshot")
public void handleSnapshot(String message) {
try {
DeviceSnapshot snapshot = JSON.parseObject(message, DeviceSnapshot.class);
log.info("處理設(shè)備快照: deviceId={}, type={}",
snapshot.getDeviceId(), snapshot.getSnapshotType());
// 存儲(chǔ)快照數(shù)據(jù),用于后續(xù)對(duì)比分析
storeDeviceSnapshot(snapshot);
} catch (Exception e) {
log.error("處理快照失敗: message={}", message, e);
}
}
private void recordProductEvent(ProductEvent event) {
String sessionKey = "vending:session:" + event.getDeviceId();
DeviceSession session = (DeviceSession) redisTemplate.opsForValue().get(sessionKey);
if (session != null) {
DeviceEvent deviceEvent = convertToDeviceEvent(event, session.getOrderId());
session.getEvents().add(deviceEvent);
redisTemplate.opsForValue().set(sessionKey, session, Duration.ofMinutes(10));
}
}
}5. 結(jié)算服務(wù) - 核心業(yè)務(wù)邏輯
@Service
@Slf4j
public class SettlementService {
@Autowired
private OrderService orderService;
@Autowired
private DeviceService deviceService;
@Autowired
private PaymentService paymentService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private NotificationService notificationService;
/**
* 啟動(dòng)結(jié)算流程
*/
@Async("settlementExecutor")
public void startSettlementProcess(String deviceId) {
log.info("開始結(jié)算流程: deviceId={}", deviceId);
try {
// 1. 獲取設(shè)備會(huì)話
DeviceSession session = getDeviceSession(deviceId);
if (session == null) {
log.error("設(shè)備會(huì)話不存在: deviceId={}", deviceId);
return;
}
// 2. 更新訂單狀態(tài)為結(jié)算中
orderService.updateOrderStatus(session.getOrderId(), "SETTLING");
// 3. 獲取設(shè)備上報(bào)的最終數(shù)據(jù)
SettlementData settlementData = collectSettlementData(deviceId);
// 4. 執(zhí)行結(jié)算計(jì)算
SettlementResult result = calculateSettlement(settlementData);
// 5. 處理支付
boolean paymentSuccess = processPayment(session, result);
// 6. 更新訂單狀態(tài)
updateOrderAfterSettlement(session, result, paymentSuccess);
// 7. 清理資源
cleanupAfterSettlement(deviceId, session.getOrderId());
log.info("結(jié)算流程完成: deviceId={}, orderId={}, success={}",
deviceId, session.getOrderId(), paymentSuccess);
} catch (Exception e) {
log.error("結(jié)算流程異常: deviceId={}", deviceId, e);
handleSettlementFailure(deviceId, e);
}
}
/**
* 收集結(jié)算所需的所有數(shù)據(jù)
*/
private SettlementData collectSettlementData(String deviceId) {
SettlementData data = new SettlementData();
// 獲取初始和最終快照
data.setInitialSnapshot(deviceService.getInitialSnapshot(deviceId));
data.setFinalSnapshot(deviceService.getFinalSnapshot(deviceId));
// 獲取重量傳感器數(shù)據(jù)
data.setWeightData(deviceService.getWeightSensorData(deviceId));
// 獲取購物事件記錄
data.setProductEvents(getRecordedEvents(deviceId));
return data;
}
/**
* 核心結(jié)算算法 - 三重校驗(yàn)
*/
private SettlementResult calculateSettlement(SettlementData data) {
// 1. 視覺對(duì)比分析
List<Product> visualProducts = analyzeVisualChanges(
data.getInitialSnapshot(),
data.getFinalSnapshot()
);
// 2. 重量變化分析
List<Product> weightProducts = analyzeWeightChanges(data.getWeightData());
// 3. 事件記錄分析
List<Product> eventProducts = analyzeEventSequence(data.getProductEvents());
// 4. 沖突解決和結(jié)果融合
return resolveProductConflicts(visualProducts, weightProducts, eventProducts);
}
/**
* 沖突解決策略
*/
private SettlementResult resolveProductConflicts(List<Product> visualProducts,
List<Product> weightProducts,
List<Product> eventProducts) {
SettlementResult result = new SettlementResult();
// 策略1: 視覺識(shí)別優(yōu)先(最直接證據(jù))
Map<String, Product> productMap = new HashMap<>();
// 首先信任視覺識(shí)別結(jié)果
for (Product product : visualProducts) {
productMap.put(product.getPosition(), product);
}
// 用重量數(shù)據(jù)驗(yàn)證和補(bǔ)充
for (Product weightProduct : weightProducts) {
Product visualProduct = productMap.get(weightProduct.getPosition());
if (visualProduct == null) {
// 視覺沒識(shí)別到但重量有變化,信任重量數(shù)據(jù)
productMap.put(weightProduct.getPosition(), weightProduct);
}
}
// 用事件記錄進(jìn)行最終校驗(yàn)
result.setFinalProducts(new ArrayList<>(productMap.values()));
result.setConflictResolved(true);
log.debug("沖突解決完成: 視覺識(shí)別{}個(gè), 重量變化{}個(gè), 最終確認(rèn){}個(gè)",
visualProducts.size(), weightProducts.size(), result.getFinalProducts().size());
return result;
}
}6. 支付服務(wù)
@Service
@Slf4j
public class PaymentService {
@Autowired
private WechatPayService wechatPayService;
@Autowired
private OrderService orderService;
/**
* 執(zhí)行支付
*/
public boolean processPayment(DeviceSession session, SettlementResult result) {
try {
PaymentRequest request = new PaymentRequest();
request.setUserId(session.getUserId());
request.setOrderId(session.getOrderId());
request.setAmount(calculateTotalAmount(result.getFinalProducts()));
request.setDescription("智能售貨柜購物");
PaymentResponse response = wechatPayService.unifiedOrder(request);
if ("SUCCESS".equals(response.getResultCode())) {
log.info("支付成功: orderId={}, amount={}",
session.getOrderId(), request.getAmount());
return true;
} else {
log.warn("支付失敗: orderId={}, error={}",
session.getOrderId(), response.getErrMsg());
return false;
}
} catch (Exception e) {
log.error("支付處理異常: orderId={}", session.getOrderId(), e);
return false;
}
}
private BigDecimal calculateTotalAmount(List<Product> products) {
return products.stream()
.map(Product::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}7. 配置類
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {
@Bean("settlementExecutor")
public TaskExecutor settlementTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("settlement-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}總結(jié)
1.異步處理:結(jié)算流程異步化,用戶無需等待 2.三重校驗(yàn):視覺+重量+事件記錄,確保準(zhǔn)確率 3.實(shí)時(shí)通信:MQTT保證設(shè)備與后臺(tái)實(shí)時(shí)通信 4.緩存優(yōu)化:Redis提升系統(tǒng)響應(yīng)速度 5.異常容錯(cuò):完善的異常處理機(jī)制
這種系統(tǒng)完美融合了物聯(lián)網(wǎng)、云計(jì)算、移動(dòng)支付等前沿技術(shù),為用戶提供了拿了就走的無感購物體驗(yàn),代表了零售行業(yè)數(shù)字化轉(zhuǎn)型的最新成果。
到此這篇關(guān)于SpringBoot + MQTT實(shí)現(xiàn)取貨就走的智能售貨柜系統(tǒng)完整流程的文章就介紹到這了,更多相關(guān)SpringBoot MQTT智能售貨柜系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Eclipse 出現(xiàn)A configuration with this name already exists問題解決方
這篇文章主要介紹了Eclipse 出現(xiàn)A configuration with this name already exists問題解決方法的相關(guān)資料,需要的朋友可以參考下2016-11-11
IDEA?Debug過程中使用Drop?Frame或Reset?Frame實(shí)現(xiàn)操作回退的方法
在IDEA中就提供了一個(gè)幫助你回退代碼的機(jī)會(huì),但這個(gè)方法并不是萬能的,好了,下面就來具體說說IDEA?Debug過程中使用Drop?Frame或Reset?Frame實(shí)現(xiàn)操作回退的方法,感興趣的朋友一起看看吧2022-04-04
在Spring框架下配置Quartz集群的詳細(xì)步驟(MySQL數(shù)據(jù)源)
Quartz 是一個(gè)功能強(qiáng)大的調(diào)度庫,可以在 Java 應(yīng)用中用于執(zhí)行定時(shí)任務(wù),本文將介紹如何在 Spring 框架下配置 Quartz 集群,并使用 MySQL 作為數(shù)據(jù)源來存儲(chǔ)調(diào)度信息,文中有詳細(xì)的代碼供大家參考,需要的朋友可以參考下2025-01-01
SpringBoot使用MyBatis的XML文件進(jìn)行SQL語句編寫
在現(xiàn)代 Java Web 開發(fā)中,Spring Boot 和 MyBatis 是兩個(gè)非常流行的技術(shù)框架,本文將詳細(xì)介紹如何在 Spring Boot 項(xiàng)目中使用 MyBatis 的 XML 文件來編寫 SQL 語句,感興趣的可以了解下2025-07-07
SpringBoot實(shí)現(xiàn)動(dòng)態(tài)端口切換黑魔法
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何實(shí)現(xiàn)動(dòng)態(tài)端口切換黑魔法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-12-12
maven package后Idea項(xiàng)目中找不到target文件的解決
在Idea中執(zhí)行mavenpackage打包后,target文件不顯示,點(diǎn)擊「ShowinExplore」可以在本地文件夾中查到,解決方法:在Idea的Maven工具窗口中,右鍵點(diǎn)擊項(xiàng)目,選擇Reimport,刷新項(xiàng)目即可2024-11-11

