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 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 194 public static <T> List<T> parseArray(String text, Class<T> clazz) {
177 195 if (StrUtil.isEmpty(text)) {
178 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 142 * @param trigger 触发器配置
143 143 * @return 是否有效
144 144 */
  145 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
145 146 public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) {
146 147 return trigger != null && trigger.getType() != null;
147 148 }
... ... @@ -152,6 +153,7 @@ public final class IotSceneRuleMatcherHelper {
152 153 * @param trigger 触发器配置
153 154 * @return 是否有效
154 155 */
  156 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
155 157 public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) {
156 158 return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue());
157 159 }
... ... @@ -163,7 +165,9 @@ public final class IotSceneRuleMatcherHelper {
163 165 * @param trigger 触发器配置
164 166 */
165 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 178 * @param reason 失败原因
175 179 */
176 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 192 * @param condition 触发条件
186 193 * @return 是否有效
187 194 */
  195 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
188 196 public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) {
189 197 return condition != null && condition.getType() != null;
190 198 }
... ... @@ -195,6 +203,7 @@ public final class IotSceneRuleMatcherHelper {
195 203 * @param condition 触发条件
196 204 * @return 是否有效
197 205 */
  206 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
198 207 public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) {
199 208 return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam());
200 209 }
... ... @@ -206,7 +215,9 @@ public final class IotSceneRuleMatcherHelper {
206 215 * @param condition 触发条件
207 216 */
208 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 228 * @param reason 失败原因
218 229 */
219 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 243 * @param actualIdentifier 实际的标识符
230 244 * @return 是否匹配
231 245 */
  246 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
232 247 public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) {
233 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 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 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 10 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9 11 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
10 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 51 return false;
46 52 }
47 53  
48   - // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配
49   - // 但如果配置了操作符和值,则需要进行条件匹配
  54 + // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配。但如果配置了操作符和值,则需要进行条件匹配
50 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 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 70 if (!matched) {
59 71 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配");
60 72 return false;
... ... @@ -65,6 +77,16 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc
65 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 90 @Override
69 91 public int getPriority() {
70 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 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
6 6 import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
7 7 import org.junit.jupiter.api.BeforeEach;
8   -import org.junit.jupiter.api.Disabled;
9 8 import org.junit.jupiter.api.Test;
10 9 import org.mockito.Mock;
11 10 import org.mockito.MockitoAnnotations;
12 11  
  12 +import java.util.HashMap;
  13 +import java.util.Map;
  14 +
13 15 import static org.junit.jupiter.api.Assertions.*;
14 16 import static org.mockito.Mockito.*;
15 17  
... ... @@ -18,7 +20,6 @@ import static org.mockito.Mockito.*;
18 20 *
19 21 * @author HUIHUI
20 22 */
21   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
22 23 class IotTcpDataRuleActionTest {
23 24  
24 25 private IotTcpDataRuleAction tcpDataRuleAction;
... ... @@ -145,10 +146,12 @@ class IotTcpDataRuleActionTest {
145 146 @Test
146 147 public void testMessageSerialization() {
147 148 // 准备参数
  149 + Map<String, Object> params = new HashMap<>();
  150 + params.put("temperature", 25.5);
148 151 IotDeviceMessage message = IotDeviceMessage.builder()
149 152 .deviceId(123L)
150 153 .method("thing.property.report")
151   - .params("{\"temperature\": 25.5}")
  154 + .params(params)
152 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 9 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 10 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
11 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 13 import org.junit.jupiter.api.Test;
14 14 import org.mockito.InjectMocks;
15 15 import org.mockito.Mock;
... ... @@ -30,7 +30,6 @@ import static org.mockito.Mockito.*;
30 30 *
31 31 * @author 芋道源码
32 32 */
33   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
34 33 public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
35 34  
36 35 @InjectMocks
... ... @@ -51,6 +50,9 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
51 50 @Mock
52 51 private IotDeviceService deviceService;
53 52  
  53 + @Mock
  54 + private IotSceneRuleTimerHandler timerHandler;
  55 +
54 56 @Test
55 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
4 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
6 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 20 import org.junit.jupiter.api.*;
20 21 import org.mockito.InjectMocks;
21 22 import org.mockito.Mock;
  23 +import org.springframework.context.support.GenericApplicationContext;
22 24  
23 25 import java.lang.reflect.Field;
24 26 import java.util.*;
... ... @@ -41,7 +43,6 @@ import static org.mockito.Mockito.*;
41 43 *
42 44 * @author HUIHUI
43 45 */
44   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
45 46 public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest {
46 47  
47 48 @InjectMocks
... ... @@ -70,6 +71,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
70 71 private static final Long DEVICE_ID = 100L;
71 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 80 @BeforeEach
74 81 void setUp() {
75 82 // 创建并注入 timerConditionEvaluator 的依赖
... ... @@ -137,14 +144,15 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
137 144 IotDevicePropertyDO property = new IotDevicePropertyDO();
138 145 property.setValue(value);
139 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 151 private void mockDeviceState(Long deviceId, Integer state) {
144 152 IotDeviceDO device = new IotDeviceDO();
145 153 device.setId(deviceId);
146 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 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
8 8 import org.junit.jupiter.api.BeforeEach;
9   -import org.junit.jupiter.api.Disabled;
10 9 import org.junit.jupiter.api.Test;
11 10  
12 11 import java.time.LocalDateTime;
... ... @@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*;
21 20 *
22 21 * @author HUIHUI
23 22 */
24   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
25 23 public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
26 24  
27 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 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
8 8 import org.junit.jupiter.api.BeforeEach;
9   -import org.junit.jupiter.api.Disabled;
10 9 import org.junit.jupiter.api.Test;
11 10  
12 11 import java.util.HashMap;
... ... @@ -20,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*;
20 19 *
21 20 * @author HUIHUI
22 21 */
23   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
24 22 public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
25 23  
26 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 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 4 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
4 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 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 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
8 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 10 import org.junit.jupiter.api.BeforeEach;
10   -import org.junit.jupiter.api.Disabled;
11 11 import org.junit.jupiter.api.Test;
12 12  
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +
13 16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
14 17 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
15 18 import static org.junit.jupiter.api.Assertions.*;
... ... @@ -19,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*;
19 22 *
20 23 * @author HUIHUI
21 24 */
22   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
23 25 public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
24 26  
25 27 private IotDeviceStateConditionMatcher matcher;
... ... @@ -344,7 +346,10 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT
344 346 private IotDeviceMessage createDeviceMessage(Integer deviceState) {
345 347 IotDeviceMessage message = new IotDeviceMessage();
346 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 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.hutool.core.map.MapUtil;
4 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
6 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 10 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 11 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 12 import org.junit.jupiter.api.BeforeEach;
10   -import org.junit.jupiter.api.Disabled;
11 13 import org.junit.jupiter.api.Test;
12 14  
13 15 import java.util.HashMap;
... ... @@ -23,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.*;
23 25 *
24 26 * @author HUIHUI
25 27 */
26   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
27 28 public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 29  
29 30 private IotDeviceEventPostTriggerMatcher matcher;
... ... @@ -356,6 +357,184 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche
356 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 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
10 10 import org.junit.jupiter.api.BeforeEach;
11   -import org.junit.jupiter.api.Disabled;
12 11 import org.junit.jupiter.api.Test;
13 12  
14 13 import java.util.HashMap;
... ... @@ -25,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.*;
25 24 *
26 25 * @author HUIHUI
27 26 */
28   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
29 27 public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
30 28  
31 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 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  7 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 10 import org.junit.jupiter.api.BeforeEach;
10   -import org.junit.jupiter.api.Disabled;
11 11 import org.junit.jupiter.api.Test;
12 12  
13 13 import java.util.HashMap;
... ... @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*;
23 23 *
24 24 * @author HUIHUI
25 25 */
26   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
27 26 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 27  
29 28 private IotDeviceServiceInvokeTriggerMatcher matcher;
... ... @@ -487,7 +486,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa
487 486 IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
488 487 trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
489 488 trigger.setIdentifier(serviceIdentifier);
490   - trigger.setOperator("=="); // 等于操作符
  489 + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());
491 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 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
10 10 import org.junit.jupiter.api.BeforeEach;
11   -import org.junit.jupiter.api.Disabled;
12 11 import org.junit.jupiter.api.Test;
13 12  
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +
14 16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
15 17 import static org.junit.jupiter.api.Assertions.*;
16 18  
... ... @@ -19,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.*;
19 21 *
20 22 * @author HUIHUI
21 23 */
22   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
23 24 public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
24 25  
25 26 private IotDeviceStateUpdateTriggerMatcher matcher;
... ... @@ -250,7 +251,9 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc
250 251 IotDeviceMessage message = new IotDeviceMessage();
251 252 message.setDeviceId(randomLongId());
252 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 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 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
7 7 import org.junit.jupiter.api.BeforeEach;
8   -import org.junit.jupiter.api.Disabled;
9 8 import org.junit.jupiter.api.Test;
10 9  
11 10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
... ... @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*;
17 16 *
18 17 * @author HUIHUI
19 18 */
20   -@Disabled // TODO @puhui999:单测有报错,先屏蔽
21 19 public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
22 20  
23 21 private IotTimerTriggerMatcher matcher;
... ... @@ -249,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
249 247 public void testMatches_rangeCronSuccess() {
250 248 // 准备参数
251 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 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 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.hutool.core.util.IdUtil;
  6 +import cn.hutool.core.util.ReflectUtil;
6 7 import cn.hutool.core.util.StrUtil;
7 8 import cn.hutool.system.SystemUtil;
8 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -54,23 +55,41 @@ public class IotDeviceMessageUtils {
54 55 * @param message 消息
55 56 * @return 标识符
56 57 */
57   - @SuppressWarnings("unchecked")
58 58 public static String getIdentifier(IotDeviceMessage message) {
59   - if (message.getParams() == null) {
  59 + if (message == null || message.getParams() == null) {
60 60 return null;
61 61 }
  62 + Object params = message.getParams();
62 63 if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
63 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 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 94 * <p>
76 95 * 对于不同消息类型的处理:
... ... @@ -82,8 +101,9 @@ public class IotDeviceMessageUtils {
82 101 * @param identifier 要检查的标识符
83 102 * @return 是否包含
84 103 */
  104 + @SuppressWarnings("unchecked")
85 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 107 return false;
88 108 }
89 109 // EVENT_POST / SERVICE_INVOKE / STATE_UPDATE:使用原有逻辑
... ... @@ -91,10 +111,17 @@ public class IotDeviceMessageUtils {
91 111 if (messageIdentifier != null) {
92 112 return identifier.equals(messageIdentifier);
93 113 }
94   - // PROPERTY_POST:检查 params 中是否包含该属性 key
  114 + // PROPERTY_POST:检查 params 中是否包含该属性 key(支持扁平和嵌套 properties 结构)
95 115 if (StrUtil.equals(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())) {
96 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 126 return false;
100 127 }
... ... @@ -132,9 +159,6 @@ public class IotDeviceMessageUtils {
132 159  
133 160 /**
134 161 * 从设备消息中提取指定标识符的属性值
135   - * - 支持多种消息格式和属性值提取策略
136   - * - 兼容现有的消息结构
137   - * - 提供统一的属性值提取接口
138 162 * <p>
139 163 * 支持的提取策略(按优先级顺序):
140 164 * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息)
... ... @@ -150,7 +174,7 @@ public class IotDeviceMessageUtils {
150 174 */
151 175 @SuppressWarnings("unchecked")
152 176 public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
153   - Object params = message.getParams();
  177 + Object params = message != null ? message.getParams() : null;
154 178 if (params == null) {
155 179 return null;
156 180 }
... ... @@ -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 248 * <p>
212 249 * 服务调用消息的 params 结构通常为:
... ... @@ -220,23 +257,16 @@ public class IotDeviceMessageUtils {
220 257 */
221 258 @SuppressWarnings("unchecked")
222 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 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 266 if (inputData instanceof Map) {
236 267 return (Map<String, Object>) inputData;
237 268 }
238   - // 尝试从 inputParams 字段获取
239   - Object inputParams = paramsMap.get("inputParams");
  269 + Object inputParams = readField(params, "inputParams");
240 270 if (inputParams instanceof Map) {
241 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 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
5 6 import org.junit.jupiter.api.Test;
6 7  
7 8 import java.util.HashMap;
... ... @@ -71,7 +72,7 @@ public class IotDeviceMessageUtilsTest {
71 72  
72 73 @Test
73 74 public void testExtractPropertyValue_valueField() {
74   - // 测试 value 字段
  75 + // 测试 value 字段(策略 5)
75 76 IotDeviceMessage message = new IotDeviceMessage();
76 77 Map<String, Object> params = new HashMap<>();
77 78 params.put("identifier", "temperature");
... ... @@ -84,7 +85,7 @@ public class IotDeviceMessageUtilsTest {
84 85  
85 86 @Test
86 87 public void testExtractPropertyValue_singleValueMap() {
87   - // 测试单值 Map(包含 identifier 和一个值)
  88 + // 测试单值 Map(策略 6:包含 identifier 和一个其他字段)
88 89 IotDeviceMessage message = new IotDeviceMessage();
89 90 Map<String, Object> params = new HashMap<>();
90 91 params.put("identifier", "temperature");
... ... @@ -139,6 +140,88 @@ public class IotDeviceMessageUtilsTest {
139 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 225 // ========== notContainsIdentifier 测试 ==========
143 226  
144 227 /**
... ...