refactor(ops,iot): 重构区域设备关联模块并添加Redis缓存
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

主要变更:
1. 将 ops_area_device_relation 表所有权移至 Ops 模块
   - 新增 OpsAreaDeviceRelationDO、Mapper、Service、Controller
   - 新增 AreaDeviceApi Feign 接口供其他模块调用
   - ���除 IoT 模块中的旧 DO 和 Mapper

2. 实现 Redis JSON 缓存(IoT 可读)
   - 统一缓存 Key: ops:area:device:{deviceId}
   - 统一缓存 Key: ops:area:{areaId}:type:{relationType}
   - TTL: 30分钟,空值缓存: 1分钟

3. IoT 模块通过 Feign 调用 Ops
   - 优先读 Redis 缓存,未命中时调用 Ops API
   - 缓存由 Ops 模块统一管理

4. 删除 IoT 模块废弃文件
   - OpsAreaDeviceRelationDO.java
   - OpsAreaDeviceRelationMapper.java

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-01-28 22:35:41 +08:00
parent 10eeb774a6
commit d87d4dd914
13 changed files with 1019 additions and 328 deletions

View File

@@ -1,184 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>viewsh-module-iot</artifactId>
<groupId>com.viewsh</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>viewsh-module-iot-server</artifactId>
<name>${project.artifactId}</name>
<description>
物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。
<!-- TODO 芋艿:后续补充下 -->
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-env</artifactId>
</dependency>
<!-- 依赖服务 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-iot-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-iot-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-job</artifactId>
</dependency>
<!-- TODO @芋艿:引入下,看看情况 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-test</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-monitor</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-excel</artifactId>
</dependency>
<!-- 消息队列相关 -->
<!-- TODO @芋艿:临时打开 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<!-- IoT 网络组件:接收来自设备的上行数据 -->
<!-- <dependency>-->
<!-- <groupId>com.viewsh</groupId>-->
<!-- <artifactId>viewsh-module-iot-net-component-http</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.viewsh</groupId>-->
<!-- <artifactId>viewsh-module-iot-net-component-emqx</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>viewsh-module-iot</artifactId>
<groupId>com.viewsh</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>viewsh-module-iot-server</artifactId>
<name>${project.artifactId}</name>
<description>
物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。
<!-- TODO 芋艿:后续补充下 -->
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-env</artifactId>
</dependency>
<!-- 依赖服务 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-ops-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-iot-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-module-iot-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-job</artifactId>
</dependency>
<!-- TODO @芋艿:引入下,看看情况 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-test</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-monitor</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.viewsh</groupId>
<artifactId>viewsh-spring-boot-starter-excel</artifactId>
</dependency>
<!-- 消息队列相关 -->
<!-- TODO @芋艿:临时打开 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<!-- IoT 网络组件:接收来自设备的上行数据 -->
<!-- <dependency>-->
<!-- <groupId>com.viewsh</groupId>-->
<!-- <artifactId>viewsh-module-iot-net-component-http</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.viewsh</groupId>-->
<!-- <artifactId>viewsh-module-iot-net-component-emqx</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -6,7 +6,7 @@ import lombok.Data;
* 保洁工单集成配置
* <p>
* 这是 IoT 设备与保洁工单集成的总配置类,包含所有子配置
* 存储在 {@link OpsAreaDeviceRelationDO#getConfigData()} 中
* 存储在 Ops 模块的 {@code ops_area_device_relation.config_data} 字段
*
* @author AI
*/

View File

@@ -1,92 +0,0 @@
package com.viewsh.module.iot.dal.dataobject.integration.clean;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
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;
/**
* 运营区域设备关联 DO
* <p>
* 用于建立运营区域与 IoT 设备的绑定关系,并存储核心检测配置
* 注意:此表在 ops 库中创建,但在 IoT 模块中<E59D97><E4B8AD>通过 Feign 或直接访问)
*
* @author AI
*/
@TableName(value = "ops_area_device_relation", autoResultMap = true)
@KeySequence("ops_area_device_relation_seq")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsAreaDeviceRelationDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 运营区域ID
* <p>
* 关联 ops_bus_area.id
*/
private Long areaId;
/**
* IoT设备ID
* <p>
* 关联 iot_device.id
*/
private Long deviceId;
/**
* 设备Key冗余字段便于查询
*/
private String deviceKey;
/**
* 产品ID
* <p>
* 关联 iot_product.id
*/
private Long productId;
/**
* 产品Key冗余字段便于查询
*/
private String productKey;
/**
* 关联类型
* <p>
* TRAFFIC_COUNTER - 客流计数器
* BEACON - 蓝牙信标
* BADGE - 工牌设备
*/
private String relationType;
/**
* 配置数据JSON格式
* <p>
* 存储保洁工单集成的各类配置
* 使用 {@link JacksonTypeHandler} 自动序列化/反序列化
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private CleanOrderIntegrationConfig configData;
/**
* 是否启用
* <p>
* true - 启用
* false - 禁用
*/
private Boolean enabled;
}

View File

@@ -1,71 +0,0 @@
package com.viewsh.module.iot.dal.mysql.integration.clean;
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 org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 运营区域设备关联 Mapper
*
* @author AI
*/
@Mapper
public interface OpsAreaDeviceRelationMapper extends BaseMapperX<OpsAreaDeviceRelationDO> {
/**
* 根据设备ID查询关联关系仅查询启用的
*
* @param deviceId 设备ID
* @return 关联关系
*/
default OpsAreaDeviceRelationDO selectByDeviceId(Long deviceId) {
return selectOne(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId)
.eq(OpsAreaDeviceRelationDO::getEnabled, true)
.orderByDesc(OpsAreaDeviceRelationDO::getCreateTime)
.last("LIMIT 1"));
}
/**
* 根据区域ID查询所有启用的关联关系
*
* @param areaId 区域ID
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByAreaId(Long areaId) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getEnabled, true)
.orderByDesc(OpsAreaDeviceRelationDO::getCreateTime));
}
/**
* 根据区域ID和关联类型查询关联关系
*
* @param areaId 区域ID
* @param relationType 关联类型TRAFFIC_COUNTER/BEACON/BADGE
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByAreaIdAndRelationType(Long areaId, String relationType) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.eq(OpsAreaDeviceRelationDO::getRelationType, relationType)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
/**
* 根据产品Key查询所有关联关系
*
* @param productKey 产品Key
* @return 关联关系列表
*/
default List<OpsAreaDeviceRelationDO> selectListByProductKey(String productKey) {
return selectList(new LambdaQueryWrapperX<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getProductKey, productKey)
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
}
}

View File

@@ -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
* <p>
* 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<AreaDeviceConfigWrapper> getConfigsByAreaId(Long areaId) {
log.debug("[CleanOrderConfig] 查询区域配置areaId={}", areaId);
// 区域配置暂不缓存,直接从数据库查询
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaId(areaId);
// 通过 Feign 调用 Ops 模块获取所有关联设备
CommonResult<List<AreaDeviceDTO>> 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<AreaDeviceConfigWrapper> getConfigsByAreaIdAndRelationType(Long areaId, String relationType) {
log.debug("[CleanOrderConfig] 查询区域配置areaId={}, relationType={}", areaId, relationType);
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
// 通过 Feign 调用 Ops 模块获取关联设备
CommonResult<List<AreaDeviceDTO>> 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<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
// 2. 缓存未命中,调用 Ops 模块
CommonResult<List<AreaDeviceDTO>> 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<DeviceRelationDTO> 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<String, Object> configData) {
if (configData == null || configData.isEmpty()) {
return null;
}
return JsonUtils.parseObject(JsonUtils.toJsonString(configData), CleanOrderIntegrationConfig.class);
}
}