现代智能售货柜系统采用分层架构设计,主要由设备层、通信层、应用层和数据层组成。这种架构设计充分考虑了系统的扩展性、稳定性和实时性需求。
设备层是整个系统的物理基础,由智能售货柜硬件设备构成。每个售货柜都配备了多种传感器:
这些传感器通过STM32F407主控芯片进行数据采集和处理,形成一个完整的物联网终端设备。主控芯片运行轻量级的FreeRTOS实时操作系统,确保传感器数据的及时采集和处理。
通信层采用MQTT协议实现设备与云端的实时通信。我们选择EMQX作为MQTT Broker,它支持:
应用层基于Spring Boot构建,采用微服务架构设计,包含以下核心服务:
数据层采用MySQL+Redis的组合方案:
在设备管理服务设计中,我们实现了以下关键功能:
订单服务的设计特别注意了分布式事务问题:
java复制@Transactional
public void createOrder(OrderDTO orderDTO) {
// 1. 扣减库存
inventoryService.deductStock(orderDTO.getItems());
// 2. 生成订单
Order order = convertToOrder(orderDTO);
orderMapper.insert(order);
// 3. 发起支付
paymentService.createPayment(order);
}
库存服务采用Redis+Lua脚本实现原子操作:
lua复制-- 库存扣减脚本
local key = KEYS[1]
local num = tonumber(ARGV[1])
local stock = tonumber(redis.call('GET', key))
if stock >= num then
redis.call('DECRBY', key, num)
return 1
else
return 0
end
我们制定了严格的MQTT主题命名规范,采用四级主题结构:
典型主题示例:
code复制smart-vending/cabinet/A1B2C3/status // 设备状态上报
smart-vending/cabinet/A1B2C3/event // 事件上报
smart-vending/controller/D4E5F6/command // 控制命令
根据业务重要性选择不同的QoS等级:
| 消息类型 | QoS等级 | 重试机制 | 应用场景 |
|---|---|---|---|
| 心跳包 | 0 | 无 | 设备在线状态检测 |
| 传感器数据 | 1 | 3次重试 | 重量、温度等数据 |
| 交易数据 | 2 | 确保送达 | 订单生成、支付结果 |
| 控制命令 | 1 | 3次重试 | 开门、重启等指令 |
对于关键业务消息,我们还在应用层实现了确认机制:
java复制public void handleDoorEvent(MqttMessage message) {
try {
processEvent(message);
// 发送处理成功的ACK
mqttTemplate.publish(
message.getProperties().getResponseTopic(),
new MqttMessage("ACK".getBytes())
);
} catch (Exception e) {
log.error("处理门状态事件失败", e);
}
}
采用JSON格式传递消息,统一字段命名规范:
json复制{
"msgId": "uuidv4",
"timestamp": 1630000000,
"deviceId": "A1B2C3",
"data": {
"eventType": "door_open",
"payload": {
"sensorValue": 1,
"triggerTime": "2023-08-01T10:00:00Z"
}
}
}
消息体包含以下必填字段:
创建自定义的MQTT配置类:
java复制@Configuration
@EnableConfigurationProperties(MqttProperties.class)
public class MqttConfig {
@Bean
public MqttConnectOptions mqttConnectOptions(MqttProperties properties) {
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[]{properties.getBrokerUrl()});
options.setUserName(properties.getUsername());
options.setPassword(properties.getPassword().toCharArray());
options.setAutomaticReconnect(true);
options.setCleanSession(true);
options.setConnectionTimeout(10);
options.setKeepAliveInterval(60);
options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
return options;
}
@Bean
public IMqttAsyncClient mqttAsyncClient(MqttProperties properties) {
try {
IMqttAsyncClient client = new MqttAsyncClient(
properties.getBrokerUrl(),
properties.getClientId(),
new MemoryPersistence()
);
client.connect(mqttConnectOptions(properties)).waitForCompletion();
return client;
} catch (MqttException e) {
throw new RuntimeException("MQTT客户端初始化失败", e);
}
}
}
配置参数说明:
实现消息监听的两种方式:
java复制@MqttListener(topics = "smart-vending/+/+/event")
public void handleDeviceEvent(String payload, @Header(MqttHeaders.RECEIVED_TOPIC) String topic) {
DeviceEvent event = parseEvent(payload);
eventProcessor.process(event);
}
java复制@Bean
public MessageProducer inbound() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(
"serverConsumer",
mqttClientFactory(),
"smart-vending/#");
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return message -> {
String topic = (String) message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC);
String payload = (String) message.getPayload();
messageDispatcher.dispatch(topic, payload);
};
}
封装统一的MQTT消息发送服务:
java复制@Service
@RequiredArgsConstructor
public class MqttMessagingService {
private final IMqttAsyncClient mqttAsyncClient;
public void publish(String topic, String payload, int qos, boolean retained) {
try {
MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
message.setQos(qos);
message.setRetained(retained);
mqttAsyncClient.publish(topic, message);
} catch (MqttException e) {
throw new RuntimeException("MQTT消息发送失败", e);
}
}
public void publishCommand(String deviceId, String command) {
String topic = String.format("smart-vending/%s/command", deviceId);
publish(topic, buildCommandMessage(command), 1, false);
}
}
完整的用户购物时序如下:
http复制POST /api/v1/cabinet/open
{
"cabinetId": "A1B2C3",
"userId": "user123",
"timestamp": 1630000000,
"signature": "xxxxxx"
}
java复制public void processOpenRequest(OpenRequest request) {
// 1. 验证用户权限
if (!accessControlService.canOpen(request.getUserId(), request.getCabinetId())) {
throw new PermissionDeniedException("无开门权限");
}
// 2. 生成开门令牌
String token = tokenGenerator.generate(request.getUserId(), request.getCabinetId());
// 3. 下发开门指令
mqttMessagingService.publishCommand(
request.getCabinetId(),
buildOpenCommand(token)
);
// 4. 记录开门日志
accessLogService.logOpen(request.getUserId(), request.getCabinetId());
}
c复制// STM32上的开门处理逻辑
void handle_open_command(const char* token) {
if (validate_token(token)) {
unlock_door();
start_monitoring();
send_status_update();
}
}
java复制public void handleWeightChange(String cabinetId, WeightChangeEvent event) {
// 1. 识别商品
List<ProductItem> items = productRecognizer.recognize(event.getChanges());
// 2. 生成预订单
PendingOrder order = orderService.createPendingOrder(
cabinetId,
getCurrentUser(),
items
);
// 3. 更新缓存库存
inventoryCacheService.updateStock(cabinetId, items);
}
java复制@Transactional
public void processCheckout(String cabinetId, String userId) {
// 1. 获取待结算订单
PendingOrder order = orderService.getPendingOrder(cabinetId, userId);
// 2. 扣减正式库存
inventoryService.deductStock(cabinetId, order.getItems());
// 3. 生成正式订单
Order formalOrder = orderService.confirmOrder(order);
// 4. 发起支付
paymentService.processPayment(formalOrder);
// 5. 清理缓存
orderService.clearPendingOrder(cabinetId, userId);
}
库存管理采用多级缓存策略:
库存同步流程:
java复制@Scheduled(fixedRate = 60000)
public void syncInventory() {
List<Cabinet> cabinets = cabinetService.getAllActiveCabinets();
cabinets.forEach(cabinet -> {
// 从Redis获取最新库存
Map<String, Integer> cacheStock = inventoryCacheService.getStock(cabinet.getId());
// 与数据库中的库存对比
Map<String, Integer> dbStock = inventoryService.getStock(cabinet.getId());
// 解决差异
reconcileStock(cabinet.getId(), cacheStock, dbStock);
});
}
private void reconcileStock(String cabinetId,
Map<String, Integer> cacheStock,
Map<String, Integer> dbStock) {
// 找出差异项
MapDifference<String, Integer> diff = Maps.difference(cacheStock, dbStock);
if (!diff.areEqual()) {
// 记录差异日志
discrepancyLogService.logDiscrepancy(cabinetId, diff);
// 以缓存数据为准更新数据库
inventoryService.batchUpdateStock(cabinetId, cacheStock);
}
}
针对网络不稳定的情况,我们实现了多级保障机制:
c复制#define MAX_QUEUE_SIZE 100
typedef struct {
char topic[128];
char message[256];
int qos;
time_t timestamp;
} MqttMessage;
MqttMessage messageQueue[MAX_QUEUE_SIZE];
int queueHead = 0;
int queueTail = 0;
void enqueueMessage(const char* topic, const char* msg, int qos) {
if ((queueTail + 1) % MAX_QUEUE_SIZE != queueHead) {
strncpy(messageQueue[queueTail].topic, topic, 127);
strncpy(messageQueue[queueTail].message, msg, 255);
messageQueue[queueTail].qos = qos;
messageQueue[queueTail].timestamp = time(NULL);
queueTail = (queueTail + 1) % MAX_QUEUE_SIZE;
}
}
void processQueue() {
while (queueHead != queueTail && mqttConnected()) {
if (publish(messageQueue[queueHead].topic,
messageQueue[queueHead].message,
messageQueue[queueHead].qos)) {
queueHead = (queueHead + 1) % MAX_QUEUE_SIZE;
} else {
break;
}
}
}
java复制public void handleOfflineMessage(String deviceId) {
List<OfflineMessage> messages = offlineMessageRepository
.findByDeviceIdAndStatus(deviceId, MessageStatus.PENDING);
if (!messages.isEmpty()) {
mqttMessagingService.reconnect(deviceId);
messages.forEach(msg -> {
try {
mqttMessagingService.publish(
msg.getTopic(),
msg.getContent(),
msg.getQos(),
false
);
msg.setStatus(MessageStatus.DELIVERED);
offlineMessageRepository.save(msg);
} catch (Exception e) {
log.error("补发消息失败: {}", msg.getId(), e);
}
});
}
}
采用分布式事务方案保证关键操作的数据一致性:
java复制public void createOrderWithTransaction(OrderDTO orderDTO) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
try {
// 1. 扣减库存
inventoryService.deductStock(orderDTO.getItems());
// 2. 创建订单
Order order = orderService.createOrder(orderDTO);
// 3. 创建支付记录
paymentService.createPayment(order);
return order;
} catch (Exception e) {
status.setRollbackOnly();
throw new RuntimeException("订单创建失败", e);
}
});
}
java复制@Transactional
public void confirmInventoryChange(String changeId) {
InventoryChange change = inventoryChangeRepository
.findById(changeId)
.orElseThrow(() -> new NotFoundException("变更记录不存在"));
if (change.getStatus() == ChangeStatus.PENDING) {
// 执行实际库存变更
inventoryRepository.adjustStock(
change.getProductId(),
change.getDelta()
);
// 更新状态为已完成
change.setStatus(ChangeStatus.COMPLETED);
inventoryChangeRepository.save(change);
}
}
@Scheduled(fixedDelay = 30000)
public void compensateInventoryChanges() {
List<InventoryChange> pendingChanges = inventoryChangeRepository
.findByStatus(ChangeStatus.PENDING);
pendingChanges.forEach(change -> {
try {
confirmInventoryChange(change.getId());
} catch (Exception e) {
log.error("补偿库存变更失败: {}", change.getId(), e);
}
});
}
sql复制-- 订单表索引
CREATE INDEX idx_order_user ON order(user_id);
CREATE INDEX idx_order_status ON order(status);
CREATE INDEX idx_order_create_time ON order(create_time);
-- 库存表联合索引
CREATE INDEX idx_inventory_product ON inventory(cabinet_id, product_id);
java复制@Repository
public interface OrderRepository extends JpaRepository<Order, String> {
@Query(value = "SELECT * FROM order WHERE user_id = :userId " +
"ORDER BY create_time DESC LIMIT :size OFFSET :offset",
nativeQuery = true)
List<Order> findUserOrders(@Param("userId") String userId,
@Param("offset") int offset,
@Param("size") int size);
@EntityGraph(attributePaths = {"items.product"})
Optional<Order> findWithItemsById(String id);
}
java复制@Transactional
public void batchUpdateStock(String cabinetId, Map<String, Integer> stockMap) {
String sql = "UPDATE inventory SET stock = ? " +
"WHERE cabinet_id = ? AND product_id = ?";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String productId = new ArrayList<>(stockMap.keySet()).get(i);
ps.setInt(1, stockMap.get(productId));
ps.setString(2, cabinetId);
ps.setString(3, productId);
}
@Override
public int getBatchSize() {
return stockMap.size();
}
});
}
采用多级缓存架构:
java复制@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
}
@Service
@CacheConfig(cacheNames = "productCache")
public class ProductService {
@Cacheable(key = "#productId")
public Product getProduct(String productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException("商品不存在"));
}
}
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
@Service
@RequiredArgsConstructor
public class InventoryCacheService {
private final RedisTemplate<String, Object> redisTemplate;
public void updateStock(String cabinetId, List<ProductItem> items) {
String key = "inventory:" + cabinetId;
Map<String, String> updates = new HashMap<>();
items.forEach(item -> {
updates.put(item.getProductId(),
String.valueOf(item.getQuantity()));
});
redisTemplate.opsForHash().putAll(key, updates);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
}
java复制@Bean
public MqttConnectOptions mqttConnectOptions(MqttProperties properties) {
MqttConnectOptions options = new MqttConnectOptions();
// ...其他配置
// 启用TLS
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(new TrustSelfSignedStrategy())
.build();
options.setSocketFactory(sslContext.getSocketFactory());
return options;
}
java复制public boolean verifySignature(OpenRequest request) {
String secret = getSecretKey(request.getUserId());
String content = request.getCabinetId() +
request.getUserId() +
request.getTimestamp();
String expected = HmacUtils.hmacSha256Hex(secret, content);
return expected.equals(request.getSignature());
}
private String getSecretKey(String userId) {
// 从安全存储中获取用户密钥
return userSecurityRepository
.findById(userId)
.orElseThrow(() -> new NotFoundException("用户不存在"))
.getSecretKey();
}
bash复制# EMQX ACL配置示例
# 设备只能发布自己的主题
{allow, {user, "%u"}, publish, ["smart-vending/%u/%c/status"]}.
# 服务端可以订阅所有主题
{allow, {user, "server"}, subscribe, ["smart-vending/#"]}.
java复制@PreAuthorize("hasPermission(#cabinetId, 'CABINET', 'READ')")
public CabinetDetail getCabinetDetail(String cabinetId) {
return cabinetService.getDetail(cabinetId);
}
@PreAuthorize("hasRole('ADMIN') || hasPermission(#request.cabinetId, 'CABINET', 'WRITE')")
public void processOpenRequest(OpenRequest request) {
// 处理开门请求
}
java复制@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics() {
return registry -> {
registry.config().commonTags("application", "smart-vending");
// 自定义指标
Gauge.builder("mqtt.connections",
() -> mqttConnectionCount())
.description("当前MQTT连接数")
.register(registry);
};
}
}
@Service
public class OrderMetrics {
private final Counter orderCounter;
private final DistributionSummary orderAmount;
public OrderMetrics(MeterRegistry registry) {
orderCounter = Counter.builder("orders.total")
.description("总订单数")
.register(registry);
orderAmount = DistributionSummary.builder("orders.amount")
.description("订单金额分布")
.baseUnit("yuan")
.register(registry);
}
public void recordOrder(Order order) {
orderCounter.increment();
orderAmount.record(order.getTotalAmount());
}
}
yaml复制groups:
- name: smart-vending.rules
rules:
- alert: HighOrderFailureRate
expr: rate(orders_failed_total[5m]) / rate(orders_total[5m]) > 0.05
for: 10m
labels:
severity: critical
annotations:
summary: "高订单失败率 ({{ $value }})"
- alert: MQTTConnectionDropped
expr: changes(mqtt_connections_disconnected_total[1m]) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "MQTT连接频繁断开"
xml复制<!-- logback-spring.xml -->
<configuration>
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
java复制@Aspect
@Component
@Slf4j
public class OrderLogAspect {
@AfterReturning(pointcut = "execution(* com.example.order..create*(..))",
returning = "result")
public void logOrderCreation(JoinPoint jp, Object result) {
if (result instanceof Order) {
Order order = (Order) result;
MDC.put("orderId", order.getId());
MDC.put("userId", order.getUserId());
log.info("订单创建成功, 金额: {}, 商品数: {}",
order.getTotalAmount(),
order.getItems().size());
MDC.clear();
}
}
@AfterThrowing(pointcut = "execution(* com.example.order..create*(..))",
throwing = "ex")
public void logOrderCreationError(JoinPoint jp, Exception ex) {
Object[] args = jp.getArgs();
if (args.length > 0 && args[0] instanceof OrderDTO) {
OrderDTO dto = (OrderDTO) args[0];
MDC.put("userId", dto.getUserId());
log.error("订单创建失败: {}", ex.getMessage(), ex);
MDC.clear();
}
}
}
根据实际部署经验,推荐以下硬件配置:
针对不同部署环境的网络优化方案:
c复制void network_loop() {
while(1) {
if(wifi_connected()) {
use_wifi_connection();
} else if(cellular_available()) {
use_cellular_connection();
} else {
enable_offline_mode();
save_to_local_storage();
sleep(10);
}
}
}
在4核8G的服务器上部署测试环境,得到以下基准数据:
使用JMeter进行系统压力测试:
xml复制<TestPlan>
<ThreadGroup>
<numThreads>500</numThreads>
<rampUp>60</rampUp>
<MQTTConnect>
<broker>tcp://test.mqtt:1883</broker>
<clientIdPrefix>loadtest_</clientIdPrefix>
</MQTTConnect>
<MQTTPublish>
<topic>smart-vending/${clientId}/status</topic>
<message>{"weight": 1000, "temp": 25}</message>
<qos>1</qos>
<rate>1/sec</rate>
</MQTTPublish>
<HTTPRequest>
<url>http://api:8080/orders</url>
<method>POST</method>
<body>{"cabinetId": "${clientId}", "items": [...]}</body>
</HTTPRequest>
</ThreadGroup>
</TestPlan>
yaml复制# Kubernetes HPA配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mqtt-adapter
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mqtt-adapter
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: External
external:
metric:
name: mqtt_connections
selector:
matchLabels:
app: mqtt-adapter
target:
type: AverageValue
averageValue: 5000