diff --git a/src/main/java/com/tuoheng/machine/MachineCommandManager.java b/src/main/java/com/tuoheng/machine/MachineCommandManager.java index dee7be8..d2e6e70 100644 --- a/src/main/java/com/tuoheng/machine/MachineCommandManager.java +++ b/src/main/java/com/tuoheng/machine/MachineCommandManager.java @@ -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); - } - /** * 获取设备当前状态 * diff --git a/src/main/java/com/tuoheng/machine/vendor/VendorRegistry.java b/src/main/java/com/tuoheng/machine/vendor/VendorRegistry.java index be2ab45..0500da3 100644 --- a/src/main/java/com/tuoheng/machine/vendor/VendorRegistry.java +++ b/src/main/java/com/tuoheng/machine/vendor/VendorRegistry.java @@ -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 vendorConfigs = new ConcurrentHashMap<>(); /** - * SN -> 厂家类型 + * SN 到厂家类型的映射存储层(支持内存、Redis+MySQL等多种实现) */ - private final Map 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); } /** diff --git a/src/main/java/com/tuoheng/machine/vendor/repository/InMemorySnVendorMappingRepository.java b/src/main/java/com/tuoheng/machine/vendor/repository/InMemorySnVendorMappingRepository.java new file mode 100644 index 0000000..a0b6496 --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/repository/InMemorySnVendorMappingRepository.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/tuoheng/machine/vendor/repository/MysqlSnVendorMappingRepository.java b/src/main/java/com/tuoheng/machine/vendor/repository/MysqlSnVendorMappingRepository.java new file mode 100644 index 0000000..652305e --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/repository/MysqlSnVendorMappingRepository.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/tuoheng/machine/vendor/repository/SnVendorMappingRepository.java b/src/main/java/com/tuoheng/machine/vendor/repository/SnVendorMappingRepository.java new file mode 100644 index 0000000..43b0834 --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/repository/SnVendorMappingRepository.java @@ -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); +} \ No newline at end of file diff --git a/src/main/java/com/tuoheng/machine/vendor/store/InMemorySnVendorMappingStore.java b/src/main/java/com/tuoheng/machine/vendor/store/InMemorySnVendorMappingStore.java new file mode 100644 index 0000000..d089b77 --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/store/InMemorySnVendorMappingStore.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/tuoheng/machine/vendor/store/RedisSnVendorMappingStore.java b/src/main/java/com/tuoheng/machine/vendor/store/RedisSnVendorMappingStore.java new file mode 100644 index 0000000..ed18618 --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/store/RedisSnVendorMappingStore.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/tuoheng/machine/vendor/store/SnVendorMappingStore.java b/src/main/java/com/tuoheng/machine/vendor/store/SnVendorMappingStore.java new file mode 100644 index 0000000..f6b8c90 --- /dev/null +++ b/src/main/java/com/tuoheng/machine/vendor/store/SnVendorMappingStore.java @@ -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); +} \ No newline at end of file diff --git a/src/test/java/com/tuoheng/old/DrcStateMachineTest.java b/src/test/java/com/tuoheng/old/DrcStateMachineTest.java index 2209cba..31a744a 100644 --- a/src/test/java/com/tuoheng/old/DrcStateMachineTest.java +++ b/src/test/java/com/tuoheng/old/DrcStateMachineTest.java @@ -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