!1417 feat: 【IoT 物联网】实现场景规则定时触发器的注册、重构设备属性设置执行器、新增设备服务调用实现

Merge pull request !1417 from puhui999/feature/iot
This commit is contained in:
芋道源码 2025-09-03 14:19:26 +00:00 committed by Gitee
commit b945970ce6
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
20 changed files with 969 additions and 175 deletions

View File

@ -17,11 +17,11 @@ 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.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -47,9 +47,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
@Resource
private IotSceneRuleMapper sceneRuleMapper;
// TODO @puhui999定时任务基于它调度
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
@Resource
private IotProductService productService;
@Resource
@ -59,11 +56,17 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
private IotSceneRuleMatcherManager sceneRuleMatcherManager;
@Resource
private List<IotSceneRuleAction> sceneRuleActions;
@Resource
private IotSceneRuleTimerHandler timerHandler;
@Override
public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
sceneRuleMapper.insert(sceneRule);
// 注册定时触发器
timerHandler.registerTimerTriggers(sceneRule);
return sceneRule.getId();
}
@ -74,6 +77,9 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
// 更新
IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
sceneRuleMapper.updateById(updateObj);
// 更新定时触发器
timerHandler.updateTimerTriggers(updateObj);
}
@Override
@ -83,12 +89,26 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
// 更新状态
IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
sceneRuleMapper.updateById(updateObj);
// 根据状态管理定时触发器
if (CommonStatusEnum.isEnable(status)) {
// 启用时获取完整的场景规则信息并注册定时触发器
IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
if (sceneRule != null) {
timerHandler.registerTimerTriggers(sceneRule);
}
} else {
// 禁用时暂停定时触发器
timerHandler.pauseTimerTriggers(id);
}
}
@Override
public void deleteSceneRule(Long id) {
// 校验存在
validateSceneRuleExists(id);
// 删除定时触发器
timerHandler.unregisterTimerTriggers(id);
// 删除
sceneRuleMapper.deleteById(id);
}

View File

@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* IoT 设备控制的 {@link IotSceneRuleAction} 实现类
* IoT 设备属性设置 {@link IotSceneRuleAction} 实现类
*
* @author 芋道源码
*/
@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceMessageService deviceMessageService;
// TODO @puhui999这里
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
//IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();
//Assert.notNull(control, "设备控制配置不能为空");
//// 遍历每个设备下发消息
//control.getDeviceNames().forEach(deviceName -> {
// IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);
// if (device == null) {
// log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);
// return;
// }
// try {
// // TODO @芋艿@puhui999这块可能要改 type => method
// IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(
// control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));
// log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);
// } catch (Exception e) {
// log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);
// }
//});
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行属性设置
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行属性设置
executePropertySetForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行属性设置
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行属性设置
for (IotDeviceDO device : devices) {
executePropertySetForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行属性设置
*/
private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建属性设置消息
IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建属性设置消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 属性设置参数格式: {"properties": {"identifier": value}}
Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
} catch (Exception e) {
log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
return null;
}
}
@Override

View File

@ -0,0 +1,145 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageService deviceMessageService;
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行服务调用
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行服务调用
executeServiceInvokeForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行服务调用
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行服务调用
for (IotDeviceDO device : devices) {
executeServiceInvokeForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行服务调用
*/
private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建服务调用消息
IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建服务调用消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
Object params = Map.of(
"identifier", actionConfig.getIdentifier(),
"params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
);
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
} catch (Exception e) {
log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
return null;
}
}
@Override
public IotSceneRuleActionTypeEnum getType() {
return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
}
}

View File

@ -91,7 +91,7 @@ public final class IotSceneRuleMatcherHelper {
Map<String, Object> springExpressionVariables = new HashMap<>();
// 设置源值
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue);
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
// 处理参数值
if (StrUtil.isNotBlank(paramValue)) {

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@ -7,6 +8,7 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
* 设备属性条件匹配器
* <p>
@ -43,10 +45,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
return false;
}
// 2.1. 获取属性值
Object propertyValue = message.getParams();
// 2.1. 获取属性值 - 使用工具类方法正确提取属性值
Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
if (propertyValue == null) {
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空");
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
return false;
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
@ -35,8 +36,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
return false;
}
// 2.1 获取设备状态值
Object stateValue = message.getParams();
// 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
// 对于设备状态条件状态值通过 getIdentifier 获取实际是从 params.state 字段
String stateValue = IotDeviceMessageUtils.getIdentifier(message);
if (stateValue == null) {
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
return false;

View File

@ -52,10 +52,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
return false;
}
// 2.1 获取属性值
Object propertyValue = message.getParams();
// 2.1 获取属性值 - 使用工具类方法正确提取属性值
Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
if (propertyValue == null) {
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空");
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
return false;
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
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.core.util.IotDeviceMessageUtils;
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.IotSceneRuleMatcherHelper;
@ -43,16 +44,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
return false;
}
// 2.1 获取设备状态值
Object stateValue = message.getParams();
if (stateValue == null) {
// 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
// 对于状态更新消息状态值通过 getIdentifier 获取实际是从 params.state 字段
String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
if (stateIdentifier == null) {
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
return false;
}
// 2.2 使用条件评估器进行匹配
// TODO @puhui999: 状态匹配重新实现
boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue());
// 状态值通常是字符串或数字直接使用标识符作为状态值
boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
if (matched) {
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
} else {

View File

@ -0,0 +1,178 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Component;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
/**
* IoT 场景规则定时触发器处理器
* <p>
* 负责管理定时触发器的注册更新删除等操作
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotSceneRuleTimerHandler {
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
/**
* 注册场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
return;
}
// 过滤出定时触发器
List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
if (CollUtil.isEmpty(timerTriggers)) {
return;
}
// 注册每个定时触发器
timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
}
/**
* 更新场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
if (sceneRule == null) {
return;
}
// 先删除旧的定时任务
unregisterTimerTriggers(sceneRule.getId());
// 如果场景规则已禁用则不重新注册
if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
return;
}
// 重新注册定时触发器
registerTimerTriggers(sceneRule);
}
/**
* 注销场景规则的定时触发器
*
* @param sceneRuleId 场景规则ID
*/
public void unregisterTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.deleteJob(jobName);
log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
}
}
/**
* 暂停场景规则的定时触发器
*
* @param sceneRuleId 场景规则ID
*/
public void pauseTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.pauseJob(jobName);
log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
}
}
/**
* 恢复场景规则的定时触发器
*
* @param sceneRuleId 场景规则ID
*/
public void resumeTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.resumeJob(jobName);
log.info("[resumeTimerTriggers][场景规则({}) 定时触发器恢复成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[resumeTimerTriggers][场景规则({}) 定时触发器恢复失败]", sceneRuleId, e);
}
}
/**
* 注册单个定时触发器
*
* @param sceneRule 场景规则
* @param trigger 定时触发器配置
*/
private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
// 1. 参数校验
if (StrUtil.isBlank(trigger.getCronExpression())) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
return;
}
// 2. 构建任务名称和数据
String jobName = buildJobName(sceneRule.getId());
try {
// 3. 注册定时任务
schedulerManager.addOrUpdateJob(
IotSceneRuleJob.class,
jobName,
trigger.getCronExpression(),
IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
);
log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功CRON: {}]",
sceneRule.getId(), trigger.getCronExpression());
} catch (SchedulerException e) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败CRON: {}]",
sceneRule.getId(), trigger.getCronExpression(), e);
}
}
/**
* 构建任务名称
*
* @param sceneRuleId 场景规则ID
* @return 任务名称
*/
private String buildJobName(Long sceneRuleId) {
return "iot_scene_rule_timer_" + sceneRuleId;
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Matcher 测试基类
* 提供通用的 Spring 测试配置
*
* @author HUIHUI
*/
@SpringJUnitConfig
public abstract class BaseMatcherTest {
/**
* 注入一下 SpringUtil解析 EL 表达式时需要
* {@link SpringExpressionUtils#parseExpression}
*/
@Configuration
static class TestConfig {
@Bean
public SpringUtil springUtil() {
return new SpringUtil();
}
}
}

View File

@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@ -20,11 +20,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest {
public class CurrentTimeConditionMatcherTest extends BaseMatcherTest {
@InjectMocks
private CurrentTimeConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new CurrentTimeConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {
// 调用

View File

@ -1,19 +1,17 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
@ -21,11 +19,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
public class DevicePropertyConditionMatcherTest extends BaseMatcherTest {
@InjectMocks
private DevicePropertyConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DevicePropertyConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {
// 调用
@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
int result = matcher.getPriority();
// 断言
assertEquals(20, result);
}
@Test
public void testIsEnabled() {
// 调用
boolean result = matcher.isEnabled();
// 断言
assertTrue(result);
assertEquals(25, result); // 修正实际返回值是 25
}
@Test
public void testMatches_temperatureEquals_success() {
// 准备参数
String propertyName = "temperature";
// 准备参数创建属性上报消息
String propertyIdentifier = "temperature";
Double propertyValue = 25.5;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_humidityGreaterThan_success() {
// 准备参数
String propertyName = "humidity";
// 准备参数创建属性上报消息
String propertyIdentifier = "humidity";
Integer propertyValue = 75;
Integer compareValue = 70;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_pressureLessThan_success() {
// 准备参数
String propertyName = "pressure";
// 准备参数创建属性上报消息
String propertyIdentifier = "pressure";
Double propertyValue = 1010.5;
Integer compareValue = 1020;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
String.valueOf(compareValue)
);
@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_statusNotEquals_success() {
// 准备参数
String propertyName = "status";
// 准备参数创建属性上报消息
String propertyIdentifier = "status";
String propertyValue = "active";
String compareValue = "inactive";
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
compareValue
);
@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_propertyMismatch_fail() {
// 准备参数
String propertyName = "temperature";
// 准备参数创建属性上报消息值不满足条件
String propertyIdentifier = "temperature";
Double propertyValue = 15.0;
Integer compareValue = 20;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
public void testMatches_propertyNotFound_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
public void testMatches_identifierMismatch_fail() {
// 准备参数标识符不匹配
String messageIdentifier = "temperature";
String conditionIdentifier = "humidity";
Double propertyValue = 25.5;
IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
randomString(), // 随机不存在的属性名
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"50"
conditionIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
// 调用
@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullCondition_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
// 调用
boolean result = matcher.matches(message, null);
@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullConditionType_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(null);
@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingIdentifier_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier(null); // 缺少标识符
@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingOperator_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingParam_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullDeviceProperties_fail() {
// 准备参数
// 准备参数消息的 params null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
public void testMatches_voltageGreaterThanOrEquals_success() {
// 准备参数
String propertyName = "voltage";
Double propertyValue = 12.0;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
public void testMatches_propertiesStructure_success() {
// 测试使用 properties 结构的消息真实的属性上报场景
String identifier = "temperature";
Double propertyValue = 25.5;
IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
identifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能正确从 properties 中提取属性值
assertTrue(result);
}
@Test
public void testMatches_simpleValueMessage_success() {
// 测试简单值消息params 直接是属性值
Double propertyValue = 25.5;
IotDeviceMessage message = createSimpleValueMessage(propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
"any", // 对于简单值消息标识符匹配会被跳过
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能处理简单值消息
// 但由于标识符匹配失败结果为 false
assertFalse(result);
}
@Test
public void testMatches_valueFieldStructure_success() {
// 测试使用 value 字段的消息结构
String identifier = "temperature";
Double propertyValue = 25.5;
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.event.post");
Map<String, Object> params = new HashMap<>();
params.put("identifier", identifier);
params.put("value", propertyValue);
message.setParams(params);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
identifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能从 value 字段提取属性值
assertTrue(result);
}
@Test
public void testMatches_voltageGreaterThanOrEquals_success() {
// 准备参数创建属性上报消息
String propertyIdentifier = "voltage";
Double propertyValue = 12.0;
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_currentLessThanOrEquals_success() {
// 准备参数
String propertyName = "current";
// 准备参数创建属性上报消息
String propertyIdentifier = "current";
Double propertyValue = 2.5;
Double compareValue = 3.0;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
String.valueOf(compareValue)
);
@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_stringProperty_success() {
// 准备参数
String propertyName = "mode";
// 准备参数创建属性上报消息
String propertyIdentifier = "mode";
String propertyValue = "auto";
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
propertyValue
);
@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_booleanProperty_success() {
// 准备参数
String propertyName = "enabled";
// 准备参数创建属性上报消息
String propertyIdentifier = "enabled";
Boolean propertyValue = true;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
assertTrue(result);
}
@Test
public void testMatches_multipleProperties_success() {
// 准备参数
Map<String, Object> properties = MapUtil.builder(new HashMap<String, Object>())
.put("temperature", 25.5)
.put("humidity", 60)
.put("status", "active")
.put("enabled", true)
.build();
IotDeviceMessage message = createDeviceMessage(properties);
String targetProperty = "humidity";
Integer targetValue = 60;
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
targetProperty,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(targetValue)
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言
assertTrue(result);
}
// ========== 辅助方法 ==========
/**
* 创建设备消息
* 创建设备消息用于测试
*
* 支持的消息格式
* 1. 直接属性值params 直接是属性值适用于简单消息
* 2. 标识符+params 包含 identifier 和对应的属性值
* 3. properties 结构params.properties[identifier] = value
* 4. data 结构params.data[identifier] = value
* 5. value 字段params.value = value
*/
private IotDeviceMessage createDeviceMessage(Map<String, Object> properties) {
private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setParams(properties);
message.setMethod("thing.event.post"); // 使用事件上报方法
// 创建符合修复后逻辑的 params 结构
Map<String, Object> params = new HashMap<>();
params.put("identifier", identifier);
// 直接将属性值放在标识符对应的字段中
params.put(identifier, value);
message.setParams(params);
return message;
}
/**
* 创建使用 properties 结构的消息模拟真实的属性上报消息
*/
private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.property.post"); // 属性上报方法
Map<String, Object> properties = new HashMap<>();
properties.put(identifier, value);
Map<String, Object> params = new HashMap<>();
params.put("properties", properties);
message.setParams(params);
return message;
}
/**
* 创建简单值消息params 直接是属性值
*/
private IotDeviceMessage createSimpleValueMessage(Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.property.post");
// 直接将属性值作为 params
message.setParams(value);
return message;
}

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@ -18,11 +18,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest {
public class DeviceStateConditionMatcherTest extends BaseMatcherTest {
@InjectMocks
private DeviceStateConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DeviceStateConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {
// 调用

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest {
public class DeviceEventPostTriggerMatcherTest extends BaseMatcherTest {
@InjectMocks
private DeviceEventPostTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DeviceEventPostTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {
// 准备参数

View File

@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@ -24,11 +24,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest {
public class DevicePropertyPostTriggerMatcherTest extends BaseMatcherTest {
@InjectMocks
private DevicePropertyPostTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DevicePropertyPostTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {
// 准备参数

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest {
public class DeviceServiceInvokeTriggerMatcherTest extends BaseMatcherTest {
@InjectMocks
private DeviceServiceInvokeTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DeviceServiceInvokeTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {
// 准备参数

View File

@ -1,14 +1,17 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
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.*;
@ -18,11 +21,24 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest {
@SpringJUnitConfig(DeviceStateUpdateTriggerMatcherTest.TestConfig.class)
public class DeviceStateUpdateTriggerMatcherTest {
@Configuration
static class TestConfig {
@Bean
public SpringUtil springUtil() {
return new SpringUtil();
}
}
@InjectMocks
private DeviceStateUpdateTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new DeviceStateUpdateTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {
// 准备参数

View File

@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@ -16,11 +16,15 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author HUIHUI
*/
public class TimerTriggerMatcherTest extends BaseMockitoUnitTest {
public class TimerTriggerMatcherTest extends BaseMatcherTest {
@InjectMocks
private TimerTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new TimerTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {
// 准备参数

View File

@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
return null;
}
/**
* 从设备消息中提取指定标识符的属性值
* - 支持多种消息格式和属性值提取策略
* - 兼容现有的消息结构
* - 提供统一的属性值提取接口
* <p>
* 支持的提取策略按优先级顺序
* 1. 直接值如果 params 不是 Map直接返回该值适用于简单消息
* 2. 标识符字段 params[identifier] 获取
* 3. properties 结构 params.properties[identifier] 获取标准属性上报
* 4. data 结构 params.data[identifier] 获取
* 5. value 字段 params.value 获取单值消息
* 6. 单值 Map如果 Map 只包含 identifier 和一个值返回该值
*
* @param message 设备消息
* @param identifier 属性标识符
* @return 属性值如果未找到则返回 null
*/
@SuppressWarnings("unchecked")
public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
Object params = message.getParams();
if (params == null) {
return null;
}
// 策略1如果 params 不是 Map直接返回该值适用于简单的单属性消息
if (!(params instanceof Map)) {
return params;
}
Map<String, Object> paramsMap = (Map<String, Object>) params;
// 策略2直接通过标识符获取属性值
Object directValue = paramsMap.get(identifier);
if (directValue != null) {
return directValue;
}
// 策略3 properties 字段中获取适用于标准属性上报消息
Object properties = paramsMap.get("properties");
if (properties instanceof Map) {
Map<String, Object> propertiesMap = (Map<String, Object>) properties;
Object propertyValue = propertiesMap.get(identifier);
if (propertyValue != null) {
return propertyValue;
}
}
// 策略4 data 字段中获取适用于某些消息格式
Object data = paramsMap.get("data");
if (data instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) data;
Object dataValue = dataMap.get(identifier);
if (dataValue != null) {
return dataValue;
}
}
// 策略5 value 字段中获取适用于单值消息
Object value = paramsMap.get("value");
if (value != null) {
return value;
}
// 策略6如果 Map 只有两个字段且包含 identifier返回另一个字段的值
if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
if (!"identifier".equals(entry.getKey())) {
return entry.getValue();
}
}
}
// 未找到对应的属性值
return null;
}
// ========== Topic 相关 ==========
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {

View File

@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* {@link IotDeviceMessageUtils} 的单元测试
*
* @author HUIHUI
*/
public class IotDeviceMessageUtilsTest {
@Test
public void testExtractPropertyValue_directValue() {
// 测试直接值 Map
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(25.5);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_directIdentifier() {
// 测试直接通过标识符获取
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_propertiesStructure() {
// 测试 properties 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 25.5);
properties.put("humidity", 60);
Map<String, Object> params = new HashMap<>();
params.put("properties", properties);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_dataStructure() {
// 测试 data 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> data = new HashMap<>();
data.put("temperature", 25.5);
Map<String, Object> params = new HashMap<>();
params.put("data", data);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_valueField() {
// 测试 value 字段
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("value", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_singleValueMap() {
// 测试单值 Map包含 identifier 和一个值
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("actualValue", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_notFound() {
// 测试未找到属性值
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("humidity", 60);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_nullParams() {
// 测试 params null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_priorityOrder() {
// 测试优先级顺序直接标识符 > properties > data > value
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 20.0);
Map<String, Object> data = new HashMap<>();
data.put("temperature", 30.0);
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5); // 最高优先级
params.put("properties", properties);
params.put("data", data);
params.put("value", 40.0);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result); // 应该返回直接标识符的值
}
}