Commit 87c8d7e242cb579b7d673c4f711d299a70ba9934

Authored by YunaiV
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 /**