mirror of
https://gitee.com/yudaocode/yudao-boot-mini.git
synced 2025-12-26 07:06:22 +08:00
!1434 完善 code review 提到的一些问题
Merge pull request !1434 from puhui999/feature/iot
This commit is contained in:
commit
c1657149a8
@ -17,6 +17,8 @@ import lombok.Data;
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"),
|
||||
@JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
|
||||
@JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
|
||||
@JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
|
||||
@JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
|
||||
@JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Data
|
||||
public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
|
||||
|
||||
/**
|
||||
* TCP 服务器地址
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* TCP 服务器端口
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* 连接超时时间(毫秒)
|
||||
*/
|
||||
private Integer connectTimeoutMs = 5000;
|
||||
|
||||
/**
|
||||
* 读取超时时间(毫秒)
|
||||
*/
|
||||
private Integer readTimeoutMs = 10000;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL
|
||||
*/
|
||||
private Boolean ssl = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径(当 ssl=true 时需要)
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* 数据格式:JSON 或 BINARY
|
||||
*/
|
||||
private String dataFormat = "JSON";
|
||||
|
||||
/**
|
||||
* 心跳间隔时间(毫秒),0 表示不启用心跳
|
||||
*/
|
||||
private Long heartbeatIntervalMs = 30000L;
|
||||
|
||||
/**
|
||||
* 重连间隔时间(毫秒)
|
||||
*/
|
||||
private Long reconnectIntervalMs = 5000L;
|
||||
|
||||
/**
|
||||
* 最大重连次数
|
||||
*/
|
||||
private Integer maxReconnectAttempts = 3;
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
|
||||
* <p>
|
||||
* 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
|
||||
* 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Data
|
||||
public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
|
||||
|
||||
/**
|
||||
* WebSocket 服务器地址
|
||||
* 例如:ws://localhost:8080/ws 或 wss://example.com/ws
|
||||
*/
|
||||
private String serverUrl;
|
||||
|
||||
/**
|
||||
* 连接超时时间(毫秒)
|
||||
*/
|
||||
private Integer connectTimeoutMs = 5000;
|
||||
|
||||
/**
|
||||
* 发送超时时间(毫秒)
|
||||
*/
|
||||
private Integer sendTimeoutMs = 10000;
|
||||
|
||||
/**
|
||||
* 心跳间隔时间(毫秒),0 表示不启用心跳
|
||||
*/
|
||||
private Long heartbeatIntervalMs = 30000L;
|
||||
|
||||
/**
|
||||
* 心跳消息内容(JSON 格式)
|
||||
*/
|
||||
private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
|
||||
|
||||
/**
|
||||
* 子协议列表(逗号分隔)
|
||||
*/
|
||||
private String subprotocols;
|
||||
|
||||
/**
|
||||
* 自定义请求头(JSON 格式)
|
||||
*/
|
||||
private String customHeaders;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL 证书验证(仅对 wss:// 生效)
|
||||
*/
|
||||
private Boolean verifySslCert = true;
|
||||
|
||||
/**
|
||||
* 数据格式:JSON 或 TEXT
|
||||
*/
|
||||
private String dataFormat = "JSON";
|
||||
|
||||
/**
|
||||
* 重连间隔时间(毫秒)
|
||||
*/
|
||||
private Long reconnectIntervalMs = 5000L;
|
||||
|
||||
/**
|
||||
* 最大重连次数
|
||||
*/
|
||||
private Integer maxReconnectAttempts = 3;
|
||||
|
||||
/**
|
||||
* 是否启用压缩
|
||||
*/
|
||||
private Boolean enableCompression = false;
|
||||
|
||||
/**
|
||||
* 消息发送重试次数
|
||||
*/
|
||||
private Integer sendRetryCount = 1;
|
||||
|
||||
/**
|
||||
* 消息发送重试间隔(毫秒)
|
||||
*/
|
||||
private Long sendRetryIntervalMs = 1000L;
|
||||
|
||||
}
|
||||
@ -76,4 +76,12 @@ public interface RedisKeyConstants {
|
||||
*/
|
||||
String DATA_SINK = "iot:data_sink";
|
||||
|
||||
/**
|
||||
* 场景联动规则的数据缓存,使用 Spring Cache 操作
|
||||
* <p>
|
||||
* KEY 格式:scene_rule_list_${productId}_${deviceId}
|
||||
* VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表
|
||||
*/
|
||||
String SCENE_RULE_LIST = "iot:scene_rule_list";
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.data.action;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* TCP 的 {@link IotDataRuleAction} 实现类
|
||||
* <p>
|
||||
* 负责将设备消息发送到外部 TCP 服务器
|
||||
* 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
|
||||
* 使用连接池管理 TCP 连接,提高性能和资源利用率
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class IotTcpDataRuleAction extends
|
||||
IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
|
||||
|
||||
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
|
||||
private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
@Override
|
||||
public Integer getType() {
|
||||
return IotDataSinkTypeEnum.TCP.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
|
||||
// 1. 参数校验
|
||||
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("TCP 服务器地址不能为空");
|
||||
}
|
||||
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
|
||||
throw new IllegalArgumentException("TCP 服务器端口无效");
|
||||
}
|
||||
|
||||
// 2. 创建 TCP 客户端
|
||||
IotTcpClient tcpClient = new IotTcpClient(
|
||||
config.getHost(),
|
||||
config.getPort(),
|
||||
config.getConnectTimeoutMs(),
|
||||
config.getReadTimeoutMs(),
|
||||
config.getSsl(),
|
||||
config.getSslCertPath(),
|
||||
config.getDataFormat()
|
||||
);
|
||||
|
||||
// 3. 连接服务器
|
||||
tcpClient.connect();
|
||||
|
||||
log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]",
|
||||
config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
|
||||
|
||||
return tcpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeProducer(IotTcpClient producer) throws Exception {
|
||||
if (producer != null) {
|
||||
producer.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
|
||||
try {
|
||||
// 1. 获取或创建 TCP 客户端
|
||||
IotTcpClient tcpClient = getProducer(config);
|
||||
|
||||
// 2. 检查连接状态,如果断开则重新连接
|
||||
if (!tcpClient.isConnected()) {
|
||||
log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
|
||||
tcpClient.connect();
|
||||
}
|
||||
|
||||
// 3. 发送消息并等待结果
|
||||
tcpClient.sendMessage(message);
|
||||
|
||||
// 4. 记录发送成功日志
|
||||
log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]",
|
||||
message, config, config.getHost(), config.getPort());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]",
|
||||
message, config, config.getHost(), config.getPort(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* IoT TCP 客户端
|
||||
* <p>
|
||||
* 负责与外部 TCP 服务器建立连接并发送设备消息
|
||||
* 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotTcpClient {
|
||||
|
||||
private final String host;
|
||||
private final Integer port;
|
||||
private final Integer connectTimeoutMs;
|
||||
private final Integer readTimeoutMs;
|
||||
private final Boolean ssl;
|
||||
private final String sslCertPath;
|
||||
private final String dataFormat;
|
||||
|
||||
private Socket socket;
|
||||
private OutputStream outputStream;
|
||||
private BufferedReader reader;
|
||||
private final AtomicBoolean connected = new AtomicBoolean(false);
|
||||
|
||||
public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
|
||||
Boolean ssl, String sslCertPath, String dataFormat) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
|
||||
this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
|
||||
this.ssl = ssl != null ? ssl : false;
|
||||
this.sslCertPath = sslCertPath;
|
||||
this.dataFormat = dataFormat != null ? dataFormat : "JSON";
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到 TCP 服务器
|
||||
*/
|
||||
public void connect() throws Exception {
|
||||
if (connected.get()) {
|
||||
log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (ssl) {
|
||||
// SSL 连接
|
||||
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
socket = sslSocketFactory.createSocket();
|
||||
} else {
|
||||
// 普通连接
|
||||
socket = new Socket();
|
||||
}
|
||||
|
||||
// 连接服务器
|
||||
socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
|
||||
socket.setSoTimeout(readTimeoutMs);
|
||||
|
||||
// 获取输入输出流
|
||||
outputStream = socket.getOutputStream();
|
||||
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
|
||||
connected.set(true);
|
||||
log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
|
||||
|
||||
} catch (Exception e) {
|
||||
close();
|
||||
log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备消息
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @throws Exception 发送异常
|
||||
*/
|
||||
public void sendMessage(IotDeviceMessage message) throws Exception {
|
||||
if (!connected.get()) {
|
||||
throw new IllegalStateException("TCP 客户端未连接");
|
||||
}
|
||||
|
||||
try {
|
||||
String messageData;
|
||||
if ("JSON".equalsIgnoreCase(dataFormat)) {
|
||||
// JSON 格式
|
||||
messageData = JsonUtils.toJsonString(message);
|
||||
} else {
|
||||
// BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
|
||||
messageData = message.toString();
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.write('\n'); // 添加换行符作为消息分隔符
|
||||
outputStream.flush();
|
||||
|
||||
log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
|
||||
message.getDeviceId(), messageData.length());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
public void close() {
|
||||
if (!connected.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 关闭资源
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("[close][关闭输入流失败]", e);
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("[close][关闭输出流失败]", e);
|
||||
}
|
||||
}
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("[close][关闭 Socket 失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
connected.set(false);
|
||||
log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[close][关闭 TCP 客户端连接异常]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*
|
||||
* @return 是否已连接
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return connected.get() && socket != null && !socket.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IotTcpClient{" +
|
||||
"host='" + host + '\'' +
|
||||
", port=" + port +
|
||||
", ssl=" + ssl +
|
||||
", dataFormat='" + dataFormat + '\'' +
|
||||
", connected=" + connected.get() +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
|
||||
import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
@ -24,6 +25,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@ -60,6 +63,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
private IotSceneRuleTimerHandler timerHandler;
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
|
||||
public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
|
||||
IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
|
||||
sceneRuleMapper.insert(sceneRule);
|
||||
@ -71,6 +75,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
|
||||
public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateSceneRuleExists(updateReqVO.getId());
|
||||
@ -83,6 +88,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
|
||||
public void updateSceneRuleStatus(Long id, Integer status) {
|
||||
// 1. 校验存在
|
||||
validateSceneRuleExists(id);
|
||||
@ -105,6 +111,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
|
||||
public void deleteSceneRule(Long id) {
|
||||
// 1. 校验存在
|
||||
validateSceneRuleExists(id);
|
||||
@ -149,15 +156,12 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
return sceneRuleMapper.selectListByStatus(status);
|
||||
}
|
||||
|
||||
// TODO @puhui999:缓存待实现
|
||||
@Override
|
||||
@Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
|
||||
@TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
|
||||
public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
|
||||
// 1. 查询启用状态的规则场景
|
||||
// TODO @puhui999:这里查询 enable 的;
|
||||
List<IotSceneRuleDO> list = sceneRuleMapper.selectList();
|
||||
List<IotSceneRuleDO> enabledList = filterList(list,
|
||||
sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus()));
|
||||
List<IotSceneRuleDO> enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 2. 根据 productKey 和 deviceName 进行匹配
|
||||
return filterList(enabledList, sceneRule -> {
|
||||
@ -190,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
|
||||
@Override
|
||||
public void executeSceneRuleByDevice(IotDeviceMessage message) {
|
||||
// TODO @puhui999:这里的 tenantId,通过设备获取;
|
||||
TenantUtils.execute(message.getTenantId(), () -> {
|
||||
// 1. 获得设备匹配的规则场景
|
||||
// 1.1 这里的 tenantId,通过设备获取;
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
TenantUtils.execute(device.getTenantId(), () -> {
|
||||
// 1.2 获得设备匹配的规则场景
|
||||
List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
|
||||
if (CollUtil.isEmpty(sceneRules)) {
|
||||
return;
|
||||
@ -238,14 +243,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
|
||||
// 1. 匹配设备
|
||||
// TODO 缓存 @puhui999:可能需要 getSelf()
|
||||
// 1.1 通过 deviceId 获取设备信息
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
if (device == null) {
|
||||
log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 1.2 通过 productId 获取产品信息
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
|
||||
if (product == null) {
|
||||
log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
|
||||
return List.of();
|
||||
|
||||
@ -16,7 +16,6 @@ import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
// TODO @puhui999:是不是 IoT 的前缀,都加下哈;
|
||||
/**
|
||||
* 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
|
||||
*
|
||||
@ -24,7 +23,7 @@ import java.util.List;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
|
||||
/**
|
||||
* 时间格式化器 - HH:mm:ss
|
||||
@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
|
||||
@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
|
||||
@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||
@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||
@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||
@ -44,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
|
||||
|
||||
// 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
|
||||
// 不需要检查操作符和值,因为服务调用本身就是触发条件
|
||||
// TODO @puhui999: 服务调用时校验输入参数是否匹配条件
|
||||
// TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
|
||||
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
|
||||
return true;
|
||||
}
|
||||
@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||
@ -16,7 +16,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||
|
||||
@Override
|
||||
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||
@ -0,0 +1,161 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.data.action;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link IotTcpDataRuleAction} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
class IotTcpDataRuleActionTest {
|
||||
|
||||
private IotTcpDataRuleAction tcpDataRuleAction;
|
||||
|
||||
@Mock
|
||||
private IotTcpClient mockTcpClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
tcpDataRuleAction = new IotTcpDataRuleAction();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetType() {
|
||||
// 准备参数
|
||||
Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
|
||||
|
||||
// 调用方法
|
||||
Integer actualType = tcpDataRuleAction.getType();
|
||||
|
||||
// 断言结果
|
||||
assertEquals(expectedType, actualType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitProducer_Success() throws Exception {
|
||||
// 准备参数
|
||||
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
|
||||
config.setHost("localhost");
|
||||
config.setPort(8080);
|
||||
config.setDataFormat("JSON");
|
||||
config.setSsl(false);
|
||||
|
||||
// 调用方法 & 断言结果
|
||||
// 此测试需要实际的 TCP 连接,在单元测试中可能不可用
|
||||
// 目前我们只验证配置是否有效
|
||||
assertNotNull(config.getHost());
|
||||
assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitProducer_InvalidHost() {
|
||||
// 准备参数
|
||||
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
|
||||
config.setHost("");
|
||||
config.setPort(8080);
|
||||
|
||||
// 调用方法 & 断言结果
|
||||
IotTcpDataRuleAction action = new IotTcpDataRuleAction();
|
||||
|
||||
// 测试验证逻辑(通常在 initProducer 方法中)
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("TCP 服务器地址不能为空");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitProducer_InvalidPort() {
|
||||
// 准备参数
|
||||
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
|
||||
config.setHost("localhost");
|
||||
config.setPort(-1);
|
||||
|
||||
// 调用方法 & 断言结果
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
|
||||
throw new IllegalArgumentException("TCP 服务器端口无效");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCloseProducer() throws Exception {
|
||||
// 准备参数
|
||||
IotTcpClient client = mock(IotTcpClient.class);
|
||||
|
||||
// 调用方法
|
||||
tcpDataRuleAction.closeProducer(client);
|
||||
|
||||
// 断言结果
|
||||
verify(client, times(1)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExecute_WithValidConfig() {
|
||||
// 准备参数
|
||||
IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
|
||||
"{\"temperature\": 25.5, \"humidity\": 60}");
|
||||
|
||||
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
|
||||
config.setHost("localhost");
|
||||
config.setPort(8080);
|
||||
config.setDataFormat("JSON");
|
||||
|
||||
// 调用方法 & 断言结果
|
||||
// 通常这需要实际的 TCP 连接
|
||||
// 在单元测试中,我们只验证输入参数
|
||||
assertNotNull(message);
|
||||
assertNotNull(config);
|
||||
assertNotNull(config.getHost());
|
||||
assertTrue(config.getPort() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfig_DefaultValues() {
|
||||
// 准备参数
|
||||
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
|
||||
|
||||
// 调用方法 & 断言结果
|
||||
// 验证默认值
|
||||
assertEquals("JSON", config.getDataFormat());
|
||||
assertEquals(5000, config.getConnectTimeoutMs());
|
||||
assertEquals(10000, config.getReadTimeoutMs());
|
||||
assertEquals(false, config.getSsl());
|
||||
assertEquals(30000L, config.getHeartbeatIntervalMs());
|
||||
assertEquals(5000L, config.getReconnectIntervalMs());
|
||||
assertEquals(3, config.getMaxReconnectAttempts());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMessageSerialization() {
|
||||
// 准备参数
|
||||
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||
.deviceId(123L)
|
||||
.method("thing.property.report")
|
||||
.params("{\"temperature\": 25.5}")
|
||||
.build();
|
||||
|
||||
// 调用方法
|
||||
String json = JsonUtils.toJsonString(message);
|
||||
|
||||
// 断言结果
|
||||
assertNotNull(json);
|
||||
assertTrue(json.contains("\"deviceId\":123"));
|
||||
assertTrue(json.contains("\"method\":\"thing.property.report\""));
|
||||
assertTrue(json.contains("\"temperature\":25.5"));
|
||||
}
|
||||
|
||||
}
|
||||
@ -6,7 +6,6 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
// TODO @puhui999:建议改成 IotBaseConditionMatcherTest
|
||||
/**
|
||||
* Matcher 测试基类
|
||||
* 提供通用的 Spring 测试配置
|
||||
@ -14,7 +13,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
public abstract class BaseMatcherTest {
|
||||
public abstract class IotBaseConditionMatcherTest {
|
||||
|
||||
/**
|
||||
* 注入一下 SpringUtil,解析 EL 表达式时需要
|
||||
@ -4,7 +4,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -16,17 +16,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link CurrentTimeConditionMatcher} 的单元测试
|
||||
* {@link IotCurrentTimeConditionMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class CurrentTimeConditionMatcherTest extends BaseMatcherTest {
|
||||
public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private CurrentTimeConditionMatcher matcher;
|
||||
private IotCurrentTimeConditionMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new CurrentTimeConditionMatcher();
|
||||
matcher = new IotCurrentTimeConditionMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -4,7 +4,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -15,17 +15,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DevicePropertyConditionMatcher} 的单元测试
|
||||
* {@link IotDevicePropertyConditionMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class DevicePropertyConditionMatcherTest extends BaseMatcherTest {
|
||||
public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private DevicePropertyConditionMatcher matcher;
|
||||
private IotDevicePropertyConditionMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DevicePropertyConditionMatcher();
|
||||
matcher = new IotDevicePropertyConditionMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -14,17 +14,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DeviceStateConditionMatcher} 的单元测试
|
||||
* {@link IotDeviceStateConditionMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class DeviceStateConditionMatcherTest extends BaseMatcherTest {
|
||||
public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private DeviceStateConditionMatcher matcher;
|
||||
private IotDeviceStateConditionMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DeviceStateConditionMatcher();
|
||||
matcher = new IotDeviceStateConditionMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -18,17 +18,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DeviceEventPostTriggerMatcher} 的单元测试
|
||||
* {@link IotDeviceEventPostTriggerMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class DeviceEventPostTriggerMatcherTest extends BaseMatcherTest {
|
||||
public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private DeviceEventPostTriggerMatcher matcher;
|
||||
private IotDeviceEventPostTriggerMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DeviceEventPostTriggerMatcher();
|
||||
matcher = new IotDeviceEventPostTriggerMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -20,17 +20,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DevicePropertyPostTriggerMatcher} 的单元测试
|
||||
* {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class DevicePropertyPostTriggerMatcherTest extends BaseMatcherTest {
|
||||
public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private DevicePropertyPostTriggerMatcher matcher;
|
||||
private IotDevicePropertyPostTriggerMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DevicePropertyPostTriggerMatcher();
|
||||
matcher = new IotDevicePropertyPostTriggerMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -18,17 +18,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DeviceServiceInvokeTriggerMatcher} 的单元测试
|
||||
* {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class DeviceServiceInvokeTriggerMatcherTest extends BaseMatcherTest {
|
||||
public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private DeviceServiceInvokeTriggerMatcher matcher;
|
||||
private IotDeviceServiceInvokeTriggerMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DeviceServiceInvokeTriggerMatcher();
|
||||
matcher = new IotDeviceServiceInvokeTriggerMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1,42 +1,30 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DeviceStateUpdateTriggerMatcher} 的单元测试
|
||||
* {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@SpringJUnitConfig(DeviceStateUpdateTriggerMatcherTest.TestConfig.class)
|
||||
public class DeviceStateUpdateTriggerMatcherTest {
|
||||
public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
@Configuration
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
public SpringUtil springUtil() {
|
||||
return new SpringUtil();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceStateUpdateTriggerMatcher matcher;
|
||||
private IotDeviceStateUpdateTriggerMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new DeviceStateUpdateTriggerMatcher();
|
||||
matcher = new IotDeviceStateUpdateTriggerMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
|
||||
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -12,17 +12,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link TimerTriggerMatcher} 的单元测试
|
||||
* {@link IotTimerTriggerMatcher} 的单元测试
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public class TimerTriggerMatcherTest extends BaseMatcherTest {
|
||||
public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
|
||||
|
||||
private TimerTriggerMatcher matcher;
|
||||
private IotTimerTriggerMatcher matcher;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
matcher = new TimerTriggerMatcher();
|
||||
matcher = new IotTimerTriggerMatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -0,0 +1,126 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
|
||||
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.quartz.SchedulerException;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link IotSceneRuleTimerHandler} 的单元测试类
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class IotSceneRuleTimerHandlerTest {
|
||||
|
||||
@Mock
|
||||
private IotSchedulerManager schedulerManager;
|
||||
|
||||
@InjectMocks
|
||||
private IotSceneRuleTimerHandler timerHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 重置 Mock 对象
|
||||
reset(schedulerManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterTimerTriggers_success() throws SchedulerException {
|
||||
// 准备参数
|
||||
Long sceneRuleId = 1L;
|
||||
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
|
||||
sceneRule.setId(sceneRuleId);
|
||||
sceneRule.setStatus(0); // 0 表示启用
|
||||
// 创建定时触发器
|
||||
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
|
||||
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
|
||||
timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
|
||||
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
|
||||
|
||||
// 调用
|
||||
timerHandler.registerTimerTriggers(sceneRule);
|
||||
|
||||
// 验证
|
||||
verify(schedulerManager, times(1)).addOrUpdateJob(
|
||||
eq(IotSceneRuleJob.class),
|
||||
eq("iot_scene_rule_timer_" + sceneRuleId),
|
||||
eq("0 0 12 * * ?"),
|
||||
eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
|
||||
// 准备参数 - 没有定时触发器
|
||||
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
|
||||
sceneRule.setStatus(0); // 0 表示启用
|
||||
// 创建非定时触发器
|
||||
IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
|
||||
deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
|
||||
sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
|
||||
|
||||
// 调用
|
||||
timerHandler.registerTimerTriggers(sceneRule);
|
||||
|
||||
// 验证 - 不应该调用调度器
|
||||
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
|
||||
// 准备参数 - CRON 表达式为空
|
||||
Long sceneRuleId = 2L;
|
||||
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
|
||||
sceneRule.setId(sceneRuleId);
|
||||
sceneRule.setStatus(0); // 0 表示启用
|
||||
// 创建定时触发器但没有 CRON 表达式
|
||||
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
|
||||
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
|
||||
timerTrigger.setCronExpression(""); // 空的 CRON 表达式
|
||||
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
|
||||
|
||||
// 调用
|
||||
timerHandler.registerTimerTriggers(sceneRule);
|
||||
|
||||
// 验证 - 不应该调用调度器
|
||||
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnregisterTimerTriggers_success() throws SchedulerException {
|
||||
// 准备参数
|
||||
Long sceneRuleId = 3L;
|
||||
|
||||
// 调用
|
||||
timerHandler.unregisterTimerTriggers(sceneRuleId);
|
||||
|
||||
// 验证
|
||||
verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPauseTimerTriggers_success() throws SchedulerException {
|
||||
// 准备参数
|
||||
Long sceneRuleId = 4L;
|
||||
|
||||
// 调用
|
||||
timerHandler.pauseTimerTriggers(sceneRuleId);
|
||||
|
||||
// 验证
|
||||
verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user