大家好,我是大华。昨天在办公楼底下,我用了一下那种开门拿货,关门自动扣费的智能售货柜,真挺方便的。
其实这种售货柜并不少见,很多无人售货店、地铁站和景区都能经常看懂。
那这种流程是怎么实现的呢?下面我们来分析一下整个实现的流程。
场景:
- 你用微信扫描售货柜上的二维码
- 柜门咔嚓一声自动打开
- 你拿出想要的商品,同时可以随意更换
- 关上柜门,然后手机自动收到扣款通知
- 整个过程中无需任何额外操作
核心技术栈(Spingboot)
| 技术组件 | 作用 |
|---|---|
| Spring Boot | 后端主框架 |
| Redis | 高速缓存 |
| MQTT | 物联网通信协议 |
| MySQL | 关系型数据库 |
| 消息队列 | 异步任务处理 |
| 计算机视觉 | 拍摄商品识别 |
完整技术流程详解
第一阶段:扫码开门(身份验证与初始化)
用户动作:微信扫码 → 授权 → 柜门打开
后台流程:
- 身份认证:验证微信账号的合法性
- 设备状态检查:确认售货柜是否可用
- 创建会话:在Redis中建立临时购物车
- 数据采集:拍摄货架初始照片,记录传感器数据
- 开门指令:通过MQTT协议发送开门命令
技术要点:
- 使用Redis存储临时会话,读写速度达到微秒级
- MQTT协议专为物联网设计,低功耗、高可靠
- 初始快照为后续对比提供基准数据
第二阶段:自由选购(实时事件追踪)
用户动作:拿取商品 → 可能更换 → 继续选购
系统监控:
- 视觉追踪:摄像头实时识别手部动作和商品变化
- 重量感应:每个货道的传感器监测重量变化
- 事件上报:实时将"拿取/放回"动作发送到后台
- 实时记录:在Redis中更新购物车状态
技术难点突破:
- 实时视频流处理,延迟控制在100ms以内
- 多传感器数据融合,提高识别准确率
- 高并发事件处理,支持多用户同时购物
第三阶段:关门结算(异步清算流程)
用户动作:关闭柜门 → 自动触发结算
核心清算流程:
关门信号 → 启动异步任务 → 数据收集 → 三重校验 → 支付扣款 → 状态更新
详细步骤:
- 触发结算:门磁传感器检测到关门动作
- 异步处理:避免用户等待,另起线程处理复杂计算
- 数据收集:获取关门快照和最终传感器数据
- 三重校验 :
- 视觉对比:开门vs关门图片差异分析
- 重量分析:各货道重量变化计算
- 事件复核:核对实时记录的事件序列
- 冲突解决:当三种方式结果不一致时的智能决策
- 支付执行:调用支付接口完成扣款
- 状态更新:标记订单完成,清理缓存
示例代码实现
1. 核心数据模型
java
// 订单实体
@Entity
@Table(name = "vending_orders")
@Data
public class VendingOrder {
@Id
private String orderId;
private String userId; // 用户ID
private String deviceId; // 设备ID
private String status; // 状态: OPEN/CLOSED/PAID/FAILED
private BigDecimal amount; // 订单金额
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// Redis缓存中的设备会话
@Data
public class DeviceSession {
private String sessionId;
private String deviceId;
private String orderId;
private String userId;
private String status; // 会话状态
private LocalDateTime startTime;
private List<DeviceEvent> events; // 购物事件记录
}
// 设备事件
@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请求
java
@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("收到开门请求: deviceId={}", deviceId);
try {
OpenResult result = vendingService.processOpenDevice(deviceId, authToken);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (BusinessException e) {
log.warn("开门业务异常: {}", e.getMessage());
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
}
}
/**
* 查询订单状态
*/
@GetMapping("/order/{orderId}")
public ResponseEntity<ApiResponse> getOrderStatus(@PathVariable String orderId) {
VendingOrder order = vendingService.getOrderById(orderId);
return ResponseEntity.ok(ApiResponse.success(order));
}
}
// 统一API响应格式
@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. 核心服务层 - 开门处理
java
@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;
/**
* 处理开门请求的核心逻辑
*/
public OpenResult processOpenDevice(String deviceId, String authToken) {
// 1. 用户身份验证
String userId = authService.verifyWechatToken(authToken);
if (userId == null) {
throw new BusinessException("用户身份验证失败");
}
// 2. 检查设备状态
DeviceStatus deviceStatus = deviceService.getDeviceStatus(deviceId);
if (!deviceStatus.isAvailable()) {
throw new BusinessException("设备暂不可用: " + deviceStatus.getStatus());
}
// 3. 创建订单
VendingOrder order = orderService.createOrder(userId, deviceId);
log.info("创建订单成功: orderId={}, userId={}, deviceId={}",
order.getOrderId(), userId, deviceId);
// 4. 创建设备会话并缓存
DeviceSession session = createDeviceSession(deviceId, order);
cacheDeviceSession(session);
// 5. 锁定设备,避免重复开门
deviceService.lockDevice(deviceId, order.getOrderId());
// 6. 发送开门指令
mqttService.sendOpenCommand(deviceId);
// 7. 请求设备上报初始状态
mqttService.requestInitialSnapshot(deviceId);
return new OpenResult(order.getOrderId(), deviceId, "开门指令已发送");
}
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("设备会话已缓存: key={}", key);
}
private String buildSessionKey(String deviceId) {
return "vending:session:" + deviceId;
}
}
4. MQTT消息处理 - 设备通信
java
@Component
@Slf4j
public class MqttMessageHandler {
@Autowired
private VendingService vendingService;
@Autowired
private SettlementService settlementService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 处理关门事件 - 触发结算流程
*/
@MqttListener(topics = "device/${spring.application.env}/+/event/door_close")
public void handleDoorClose(String message) {
try {
DoorCloseEvent event = JSON.parseObject(message, DoorCloseEvent.class);
log.info("收到关门事件: deviceId={}", event.getDeviceId());
// 异步处理结算,不阻塞MQTT线程
CompletableFuture.runAsync(() -> {
settlementService.startSettlementProcess(event.getDeviceId());
});
} catch (Exception e) {
log.error("处理关门事件失败: 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);
}
}
/**
* 处理设备上报的初始/最终快照
*/
@MqttListener(topics = "device/${spring.application.env}/+/snapshot")
public void handleSnapshot(String message) {
try {
DeviceSnapshot snapshot = JSON.parseObject(message, DeviceSnapshot.class);
log.info("处理设备快照: deviceId={}, type={}",
snapshot.getDeviceId(), snapshot.getSnapshotType());
// 存储快照数据,用于后续对比分析
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. 结算服务 - 核心业务逻辑
java
@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;
/**
* 启动结算流程
*/
@Async("settlementExecutor")
public void startSettlementProcess(String deviceId) {
log.info("开始结算流程: deviceId={}", deviceId);
try {
// 1. 获取设备会话
DeviceSession session = getDeviceSession(deviceId);
if (session == null) {
log.error("设备会话不存在: deviceId={}", deviceId);
return;
}
// 2. 更新订单状态为结算中
orderService.updateOrderStatus(session.getOrderId(), "SETTLING");
// 3. 获取设备上报的最终数据
SettlementData settlementData = collectSettlementData(deviceId);
// 4. 执行结算计算
SettlementResult result = calculateSettlement(settlementData);
// 5. 处理支付
boolean paymentSuccess = processPayment(session, result);
// 6. 更新订单状态
updateOrderAfterSettlement(session, result, paymentSuccess);
// 7. 清理资源
cleanupAfterSettlement(deviceId, session.getOrderId());
log.info("结算流程完成: deviceId={}, orderId={}, success={}",
deviceId, session.getOrderId(), paymentSuccess);
} catch (Exception e) {
log.error("结算流程异常: deviceId={}", deviceId, e);
handleSettlementFailure(deviceId, e);
}
}
/**
* 收集结算所需的所有数据
*/
private SettlementData collectSettlementData(String deviceId) {
SettlementData data = new SettlementData();
// 获取初始和最终快照
data.setInitialSnapshot(deviceService.getInitialSnapshot(deviceId));
data.setFinalSnapshot(deviceService.getFinalSnapshot(deviceId));
// 获取重量传感器数据
data.setWeightData(deviceService.getWeightSensorData(deviceId));
// 获取购物事件记录
data.setProductEvents(getRecordedEvents(deviceId));
return data;
}
/**
* 核心结算算法 - 三重校验
*/
private SettlementResult calculateSettlement(SettlementData data) {
// 1. 视觉对比分析
List<Product> visualProducts = analyzeVisualChanges(
data.getInitialSnapshot(),
data.getFinalSnapshot()
);
// 2. 重量变化分析
List<Product> weightProducts = analyzeWeightChanges(data.getWeightData());
// 3. 事件记录分析
List<Product> eventProducts = analyzeEventSequence(data.getProductEvents());
// 4. 冲突解决和结果融合
return resolveProductConflicts(visualProducts, weightProducts, eventProducts);
}
/**
* 冲突解决策略
*/
private SettlementResult resolveProductConflicts(List<Product> visualProducts,
List<Product> weightProducts,
List<Product> eventProducts) {
SettlementResult result = new SettlementResult();
// 策略1: 视觉识别优先(最直接证据)
Map<String, Product> productMap = new HashMap<>();
// 首先信任视觉识别结果
for (Product product : visualProducts) {
productMap.put(product.getPosition(), product);
}
// 用重量数据验证和补充
for (Product weightProduct : weightProducts) {
Product visualProduct = productMap.get(weightProduct.getPosition());
if (visualProduct == null) {
// 视觉没识别到但重量有变化,信任重量数据
productMap.put(weightProduct.getPosition(), weightProduct);
}
}
// 用事件记录进行最终校验
result.setFinalProducts(new ArrayList<>(productMap.values()));
result.setConflictResolved(true);
log.debug("冲突解决完成: 视觉识别{}个, 重量变化{}个, 最终确认{}个",
visualProducts.size(), weightProducts.size(), result.getFinalProducts().size());
return result;
}
}
6. 支付服务
java
@Service
@Slf4j
public class PaymentService {
@Autowired
private WechatPayService wechatPayService;
@Autowired
private OrderService orderService;
/**
* 执行支付
*/
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. 配置类
java
@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;
}
}
总结
1.异步处理 :结算流程异步化,用户无需等待 2.三重校验 :视觉+重量+事件记录,确保准确率 3.实时通信 :MQTT保证设备与后台实时通信 4.缓存优化 :Redis提升系统响应速度 5.异常容错:完善的异常处理机制
这种系统完美融合了物联网、云计算、移动支付等前沿技术,为用户提供了拿了就走的无感购物体验,代表了零售行业数字化转型的最新成果。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+MySQL+Vue实现文件共享系统》
《这 10 个 MySQL 高级用法,让你的代码又快又好看》