Commit 87c8d7e242cb579b7d673c4f711d299a70ba9934
1 parent
99c107f7
【修复】IoT 场景联动:事件触发器配置比较值后永远不触发,并支持结构体事件值 JSON 整体相等
Showing
17 changed files
with
429 additions
and
70 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
| @@ -173,6 +173,24 @@ public class JsonUtils { | @@ -173,6 +173,24 @@ public class JsonUtils { | ||
| 173 | } | 173 | } |
| 174 | } | 174 | } |
| 175 | 175 | ||
| 176 | + /** | ||
| 177 | + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null | ||
| 178 | + * | ||
| 179 | + * @param text 字符串 | ||
| 180 | + * @param clazz 类型 | ||
| 181 | + * @return 指定类型的对象 | ||
| 182 | + */ | ||
| 183 | + public static <T> T parseObjectQuietly(String text, Class<T> clazz) { | ||
| 184 | + if (StrUtil.isEmpty(text)) { | ||
| 185 | + return null; | ||
| 186 | + } | ||
| 187 | + try { | ||
| 188 | + return objectMapper.readValue(text, clazz); | ||
| 189 | + } catch (IOException e) { | ||
| 190 | + return null; | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + | ||
| 176 | public static <T> List<T> parseArray(String text, Class<T> clazz) { | 194 | public static <T> List<T> parseArray(String text, Class<T> clazz) { |
| 177 | if (StrUtil.isEmpty(text)) { | 195 | if (StrUtil.isEmpty(text)) { |
| 178 | return new ArrayList<>(); | 196 | return new ArrayList<>(); |
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 { | @@ -142,6 +142,7 @@ public final class IotSceneRuleMatcherHelper { | ||
| 142 | * @param trigger 触发器配置 | 142 | * @param trigger 触发器配置 |
| 143 | * @return 是否有效 | 143 | * @return 是否有效 |
| 144 | */ | 144 | */ |
| 145 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 145 | public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { | 146 | public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { |
| 146 | return trigger != null && trigger.getType() != null; | 147 | return trigger != null && trigger.getType() != null; |
| 147 | } | 148 | } |
| @@ -152,6 +153,7 @@ public final class IotSceneRuleMatcherHelper { | @@ -152,6 +153,7 @@ public final class IotSceneRuleMatcherHelper { | ||
| 152 | * @param trigger 触发器配置 | 153 | * @param trigger 触发器配置 |
| 153 | * @return 是否有效 | 154 | * @return 是否有效 |
| 154 | */ | 155 | */ |
| 156 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 155 | public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { | 157 | public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { |
| 156 | return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); | 158 | return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); |
| 157 | } | 159 | } |
| @@ -163,7 +165,9 @@ public final class IotSceneRuleMatcherHelper { | @@ -163,7 +165,9 @@ public final class IotSceneRuleMatcherHelper { | ||
| 163 | * @param trigger 触发器配置 | 165 | * @param trigger 触发器配置 |
| 164 | */ | 166 | */ |
| 165 | public static void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { | 167 | public static void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { |
| 166 | - log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", message.getRequestId(), trigger.getType()); | 168 | + log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", |
| 169 | + message != null ? message.getRequestId() : null, | ||
| 170 | + trigger != null ? trigger.getType() : null); | ||
| 167 | } | 171 | } |
| 168 | 172 | ||
| 169 | /** | 173 | /** |
| @@ -174,7 +178,10 @@ public final class IotSceneRuleMatcherHelper { | @@ -174,7 +178,10 @@ public final class IotSceneRuleMatcherHelper { | ||
| 174 | * @param reason 失败原因 | 178 | * @param reason 失败原因 |
| 175 | */ | 179 | */ |
| 176 | public static void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { | 180 | public static void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { |
| 177 | - log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", message.getRequestId(), trigger.getType(), reason); | 181 | + log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", |
| 182 | + message != null ? message.getRequestId() : null, | ||
| 183 | + trigger != null ? trigger.getType() : null, | ||
| 184 | + reason); | ||
| 178 | } | 185 | } |
| 179 | 186 | ||
| 180 | // ========== 【条件】相关工具方法 ========== | 187 | // ========== 【条件】相关工具方法 ========== |
| @@ -185,6 +192,7 @@ public final class IotSceneRuleMatcherHelper { | @@ -185,6 +192,7 @@ public final class IotSceneRuleMatcherHelper { | ||
| 185 | * @param condition 触发条件 | 192 | * @param condition 触发条件 |
| 186 | * @return 是否有效 | 193 | * @return 是否有效 |
| 187 | */ | 194 | */ |
| 195 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 188 | public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { | 196 | public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { |
| 189 | return condition != null && condition.getType() != null; | 197 | return condition != null && condition.getType() != null; |
| 190 | } | 198 | } |
| @@ -195,6 +203,7 @@ public final class IotSceneRuleMatcherHelper { | @@ -195,6 +203,7 @@ public final class IotSceneRuleMatcherHelper { | ||
| 195 | * @param condition 触发条件 | 203 | * @param condition 触发条件 |
| 196 | * @return 是否有效 | 204 | * @return 是否有效 |
| 197 | */ | 205 | */ |
| 206 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 198 | public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { | 207 | public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { |
| 199 | return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); | 208 | return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); |
| 200 | } | 209 | } |
| @@ -206,7 +215,9 @@ public final class IotSceneRuleMatcherHelper { | @@ -206,7 +215,9 @@ public final class IotSceneRuleMatcherHelper { | ||
| 206 | * @param condition 触发条件 | 215 | * @param condition 触发条件 |
| 207 | */ | 216 | */ |
| 208 | public static void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { | 217 | public static void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { |
| 209 | - log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", message.getRequestId(), condition.getType()); | 218 | + log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", |
| 219 | + message != null ? message.getRequestId() : null, | ||
| 220 | + condition != null ? condition.getType() : null); | ||
| 210 | } | 221 | } |
| 211 | 222 | ||
| 212 | /** | 223 | /** |
| @@ -217,7 +228,10 @@ public final class IotSceneRuleMatcherHelper { | @@ -217,7 +228,10 @@ public final class IotSceneRuleMatcherHelper { | ||
| 217 | * @param reason 失败原因 | 228 | * @param reason 失败原因 |
| 218 | */ | 229 | */ |
| 219 | public static void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { | 230 | public static void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { |
| 220 | - log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", message.getRequestId(), condition.getType(), reason); | 231 | + log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", |
| 232 | + message != null ? message.getRequestId() : null, | ||
| 233 | + condition != null ? condition.getType() : null, | ||
| 234 | + reason); | ||
| 221 | } | 235 | } |
| 222 | 236 | ||
| 223 | // ========== 【通用】工具方法 ========== | 237 | // ========== 【通用】工具方法 ========== |
| @@ -229,6 +243,7 @@ public final class IotSceneRuleMatcherHelper { | @@ -229,6 +243,7 @@ public final class IotSceneRuleMatcherHelper { | ||
| 229 | * @param actualIdentifier 实际的标识符 | 243 | * @param actualIdentifier 实际的标识符 |
| 230 | * @return 是否匹配 | 244 | * @return 是否匹配 |
| 231 | */ | 245 | */ |
| 246 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 232 | public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { | 247 | public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { |
| 233 | return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); | 248 | return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); |
| 234 | } | 249 | } |
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 | @@ -30,10 +30,10 @@ public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionM | ||
| 30 | return false; | 30 | return false; |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | - // 1.2 检查标识符是否匹配 | ||
| 34 | - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); | ||
| 35 | - if (!IotSceneRuleMatcherHelper.isIdentifierMatched(condition.getIdentifier(), messageIdentifier)) { | ||
| 36 | - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "标识符不匹配,期望: " + condition.getIdentifier() + ", 实际: " + messageIdentifier); | 33 | + // 1.2 检查消息中是否包含条件指定的属性标识符 |
| 34 | + // 注意:属性上报可能同时上报多个属性,所以需要判断 condition.getIdentifier() 是否在 message 的 params 中 | ||
| 35 | + if (IotDeviceMessageUtils.notContainsIdentifier(message, condition.getIdentifier())) { | ||
| 36 | + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中不包含属性: " + condition.getIdentifier()); | ||
| 37 | return false; | 37 | return false; |
| 38 | } | 38 | } |
| 39 | 39 |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
| 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 9 | +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 10 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; | 11 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; |
| 10 | import org.springframework.stereotype.Component; | 12 | import org.springframework.stereotype.Component; |
| 11 | 13 | ||
| 14 | +import java.util.Collection; | ||
| 15 | +import java.util.Map; | ||
| 16 | +import java.util.Objects; | ||
| 17 | + | ||
| 12 | /** | 18 | /** |
| 13 | * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑 | 19 | * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑 |
| 14 | * | 20 | * |
| @@ -45,16 +51,22 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc | @@ -45,16 +51,22 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc | ||
| 45 | return false; | 51 | return false; |
| 46 | } | 52 | } |
| 47 | 53 | ||
| 48 | - // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 | ||
| 49 | - // 但如果配置了操作符和值,则需要进行条件匹配 | 54 | + // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配。但如果配置了操作符和值,则需要进行条件匹配 |
| 50 | if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { | 55 | if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { |
| 51 | - Object eventParams = message.getParams(); | ||
| 52 | - if (eventParams == null) { | ||
| 53 | - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件参数为空"); | 56 | + Object eventValue = IotDeviceMessageUtils.extractEventValue(message); |
| 57 | + if (eventValue == null) { | ||
| 58 | + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件值为空"); | ||
| 54 | return false; | 59 | return false; |
| 55 | } | 60 | } |
| 56 | 61 | ||
| 57 | - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(eventParams, trigger.getOperator(), trigger.getValue()); | 62 | + boolean matched; |
| 63 | + if (eventValue instanceof Map || eventValue instanceof Collection) { | ||
| 64 | + // 结构体/数组事件值:把比较值按 JSON 解析后整体相等比较;HashMap.equals 与 key 顺序无关;仅支持 = / != | ||
| 65 | + matched = matchStructuredEventValue(eventValue, trigger); | ||
| 66 | + } else { | ||
| 67 | + // 标量事件值(字符串/数字/布尔):走 SpEL,支持 = != > < 等运算 | ||
| 68 | + matched = IotSceneRuleMatcherHelper.evaluateCondition(eventValue, trigger.getOperator(), trigger.getValue()); | ||
| 69 | + } | ||
| 58 | if (!matched) { | 70 | if (!matched) { |
| 59 | IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); | 71 | IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); |
| 60 | return false; | 72 | return false; |
| @@ -65,6 +77,16 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc | @@ -65,6 +77,16 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc | ||
| 65 | return true; | 77 | return true; |
| 66 | } | 78 | } |
| 67 | 79 | ||
| 80 | + private boolean matchStructuredEventValue(Object eventValue, IotSceneRuleDO.Trigger trigger) { | ||
| 81 | + // 比较值非合法 JSON 时返回 null,结构体场景下视为不匹配 | ||
| 82 | + Object expected = JsonUtils.parseObjectQuietly(trigger.getValue(), Object.class); | ||
| 83 | + if (expected == null) { | ||
| 84 | + return false; | ||
| 85 | + } | ||
| 86 | + boolean equal = Objects.equals(eventValue, expected); | ||
| 87 | + return IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator().equals(trigger.getOperator()) != equal; | ||
| 88 | + } | ||
| 89 | + | ||
| 68 | @Override | 90 | @Override |
| 69 | public int getPriority() { | 91 | public int getPriority() { |
| 70 | return 30; // 中等优先级 | 92 | return 30; // 中等优先级 |
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; | @@ -5,11 +5,13 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; |
| 6 | import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; | 6 | import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; |
| 7 | import org.junit.jupiter.api.BeforeEach; | 7 | import org.junit.jupiter.api.BeforeEach; |
| 8 | -import org.junit.jupiter.api.Disabled; | ||
| 9 | import org.junit.jupiter.api.Test; | 8 | import org.junit.jupiter.api.Test; |
| 10 | import org.mockito.Mock; | 9 | import org.mockito.Mock; |
| 11 | import org.mockito.MockitoAnnotations; | 10 | import org.mockito.MockitoAnnotations; |
| 12 | 11 | ||
| 12 | +import java.util.HashMap; | ||
| 13 | +import java.util.Map; | ||
| 14 | + | ||
| 13 | import static org.junit.jupiter.api.Assertions.*; | 15 | import static org.junit.jupiter.api.Assertions.*; |
| 14 | import static org.mockito.Mockito.*; | 16 | import static org.mockito.Mockito.*; |
| 15 | 17 | ||
| @@ -18,7 +20,6 @@ import static org.mockito.Mockito.*; | @@ -18,7 +20,6 @@ import static org.mockito.Mockito.*; | ||
| 18 | * | 20 | * |
| 19 | * @author HUIHUI | 21 | * @author HUIHUI |
| 20 | */ | 22 | */ |
| 21 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 22 | class IotTcpDataRuleActionTest { | 23 | class IotTcpDataRuleActionTest { |
| 23 | 24 | ||
| 24 | private IotTcpDataRuleAction tcpDataRuleAction; | 25 | private IotTcpDataRuleAction tcpDataRuleAction; |
| @@ -145,10 +146,12 @@ class IotTcpDataRuleActionTest { | @@ -145,10 +146,12 @@ class IotTcpDataRuleActionTest { | ||
| 145 | @Test | 146 | @Test |
| 146 | public void testMessageSerialization() { | 147 | public void testMessageSerialization() { |
| 147 | // 准备参数 | 148 | // 准备参数 |
| 149 | + Map<String, Object> params = new HashMap<>(); | ||
| 150 | + params.put("temperature", 25.5); | ||
| 148 | IotDeviceMessage message = IotDeviceMessage.builder() | 151 | IotDeviceMessage message = IotDeviceMessage.builder() |
| 149 | .deviceId(123L) | 152 | .deviceId(123L) |
| 150 | .method("thing.property.report") | 153 | .method("thing.property.report") |
| 151 | - .params("{\"temperature\": 25.5}") | 154 | + .params(params) |
| 152 | .build(); | 155 | .build(); |
| 153 | 156 | ||
| 154 | // 调用方法 | 157 | // 调用方法 |
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; | @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; | ||
| 9 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 9 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 10 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 10 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 11 | import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; | 11 | import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; |
| 12 | -import org.junit.jupiter.api.Disabled; | 12 | +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; |
| 13 | import org.junit.jupiter.api.Test; | 13 | import org.junit.jupiter.api.Test; |
| 14 | import org.mockito.InjectMocks; | 14 | import org.mockito.InjectMocks; |
| 15 | import org.mockito.Mock; | 15 | import org.mockito.Mock; |
| @@ -30,7 +30,6 @@ import static org.mockito.Mockito.*; | @@ -30,7 +30,6 @@ import static org.mockito.Mockito.*; | ||
| 30 | * | 30 | * |
| 31 | * @author 芋道源码 | 31 | * @author 芋道源码 |
| 32 | */ | 32 | */ |
| 33 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 34 | public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { | 33 | public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { |
| 35 | 34 | ||
| 36 | @InjectMocks | 35 | @InjectMocks |
| @@ -51,6 +50,9 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { | @@ -51,6 +50,9 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { | ||
| 51 | @Mock | 50 | @Mock |
| 52 | private IotDeviceService deviceService; | 51 | private IotDeviceService deviceService; |
| 53 | 52 | ||
| 53 | + @Mock | ||
| 54 | + private IotSceneRuleTimerHandler timerHandler; | ||
| 55 | + | ||
| 54 | @Test | 56 | @Test |
| 55 | public void testCreateScene_Rule_success() { | 57 | public void testCreateScene_Rule_success() { |
| 56 | // 准备参数 | 58 | // 准备参数 |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java
| 1 | package cn.iocoder.yudao.module.iot.service.rule.scene; | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.ListUtil; | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 5 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | 7 | 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 | @@ -19,6 +20,7 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEva | ||
| 19 | import org.junit.jupiter.api.*; | 20 | import org.junit.jupiter.api.*; |
| 20 | import org.mockito.InjectMocks; | 21 | import org.mockito.InjectMocks; |
| 21 | import org.mockito.Mock; | 22 | import org.mockito.Mock; |
| 23 | +import org.springframework.context.support.GenericApplicationContext; | ||
| 22 | 24 | ||
| 23 | import java.lang.reflect.Field; | 25 | import java.lang.reflect.Field; |
| 24 | import java.util.*; | 26 | import java.util.*; |
| @@ -41,7 +43,6 @@ import static org.mockito.Mockito.*; | @@ -41,7 +43,6 @@ import static org.mockito.Mockito.*; | ||
| 41 | * | 43 | * |
| 42 | * @author HUIHUI | 44 | * @author HUIHUI |
| 43 | */ | 45 | */ |
| 44 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 45 | public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { | 46 | public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { |
| 46 | 47 | ||
| 47 | @InjectMocks | 48 | @InjectMocks |
| @@ -70,6 +71,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe | @@ -70,6 +71,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe | ||
| 70 | private static final Long DEVICE_ID = 100L; | 71 | private static final Long DEVICE_ID = 100L; |
| 71 | private static final String PROPERTY_IDENTIFIER = "temperature"; | 72 | private static final String PROPERTY_IDENTIFIER = "temperature"; |
| 72 | 73 | ||
| 74 | + @BeforeAll | ||
| 75 | + static void initSpringContext() { | ||
| 76 | + // SpringExpressionUtils 依赖 SpringUtil 持有的 ApplicationContext | ||
| 77 | + new SpringUtil().setApplicationContext(new GenericApplicationContext()); | ||
| 78 | + } | ||
| 79 | + | ||
| 73 | @BeforeEach | 80 | @BeforeEach |
| 74 | void setUp() { | 81 | void setUp() { |
| 75 | // 创建并注入 timerConditionEvaluator 的依赖 | 82 | // 创建并注入 timerConditionEvaluator 的依赖 |
| @@ -137,14 +144,15 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe | @@ -137,14 +144,15 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe | ||
| 137 | IotDevicePropertyDO property = new IotDevicePropertyDO(); | 144 | IotDevicePropertyDO property = new IotDevicePropertyDO(); |
| 138 | property.setValue(value); | 145 | property.setValue(value); |
| 139 | properties.put(identifier, property); | 146 | properties.put(identifier, property); |
| 140 | - when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); | 147 | + // 多条件组 OR 短路时部分 stub 用不上,使用 lenient 容忍未消费的 stub |
| 148 | + lenient().when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); | ||
| 141 | } | 149 | } |
| 142 | 150 | ||
| 143 | private void mockDeviceState(Long deviceId, Integer state) { | 151 | private void mockDeviceState(Long deviceId, Integer state) { |
| 144 | IotDeviceDO device = new IotDeviceDO(); | 152 | IotDeviceDO device = new IotDeviceDO(); |
| 145 | device.setId(deviceId); | 153 | device.setId(deviceId); |
| 146 | device.setState(state); | 154 | device.setState(state); |
| 147 | - when(deviceService.getDevice(deviceId)).thenReturn(device); | 155 | + lenient().when(deviceService.getDevice(deviceId)).thenReturn(device); |
| 148 | } | 156 | } |
| 149 | 157 | ||
| 150 | /** | 158 | /** |
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; | @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; |
| 7 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 7 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 8 | import org.junit.jupiter.api.BeforeEach; | 8 | import org.junit.jupiter.api.BeforeEach; |
| 9 | -import org.junit.jupiter.api.Disabled; | ||
| 10 | import org.junit.jupiter.api.Test; | 9 | import org.junit.jupiter.api.Test; |
| 11 | 10 | ||
| 12 | import java.time.LocalDateTime; | 11 | import java.time.LocalDateTime; |
| @@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 21 | * | 20 | * |
| 22 | * @author HUIHUI | 21 | * @author HUIHUI |
| 23 | */ | 22 | */ |
| 24 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 25 | public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest { | 23 | public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest { |
| 26 | 24 | ||
| 27 | private IotCurrentTimeConditionMatcher matcher; | 25 | private IotCurrentTimeConditionMatcher matcher; |
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; | @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; |
| 7 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 7 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 8 | import org.junit.jupiter.api.BeforeEach; | 8 | import org.junit.jupiter.api.BeforeEach; |
| 9 | -import org.junit.jupiter.api.Disabled; | ||
| 10 | import org.junit.jupiter.api.Test; | 9 | import org.junit.jupiter.api.Test; |
| 11 | 10 | ||
| 12 | import java.util.HashMap; | 11 | import java.util.HashMap; |
| @@ -20,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -20,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 20 | * | 19 | * |
| 21 | * @author HUIHUI | 20 | * @author HUIHUI |
| 22 | */ | 21 | */ |
| 23 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 24 | public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest { | 22 | public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest { |
| 25 | 23 | ||
| 26 | private IotDevicePropertyConditionMatcher matcher; | 24 | private IotDevicePropertyConditionMatcher matcher; |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
| 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| @@ -7,9 +8,11 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | @@ -7,9 +8,11 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; |
| 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 9 | import org.junit.jupiter.api.BeforeEach; | 10 | import org.junit.jupiter.api.BeforeEach; |
| 10 | -import org.junit.jupiter.api.Disabled; | ||
| 11 | import org.junit.jupiter.api.Test; | 11 | import org.junit.jupiter.api.Test; |
| 12 | 12 | ||
| 13 | +import java.util.HashMap; | ||
| 14 | +import java.util.Map; | ||
| 15 | + | ||
| 13 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | 16 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 14 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; | 17 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; |
| 15 | import static org.junit.jupiter.api.Assertions.*; | 18 | import static org.junit.jupiter.api.Assertions.*; |
| @@ -19,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -19,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 19 | * | 22 | * |
| 20 | * @author HUIHUI | 23 | * @author HUIHUI |
| 21 | */ | 24 | */ |
| 22 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 23 | public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest { | 25 | public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest { |
| 24 | 26 | ||
| 25 | private IotDeviceStateConditionMatcher matcher; | 27 | private IotDeviceStateConditionMatcher matcher; |
| @@ -344,7 +346,10 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT | @@ -344,7 +346,10 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT | ||
| 344 | private IotDeviceMessage createDeviceMessage(Integer deviceState) { | 346 | private IotDeviceMessage createDeviceMessage(Integer deviceState) { |
| 345 | IotDeviceMessage message = new IotDeviceMessage(); | 347 | IotDeviceMessage message = new IotDeviceMessage(); |
| 346 | message.setDeviceId(randomLongId()); | 348 | message.setDeviceId(randomLongId()); |
| 347 | - message.setParams(deviceState); | 349 | + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); |
| 350 | + Map<String, Object> params = new HashMap<>(); | ||
| 351 | + params.put("state", deviceState); | ||
| 352 | + message.setParams(params); | ||
| 348 | return message; | 353 | return message; |
| 349 | } | 354 | } |
| 350 | 355 |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
| 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.ListUtil; | ||
| 3 | import cn.hutool.core.map.MapUtil; | 4 | import cn.hutool.core.map.MapUtil; |
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | ||
| 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 9 | +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 10 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 11 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 9 | import org.junit.jupiter.api.BeforeEach; | 12 | import org.junit.jupiter.api.BeforeEach; |
| 10 | -import org.junit.jupiter.api.Disabled; | ||
| 11 | import org.junit.jupiter.api.Test; | 13 | import org.junit.jupiter.api.Test; |
| 12 | 14 | ||
| 13 | import java.util.HashMap; | 15 | import java.util.HashMap; |
| @@ -23,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -23,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 23 | * | 25 | * |
| 24 | * @author HUIHUI | 26 | * @author HUIHUI |
| 25 | */ | 27 | */ |
| 26 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 27 | public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest { | 28 | public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest { |
| 28 | 29 | ||
| 29 | private IotDeviceEventPostTriggerMatcher matcher; | 30 | private IotDeviceEventPostTriggerMatcher matcher; |
| @@ -356,6 +357,184 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche | @@ -356,6 +357,184 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche | ||
| 356 | assertFalse(result); | 357 | assertFalse(result); |
| 357 | } | 358 | } |
| 358 | 359 | ||
| 360 | + @Test | ||
| 361 | + public void testMatches_scalarValueEqualsSuccess() { | ||
| 362 | + // 标量事件值 + operator='=' + value 命中:必须匹配;防止"把整个 params Map 当作源值"的回归 | ||
| 363 | + String eventIdentifier = randomString(); | ||
| 364 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 365 | + .put("identifier", eventIdentifier) | ||
| 366 | + .put("value", "normal") | ||
| 367 | + .build(); | ||
| 368 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 369 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 370 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 371 | + trigger.setValue("normal"); | ||
| 372 | + | ||
| 373 | + boolean result = matcher.matches(message, trigger); | ||
| 374 | + | ||
| 375 | + assertTrue(result); | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + @Test | ||
| 379 | + public void testMatches_scalarValueEqualsMismatch() { | ||
| 380 | + // 标量事件值不等于配置值:应不匹配 | ||
| 381 | + String eventIdentifier = randomString(); | ||
| 382 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 383 | + .put("identifier", eventIdentifier) | ||
| 384 | + .put("value", "abnormal") | ||
| 385 | + .build(); | ||
| 386 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 387 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 388 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 389 | + trigger.setValue("normal"); | ||
| 390 | + | ||
| 391 | + boolean result = matcher.matches(message, trigger); | ||
| 392 | + | ||
| 393 | + assertFalse(result); | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + @Test | ||
| 397 | + public void testMatches_pojoParamsScalarValueSuccess() { | ||
| 398 | + // 本地总线场景:params 是 IotDeviceEventPostReqDTO POJO 而非 Map,仍应能正确匹配 | ||
| 399 | + String eventIdentifier = randomString(); | ||
| 400 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 401 | + message.setDeviceId(randomLongId()); | ||
| 402 | + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); | ||
| 403 | + message.setParams(IotDeviceEventPostReqDTO.of(eventIdentifier, "normal")); | ||
| 404 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 405 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 406 | + trigger.setValue("normal"); | ||
| 407 | + | ||
| 408 | + boolean result = matcher.matches(message, trigger); | ||
| 409 | + | ||
| 410 | + assertTrue(result); | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + @Test | ||
| 414 | + public void testMatches_structValueEqualsSuccess() { | ||
| 415 | + // 结构体事件值 + 比较值是 JSON 对象字面量:JSON 反序列化后整体相等 | ||
| 416 | + String eventIdentifier = randomString(); | ||
| 417 | + Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>()) | ||
| 418 | + .put("level", "high") | ||
| 419 | + .put("message", "over temperature") | ||
| 420 | + .build(); | ||
| 421 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 422 | + .put("identifier", eventIdentifier) | ||
| 423 | + .put("value", eventValue) | ||
| 424 | + .build(); | ||
| 425 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 426 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 427 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 428 | + trigger.setValue("{\"level\":\"high\",\"message\":\"over temperature\"}"); | ||
| 429 | + | ||
| 430 | + boolean result = matcher.matches(message, trigger); | ||
| 431 | + | ||
| 432 | + assertTrue(result); | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + @Test | ||
| 436 | + public void testMatches_structValueEqualsKeyOrderInsensitive() { | ||
| 437 | + // 比较值的 JSON 字段顺序与事件 value 不同,应仍然匹配(HashMap.equals 与顺序无关) | ||
| 438 | + String eventIdentifier = randomString(); | ||
| 439 | + Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>()) | ||
| 440 | + .put("level", "high") | ||
| 441 | + .put("code", 500) | ||
| 442 | + .build(); | ||
| 443 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 444 | + .put("identifier", eventIdentifier) | ||
| 445 | + .put("value", eventValue) | ||
| 446 | + .build(); | ||
| 447 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 448 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 449 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 450 | + trigger.setValue("{\"code\":500,\"level\":\"high\"}"); | ||
| 451 | + | ||
| 452 | + boolean result = matcher.matches(message, trigger); | ||
| 453 | + | ||
| 454 | + assertTrue(result); | ||
| 455 | + } | ||
| 456 | + | ||
| 457 | + @Test | ||
| 458 | + public void testMatches_structValueEqualsMismatch() { | ||
| 459 | + // 结构体值字段不一致:应不匹配 | ||
| 460 | + String eventIdentifier = randomString(); | ||
| 461 | + Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>()) | ||
| 462 | + .put("level", "low") | ||
| 463 | + .build(); | ||
| 464 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 465 | + .put("identifier", eventIdentifier) | ||
| 466 | + .put("value", eventValue) | ||
| 467 | + .build(); | ||
| 468 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 469 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 470 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 471 | + trigger.setValue("{\"level\":\"high\"}"); | ||
| 472 | + | ||
| 473 | + boolean result = matcher.matches(message, trigger); | ||
| 474 | + | ||
| 475 | + assertFalse(result); | ||
| 476 | + } | ||
| 477 | + | ||
| 478 | + @Test | ||
| 479 | + public void testMatches_structValueNotEqualsSuccess() { | ||
| 480 | + // 结构体值 + != 操作符:内容不一致时应匹配 | ||
| 481 | + String eventIdentifier = randomString(); | ||
| 482 | + Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>()) | ||
| 483 | + .put("level", "low") | ||
| 484 | + .build(); | ||
| 485 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 486 | + .put("identifier", eventIdentifier) | ||
| 487 | + .put("value", eventValue) | ||
| 488 | + .build(); | ||
| 489 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 490 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 491 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator()); | ||
| 492 | + trigger.setValue("{\"level\":\"high\"}"); | ||
| 493 | + | ||
| 494 | + boolean result = matcher.matches(message, trigger); | ||
| 495 | + | ||
| 496 | + assertTrue(result); | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + @Test | ||
| 500 | + public void testMatches_arrayValueEqualsSuccess() { | ||
| 501 | + // 数组事件值:JSON 解析后按序相等 | ||
| 502 | + String eventIdentifier = randomString(); | ||
| 503 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 504 | + .put("identifier", eventIdentifier) | ||
| 505 | + .put("value", ListUtil.of("a", "b", "c")) | ||
| 506 | + .build(); | ||
| 507 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 508 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 509 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 510 | + trigger.setValue("[\"a\",\"b\",\"c\"]"); | ||
| 511 | + | ||
| 512 | + boolean result = matcher.matches(message, trigger); | ||
| 513 | + | ||
| 514 | + assertTrue(result); | ||
| 515 | + } | ||
| 516 | + | ||
| 517 | + @Test | ||
| 518 | + public void testMatches_structValueInvalidJsonComparator() { | ||
| 519 | + // 比较值不是合法 JSON:结构体场景下视为不匹配,不抛异常 | ||
| 520 | + String eventIdentifier = randomString(); | ||
| 521 | + Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>()) | ||
| 522 | + .put("level", "high") | ||
| 523 | + .build(); | ||
| 524 | + Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>()) | ||
| 525 | + .put("identifier", eventIdentifier) | ||
| 526 | + .put("value", eventValue) | ||
| 527 | + .build(); | ||
| 528 | + IotDeviceMessage message = createEventPostMessage(eventParams); | ||
| 529 | + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); | ||
| 530 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); | ||
| 531 | + trigger.setValue("not a json"); | ||
| 532 | + | ||
| 533 | + boolean result = matcher.matches(message, trigger); | ||
| 534 | + | ||
| 535 | + assertFalse(result); | ||
| 536 | + } | ||
| 537 | + | ||
| 359 | // ========== 辅助方法 ========== | 538 | // ========== 辅助方法 ========== |
| 360 | 539 | ||
| 361 | /** | 540 | /** |
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; | @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 10 | import org.junit.jupiter.api.BeforeEach; | 10 | import org.junit.jupiter.api.BeforeEach; |
| 11 | -import org.junit.jupiter.api.Disabled; | ||
| 12 | import org.junit.jupiter.api.Test; | 11 | import org.junit.jupiter.api.Test; |
| 13 | 12 | ||
| 14 | import java.util.HashMap; | 13 | import java.util.HashMap; |
| @@ -25,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -25,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 25 | * | 24 | * |
| 26 | * @author HUIHUI | 25 | * @author HUIHUI |
| 27 | */ | 26 | */ |
| 28 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 29 | public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest { | 27 | public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest { |
| 30 | 28 | ||
| 31 | private IotDevicePropertyPostTriggerMatcher matcher; | 29 | private IotDevicePropertyPostTriggerMatcher matcher; |
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; | @@ -4,10 +4,10 @@ import cn.hutool.core.map.MapUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 7 | +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 9 | import org.junit.jupiter.api.BeforeEach; | 10 | import org.junit.jupiter.api.BeforeEach; |
| 10 | -import org.junit.jupiter.api.Disabled; | ||
| 11 | import org.junit.jupiter.api.Test; | 11 | import org.junit.jupiter.api.Test; |
| 12 | 12 | ||
| 13 | import java.util.HashMap; | 13 | import java.util.HashMap; |
| @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 23 | * | 23 | * |
| 24 | * @author HUIHUI | 24 | * @author HUIHUI |
| 25 | */ | 25 | */ |
| 26 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 27 | public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { | 26 | public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { |
| 28 | 27 | ||
| 29 | private IotDeviceServiceInvokeTriggerMatcher matcher; | 28 | private IotDeviceServiceInvokeTriggerMatcher matcher; |
| @@ -487,7 +486,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa | @@ -487,7 +486,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa | ||
| 487 | IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); | 486 | IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); |
| 488 | trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); | 487 | trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); |
| 489 | trigger.setIdentifier(serviceIdentifier); | 488 | trigger.setIdentifier(serviceIdentifier); |
| 490 | - trigger.setOperator("=="); // 等于操作符 | 489 | + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); |
| 491 | trigger.setValue("auto"); | 490 | trigger.setValue("auto"); |
| 492 | 491 | ||
| 493 | // 调用 | 492 | // 调用 |
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; | @@ -8,9 +8,11 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 9 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 10 | import org.junit.jupiter.api.BeforeEach; | 10 | import org.junit.jupiter.api.BeforeEach; |
| 11 | -import org.junit.jupiter.api.Disabled; | ||
| 12 | import org.junit.jupiter.api.Test; | 11 | import org.junit.jupiter.api.Test; |
| 13 | 12 | ||
| 13 | +import java.util.HashMap; | ||
| 14 | +import java.util.Map; | ||
| 15 | + | ||
| 14 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | 16 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 15 | import static org.junit.jupiter.api.Assertions.*; | 17 | import static org.junit.jupiter.api.Assertions.*; |
| 16 | 18 | ||
| @@ -19,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -19,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 19 | * | 21 | * |
| 20 | * @author HUIHUI | 22 | * @author HUIHUI |
| 21 | */ | 23 | */ |
| 22 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 23 | public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest { | 24 | public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest { |
| 24 | 25 | ||
| 25 | private IotDeviceStateUpdateTriggerMatcher matcher; | 26 | private IotDeviceStateUpdateTriggerMatcher matcher; |
| @@ -250,7 +251,9 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc | @@ -250,7 +251,9 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc | ||
| 250 | IotDeviceMessage message = new IotDeviceMessage(); | 251 | IotDeviceMessage message = new IotDeviceMessage(); |
| 251 | message.setDeviceId(randomLongId()); | 252 | message.setDeviceId(randomLongId()); |
| 252 | message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); | 253 | message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); |
| 253 | - message.setParams(state); | 254 | + Map<String, Object> params = new HashMap<>(); |
| 255 | + params.put("state", state); | ||
| 256 | + message.setParams(params); | ||
| 254 | return message; | 257 | return message; |
| 255 | } | 258 | } |
| 256 | 259 |
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; | @@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | ||
| 5 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; | 5 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 6 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; | 6 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; |
| 7 | import org.junit.jupiter.api.BeforeEach; | 7 | import org.junit.jupiter.api.BeforeEach; |
| 8 | -import org.junit.jupiter.api.Disabled; | ||
| 9 | import org.junit.jupiter.api.Test; | 8 | import org.junit.jupiter.api.Test; |
| 10 | 9 | ||
| 11 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | 10 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; | @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; | ||
| 17 | * | 16 | * |
| 18 | * @author HUIHUI | 17 | * @author HUIHUI |
| 19 | */ | 18 | */ |
| 20 | -@Disabled // TODO @puhui999:单测有报错,先屏蔽 | ||
| 21 | public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { | 19 | public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { |
| 22 | 20 | ||
| 23 | private IotTimerTriggerMatcher matcher; | 21 | private IotTimerTriggerMatcher matcher; |
| @@ -249,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { | @@ -249,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { | ||
| 249 | public void testMatches_rangeCronSuccess() { | 247 | public void testMatches_rangeCronSuccess() { |
| 250 | // 准备参数 | 248 | // 准备参数 |
| 251 | IotDeviceMessage message = createDeviceMessage(); | 249 | IotDeviceMessage message = createDeviceMessage(); |
| 252 | - IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 * * MON-FRI"); // 工作日9-17点 | 250 | + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 ? * MON-FRI"); // 工作日 9-17 点 |
| 253 | 251 | ||
| 254 | // 调用 | 252 | // 调用 |
| 255 | boolean result = matcher.matches(message, trigger); | 253 | boolean result = matcher.matches(message, trigger); |
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; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.core.util; | ||
| 3 | import cn.hutool.core.lang.Assert; | 3 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.core.map.MapUtil; | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | import cn.hutool.core.util.IdUtil; | 5 | import cn.hutool.core.util.IdUtil; |
| 6 | +import cn.hutool.core.util.ReflectUtil; | ||
| 6 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 7 | import cn.hutool.system.SystemUtil; | 8 | import cn.hutool.system.SystemUtil; |
| 8 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 9 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| @@ -54,23 +55,41 @@ public class IotDeviceMessageUtils { | @@ -54,23 +55,41 @@ public class IotDeviceMessageUtils { | ||
| 54 | * @param message 消息 | 55 | * @param message 消息 |
| 55 | * @return 标识符 | 56 | * @return 标识符 |
| 56 | */ | 57 | */ |
| 57 | - @SuppressWarnings("unchecked") | ||
| 58 | public static String getIdentifier(IotDeviceMessage message) { | 58 | public static String getIdentifier(IotDeviceMessage message) { |
| 59 | - if (message.getParams() == null) { | 59 | + if (message == null || message.getParams() == null) { |
| 60 | return null; | 60 | return null; |
| 61 | } | 61 | } |
| 62 | + Object params = message.getParams(); | ||
| 62 | if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | 63 | if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), |
| 63 | IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod())) { | 64 | IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod())) { |
| 64 | - Map<String, Object> params = (Map<String, Object>) message.getParams(); | ||
| 65 | - return MapUtil.getStr(params, "identifier"); | ||
| 66 | - } else if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { | ||
| 67 | - Map<String, Object> params = (Map<String, Object>) message.getParams(); | ||
| 68 | - return MapUtil.getStr(params, "state"); | 65 | + return StrUtil.toStringOrNull(readField(params, "identifier")); |
| 66 | + } else if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { | ||
| 67 | + return StrUtil.toStringOrNull(readField(params, "state")); | ||
| 69 | } | 68 | } |
| 70 | return null; | 69 | return null; |
| 71 | } | 70 | } |
| 72 | 71 | ||
| 73 | /** | 72 | /** |
| 73 | + * 从 params 中读取字段值,兼容 Map 和 POJO(DTO)两种形态 | ||
| 74 | + * | ||
| 75 | + * Why:MQ 消息经 JSON 反序列化后 params 是 Map,但本地总线场景 producer 可能直接传 DTO 对象(如 IotDeviceEventPostReqDTO), | ||
| 76 | + * matcher 必须同时支持两种形态,避免事件触发器在同 JVM 内部消息总线下匹配失败 | ||
| 77 | + */ | ||
| 78 | + private static Object readField(Object params, String fieldName) { | ||
| 79 | + if (params == null) { | ||
| 80 | + return null; | ||
| 81 | + } | ||
| 82 | + if (params instanceof Map) { | ||
| 83 | + return ((Map<?, ?>) params).get(fieldName); | ||
| 84 | + } | ||
| 85 | + try { | ||
| 86 | + return ReflectUtil.getFieldValue(params, fieldName); | ||
| 87 | + } catch (Exception ignored) { | ||
| 88 | + return null; | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** | ||
| 74 | * 判断消息中是否包含指定的标识符 | 93 | * 判断消息中是否包含指定的标识符 |
| 75 | * <p> | 94 | * <p> |
| 76 | * 对于不同消息类型的处理: | 95 | * 对于不同消息类型的处理: |
| @@ -82,8 +101,9 @@ public class IotDeviceMessageUtils { | @@ -82,8 +101,9 @@ public class IotDeviceMessageUtils { | ||
| 82 | * @param identifier 要检查的标识符 | 101 | * @param identifier 要检查的标识符 |
| 83 | * @return 是否包含 | 102 | * @return 是否包含 |
| 84 | */ | 103 | */ |
| 104 | + @SuppressWarnings("unchecked") | ||
| 85 | public static boolean containsIdentifier(IotDeviceMessage message, String identifier) { | 105 | public static boolean containsIdentifier(IotDeviceMessage message, String identifier) { |
| 86 | - if (message.getParams() == null || StrUtil.isBlank(identifier)) { | 106 | + if (message == null || message.getParams() == null || StrUtil.isBlank(identifier)) { |
| 87 | return false; | 107 | return false; |
| 88 | } | 108 | } |
| 89 | // EVENT_POST / SERVICE_INVOKE / STATE_UPDATE:使用原有逻辑 | 109 | // EVENT_POST / SERVICE_INVOKE / STATE_UPDATE:使用原有逻辑 |
| @@ -91,10 +111,17 @@ public class IotDeviceMessageUtils { | @@ -91,10 +111,17 @@ public class IotDeviceMessageUtils { | ||
| 91 | if (messageIdentifier != null) { | 111 | if (messageIdentifier != null) { |
| 92 | return identifier.equals(messageIdentifier); | 112 | return identifier.equals(messageIdentifier); |
| 93 | } | 113 | } |
| 94 | - // PROPERTY_POST:检查 params 中是否包含该属性 key | 114 | + // PROPERTY_POST:检查 params 中是否包含该属性 key(支持扁平和嵌套 properties 结构) |
| 95 | if (StrUtil.equals(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())) { | 115 | if (StrUtil.equals(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())) { |
| 96 | Map<String, Object> params = parseParamsToMap(message.getParams()); | 116 | Map<String, Object> params = parseParamsToMap(message.getParams()); |
| 97 | - return params != null && params.containsKey(identifier); | 117 | + if (params == null) { |
| 118 | + return false; | ||
| 119 | + } | ||
| 120 | + if (params.containsKey(identifier)) { | ||
| 121 | + return true; | ||
| 122 | + } | ||
| 123 | + Object properties = params.get("properties"); | ||
| 124 | + return properties instanceof Map && ((Map<String, Object>) properties).containsKey(identifier); | ||
| 98 | } | 125 | } |
| 99 | return false; | 126 | return false; |
| 100 | } | 127 | } |
| @@ -132,9 +159,6 @@ public class IotDeviceMessageUtils { | @@ -132,9 +159,6 @@ public class IotDeviceMessageUtils { | ||
| 132 | 159 | ||
| 133 | /** | 160 | /** |
| 134 | * 从设备消息中提取指定标识符的属性值 | 161 | * 从设备消息中提取指定标识符的属性值 |
| 135 | - * - 支持多种消息格式和属性值提取策略 | ||
| 136 | - * - 兼容现有的消息结构 | ||
| 137 | - * - 提供统一的属性值提取接口 | ||
| 138 | * <p> | 162 | * <p> |
| 139 | * 支持的提取策略(按优先级顺序): | 163 | * 支持的提取策略(按优先级顺序): |
| 140 | * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息) | 164 | * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息) |
| @@ -150,7 +174,7 @@ public class IotDeviceMessageUtils { | @@ -150,7 +174,7 @@ public class IotDeviceMessageUtils { | ||
| 150 | */ | 174 | */ |
| 151 | @SuppressWarnings("unchecked") | 175 | @SuppressWarnings("unchecked") |
| 152 | public static Object extractPropertyValue(IotDeviceMessage message, String identifier) { | 176 | public static Object extractPropertyValue(IotDeviceMessage message, String identifier) { |
| 153 | - Object params = message.getParams(); | 177 | + Object params = message != null ? message.getParams() : null; |
| 154 | if (params == null) { | 178 | if (params == null) { |
| 155 | return null; | 179 | return null; |
| 156 | } | 180 | } |
| @@ -207,6 +231,19 @@ public class IotDeviceMessageUtils { | @@ -207,6 +231,19 @@ public class IotDeviceMessageUtils { | ||
| 207 | } | 231 | } |
| 208 | 232 | ||
| 209 | /** | 233 | /** |
| 234 | + * 从设备事件上报消息中提取事件值 | ||
| 235 | + * <p> | ||
| 236 | + * 事件上报的 params 结构为:{"identifier": "xxx", "value": ...},事件值即 value 字段。 | ||
| 237 | + * value 可能是标量(字符串/数字/布尔),也可能是结构体(如告警事件 {level, message}) | ||
| 238 | + * | ||
| 239 | + * @param message 设备消息 | ||
| 240 | + * @return 事件值,如果未找到则返回 null | ||
| 241 | + */ | ||
| 242 | + public static Object extractEventValue(IotDeviceMessage message) { | ||
| 243 | + return readField(message != null ? message.getParams() : null, "value"); | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + /** | ||
| 210 | * 从服务调用消息中提取输入参数 | 247 | * 从服务调用消息中提取输入参数 |
| 211 | * <p> | 248 | * <p> |
| 212 | * 服务调用消息的 params 结构通常为: | 249 | * 服务调用消息的 params 结构通常为: |
| @@ -220,23 +257,16 @@ public class IotDeviceMessageUtils { | @@ -220,23 +257,16 @@ public class IotDeviceMessageUtils { | ||
| 220 | */ | 257 | */ |
| 221 | @SuppressWarnings("unchecked") | 258 | @SuppressWarnings("unchecked") |
| 222 | public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) { | 259 | public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) { |
| 223 | - // 1. 参数校验 | ||
| 224 | - Object params = message.getParams(); | ||
| 225 | - if (params == null) { | ||
| 226 | - return null; | ||
| 227 | - } | ||
| 228 | - if (!(params instanceof Map)) { | 260 | + if (message == null || message.getParams() == null) { |
| 229 | return null; | 261 | return null; |
| 230 | } | 262 | } |
| 231 | - Map<String, Object> paramsMap = (Map<String, Object>) params; | ||
| 232 | - | ||
| 233 | - // 尝试从 inputData 字段获取 | ||
| 234 | - Object inputData = paramsMap.get("inputData"); | 263 | + Object params = message.getParams(); |
| 264 | + // 兼容 Map 和 POJO(如 IotDeviceServiceInvokeReqDTO)两种 params 形态 | ||
| 265 | + Object inputData = readField(params, "inputData"); | ||
| 235 | if (inputData instanceof Map) { | 266 | if (inputData instanceof Map) { |
| 236 | return (Map<String, Object>) inputData; | 267 | return (Map<String, Object>) inputData; |
| 237 | } | 268 | } |
| 238 | - // 尝试从 inputParams 字段获取 | ||
| 239 | - Object inputParams = paramsMap.get("inputParams"); | 269 | + Object inputParams = readField(params, "inputParams"); |
| 240 | if (inputParams instanceof Map) { | 270 | if (inputParams instanceof Map) { |
| 241 | return (Map<String, Object>) inputParams; | 271 | return (Map<String, Object>) inputParams; |
| 242 | } | 272 | } |
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; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.core.util; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | ||
| 5 | import org.junit.jupiter.api.Test; | 6 | import org.junit.jupiter.api.Test; |
| 6 | 7 | ||
| 7 | import java.util.HashMap; | 8 | import java.util.HashMap; |
| @@ -71,7 +72,7 @@ public class IotDeviceMessageUtilsTest { | @@ -71,7 +72,7 @@ public class IotDeviceMessageUtilsTest { | ||
| 71 | 72 | ||
| 72 | @Test | 73 | @Test |
| 73 | public void testExtractPropertyValue_valueField() { | 74 | public void testExtractPropertyValue_valueField() { |
| 74 | - // 测试 value 字段 | 75 | + // 测试 value 字段(策略 5) |
| 75 | IotDeviceMessage message = new IotDeviceMessage(); | 76 | IotDeviceMessage message = new IotDeviceMessage(); |
| 76 | Map<String, Object> params = new HashMap<>(); | 77 | Map<String, Object> params = new HashMap<>(); |
| 77 | params.put("identifier", "temperature"); | 78 | params.put("identifier", "temperature"); |
| @@ -84,7 +85,7 @@ public class IotDeviceMessageUtilsTest { | @@ -84,7 +85,7 @@ public class IotDeviceMessageUtilsTest { | ||
| 84 | 85 | ||
| 85 | @Test | 86 | @Test |
| 86 | public void testExtractPropertyValue_singleValueMap() { | 87 | public void testExtractPropertyValue_singleValueMap() { |
| 87 | - // 测试单值 Map(包含 identifier 和一个值) | 88 | + // 测试单值 Map(策略 6:包含 identifier 和一个其他字段) |
| 88 | IotDeviceMessage message = new IotDeviceMessage(); | 89 | IotDeviceMessage message = new IotDeviceMessage(); |
| 89 | Map<String, Object> params = new HashMap<>(); | 90 | Map<String, Object> params = new HashMap<>(); |
| 90 | params.put("identifier", "temperature"); | 91 | params.put("identifier", "temperature"); |
| @@ -139,6 +140,88 @@ public class IotDeviceMessageUtilsTest { | @@ -139,6 +140,88 @@ public class IotDeviceMessageUtilsTest { | ||
| 139 | assertEquals(25.5, result); // 应该返回直接标识符的值 | 140 | assertEquals(25.5, result); // 应该返回直接标识符的值 |
| 140 | } | 141 | } |
| 141 | 142 | ||
| 143 | + // ========== extractEventValue 测试 ========== | ||
| 144 | + | ||
| 145 | + @Test | ||
| 146 | + public void testExtractEventValue_scalar() { | ||
| 147 | + // 标量事件值:{identifier: "gzzt", value: "normal"} | ||
| 148 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 149 | + Map<String, Object> params = new HashMap<>(); | ||
| 150 | + params.put("identifier", "gzzt"); | ||
| 151 | + params.put("value", "normal"); | ||
| 152 | + message.setParams(params); | ||
| 153 | + | ||
| 154 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 155 | + assertEquals("normal", result); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + @Test | ||
| 159 | + public void testExtractEventValue_struct() { | ||
| 160 | + // 结构体事件值:{identifier: "alarm", value: {level: "high", message: "..."}} | ||
| 161 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 162 | + Map<String, Object> eventValue = new HashMap<>(); | ||
| 163 | + eventValue.put("level", "high"); | ||
| 164 | + eventValue.put("message", "over temperature"); | ||
| 165 | + | ||
| 166 | + Map<String, Object> params = new HashMap<>(); | ||
| 167 | + params.put("identifier", "alarm"); | ||
| 168 | + params.put("value", eventValue); | ||
| 169 | + message.setParams(params); | ||
| 170 | + | ||
| 171 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 172 | + assertEquals(eventValue, result); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + @Test | ||
| 176 | + public void testExtractEventValue_nullParams() { | ||
| 177 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 178 | + message.setParams(null); | ||
| 179 | + | ||
| 180 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 181 | + assertNull(result); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + @Test | ||
| 185 | + public void testExtractEventValue_paramsWithoutValueField() { | ||
| 186 | + // params 是字符串等非结构化对象,无 value 字段,应返回 null | ||
| 187 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 188 | + message.setParams("not a map"); | ||
| 189 | + | ||
| 190 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 191 | + assertNull(result); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + @Test | ||
| 195 | + public void testExtractEventValue_missingValueField() { | ||
| 196 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 197 | + Map<String, Object> params = new HashMap<>(); | ||
| 198 | + params.put("identifier", "gzzt"); | ||
| 199 | + message.setParams(params); | ||
| 200 | + | ||
| 201 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 202 | + assertNull(result); | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + @Test | ||
| 206 | + public void testExtractEventValue_pojoParams() { | ||
| 207 | + // 本地总线场景:params 是 IotDeviceEventPostReqDTO POJO(未经 JSON 反序列化),应能反射取到 value | ||
| 208 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 209 | + message.setParams(IotDeviceEventPostReqDTO.of("gzzt", "normal")); | ||
| 210 | + | ||
| 211 | + Object result = IotDeviceMessageUtils.extractEventValue(message); | ||
| 212 | + assertEquals("normal", result); | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + @Test | ||
| 216 | + public void testGetIdentifier_eventPostPojoParams() { | ||
| 217 | + // 本地总线场景:EVENT_POST 消息 params 是 DTO POJO,仍应能解析出 identifier | ||
| 218 | + IotDeviceMessage message = new IotDeviceMessage(); | ||
| 219 | + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); | ||
| 220 | + message.setParams(IotDeviceEventPostReqDTO.of("gzzt", "normal")); | ||
| 221 | + | ||
| 222 | + assertEquals("gzzt", IotDeviceMessageUtils.getIdentifier(message)); | ||
| 223 | + } | ||
| 224 | + | ||
| 142 | // ========== notContainsIdentifier 测试 ========== | 225 | // ========== notContainsIdentifier 测试 ========== |
| 143 | 226 | ||
| 144 | /** | 227 | /** |