添加SN对应厂家的配置方案

This commit is contained in:
孙小云 2025-12-18 15:35:05 +08:00
parent 9558a4ee30
commit b31211e8f2
9 changed files with 472 additions and 32 deletions

View File

@ -48,29 +48,6 @@ public class MachineCommandManager {
this.mqttClient = mqttClient;
}
/**
* 绑定设备到厂家
*
* @param sn 设备SN号
* @param vendorType 厂家类型
*/
public void bindMachine(String sn, String vendorType) {
vendorRegistry.bindSnToVendor(sn, vendorType);
log.info("绑定设备到厂家: sn={}, vendorType={}", sn, vendorType);
}
/**
* 解绑设备
*
* @param sn 设备SN号
*/
public void unbindMachine(String sn) {
vendorRegistry.unbindSn(sn);
executingCommands.remove(sn);
stateManager.removeStates(sn);
log.info("解绑设备: sn={}", sn);
}
/**
* 获取设备当前状态
*

View File

@ -1,5 +1,6 @@
package com.tuoheng.machine.vendor;
import com.tuoheng.machine.vendor.store.SnVendorMappingStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -8,21 +9,30 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* 厂家注册中心
* 管理SN到厂家的映射关系
* 管理厂家配置和 SN 到厂家的映射关系
*
* 架构说明
* - 厂家配置存储在本地内存vendorConfigs
* - SN 到厂家类型的映射通过 SnVendorMappingStore 存储支持内存/Redis+MySQL
*/
@Slf4j
@Component
public class VendorRegistry {
/**
* 厂家类型 -> 厂家配置
* 厂家类型 -> 厂家配置存储在本地内存
*/
private final Map<String, VendorConfig> vendorConfigs = new ConcurrentHashMap<>();
/**
* SN -> 厂家类型
* SN 厂家类型的映射存储层支持内存Redis+MySQL等多种实现
*/
private final Map<String, String> snToVendorMap = new ConcurrentHashMap<>();
private final SnVendorMappingStore mappingStore;
public VendorRegistry(SnVendorMappingStore mappingStore) {
this.mappingStore = mappingStore;
log.info("厂家注册中心初始化完成,使用映射存储实现: {}", mappingStore.getClass().getSimpleName());
}
/**
* 注册厂家配置
@ -35,32 +45,47 @@ public class VendorRegistry {
/**
* 绑定SN到厂家
*
* @deprecated 不建议直接调用此方法应该通过数据库或配置文件管理 SN 映射关系
*/
@Deprecated
public void bindSnToVendor(String sn, String vendorType) {
if (!vendorConfigs.containsKey(vendorType)) {
throw new IllegalArgumentException("未注册的厂家类型: " + vendorType);
}
snToVendorMap.put(sn, vendorType);
mappingStore.saveMapping(sn, vendorType);
log.debug("绑定SN到厂家: sn={}, vendorType={}", sn, vendorType);
}
/**
* 解绑SN
*
* @deprecated 不建议直接调用此方法应该通过数据库或配置文件管理 SN 映射关系
*/
@Deprecated
public void unbindSn(String sn) {
snToVendorMap.remove(sn);
mappingStore.removeMapping(sn);
log.debug("解绑SN: sn={}", sn);
}
/**
* 获取SN对应的厂家类型
*
* 查询顺序Redis+MySQL 模式
* 1. 先从 Redis 缓存获取
* 2. Redis 没有则从 MySQL 数据库获取
* 3. 获取到后存入 Redis 缓存
*/
public String getVendorType(String sn) {
return snToVendorMap.get(sn);
return mappingStore.getVendorType(sn);
}
/**
* 获取SN对应的厂家配置
*
* 查询顺序Redis+MySQL 模式
* 1. 通过 mappingStore 获取厂家类型Redis MySQL 缓存
* 2. 根据厂家类型获取厂家配置
*/
public VendorConfig getVendorConfig(String sn) {
String vendorType = getVendorType(sn);
@ -81,7 +106,7 @@ public class VendorRegistry {
* 判断SN是否已绑定厂家
*/
public boolean isSnBound(String sn) {
return snToVendorMap.containsKey(sn);
return mappingStore.exists(sn);
}
/**

View File

@ -0,0 +1,49 @@
package com.tuoheng.machine.vendor.repository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于内存的 SN 到厂家类型映射持久化存储实现
* 适用于单节点部署或开发测试环境
*
* 注意这是默认实现当没有配置数据库时使用
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
public class InMemorySnVendorMappingRepository implements SnVendorMappingRepository {
/**
* SN -> 厂家类型模拟数据库存储
*/
private final Map<String, String> dataStore = new ConcurrentHashMap<>();
@Override
public String findVendorTypeBySn(String sn) {
String vendorType = dataStore.get(sn);
log.debug("从内存数据库查询 SN 映射: sn={}, vendorType={}", sn, vendorType);
return vendorType;
}
@Override
public void save(String sn, String vendorType) {
dataStore.put(sn, vendorType);
log.debug("保存 SN 映射到内存数据库: sn={}, vendorType={}", sn, vendorType);
}
@Override
public void delete(String sn) {
dataStore.remove(sn);
log.debug("从内存数据库删除 SN 映射: sn={}", sn);
}
@Override
public boolean exists(String sn) {
return dataStore.containsKey(sn);
}
}

View File

@ -0,0 +1,97 @@
package com.tuoheng.machine.vendor.repository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* 基于 MySQL SN 到厂家类型映射持久化存储实现
* 适用于生产环境
*
* MySQL 表结构示例
* CREATE TABLE sn_vendor_mapping (
* sn VARCHAR(64) PRIMARY KEY COMMENT '设备SN号',
* vendor_type VARCHAR(32) NOT NULL COMMENT '厂家类型',
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
* updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
* INDEX idx_vendor_type (vendor_type)
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SN到厂家类型映射表';
*
* 使用方式
* 1. application.properties 中配置machine.state.store.type=redis
* 2. 配置 MySQL 数据源
* 3. 创建上述表结构
* 4. 实现下面的 CRUD 方法
*
* 注意当前为空实现需要在连接数据库后完善
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
public class MysqlSnVendorMappingRepository implements SnVendorMappingRepository {
// TODO: 注入 JdbcTemplate MyBatis Mapper
// private final JdbcTemplate jdbcTemplate;
//
// public MysqlSnVendorMappingRepository(JdbcTemplate jdbcTemplate) {
// this.jdbcTemplate = jdbcTemplate;
// }
public MysqlSnVendorMappingRepository() {
log.warn("使用 MySQL SN 映射持久化存储实现,但当前为空实现,请在连接数据库后完善");
}
@Override
public String findVendorTypeBySn(String sn) {
// TODO: 实现从 MySQL 查询
// try {
// return jdbcTemplate.queryForObject(
// "SELECT vendor_type FROM sn_vendor_mapping WHERE sn = ?",
// String.class,
// sn
// );
// } catch (EmptyResultDataAccessException e) {
// log.debug("MySQL 中不存在 SN 映射: sn={}", sn);
// return null;
// }
log.warn("MySQL SN 映射持久化存储未实现,返回 null: sn={}", sn);
return null;
}
@Override
public void save(String sn, String vendorType) {
// TODO: 实现保存到 MySQL
// jdbcTemplate.update(
// "INSERT INTO sn_vendor_mapping (sn, vendor_type) VALUES (?, ?) " +
// "ON DUPLICATE KEY UPDATE vendor_type = ?, updated_at = CURRENT_TIMESTAMP",
// sn, vendorType, vendorType
// );
// log.debug("保存 SN 映射到 MySQL: sn={}, vendorType={}", sn, vendorType);
log.warn("MySQL SN 映射持久化存储未实现,跳过保存: sn={}, vendorType={}", sn, vendorType);
}
@Override
public void delete(String sn) {
// TODO: 实现从 MySQL 删除
// jdbcTemplate.update("DELETE FROM sn_vendor_mapping WHERE sn = ?", sn);
// log.debug("从 MySQL 删除 SN 映射: sn={}", sn);
log.warn("MySQL SN 映射持久化存储未实现,跳过删除: sn={}", sn);
}
@Override
public boolean exists(String sn) {
// TODO: 实现检查 MySQL 中是否存在
// Integer count = jdbcTemplate.queryForObject(
// "SELECT COUNT(*) FROM sn_vendor_mapping WHERE sn = ?",
// Integer.class,
// sn
// );
// return count != null && count > 0;
log.warn("MySQL SN 映射持久化存储未实现,返回 false: sn={}", sn);
return false;
}
}

View File

@ -0,0 +1,46 @@
package com.tuoheng.machine.vendor.repository;
/**
* SN 到厂家类型映射的持久化存储接口
* 提供数据库层的 CRUD 操作抽象支持多种实现内存MySQL等
*
* 职责说明
* - 这是持久化层数据库层的抽象
* - SnVendorMappingStore 的区别
* - SnVendorMappingStore包含缓存逻辑Redis + Repository
* - SnVendorMappingRepository纯粹的持久化存储数据库
*/
public interface SnVendorMappingRepository {
/**
* 从持久化存储中查询 SN 对应的厂家类型
*
* @param sn 设备SN号
* @return 厂家类型如果不存在返回 null
*/
String findVendorTypeBySn(String sn);
/**
* 保存 SN 到厂家类型的映射到持久化存储
* 如果已存在则更新
*
* @param sn 设备SN号
* @param vendorType 厂家类型
*/
void save(String sn, String vendorType);
/**
* 从持久化存储中删除 SN 的映射关系
*
* @param sn 设备SN号
*/
void delete(String sn);
/**
* 检查持久化存储中是否存在 SN 的映射关系
*
* @param sn 设备SN号
* @return 是否存在
*/
boolean exists(String sn);
}

View File

@ -0,0 +1,47 @@
package com.tuoheng.machine.vendor.store;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于内存的 SN 到厂家类型映射存储实现
* 适用于单节点部署或开发测试环境
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
public class InMemorySnVendorMappingStore implements SnVendorMappingStore {
/**
* SN -> 厂家类型
*/
private final Map<String, String> snToVendorMap = new ConcurrentHashMap<>();
@Override
public String getVendorType(String sn) {
String vendorType = snToVendorMap.get(sn);
log.debug("从内存获取 SN 映射: sn={}, vendorType={}", sn, vendorType);
return vendorType;
}
@Override
public void saveMapping(String sn, String vendorType) {
snToVendorMap.put(sn, vendorType);
log.debug("保存 SN 映射到内存: sn={}, vendorType={}", sn, vendorType);
}
@Override
public void removeMapping(String sn) {
snToVendorMap.remove(sn);
log.debug("从内存中移除 SN 映射: sn={}", sn);
}
@Override
public boolean exists(String sn) {
return snToVendorMap.containsKey(sn);
}
}

View File

@ -0,0 +1,144 @@
package com.tuoheng.machine.vendor.store;
import com.tuoheng.machine.vendor.repository.SnVendorMappingRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* 基于 Redis + Repository SN 到厂家类型映射存储实现
* 适用于多节点部署的生产环境
*
* 架构说明
* 1. Redis 作为缓存层提供快速查询
* 2. Repository 作为持久化层支持内存/MySQL等实现
* 3. 查询流程Redis Repository 回写 Redis
*
* Redis 数据结构
* - Key: machine:sn:vendor:{sn}
* - Value: {vendorType}
* - TTL: 86400 24小时
*
* 使用方式
* 1. application.properties 中配置machine.state.store.type=redis
* 2. 配置 Redis 连接信息
* 3. Repository 会根据配置自动选择实现内存/MySQL
* 4. 实现 Redis 的缓存逻辑
*
* 注意当前为空实现需要在生产环境部署时完善
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
public class RedisSnVendorMappingStore implements SnVendorMappingStore {
// TODO: 注入 RedisTemplate
// private final StringRedisTemplate redisTemplate;
/**
* 持久化存储层支持内存/MySQL等实现
*/
private final SnVendorMappingRepository repository;
// Redis key 前缀
private static final String KEY_PREFIX = "machine:sn:vendor:";
// TODO: 配置缓存过期时间
// private static final long CACHE_EXPIRE_SECONDS = 86400; // 24小时
public RedisSnVendorMappingStore(SnVendorMappingRepository repository) {
this.repository = repository;
log.warn("使用 Redis+Repository SN 映射存储实现,但当前为空实现,请在生产环境部署前完善");
log.info("持久化层实现: {}", repository.getClass().getSimpleName());
}
@Override
public String getVendorType(String sn) {
// TODO: 实现从 Redis + MySQL 获取映射
// 1. 先从 Redis 缓存获取
// String key = KEY_PREFIX + sn;
// String vendorType = redisTemplate.opsForValue().get(key);
// if (vendorType != null) {
// log.debug("从 Redis 缓存获取 SN 映射: sn={}, vendorType={}", sn, vendorType);
// return vendorType;
// }
//
// 2. Redis 没有 MySQL 数据库获取
// try {
// vendorType = jdbcTemplate.queryForObject(
// "SELECT vendor_type FROM sn_vendor_mapping WHERE sn = ?",
// String.class,
// sn
// );
//
// if (vendorType != null) {
// // 3. 获取到后存入 Redis 缓存
// redisTemplate.opsForValue().set(key, vendorType, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
// log.debug("从 MySQL 获取 SN 映射并缓存到 Redis: sn={}, vendorType={}", sn, vendorType);
// return vendorType;
// }
// } catch (EmptyResultDataAccessException e) {
// log.debug("MySQL 中不存在 SN 映射: sn={}", sn);
// }
//
// return null;
log.warn("Redis+MySQL SN 映射存储未实现,返回 null: sn={}", sn);
return null;
}
@Override
public void saveMapping(String sn, String vendorType) {
// TODO: 实现保存映射到 Redis + MySQL
// 1. 保存到 MySQL 数据库持久化
// jdbcTemplate.update(
// "INSERT INTO sn_vendor_mapping (sn, vendor_type) VALUES (?, ?) " +
// "ON DUPLICATE KEY UPDATE vendor_type = ?, updated_at = CURRENT_TIMESTAMP",
// sn, vendorType, vendorType
// );
//
// 2. 保存到 Redis 缓存
// String key = KEY_PREFIX + sn;
// redisTemplate.opsForValue().set(key, vendorType, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
//
// log.debug("保存 SN 映射到 Redis+MySQL: sn={}, vendorType={}", sn, vendorType);
log.warn("Redis+MySQL SN 映射存储未实现,跳过保存: sn={}, vendorType={}", sn, vendorType);
}
@Override
public void removeMapping(String sn) {
// TODO: 实现从 Redis + MySQL 删除映射
// 1. MySQL 删除
// jdbcTemplate.update("DELETE FROM sn_vendor_mapping WHERE sn = ?", sn);
//
// 2. Redis 删除
// String key = KEY_PREFIX + sn;
// redisTemplate.delete(key);
//
// log.debug("从 Redis+MySQL 中移除 SN 映射: sn={}", sn);
log.warn("Redis+MySQL SN 映射存储未实现,跳过删除: sn={}", sn);
}
@Override
public boolean exists(String sn) {
// TODO: 实现检查映射是否存在
// 1. 先检查 Redis
// String key = KEY_PREFIX + sn;
// if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
// return true;
// }
//
// 2. 再检查 MySQL
// Integer count = jdbcTemplate.queryForObject(
// "SELECT COUNT(*) FROM sn_vendor_mapping WHERE sn = ?",
// Integer.class,
// sn
// );
// return count != null && count > 0;
log.warn("Redis+MySQL SN 映射存储未实现,返回 false: sn={}", sn);
return false;
}
}

View File

@ -0,0 +1,50 @@
package com.tuoheng.machine.vendor.store;
/**
* SN 到厂家类型映射的存储接口
* 提供 SN 与厂家类型映射关系的存储和获取抽象支持多种实现内存Redis+MySQL等
*
* 数据流
* 1. 先从 Redis 缓存获取
* 2. Redis 没有则从 MySQL 数据库获取
* 3. 获取到后存入 Redis 缓存
* 4. 都没有则返回 null
*/
public interface SnVendorMappingStore {
/**
* 获取 SN 对应的厂家类型
*
* 查询顺序
* 1. 先从 Redis 缓存获取
* 2. Redis 没有则从 MySQL 数据库获取
* 3. 获取到后存入 Redis 缓存
*
* @param sn 设备SN号
* @return 厂家类型如果不存在返回 null
*/
String getVendorType(String sn);
/**
* 保存 SN 到厂家类型的映射
*
* @param sn 设备SN号
* @param vendorType 厂家类型
*/
void saveMapping(String sn, String vendorType);
/**
* 删除 SN 的映射关系
*
* @param sn 设备SN号
*/
void removeMapping(String sn);
/**
* 检查 SN 是否已有映射关系
*
* @param sn 设备SN号
* @return 是否存在映射
*/
boolean exists(String sn);
}

View File

@ -5,6 +5,7 @@ import com.tuoheng.machine.command.CommandResult;
import com.tuoheng.machine.command.CommandType;
import com.tuoheng.machine.mqtt.MqttCallbackRegistry;
import com.tuoheng.machine.state.*;
import com.tuoheng.machine.vendor.VendorRegistry;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
@ -37,6 +38,9 @@ public class DrcStateMachineTest {
@Autowired
MqttCallbackRegistry mqttCallbackRegistry;
@Autowired
VendorRegistry vendorRegistry;
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
@ -51,7 +55,8 @@ public class DrcStateMachineTest {
return;
}
initState = true;
machineCommandManager.bindMachine(SN,"DJI");
// 使用 VendorRegistry 绑定 SN 到厂家
vendorRegistry.bindSnToVendor(SN, "DJI");
}
@Test