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);
+ }
+}