diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java index 7711ae0..ec13680 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -173,6 +173,24 @@ public class JsonUtils { } } + /** + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null + * + * @param text 字符串 + * @param clazz 类型 + * @return 指定类型的对象 + */ + public static T parseObjectQuietly(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + return null; + } + } + public static List parseArray(String text, Class clazz) { if (StrUtil.isEmpty(text)) { return new ArrayList<>(); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java index 937add3..5d62bab 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java @@ -142,6 +142,7 @@ public final class IotSceneRuleMatcherHelper { * @param trigger 触发器配置 * @return 是否有效 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { return trigger != null && trigger.getType() != null; } @@ -152,6 +153,7 @@ public final class IotSceneRuleMatcherHelper { * @param trigger 触发器配置 * @return 是否有效 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); } @@ -163,7 +165,9 @@ public final class IotSceneRuleMatcherHelper { * @param trigger 触发器配置 */ public static void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", message.getRequestId(), trigger.getType()); + log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", + message != null ? message.getRequestId() : null, + trigger != null ? trigger.getType() : null); } /** @@ -174,7 +178,10 @@ public final class IotSceneRuleMatcherHelper { * @param reason 失败原因 */ public static void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { - log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", message.getRequestId(), trigger.getType(), reason); + log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", + message != null ? message.getRequestId() : null, + trigger != null ? trigger.getType() : null, + reason); } // ========== 【条件】相关工具方法 ========== @@ -185,6 +192,7 @@ public final class IotSceneRuleMatcherHelper { * @param condition 触发条件 * @return 是否有效 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { return condition != null && condition.getType() != null; } @@ -195,6 +203,7 @@ public final class IotSceneRuleMatcherHelper { * @param condition 触发条件 * @return 是否有效 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); } @@ -206,7 +215,9 @@ public final class IotSceneRuleMatcherHelper { * @param condition 触发条件 */ public static void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", message.getRequestId(), condition.getType()); + log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", + message != null ? message.getRequestId() : null, + condition != null ? condition.getType() : null); } /** @@ -217,7 +228,10 @@ public final class IotSceneRuleMatcherHelper { * @param reason 失败原因 */ public static void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { - log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", message.getRequestId(), condition.getType(), reason); + log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", + message != null ? message.getRequestId() : null, + condition != null ? condition.getType() : null, + reason); } // ========== 【通用】工具方法 ========== @@ -229,6 +243,7 @@ public final class IotSceneRuleMatcherHelper { * @param actualIdentifier 实际的标识符 * @return 是否匹配 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java index c130c55..5741b95 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java @@ -30,10 +30,10 @@ public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionM return false; } - // 1.2 检查标识符是否匹配 - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); - if (!IotSceneRuleMatcherHelper.isIdentifierMatched(condition.getIdentifier(), messageIdentifier)) { - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "标识符不匹配,期望: " + condition.getIdentifier() + ", 实际: " + messageIdentifier); + // 1.2 检查消息中是否包含条件指定的属性标识符 + // 注意:属性上报可能同时上报多个属性,所以需要判断 condition.getIdentifier() 是否在 message 的 params 中 + if (IotDeviceMessageUtils.notContainsIdentifier(message, condition.getIdentifier())) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中不包含属性: " + condition.getIdentifier()); return false; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java index c8c0883..d997e46 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java @@ -1,14 +1,20 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; import org.springframework.stereotype.Component; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + /** * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑 * @@ -45,16 +51,22 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc return false; } - // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 - // 但如果配置了操作符和值,则需要进行条件匹配 + // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配。但如果配置了操作符和值,则需要进行条件匹配 if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { - Object eventParams = message.getParams(); - if (eventParams == null) { - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件参数为空"); + Object eventValue = IotDeviceMessageUtils.extractEventValue(message); + if (eventValue == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件值为空"); return false; } - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(eventParams, trigger.getOperator(), trigger.getValue()); + boolean matched; + if (eventValue instanceof Map || eventValue instanceof Collection) { + // 结构体/数组事件值:把比较值按 JSON 解析后整体相等比较;HashMap.equals 与 key 顺序无关;仅支持 = / != + matched = matchStructuredEventValue(eventValue, trigger); + } else { + // 标量事件值(字符串/数字/布尔):走 SpEL,支持 = != > < 等运算 + matched = IotSceneRuleMatcherHelper.evaluateCondition(eventValue, trigger.getOperator(), trigger.getValue()); + } if (!matched) { IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); return false; @@ -65,6 +77,16 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc return true; } + private boolean matchStructuredEventValue(Object eventValue, IotSceneRuleDO.Trigger trigger) { + // 比较值非合法 JSON 时返回 null,结构体场景下视为不匹配 + Object expected = JsonUtils.parseObjectQuietly(trigger.getValue(), Object.class); + if (expected == null) { + return false; + } + boolean equal = Objects.equals(eventValue, expected); + return IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator().equals(trigger.getOperator()) != equal; + } + @Override public int getPriority() { return 30; // 中等优先级 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java index 0df8003..73e1867 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java @@ -5,11 +5,13 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -18,7 +20,6 @@ import static org.mockito.Mockito.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 class IotTcpDataRuleActionTest { private IotTcpDataRuleAction tcpDataRuleAction; @@ -145,10 +146,12 @@ class IotTcpDataRuleActionTest { @Test public void testMessageSerialization() { // 准备参数 + Map params = new HashMap<>(); + params.put("temperature", 25.5); IotDeviceMessage message = IotDeviceMessage.builder() .deviceId(123L) .method("thing.property.report") - .params("{\"temperature\": 25.5}") + .params(params) .build(); // 调用方法 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java index 1914d78..f6e7c5f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java @@ -9,7 +9,7 @@ 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 org.junit.jupiter.api.Disabled; +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -30,7 +30,6 @@ import static org.mockito.Mockito.*; * * @author 芋道源码 */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { @InjectMocks @@ -51,6 +50,9 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { @Mock private IotDeviceService deviceService; + @Mock + private IotSceneRuleTimerHandler timerHandler; + @Test public void testCreateScene_Rule_success() { // 准备参数 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java index 7f4ec70..bf17f11 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene; import cn.hutool.core.collection.ListUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; @@ -19,6 +20,7 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEva import org.junit.jupiter.api.*; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.springframework.context.support.GenericApplicationContext; import java.lang.reflect.Field; import java.util.*; @@ -41,7 +43,6 @@ import static org.mockito.Mockito.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { @InjectMocks @@ -70,6 +71,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe private static final Long DEVICE_ID = 100L; private static final String PROPERTY_IDENTIFIER = "temperature"; + @BeforeAll + static void initSpringContext() { + // SpringExpressionUtils 依赖 SpringUtil 持有的 ApplicationContext + new SpringUtil().setApplicationContext(new GenericApplicationContext()); + } + @BeforeEach void setUp() { // 创建并注入 timerConditionEvaluator 的依赖 @@ -137,14 +144,15 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe IotDevicePropertyDO property = new IotDevicePropertyDO(); property.setValue(value); properties.put(identifier, property); - when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); + // 多条件组 OR 短路时部分 stub 用不上,使用 lenient 容忍未消费的 stub + lenient().when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); } private void mockDeviceState(Long deviceId, Integer state) { IotDeviceDO device = new IotDeviceDO(); device.setId(deviceId); device.setState(state); - when(deviceService.getDevice(deviceId)).thenReturn(device); + lenient().when(deviceService.getDevice(deviceId)).thenReturn(device); } /** diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java index 25c2ff5..586d948 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java @@ -6,7 +6,6 @@ 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.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; @@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest { private IotCurrentTimeConditionMatcher matcher; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java index a4abe16..5a40995 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java @@ -6,7 +6,6 @@ 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.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -20,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest { private IotDevicePropertyConditionMatcher matcher; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java index b83e0b0..500a739 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; @@ -7,9 +8,11 @@ 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.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +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.*; @@ -19,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest { private IotDeviceStateConditionMatcher matcher; @@ -344,7 +346,10 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT private IotDeviceMessage createDeviceMessage(Integer deviceState) { IotDeviceMessage message = new IotDeviceMessage(); message.setDeviceId(randomLongId()); - message.setParams(deviceState); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + Map params = new HashMap<>(); + params.put("state", deviceState); + message.setParams(params); return message; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java index b29f3c3..bd1cba4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java @@ -1,13 +1,15 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; 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.topic.event.IotDeviceEventPostReqDTO; 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.Disabled; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -23,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest { private IotDeviceEventPostTriggerMatcher matcher; @@ -356,6 +357,184 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche assertFalse(result); } + @Test + public void testMatches_scalarValueEqualsSuccess() { + // 标量事件值 + operator='=' + value 命中:必须匹配;防止"把整个 params Map 当作源值"的回归 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", "normal") + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("normal"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_scalarValueEqualsMismatch() { + // 标量事件值不等于配置值:应不匹配 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", "abnormal") + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("normal"); + + boolean result = matcher.matches(message, trigger); + + assertFalse(result); + } + + @Test + public void testMatches_pojoParamsScalarValueSuccess() { + // 本地总线场景:params 是 IotDeviceEventPostReqDTO POJO 而非 Map,仍应能正确匹配 + String eventIdentifier = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(IotDeviceEventPostReqDTO.of(eventIdentifier, "normal")); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("normal"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_structValueEqualsSuccess() { + // 结构体事件值 + 比较值是 JSON 对象字面量:JSON 反序列化后整体相等 + String eventIdentifier = randomString(); + Map eventValue = MapUtil.builder(new HashMap()) + .put("level", "high") + .put("message", "over temperature") + .build(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", eventValue) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("{\"level\":\"high\",\"message\":\"over temperature\"}"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_structValueEqualsKeyOrderInsensitive() { + // 比较值的 JSON 字段顺序与事件 value 不同,应仍然匹配(HashMap.equals 与顺序无关) + String eventIdentifier = randomString(); + Map eventValue = MapUtil.builder(new HashMap()) + .put("level", "high") + .put("code", 500) + .build(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", eventValue) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("{\"code\":500,\"level\":\"high\"}"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_structValueEqualsMismatch() { + // 结构体值字段不一致:应不匹配 + String eventIdentifier = randomString(); + Map eventValue = MapUtil.builder(new HashMap()) + .put("level", "low") + .build(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", eventValue) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("{\"level\":\"high\"}"); + + boolean result = matcher.matches(message, trigger); + + assertFalse(result); + } + + @Test + public void testMatches_structValueNotEqualsSuccess() { + // 结构体值 + != 操作符:内容不一致时应匹配 + String eventIdentifier = randomString(); + Map eventValue = MapUtil.builder(new HashMap()) + .put("level", "low") + .build(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", eventValue) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator()); + trigger.setValue("{\"level\":\"high\"}"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_arrayValueEqualsSuccess() { + // 数组事件值:JSON 解析后按序相等 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", ListUtil.of("a", "b", "c")) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("[\"a\",\"b\",\"c\"]"); + + boolean result = matcher.matches(message, trigger); + + assertTrue(result); + } + + @Test + public void testMatches_structValueInvalidJsonComparator() { + // 比较值不是合法 JSON:结构体场景下视为不匹配,不抛异常 + String eventIdentifier = randomString(); + Map eventValue = MapUtil.builder(new HashMap()) + .put("level", "high") + .build(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", eventValue) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue("not a json"); + + boolean result = matcher.matches(message, trigger); + + assertFalse(result); + } + // ========== 辅助方法 ========== /** diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java index 825c7bf..fb15576 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java @@ -8,7 +8,6 @@ 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.Disabled; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -25,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest { private IotDevicePropertyPostTriggerMatcher matcher; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java index f2f436e..c617f42 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java @@ -4,10 +4,10 @@ import cn.hutool.core.map.MapUtil; 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.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { private IotDeviceServiceInvokeTriggerMatcher matcher; @@ -487,7 +486,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); trigger.setIdentifier(serviceIdentifier); - trigger.setOperator("=="); // 等于操作符 + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); trigger.setValue("auto"); // 调用 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java index 79511aa..5d6bf88 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java @@ -8,9 +8,11 @@ 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.Disabled; import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; + import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static org.junit.jupiter.api.Assertions.*; @@ -19,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest { private IotDeviceStateUpdateTriggerMatcher matcher; @@ -250,7 +251,9 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc IotDeviceMessage message = new IotDeviceMessage(); message.setDeviceId(randomLongId()); message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); - message.setParams(state); + Map params = new HashMap<>(); + params.put("state", state); + message.setParams(params); return message; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java index 9473cd0..d7585d6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java @@ -5,7 +5,6 @@ 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.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author HUIHUI */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { private IotTimerTriggerMatcher matcher; @@ -249,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { public void testMatches_rangeCronSuccess() { // 准备参数 IotDeviceMessage message = createDeviceMessage(); - IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 * * MON-FRI"); // 工作日9-17点 + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 ? * MON-FRI"); // 工作日 9-17 点 // 调用 boolean result = matcher.matches(message, trigger); diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java index b7d9894..1c9f8d7 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.core.util; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -54,23 +55,41 @@ public class IotDeviceMessageUtils { * @param message 消息 * @return 标识符 */ - @SuppressWarnings("unchecked") public static String getIdentifier(IotDeviceMessage message) { - if (message.getParams() == null) { + if (message == null || message.getParams() == null) { return null; } + Object params = message.getParams(); if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod())) { - Map params = (Map) message.getParams(); - return MapUtil.getStr(params, "identifier"); - } else if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { - Map params = (Map) message.getParams(); - return MapUtil.getStr(params, "state"); + return StrUtil.toStringOrNull(readField(params, "identifier")); + } else if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { + return StrUtil.toStringOrNull(readField(params, "state")); } return null; } /** + * 从 params 中读取字段值,兼容 Map 和 POJO(DTO)两种形态 + * + * Why:MQ 消息经 JSON 反序列化后 params 是 Map,但本地总线场景 producer 可能直接传 DTO 对象(如 IotDeviceEventPostReqDTO), + * matcher 必须同时支持两种形态,避免事件触发器在同 JVM 内部消息总线下匹配失败 + */ + private static Object readField(Object params, String fieldName) { + if (params == null) { + return null; + } + if (params instanceof Map) { + return ((Map) params).get(fieldName); + } + try { + return ReflectUtil.getFieldValue(params, fieldName); + } catch (Exception ignored) { + return null; + } + } + + /** * 判断消息中是否包含指定的标识符 *

* 对于不同消息类型的处理: @@ -82,8 +101,9 @@ public class IotDeviceMessageUtils { * @param identifier 要检查的标识符 * @return 是否包含 */ + @SuppressWarnings("unchecked") public static boolean containsIdentifier(IotDeviceMessage message, String identifier) { - if (message.getParams() == null || StrUtil.isBlank(identifier)) { + if (message == null || message.getParams() == null || StrUtil.isBlank(identifier)) { return false; } // EVENT_POST / SERVICE_INVOKE / STATE_UPDATE:使用原有逻辑 @@ -91,10 +111,17 @@ public class IotDeviceMessageUtils { if (messageIdentifier != null) { return identifier.equals(messageIdentifier); } - // PROPERTY_POST:检查 params 中是否包含该属性 key + // PROPERTY_POST:检查 params 中是否包含该属性 key(支持扁平和嵌套 properties 结构) if (StrUtil.equals(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())) { Map params = parseParamsToMap(message.getParams()); - return params != null && params.containsKey(identifier); + if (params == null) { + return false; + } + if (params.containsKey(identifier)) { + return true; + } + Object properties = params.get("properties"); + return properties instanceof Map && ((Map) properties).containsKey(identifier); } return false; } @@ -132,9 +159,6 @@ public class IotDeviceMessageUtils { /** * 从设备消息中提取指定标识符的属性值 - * - 支持多种消息格式和属性值提取策略 - * - 兼容现有的消息结构 - * - 提供统一的属性值提取接口 *

* 支持的提取策略(按优先级顺序): * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息) @@ -150,7 +174,7 @@ public class IotDeviceMessageUtils { */ @SuppressWarnings("unchecked") public static Object extractPropertyValue(IotDeviceMessage message, String identifier) { - Object params = message.getParams(); + Object params = message != null ? message.getParams() : null; if (params == null) { return null; } @@ -207,6 +231,19 @@ public class IotDeviceMessageUtils { } /** + * 从设备事件上报消息中提取事件值 + *

+ * 事件上报的 params 结构为:{"identifier": "xxx", "value": ...},事件值即 value 字段。 + * value 可能是标量(字符串/数字/布尔),也可能是结构体(如告警事件 {level, message}) + * + * @param message 设备消息 + * @return 事件值,如果未找到则返回 null + */ + public static Object extractEventValue(IotDeviceMessage message) { + return readField(message != null ? message.getParams() : null, "value"); + } + + /** * 从服务调用消息中提取输入参数 *

* 服务调用消息的 params 结构通常为: @@ -220,23 +257,16 @@ public class IotDeviceMessageUtils { */ @SuppressWarnings("unchecked") public static Map extractServiceInputParams(IotDeviceMessage message) { - // 1. 参数校验 - Object params = message.getParams(); - if (params == null) { - return null; - } - if (!(params instanceof Map)) { + if (message == null || message.getParams() == null) { return null; } - Map paramsMap = (Map) params; - - // 尝试从 inputData 字段获取 - Object inputData = paramsMap.get("inputData"); + Object params = message.getParams(); + // 兼容 Map 和 POJO(如 IotDeviceServiceInvokeReqDTO)两种 params 形态 + Object inputData = readField(params, "inputData"); if (inputData instanceof Map) { return (Map) inputData; } - // 尝试从 inputParams 字段获取 - Object inputParams = paramsMap.get("inputParams"); + Object inputParams = readField(params, "inputParams"); if (inputParams instanceof Map) { return (Map) inputParams; } diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java index b0d39be..4f7d614 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java +++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.core.util; 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.topic.event.IotDeviceEventPostReqDTO; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -71,7 +72,7 @@ public class IotDeviceMessageUtilsTest { @Test public void testExtractPropertyValue_valueField() { - // 测试 value 字段 + // 测试 value 字段(策略 5) IotDeviceMessage message = new IotDeviceMessage(); Map params = new HashMap<>(); params.put("identifier", "temperature"); @@ -84,7 +85,7 @@ public class IotDeviceMessageUtilsTest { @Test public void testExtractPropertyValue_singleValueMap() { - // 测试单值 Map(包含 identifier 和一个值) + // 测试单值 Map(策略 6:包含 identifier 和一个其他字段) IotDeviceMessage message = new IotDeviceMessage(); Map params = new HashMap<>(); params.put("identifier", "temperature"); @@ -139,6 +140,88 @@ public class IotDeviceMessageUtilsTest { assertEquals(25.5, result); // 应该返回直接标识符的值 } + // ========== extractEventValue 测试 ========== + + @Test + public void testExtractEventValue_scalar() { + // 标量事件值:{identifier: "gzzt", value: "normal"} + IotDeviceMessage message = new IotDeviceMessage(); + Map params = new HashMap<>(); + params.put("identifier", "gzzt"); + params.put("value", "normal"); + message.setParams(params); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertEquals("normal", result); + } + + @Test + public void testExtractEventValue_struct() { + // 结构体事件值:{identifier: "alarm", value: {level: "high", message: "..."}} + IotDeviceMessage message = new IotDeviceMessage(); + Map eventValue = new HashMap<>(); + eventValue.put("level", "high"); + eventValue.put("message", "over temperature"); + + Map params = new HashMap<>(); + params.put("identifier", "alarm"); + params.put("value", eventValue); + message.setParams(params); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertEquals(eventValue, result); + } + + @Test + public void testExtractEventValue_nullParams() { + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(null); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertNull(result); + } + + @Test + public void testExtractEventValue_paramsWithoutValueField() { + // params 是字符串等非结构化对象,无 value 字段,应返回 null + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams("not a map"); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertNull(result); + } + + @Test + public void testExtractEventValue_missingValueField() { + IotDeviceMessage message = new IotDeviceMessage(); + Map params = new HashMap<>(); + params.put("identifier", "gzzt"); + message.setParams(params); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertNull(result); + } + + @Test + public void testExtractEventValue_pojoParams() { + // 本地总线场景:params 是 IotDeviceEventPostReqDTO POJO(未经 JSON 反序列化),应能反射取到 value + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(IotDeviceEventPostReqDTO.of("gzzt", "normal")); + + Object result = IotDeviceMessageUtils.extractEventValue(message); + assertEquals("normal", result); + } + + @Test + public void testGetIdentifier_eventPostPojoParams() { + // 本地总线场景:EVENT_POST 消息 params 是 DTO POJO,仍应能解析出 identifier + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(IotDeviceEventPostReqDTO.of("gzzt", "normal")); + + assertEquals("gzzt", IotDeviceMessageUtils.getIdentifier(message)); + } + // ========== notContainsIdentifier 测试 ========== /**