diff --git a/viewsh-module-iot/viewsh-module-iot-server/pom.xml b/viewsh-module-iot/viewsh-module-iot-server/pom.xml index 819bb14..3d84f0a 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/pom.xml +++ b/viewsh-module-iot/viewsh-module-iot-server/pom.xml @@ -1,184 +1,189 @@ - - - - viewsh-module-iot - com.viewsh - ${revision} - - 4.0.0 - jar - - viewsh-module-iot-server - - ${project.artifactId} - - 物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。 - - - - - - - com.viewsh - viewsh-spring-boot-starter-env - - - - - com.viewsh - viewsh-module-system-api - ${revision} - - - com.viewsh - viewsh-module-iot-api - ${revision} - - - com.viewsh - viewsh-module-iot-core - ${revision} - - - - - com.viewsh - viewsh-spring-boot-starter-biz-tenant - - - - - com.viewsh - viewsh-spring-boot-starter-web - - - - com.viewsh - viewsh-spring-boot-starter-security - - - - - com.taosdata.jdbc - taos-jdbcdriver - - - - com.viewsh - viewsh-spring-boot-starter-mybatis - - - - com.viewsh - viewsh-spring-boot-starter-redis - - - - - com.viewsh - viewsh-spring-boot-starter-rpc - - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-discovery - - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-config - - - - - com.viewsh - viewsh-spring-boot-starter-job - - - - - org.quartz-scheduler - quartz - 2.3.2 - - - - - com.viewsh - viewsh-spring-boot-starter-mq - - - - - com.viewsh - viewsh-spring-boot-starter-test - - - - - com.viewsh - viewsh-spring-boot-starter-monitor - - - - - com.viewsh - viewsh-spring-boot-starter-excel - - - - - - org.apache.rocketmq - rocketmq-spring-boot-starter - true - - - org.springframework.kafka - spring-kafka - true - - - org.springframework.boot - spring-boot-starter-amqp - true - - - - - - - - - - - - - - - - - - - ${project.artifactId} - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - - - repackage - - - - - - - + + + + viewsh-module-iot + com.viewsh + ${revision} + + 4.0.0 + jar + + viewsh-module-iot-server + + ${project.artifactId} + + 物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。 + + + + + + + com.viewsh + viewsh-spring-boot-starter-env + + + + + com.viewsh + viewsh-module-system-api + ${revision} + + + com.viewsh + viewsh-module-ops-api + ${revision} + + + com.viewsh + viewsh-module-iot-api + ${revision} + + + com.viewsh + viewsh-module-iot-core + ${revision} + + + + + com.viewsh + viewsh-spring-boot-starter-biz-tenant + + + + + com.viewsh + viewsh-spring-boot-starter-web + + + + com.viewsh + viewsh-spring-boot-starter-security + + + + + com.taosdata.jdbc + taos-jdbcdriver + + + + com.viewsh + viewsh-spring-boot-starter-mybatis + + + + com.viewsh + viewsh-spring-boot-starter-redis + + + + + com.viewsh + viewsh-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.viewsh + viewsh-spring-boot-starter-job + + + + + org.quartz-scheduler + quartz + 2.3.2 + + + + + com.viewsh + viewsh-spring-boot-starter-mq + + + + + com.viewsh + viewsh-spring-boot-starter-test + + + + + com.viewsh + viewsh-spring-boot-starter-monitor + + + + + com.viewsh + viewsh-spring-boot-starter-excel + + + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + org.springframework.kafka + spring-kafka + true + + + org.springframework.boot + spring-boot-starter-amqp + true + + + + + + + + + + + + + + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java index 741060d..c6e3f23 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/CleanOrderIntegrationConfig.java @@ -6,7 +6,7 @@ import lombok.Data; * 保洁工单集成配置 *

* 这是 IoT 设备与保洁工单集成的总配置类,包含所有子配置 - * 存储在 {@link OpsAreaDeviceRelationDO#getConfigData()} 中 + * 存储在 Ops 模块的 {@code ops_area_device_relation.config_data} 字段中 * * @author AI */ diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java index d9caf36..3b190b5 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/integration/clean/CleanOrderIntegrationConfigServiceImpl.java @@ -1,15 +1,17 @@ package com.viewsh.module.iot.service.integration.clean; +import com.viewsh.framework.common.pojo.CommonResult; import com.viewsh.framework.common.util.json.JsonUtils; -import com.viewsh.module.iot.dal.dataobject.integration.clean.OpsAreaDeviceRelationDO; -import com.viewsh.module.iot.dal.mysql.integration.clean.OpsAreaDeviceRelationMapper; +import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig; +import com.viewsh.module.ops.api.area.AreaDeviceApi; +import com.viewsh.module.ops.api.area.AreaDeviceDTO; +import com.viewsh.module.ops.api.area.DeviceRelationDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -22,25 +24,17 @@ import java.util.stream.Collectors; public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegrationConfigService { /** - * 配置缓存 Key 模式 + * Redis Key 前缀(与 Ops 模块保持一致) */ - private static final String CONFIG_WRAPPER_KEY_PATTERN = "iot:clean:config:wrapper:%s"; - private static final String CONFIG_AREA_TYPE_KEY_PATTERN = "iot:clean:config:area:%s:type:%s"; + private static final String DEVICE_CACHE_KEY_PREFIX = "ops:area:device:"; + private static final String AREA_TYPE_CACHE_KEY_PREFIX = "ops:area:%s:type:%s"; + private static final String NULL_CACHE = "NULL"; /** - * 配置缓存 TTL(秒) - *

- * 5 分钟自动过期 + * Ops 模块区域设备 API Client */ - private static final int CONFIG_CACHE_TTL_SECONDS = 300; - - /** - * 空值缓存标记(防止缓存穿透) - */ - private static final String NULL_CACHE_VALUE = "NULL"; - @Resource - private OpsAreaDeviceRelationMapper relationMapper; + private AreaDeviceApi areaDeviceApi; @Resource private StringRedisTemplate stringRedisTemplate; @@ -49,11 +43,16 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra public List getConfigsByAreaId(Long areaId) { log.debug("[CleanOrderConfig] 查询区域配置:areaId={}", areaId); - // 区域配置暂不缓存,直接从数据库查询 - List relations = relationMapper.selectListByAreaId(areaId); + // 通过 Feign 调用 Ops 模块获取所有关联设备 + CommonResult> result = areaDeviceApi.getDevicesByAreaAndType(areaId, null); - return relations.stream() - .map(this::wrapConfig) + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[CleanOrderConfig] 调用 Ops 模块获取区域配置失败:areaId={}", areaId); + return List.of(); + } + + return result.getData().stream() + .map(this::wrapDto) .collect(Collectors.toList()); } @@ -61,10 +60,16 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra public List getConfigsByAreaIdAndRelationType(Long areaId, String relationType) { log.debug("[CleanOrderConfig] 查询区域配置:areaId={}, relationType={}", areaId, relationType); - List relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType); + // 通过 Feign 调用 Ops 模块获取关联设备 + CommonResult> result = areaDeviceApi.getDevicesByAreaAndType(areaId, relationType); - return relations.stream() - .map(this::wrapConfig) + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[CleanOrderConfig] 调用 Ops 模块获取区域配置失败:areaId={}, relationType={}", areaId, relationType); + return List.of(); + } + + return result.getData().stream() + .map(this::wrapDto) .collect(Collectors.toList()); } @@ -72,132 +77,133 @@ public class CleanOrderIntegrationConfigServiceImpl implements CleanOrderIntegra public AreaDeviceConfigWrapper getConfigByAreaIdAndRelationType(Long areaId, String relationType) { log.debug("[CleanOrderConfig] 查询单个区域配置:areaId={}, relationType={}", areaId, relationType); - // 1. 尝试从缓存读取 - String cacheKey = formatAreaTypeKey(areaId, relationType); - String cachedValue = stringRedisTemplate.opsForValue().get(cacheKey); - - if (cachedValue != null) { - if (NULL_CACHE_VALUE.equals(cachedValue)) { - log.debug("[CleanOrderConfig] 命中空值缓存:areaId={}, relationType={}", areaId, relationType); - return null; + // 1. 先读 Redis 缓存 + String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, areaId, relationType); + try { + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + if (NULL_CACHE.equals(cached)) { + log.debug("[CleanOrderConfig] 命中空值缓存:areaId={}, relationType={}", areaId, relationType); + return null; + } + log.debug("[CleanOrderConfig] 命中 Redis 缓存:areaId={}, relationType={}", areaId, relationType); + AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class); + return wrapDto(dto); } - log.debug("[CleanOrderConfig] 命中区域类型缓存:areaId={}, relationType={}", areaId, relationType); - return JsonUtils.parseObject(cachedValue, AreaDeviceConfigWrapper.class); + } catch (Exception e) { + log.warn("[CleanOrderConfig] 读取 Redis 缓存失败:areaId={}, relationType={}", areaId, relationType, e); } - // 2. 从数据库查询 - List relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType); + // 2. 缓存未命中,调用 Ops 模块 + CommonResult> result = areaDeviceApi.getDevicesByAreaAndType(areaId, relationType); - if (relations.isEmpty()) { - // 缓存空值,防止缓存穿透 - stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[CleanOrderConfig] 调用 Ops 模块获取区域配置失败:areaId={}, relationType={}", areaId, relationType); return null; } // 返回第一个启用的配置 - AreaDeviceConfigWrapper wrapper = relations.stream() - .filter(r -> r.getEnabled()) + return result.getData().stream() + .filter(dto -> dto.getEnabled() != null && dto.getEnabled()) .findFirst() - .map(this::wrapConfig) + .map(this::wrapDto) .orElse(null); - - // 3. 写入缓存 - if (wrapper != null) { - stringRedisTemplate.opsForValue().set( - cacheKey, - JsonUtils.toJsonString(wrapper), - CONFIG_CACHE_TTL_SECONDS, - TimeUnit.SECONDS - ); - } else { - // 缓存空值 - stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); - } - - return wrapper; } @Override public AreaDeviceConfigWrapper getConfigWrapperByDeviceId(Long deviceId) { log.debug("[CleanOrderConfig] 查询设备完整配置:deviceId={}", deviceId); - // 1. 尝试从缓存读取 - String cacheKey = formatWrapperKey(deviceId); - String cachedValue = stringRedisTemplate.opsForValue().get(cacheKey); - - if (cachedValue != null) { - if (NULL_CACHE_VALUE.equals(cachedValue)) { - log.debug("[CleanOrderConfig] 命中空值缓存:deviceId={}", deviceId); - return null; + // 1. 先读 Redis 缓存 + String cacheKey = DEVICE_CACHE_KEY_PREFIX + deviceId; + try { + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + if (NULL_CACHE.equals(cached)) { + log.debug("[CleanOrderConfig] 命中空值缓存:deviceId={}", deviceId); + return null; + } + log.debug("[CleanOrderConfig] 命中 Redis 缓存:deviceId={}", deviceId); + AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class); + return wrapDto(dto); } - log.debug("[CleanOrderConfig] 命中 Wrapper 缓存:deviceId={}", deviceId); - return JsonUtils.parseObject(cachedValue, AreaDeviceConfigWrapper.class); + } catch (Exception e) { + log.warn("[CleanOrderConfig] 读取 Redis 缓存失败:deviceId={}", deviceId, e); } - // 2. 从数据库查询 - OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId); + // 2. 缓存未命中,调用 Ops 模块 + CommonResult result = areaDeviceApi.getDeviceRelationWithConfig(deviceId); - if (relation == null || !relation.getEnabled()) { - // 缓存空值,防止缓存穿透(TTL 较短:60秒) - stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE_VALUE, 60, TimeUnit.SECONDS); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[CleanOrderConfig] 调用 Ops 模块获取设备配置失败:deviceId={}", deviceId); return null; } - AreaDeviceConfigWrapper wrapper = wrapConfig(relation); + DeviceRelationDTO dto = result.getData(); - // 3. 写入缓存 - stringRedisTemplate.opsForValue().set( - cacheKey, - JsonUtils.toJsonString(wrapper), - CONFIG_CACHE_TTL_SECONDS, - TimeUnit.SECONDS - ); + // 检查是否启用 + if (dto.getEnabled() == null || !dto.getEnabled()) { + return null; + } - return wrapper; + return wrapDto(dto); } @Override public void evictCache(Long deviceId) { - // 清除设备配置包装器缓存 - stringRedisTemplate.delete(formatWrapperKey(deviceId)); - log.info("[CleanOrderConfig] 清除设备配置缓存:deviceId={}", deviceId); + // 缓存已由 Ops 模块管理,无需操作 + log.debug("[CleanOrderConfig] evictCache 由 Ops 模块管理:deviceId={}", deviceId); } @Override public void evictAreaCache(Long areaId) { - // 清除区域+类型配置缓存 - stringRedisTemplate.delete(formatAreaTypeKey(areaId, "TRAFFIC_COUNTER")); - stringRedisTemplate.delete(formatAreaTypeKey(areaId, "BEACON")); - stringRedisTemplate.delete(formatAreaTypeKey(areaId, "BADGE")); - log.info("[CleanOrderConfig] 清除区域配置缓存:areaId={}", areaId); + // 缓存已由 Ops 模块管理,无需操作 + log.debug("[CleanOrderConfig] evictAreaCache 由 Ops 模块管理:areaId={}", areaId); } /** * 包装配置数据 */ - private AreaDeviceConfigWrapper wrapConfig(OpsAreaDeviceRelationDO relation) { + private AreaDeviceConfigWrapper wrapDto(AreaDeviceDTO dto) { + if (dto == null) { + return null; + } return new AreaDeviceConfigWrapper( - relation.getDeviceId(), - relation.getDeviceKey(), - relation.getProductId(), - relation.getProductKey(), - relation.getAreaId(), - relation.getRelationType(), - relation.getConfigData() + dto.getDeviceId(), + dto.getDeviceKey(), + dto.getProductId(), + dto.getProductKey(), + dto.getAreaId(), + dto.getRelationType(), + convertConfig(dto.getConfigData()) ); } /** - * 格式化设备配置包装器缓存 Key + * 包装配置数据 */ - private static String formatWrapperKey(Long deviceId) { - return String.format(CONFIG_WRAPPER_KEY_PATTERN, deviceId); + private AreaDeviceConfigWrapper wrapDto(DeviceRelationDTO dto) { + if (dto == null) { + return null; + } + return new AreaDeviceConfigWrapper( + dto.getDeviceId(), + dto.getDeviceKey(), + dto.getProductId(), + dto.getProductKey(), + dto.getAreaId(), + dto.getRelationType(), + convertConfig(dto.getConfigData()) + ); } /** - * 格式化区域+类型配置缓存 Key + * 转换配置数据 Map -> CleanOrderIntegrationConfig */ - private static String formatAreaTypeKey(Long areaId, String relationType) { - return String.format(CONFIG_AREA_TYPE_KEY_PATTERN, areaId, relationType); + private CleanOrderIntegrationConfig convertConfig(java.util.Map configData) { + if (configData == null || configData.isEmpty()) { + return null; + } + return JsonUtils.parseObject(JsonUtils.toJsonString(configData), CleanOrderIntegrationConfig.class); } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java index ea95170..b219dc3 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java @@ -2,7 +2,9 @@ package com.viewsh.module.ops.environment.service.badge; import com.fasterxml.jackson.databind.ObjectMapper; import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO; +import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum; +import com.viewsh.module.ops.service.area.AreaDeviceService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; @@ -42,6 +44,9 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I @Resource private ObjectMapper objectMapper; + @Resource + private AreaDeviceService areaDeviceService; + /** * Redis Key 前缀 */ @@ -489,18 +494,18 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I @Override public void initAreaDeviceIndex() { - log.info("开始初始化区域设备索引..."); - // TODO: 从数据库加载区域设备关系并建立索引 - // 这里需要查询 ops_area_device_relation 表,relation_type = 'BADGE' - // 由于该表在 IoT 模块,暂时留空,后续可以通过 Feign 调用或创建本地 Mapper - log.info("区域设备索引初始化完成"); + log.info("[BadgeDeviceStatusService] 开始初始化区域设备索引..."); + // 委托给 AreaDeviceService 处理 + areaDeviceService.initAreaDeviceIndex(); + log.info("[BadgeDeviceStatusService] 区域设备索引初始化完成"); } @Override public void refreshAreaDeviceIndex() { - log.info("开始刷新区域设备索引..."); - // TODO: 重新从数据库加载 - initAreaDeviceIndex(); + log.info("[BadgeDeviceStatusService] 开始刷新区域设备索引..."); + // 委托给 AreaDeviceService 处理 + areaDeviceService.refreshAreaDeviceIndex(); + log.info("[BadgeDeviceStatusService] 区域设备索引刷新完成"); } @Override diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceApi.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceApi.java new file mode 100644 index 0000000..5dd472a --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceApi.java @@ -0,0 +1,66 @@ +package com.viewsh.module.ops.api.area; + +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.module.ops.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * 区域设备关联 API + *

+ * 提供 RPC 接口供其他模块(如 IoT 模块)查询区域设备关联关系 + *

+ * 支持功能: + * - 查询区域的工牌设备列表 + * - 查询设备的关联关系 + * - 按类型查询区域设备 + * + * @author lzh + */ +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +@Tag(name = "RPC 服务 - 区域设备关联") +public interface AreaDeviceApi { + + String PREFIX = ApiConstants.PREFIX + "/area-device"; + + // ==================== 按区域查询 ==================== + + @GetMapping(PREFIX + "/{areaId}/badges") + @Operation(summary = "查询区域的工牌设备列表") + CommonResult> getBadgesByArea( + @Parameter(description = "区域ID", required = true, example = "1302") + @PathVariable("areaId") Long areaId + ); + + @GetMapping(PREFIX + "/{areaId}/devices") + @Operation(summary = "查询区域设备列表(按类型)") + CommonResult> getDevicesByAreaAndType( + @Parameter(description = "区域ID", required = true, example = "1302") + @PathVariable("areaId") Long areaId, + @Parameter(description = "关联类型(BADGE/TRAFFIC_COUNTER/BEACON)", required = true, example = "BADGE") + @RequestParam("relationType") String relationType + ); + + // ==================== 按设备查询 ==================== + + @GetMapping(PREFIX + "/device/{deviceId}/relation") + @Operation(summary = "查询设备的关联关系") + CommonResult getDeviceRelation( + @Parameter(description = "设备ID", required = true, example = "34") + @PathVariable("deviceId") Long deviceId + ); + + @GetMapping(PREFIX + "/device/{deviceId}/full-relation") + @Operation(summary = "查询设备的完整关联关系(包含集成配置)") + CommonResult getDeviceRelationWithConfig( + @Parameter(description = "设备ID", required = true, example = "34") + @PathVariable("deviceId") Long deviceId + ); +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceDTO.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceDTO.java new file mode 100644 index 0000000..1c5fb2c --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/AreaDeviceDTO.java @@ -0,0 +1,48 @@ +package com.viewsh.module.ops.api.area; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 区域设备 DTO + *

+ * 表示运营区域与 IoT 设备的关联关系 + * + * @author lzh + */ +@Schema(description = "区域设备关联") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AreaDeviceDTO { + + @Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "34") + private Long deviceId; + + @Schema(description = "设备Key", example = "09207457042") + private String deviceKey; + + @Schema(description = "产品ID", example = "19") + private Long productId; + + @Schema(description = "产品Key", example = "AOQwO9pJWKgfFTk4") + private String productKey; + + @Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1302") + private Long areaId; + + @Schema(description = "关联类型", example = "BADGE") + private String relationType; + + @Schema(description = "是否启用", example = "true") + private Boolean enabled; + + @Schema(description = "集成配置(JSON)") + private Map configData; +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/DeviceRelationDTO.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/DeviceRelationDTO.java new file mode 100644 index 0000000..46a60d6 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/area/DeviceRelationDTO.java @@ -0,0 +1,48 @@ +package com.viewsh.module.ops.api.area; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 设备关联 DTO + *

+ * 表示设备的区域关联关系 + * + * @author lzh + */ +@Schema(description = "设备关联关系") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeviceRelationDTO { + + @Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "34") + private Long deviceId; + + @Schema(description = "设备Key", example = "09207457042") + private String deviceKey; + + @Schema(description = "产品ID", example = "19") + private Long productId; + + @Schema(description = "产品Key", example = "AOQwO9pJWKgfFTk4") + private String productKey; + + @Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1302") + private Long areaId; + + @Schema(description = "关联类型", example = "BADGE") + private String relationType; + + @Schema(description = "是否启用", example = "true") + private Boolean enabled; + + @Schema(description = "集成配置(JSON)") + private Map configData; +} diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ApiConstants.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ApiConstants.java index 203cb15..f6ce4d4 100644 --- a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ApiConstants.java +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ApiConstants.java @@ -9,6 +9,13 @@ import com.viewsh.framework.common.enums.RpcConstants; */ public class ApiConstants { + /** + * 服务名 + * + * 注意,需要保证和 spring.application.name 保持一致 + */ + public static final String NAME = "ops-server"; + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/ops"; public static final String VERSION = "1.0.0"; diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/area/OpsAreaDeviceRelationDO.java similarity index 71% rename from viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java rename to viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/area/OpsAreaDeviceRelationDO.java index e2490cb..4bb467a 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/dataobject/integration/clean/OpsAreaDeviceRelationDO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/area/OpsAreaDeviceRelationDO.java @@ -1,4 +1,4 @@ -package com.viewsh.module.iot.dal.dataobject.integration.clean; +package com.viewsh.module.ops.dal.dataobject.area; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; @@ -6,22 +6,24 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.viewsh.framework.mybatis.core.dataobject.BaseDO; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; + +import java.util.Map; /** * 运营区域设备关联 DO *

- * 用于建立运营区域与 IoT 设备的绑定关系,并存储核心检测配置 - * 注意:此表在 ops 库中创建,但在 IoT 模块中��问(通过 Feign 或直接访问) + * 用于建立运营区域与 IoT 设备的绑定关系 + *

+ * 注意:此表在 ops 库中,由 Ops 模块管理,IoT 模块通过 Feign API 访问 * - * @author AI + * @author lzh */ @TableName(value = "ops_area_device_relation", autoResultMap = true) @KeySequence("ops_area_device_relation_seq") @Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -76,11 +78,11 @@ public class OpsAreaDeviceRelationDO extends BaseDO { /** * 配置数据(JSON格式) *

- * 存储保洁工单集成的各类配置 - * 使用 {@link JacksonTypeHandler} 自动序列化/反序列化 + * 存储 IoT 集成配置,如客流阈值、信标检测、按键事件等 + * 使用 JacksonTypeHandler 自动序列化/反序列化 */ @TableField(typeHandler = JacksonTypeHandler.class) - private CleanOrderIntegrationConfig configData; + private Map configData; /** * 是否启用 @@ -89,4 +91,5 @@ public class OpsAreaDeviceRelationDO extends BaseDO { * false - 禁用 */ private Boolean enabled; + } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/area/OpsAreaDeviceRelationMapper.java similarity index 83% rename from viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java rename to viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/area/OpsAreaDeviceRelationMapper.java index 1acae0d..74f2d7a 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/dal/mysql/integration/clean/OpsAreaDeviceRelationMapper.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/area/OpsAreaDeviceRelationMapper.java @@ -1,9 +1,8 @@ -package com.viewsh.module.iot.dal.mysql.integration.clean; +package com.viewsh.module.ops.dal.mysql.area; -import com.viewsh.framework.common.pojo.PageResult; import com.viewsh.framework.mybatis.core.mapper.BaseMapperX; import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX; -import com.viewsh.module.iot.dal.dataobject.integration.clean.OpsAreaDeviceRelationDO; +import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -11,23 +10,24 @@ import java.util.List; /** * 运营区域设备关联 Mapper * - * @author AI + * @author lzh */ @Mapper public interface OpsAreaDeviceRelationMapper extends BaseMapperX { /** - * 根据设备ID查询关联关系(仅查询启用的) + * 根据区域ID和关联类型查询关联关系 * - * @param deviceId 设备ID - * @return 关联关系 + * @param areaId 区域ID + * @param relationType 关联类型(TRAFFIC_COUNTER/BEACON/BADGE) + * @return 关联关系列表 */ - default OpsAreaDeviceRelationDO selectByDeviceId(Long deviceId) { - return selectOne(new LambdaQueryWrapperX() - .eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId) + default List selectListByAreaIdAndRelationType(Long areaId, String relationType) { + return selectList(new LambdaQueryWrapperX() + .eq(OpsAreaDeviceRelationDO::getAreaId, areaId) + .eq(OpsAreaDeviceRelationDO::getRelationType, relationType) .eq(OpsAreaDeviceRelationDO::getEnabled, true) - .orderByDesc(OpsAreaDeviceRelationDO::getCreateTime) - .last("LIMIT 1")); + .orderByDesc(OpsAreaDeviceRelationDO::getCreateTime)); } /** @@ -44,17 +44,17 @@ public interface OpsAreaDeviceRelationMapper extends BaseMapperX selectListByAreaIdAndRelationType(Long areaId, String relationType) { - return selectList(new LambdaQueryWrapperX() - .eq(OpsAreaDeviceRelationDO::getAreaId, areaId) - .eq(OpsAreaDeviceRelationDO::getRelationType, relationType) - .eq(OpsAreaDeviceRelationDO::getEnabled, true)); + default OpsAreaDeviceRelationDO selectByDeviceId(Long deviceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId) + .eq(OpsAreaDeviceRelationDO::getEnabled, true) + .orderByDesc(OpsAreaDeviceRelationDO::getCreateTime) + .last("LIMIT 1")); } /** diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceService.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceService.java new file mode 100644 index 0000000..4f66ae7 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceService.java @@ -0,0 +1,96 @@ +package com.viewsh.module.ops.service.area; + +import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; + +import java.util.List; + +/** + * 区域设备关联服务 + *

+ * 职责:管理运营区域与 IoT 设备的关联关系 + * 提供缓存能力,支持快速查询 + * + * @author lzh + */ +public interface AreaDeviceService { + + /** + * 根据区域ID和关联类型查询关联关系 + * + * @param areaId 区域ID + * @param relationType 关联类型(TRAFFIC_COUNTER/BEACON/BADGE) + * @return 关联关系列表 + */ + List listByAreaIdAndType(Long areaId, String relationType); + + /** + * 根据区域ID和关联类型查询单个启用的关联关系 + *

+ * 带缓存,返回第一个启用的配置 + * + * @param areaId 区域ID + * @param relationType 关联类型 + * @return 关联关系,不存在返回 null + */ + OpsAreaDeviceRelationDO getByAreaIdAndType(Long areaId, String relationType); + + /** + * 根据区域ID查询所有启用的关联关系 + * + * @param areaId 区域ID + * @return 关联关系列表 + */ + List listByAreaId(Long areaId); + + /** + * 根据设备ID查询关联关系 + * + * @param deviceId 设备ID + * @return 关联关系,不存在返回 null + */ + OpsAreaDeviceRelationDO getByDeviceId(Long deviceId); + + /** + * 初始化区域设备索引 + *

+ * 从数据库加载所有区域设备关系,建立 Redis 索引 + */ + void initAreaDeviceIndex(); + + /** + * 刷新区域设备索引 + *

+ * 重新从数据库加载并建立索引 + */ + void refreshAreaDeviceIndex(); + + /** + * 添加设备到区域索引 + * + * @param deviceId 设备ID + * @param areaId 区域ID + */ + void addToAreaIndex(Long deviceId, Long areaId); + + /** + * 从区域索引移除设备 + * + * @param deviceId 设备ID + * @param areaId 区域ID + */ + void removeFromAreaIndex(Long deviceId, Long areaId); + + /** + * 清除区域缓存 + * + * @param areaId 区域ID + */ + void evictAreaCache(Long areaId); + + /** + * 清除设备缓存 + * + * @param deviceId 设备ID + */ + void evictDeviceCache(Long deviceId); +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceServiceImpl.java new file mode 100644 index 0000000..585970e --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceServiceImpl.java @@ -0,0 +1,334 @@ +package com.viewsh.module.ops.service.area; + +import com.viewsh.framework.common.util.json.JsonUtils; +import com.viewsh.module.ops.api.area.AreaDeviceDTO; +import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; +import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * 区域设备关联服务实现 + *

+ * 职责: + * 1. 管理运营区域与 IoT 设备的关联关系 + * 2. 维护 Redis 缓存索引 + * 3. 提供快速查询能力 + * + * @author lzh + */ +@Slf4j +@Service +public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBean { + + @Resource + private OpsAreaDeviceRelationMapper relationMapper; + + @Resource + private RedisTemplate redisTemplate; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * Redis Key 前缀 + */ + private static final String AREA_BADGES_KEY_PREFIX = "ops:area:badges:"; + + /** + * 设备配置缓存 Key 前缀(JSON 格式,IoT 可读) + *

+ * 格式: ops:area:device:{deviceId} + */ + private static final String DEVICE_CACHE_KEY_PREFIX = "ops:area:device:"; + + /** + * 区域+类型配置缓存 Key 前缀(JSON 格式,IoT 可读) + *

+ * 格式: ops:area:{areaId}:type:{relationType} + */ + private static final String AREA_TYPE_CACHE_KEY_PREFIX = "ops:area:%s:type:%s"; + + /** + * 空值缓存标记 + */ + private static final String NULL_CACHE = "NULL"; + + /** + * 缓存 TTL(30 分钟) + */ + private static final int CACHE_TTL_MINUTES = 30; + + @Override + public void afterPropertiesSet() { + // 启动时初始化区域设备索引 + initAreaDeviceIndex(); + } + + @Override + public List listByAreaIdAndType(Long areaId, String relationType) { + log.debug("[AreaDevice] 查询区域设备关联:areaId={}, relationType={}", areaId, relationType); + return relationMapper.selectListByAreaIdAndRelationType(areaId, relationType); + } + + @Override + public List listByAreaId(Long areaId) { + log.debug("[AreaDevice] 查询区域所有设备:areaId={}", areaId); + return relationMapper.selectListByAreaId(areaId); + } + + @Override + public OpsAreaDeviceRelationDO getByAreaIdAndType(Long areaId, String relationType) { + if (areaId == null || relationType == null) { + return null; + } + + // 先从缓存读取(缓存存储的是 AreaDeviceDTO JSON) + String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, areaId, relationType); + try { + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + if (NULL_CACHE.equals(cached)) { + return null; + } + log.debug("[AreaDevice] 命中区域类型配置缓存:areaId={}, type={}", areaId, relationType); + AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class); + return toAreaDeviceRelationDO(dto); + } + } catch (Exception e) { + log.warn("[AreaDevice] 读取区域类型配置缓存失败:areaId={}, type={}", areaId, relationType, e); + } + + // 从数据库查询 + List relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType); + + // 返回第一个启用的 + OpsAreaDeviceRelationDO relation = relations.stream() + .filter(r -> r.getEnabled()) + .findFirst() + .orElse(null); + + // 写入 JSON 缓存(IoT 可读,存储 AreaDeviceDTO) + try { + if (relation != null) { + AreaDeviceDTO dto = toAreaDeviceDTO(relation); + stringRedisTemplate.opsForValue().set( + cacheKey, + JsonUtils.toJsonString(dto), + CACHE_TTL_MINUTES, + TimeUnit.MINUTES + ); + } else { + // 空值缓存,防止穿透 + stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES); + } + } catch (Exception e) { + log.warn("[AreaDevice] 写入区域类型配置缓存失败:areaId={}, type={}", areaId, relationType, e); + } + + return relation; + } + + @Override + public OpsAreaDeviceRelationDO getByDeviceId(Long deviceId) { + if (deviceId == null) { + return null; + } + + // 先从缓存读取(缓存存储的是 AreaDeviceDTO JSON) + String cacheKey = DEVICE_CACHE_KEY_PREFIX + deviceId; + try { + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + if (NULL_CACHE.equals(cached)) { + return null; + } + log.debug("[AreaDevice] 命中设备配置缓存:deviceId={}", deviceId); + AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class); + return toAreaDeviceRelationDO(dto); + } + } catch (Exception e) { + log.warn("[AreaDevice] 读取设备配置缓存失败:deviceId={}", deviceId, e); + } + + // 从数据库查询 + OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId); + + // 写入 JSON 缓存(IoT 可读,存储 AreaDeviceDTO) + try { + if (relation != null) { + AreaDeviceDTO dto = toAreaDeviceDTO(relation); + stringRedisTemplate.opsForValue().set( + cacheKey, + JsonUtils.toJsonString(dto), + CACHE_TTL_MINUTES, + TimeUnit.MINUTES + ); + } else { + // 空值缓存,防止穿透 + stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES); + } + } catch (Exception e) { + log.warn("[AreaDevice] 写入设备配置缓存失败:deviceId={}", deviceId, e); + } + + return relation; + } + + @Override + public void initAreaDeviceIndex() { + log.info("[AreaDevice] 开始初始化区域设备索引..."); + + try { + // 查询所有工牌设备关联 + List badgeRelations = relationMapper + .selectListByAreaIdAndRelationType(null, "BADGE"); + + int count = 0; + for (OpsAreaDeviceRelationDO relation : badgeRelations) { + if (relation.getAreaId() != null && relation.getDeviceId() != null) { + addToAreaIndex(relation.getDeviceId(), relation.getAreaId()); + count++; + } + } + + log.info("[AreaDevice] 区域设备索引初始化完成:共加载 {} 个工牌设备关联", count); + + } catch (Exception e) { + log.error("[AreaDevice] 区域设备索引初始化失败", e); + } + } + + @Override + public void refreshAreaDeviceIndex() { + log.info("[AreaDevice] 开始刷新区域设备索引..."); + initAreaDeviceIndex(); + } + + @Override + public void addToAreaIndex(Long deviceId, Long areaId) { + if (deviceId == null || areaId == null) { + return; + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + redisTemplate.opsForSet().add(areaKey, deviceId.toString()); + redisTemplate.expire(areaKey, CACHE_TTL_MINUTES, TimeUnit.MINUTES); + log.debug("[AreaDevice] 添加设备到区域索引:deviceId={}, areaId={}", deviceId, areaId); + } catch (Exception e) { + log.error("[AreaDevice] 添加设备到区域索引失败:deviceId={}, areaId={}", deviceId, areaId, e); + } + } + + @Override + public void removeFromAreaIndex(Long deviceId, Long areaId) { + if (deviceId == null || areaId == null) { + return; + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + redisTemplate.opsForSet().remove(areaKey, deviceId.toString()); + log.debug("[AreaDevice] 从区域索引移除设备:deviceId={}, areaId={}", deviceId, areaId); + } catch (Exception e) { + log.error("[AreaDevice] 从区域索引移除设备失败:deviceId={}, areaId={}", deviceId, areaId, e); + } + } + + @Override + public void evictAreaCache(Long areaId) { + if (areaId == null) { + return; + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + redisTemplate.delete(areaKey); + log.info("[AreaDevice] 清除区域缓存:areaId={}", areaId); + } catch (Exception e) { + log.error("[AreaDevice] 清除区域缓存失败:areaId={}", areaId, e); + } + } + + @Override + public void evictDeviceCache(Long deviceId) { + if (deviceId == null) { + return; + } + + try { + String cacheKey = DEVICE_CACHE_KEY_PREFIX + deviceId; + stringRedisTemplate.delete(cacheKey); + log.info("[AreaDevice] 清除设备配置缓存:deviceId={}", deviceId); + } catch (Exception e) { + log.error("[AreaDevice] 清除设备配置缓存失败:deviceId={}", deviceId, e); + } + } + + /** + * 清除区域+类型配置缓存 + * + * @param areaId 区域ID + * @param relationType 关联类型 + */ + public void evictAreaTypeCache(Long areaId, String relationType) { + if (areaId == null || relationType == null) { + return; + } + + try { + String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, areaId, relationType); + stringRedisTemplate.delete(cacheKey); + log.info("[AreaDevice] 清除区域类型配置缓存:areaId={}, type={}", areaId, relationType); + } catch (Exception e) { + log.error("[AreaDevice] 清除区域类型配置缓存失败:areaId={}, type={}", areaId, relationType, e); + } + } + + /** + * 转换为 AreaDeviceDTO + */ + private AreaDeviceDTO toAreaDeviceDTO(OpsAreaDeviceRelationDO relation) { + if (relation == null) { + return null; + } + AreaDeviceDTO dto = new AreaDeviceDTO(); + dto.setDeviceId(relation.getDeviceId()); + dto.setDeviceKey(relation.getDeviceKey()); + dto.setProductId(relation.getProductId()); + dto.setProductKey(relation.getProductKey()); + dto.setAreaId(relation.getAreaId()); + dto.setRelationType(relation.getRelationType()); + dto.setEnabled(relation.getEnabled()); + dto.setConfigData(relation.getConfigData()); + return dto; + } + + /** + * 转换为 OpsAreaDeviceRelationDO + */ + private OpsAreaDeviceRelationDO toAreaDeviceRelationDO(AreaDeviceDTO dto) { + if (dto == null) { + return null; + } + OpsAreaDeviceRelationDO relation = new OpsAreaDeviceRelationDO(); + relation.setDeviceId(dto.getDeviceId()); + relation.setDeviceKey(dto.getDeviceKey()); + relation.setProductId(dto.getProductId()); + relation.setProductKey(dto.getProductKey()); + relation.setAreaId(dto.getAreaId()); + relation.setRelationType(dto.getRelationType()); + relation.setEnabled(dto.getEnabled()); + relation.setConfigData(dto.getConfigData()); + return relation; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/area/AreaDeviceController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/area/AreaDeviceController.java new file mode 100644 index 0000000..7ab3738 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/area/AreaDeviceController.java @@ -0,0 +1,73 @@ +package com.viewsh.module.ops.controller.area; + +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.framework.common.util.object.BeanUtils; +import com.viewsh.module.ops.api.area.AreaDeviceDTO; +import com.viewsh.module.ops.api.area.DeviceRelationDTO; +import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; +import com.viewsh.module.ops.service.area.AreaDeviceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.viewsh.framework.common.pojo.CommonResult.success; + +/** + * 区域设备关联 Controller + *

+ * 提供 RPC 接口供其他模块(如 IoT 模块)查询区域设备关联关系 + * + * @author lzh + */ +@Tag(name = "RPC 服务 - 区域设备关联") +@Slf4j +@RestController +@RequestMapping("/ops-api/area-device") +@Validated +public class AreaDeviceController { + + @Resource + private AreaDeviceService areaDeviceService; + + @GetMapping("/{areaId}/badges") + @Operation(summary = "查询区域的工牌设备列表") + public CommonResult> getBadgesByArea( + @PathVariable("areaId") Long areaId) { + List relations = areaDeviceService.listByAreaIdAndType(areaId, "BADGE"); + return success(BeanUtils.toBean(relations, AreaDeviceDTO.class)); + } + + @GetMapping("/{areaId}/devices") + @Operation(summary = "查询区域设备列表(按类型)") + public CommonResult> getDevicesByAreaAndType( + @PathVariable("areaId") Long areaId, + @RequestParam("relationType") String relationType) { + List relations = areaDeviceService.listByAreaIdAndType(areaId, relationType); + return success(BeanUtils.toBean(relations, AreaDeviceDTO.class)); + } + + @GetMapping("/device/{deviceId}/relation") + @Operation(summary = "查询设备的关联关系") + public CommonResult getDeviceRelation( + @PathVariable("deviceId") Long deviceId) { + OpsAreaDeviceRelationDO relation = areaDeviceService.getByDeviceId(deviceId); + return success(BeanUtils.toBean(relation, DeviceRelationDTO.class)); + } + + @GetMapping("/device/{deviceId}/full-relation") + @Operation(summary = "查询设备的完整关联关系(包含集成配置)") + public CommonResult getDeviceRelationWithConfig( + @PathVariable("deviceId") Long deviceId) { + OpsAreaDeviceRelationDO relation = areaDeviceService.getByDeviceId(deviceId); + DeviceRelationDTO dto = BeanUtils.toBean(relation, DeviceRelationDTO.class); + // BeanUtils 会自动复制 configData Map 字段 + return success(dto); + } +}