Commit 4ad4fcf6cfca7c3f5911011b858944be28fd725a

Authored by puhui999
1 parent 5bc8a4e4

perf:【iot】增强 IotDeviceServiceInvokeTriggerMatcher 参数校验

yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.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.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
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.core.util.IotDeviceMessageUtils; 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
@@ -8,6 +9,8 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; @@ -8,6 +9,8 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
9 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
10 11
  12 +import java.util.Map;
  13 +
11 /** 14 /**
12 * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑 15 * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
13 * 16 *
@@ -42,13 +45,58 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger @@ -42,13 +45,58 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger
42 return false; 45 return false;
43 } 46 }
44 47
45 - // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可  
46 - // 不需要检查操作符和值,因为服务调用本身就是触发条件  
47 - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件? 48 + // 2. 检查是否配置了参数条件
  49 + if (hasParameterCondition(trigger)) {
  50 + return matchParameterCondition(message, trigger);
  51 + }
  52 +
  53 + // 3. 无参数条件时,标识符匹配即成功
48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 54 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
49 return true; 55 return true;
50 } 56 }
51 57
  58 + /**
  59 + * 判断触发器是否配置了参数条件
  60 + *
  61 + * @param trigger 触发器配置
  62 + * @return 是否配置了参数条件
  63 + */
  64 + private boolean hasParameterCondition(IotSceneRuleDO.Trigger trigger) {
  65 + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue());
  66 + }
  67 +
  68 + /**
  69 + * 匹配参数条件
  70 + *
  71 + * @param message 设备消息
  72 + * @param trigger 触发器配置
  73 + * @return 是否匹配
  74 + */
  75 + private boolean matchParameterCondition(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  76 + // 从消息中提取服务调用的输入参数
  77 + Map<String, Object> inputParams = IotDeviceMessageUtils.extractServiceInputParams(message);
  78 + if (inputParams == null) {
  79 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中缺少服务输入参数");
  80 + return false;
  81 + }
  82 +
  83 + // 获取要匹配的参数值(使用 identifier 作为参数名)
  84 + Object paramValue = inputParams.get(trigger.getIdentifier());
  85 + if (paramValue == null) {
  86 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "服务输入参数中缺少指定参数: " + trigger.getIdentifier());
  87 + return false;
  88 + }
  89 +
  90 + // 使用条件评估器进行匹配
  91 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(paramValue, trigger.getOperator(), trigger.getValue());
  92 + if (matched) {
  93 + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
  94 + } else {
  95 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "服务输入参数条件不匹配");
  96 + }
  97 + return matched;
  98 + }
  99 +
52 @Override 100 @Override
53 public int getPriority() { 101 public int getPriority() {
54 return 40; // 较低优先级 102 return 40; // 较低优先级
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 import org.junit.jupiter.api.BeforeEach; 9 import org.junit.jupiter.api.BeforeEach;
10 -import org.junit.jupiter.api.Disabled;  
11 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
12 11
13 import java.util.HashMap; 12 import java.util.HashMap;
@@ -23,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; @@ -23,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*;
23 * 22 *
24 * @author HUIHUI 23 * @author HUIHUI
25 */ 24 */
26 -@Disabled // TODO @puhui999:单测有报错,先屏蔽  
27 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { 25 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 26
29 private IotDeviceServiceInvokeTriggerMatcher matcher; 27 private IotDeviceServiceInvokeTriggerMatcher matcher;
@@ -378,6 +376,268 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa @@ -378,6 +376,268 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa
378 assertFalse(result); 376 assertFalse(result);
379 } 377 }
380 378
  379 +
  380 + // ========== 参数条件匹配测试 ==========
  381 +
  382 + /**
  383 + * 测试无参数条件时的匹配逻辑 - 只要标识符匹配就返回 true
  384 + * **Property 4: 服务调用触发器参数匹配逻辑**
  385 + * **Validates: Requirements 5.2**
  386 + */
  387 + @Test
  388 + public void testMatches_noParameterCondition_success() {
  389 + // 准备参数
  390 + String serviceIdentifier = "testService";
  391 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  392 + .put("identifier", serviceIdentifier)
  393 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  394 + .put("level", 5)
  395 + .build())
  396 + .build();
  397 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  398 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  399 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  400 + trigger.setIdentifier(serviceIdentifier);
  401 + trigger.setOperator(null); // 无参数条件
  402 + trigger.setValue(null);
  403 +
  404 + // 调用
  405 + boolean result = matcher.matches(message, trigger);
  406 +
  407 + // 断言
  408 + assertTrue(result);
  409 + }
  410 +
  411 + /**
  412 + * 测试有参数条件时的匹配逻辑 - 参数条件匹配成功
  413 + * **Property 4: 服务调用触发器参数匹配逻辑**
  414 + * **Validates: Requirements 5.1**
  415 + */
  416 + @Test
  417 + public void testMatches_withParameterCondition_greaterThan_success() {
  418 + // 准备参数
  419 + String serviceIdentifier = "level";
  420 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  421 + .put("identifier", serviceIdentifier)
  422 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  423 + .put("level", 5)
  424 + .build())
  425 + .build();
  426 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  427 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  428 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  429 + trigger.setIdentifier(serviceIdentifier);
  430 + trigger.setOperator(">"); // 大于操作符
  431 + trigger.setValue("3");
  432 +
  433 + // 调用
  434 + boolean result = matcher.matches(message, trigger);
  435 +
  436 + // 断言
  437 + assertTrue(result);
  438 + }
  439 +
  440 + /**
  441 + * 测试有参数条件时的匹配逻辑 - 参数条件匹配失败
  442 + * **Property 4: 服务调用触发器参数匹配逻辑**
  443 + * **Validates: Requirements 5.1**
  444 + */
  445 + @Test
  446 + public void testMatches_withParameterCondition_greaterThan_failure() {
  447 + // 准备参数
  448 + String serviceIdentifier = "level";
  449 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  450 + .put("identifier", serviceIdentifier)
  451 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  452 + .put("level", 2)
  453 + .build())
  454 + .build();
  455 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  456 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  457 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  458 + trigger.setIdentifier(serviceIdentifier);
  459 + trigger.setOperator(">"); // 大于操作符
  460 + trigger.setValue("3");
  461 +
  462 + // 调用
  463 + boolean result = matcher.matches(message, trigger);
  464 +
  465 + // 断言
  466 + assertFalse(result);
  467 + }
  468 +
  469 + /**
  470 + * 测试有参数条件时的匹配逻辑 - 等于操作符
  471 + * **Property 4: 服务调用触发器参数匹配逻辑**
  472 + * **Validates: Requirements 5.1**
  473 + */
  474 + @Test
  475 + public void testMatches_withParameterCondition_equals_success() {
  476 + // 准备参数
  477 + String serviceIdentifier = "mode";
  478 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  479 + .put("identifier", serviceIdentifier)
  480 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  481 + .put("mode", "auto")
  482 + .build())
  483 + .build();
  484 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  485 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  486 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  487 + trigger.setIdentifier(serviceIdentifier);
  488 + trigger.setOperator("=="); // 等于操作符
  489 + trigger.setValue("auto");
  490 +
  491 + // 调用
  492 + boolean result = matcher.matches(message, trigger);
  493 +
  494 + // 断言
  495 + assertTrue(result);
  496 + }
  497 +
  498 + /**
  499 + * 测试参数缺失时的处理 - 消息中缺少 inputData
  500 + * **Property 4: 服务调用触发器参数匹配逻辑**
  501 + * **Validates: Requirements 5.3**
  502 + */
  503 + @Test
  504 + public void testMatches_withParameterCondition_missingInputData() {
  505 + // 准备参数
  506 + String serviceIdentifier = "testService";
  507 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  508 + .put("identifier", serviceIdentifier)
  509 + // 缺少 inputData 字段
  510 + .build();
  511 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  512 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  513 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  514 + trigger.setIdentifier(serviceIdentifier);
  515 + trigger.setOperator(">"); // 配置了参数条件
  516 + trigger.setValue("3");
  517 +
  518 + // 调用
  519 + boolean result = matcher.matches(message, trigger);
  520 +
  521 + // 断言
  522 + assertFalse(result);
  523 + }
  524 +
  525 + /**
  526 + * 测试参数缺失时的处理 - inputData 中缺少指定参数
  527 + * **Property 4: 服务调用触发器参数匹配逻辑**
  528 + * **Validates: Requirements 5.3**
  529 + */
  530 + @Test
  531 + public void testMatches_withParameterCondition_missingParam() {
  532 + // 准备参数
  533 + String serviceIdentifier = "level";
  534 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  535 + .put("identifier", serviceIdentifier)
  536 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  537 + .put("otherParam", 5) // 不是 level 参数
  538 + .build())
  539 + .build();
  540 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  541 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  542 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  543 + trigger.setIdentifier(serviceIdentifier);
  544 + trigger.setOperator(">"); // 配置了参数条件
  545 + trigger.setValue("3");
  546 +
  547 + // 调用
  548 + boolean result = matcher.matches(message, trigger);
  549 +
  550 + // 断言
  551 + assertFalse(result);
  552 + }
  553 +
  554 + /**
  555 + * 测试只有 operator 没有 value 时不触发参数条件匹配
  556 + * **Property 4: 服务调用触发器参数匹配逻辑**
  557 + * **Validates: Requirements 5.2**
  558 + */
  559 + @Test
  560 + public void testMatches_onlyOperator_noValue() {
  561 + // 准备参数
  562 + String serviceIdentifier = "testService";
  563 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  564 + .put("identifier", serviceIdentifier)
  565 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  566 + .put("level", 5)
  567 + .build())
  568 + .build();
  569 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  570 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  571 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  572 + trigger.setIdentifier(serviceIdentifier);
  573 + trigger.setOperator(">"); // 只有 operator
  574 + trigger.setValue(null); // 没有 value
  575 +
  576 + // 调用
  577 + boolean result = matcher.matches(message, trigger);
  578 +
  579 + // 断言:只有 operator 没有 value 时,不触发参数条件匹配,标识符匹配即成功
  580 + assertTrue(result);
  581 + }
  582 +
  583 + /**
  584 + * 测试只有 value 没有 operator 时不触发参数条件匹配
  585 + * **Property 4: 服务调用触发器参数匹配逻辑**
  586 + * **Validates: Requirements 5.2**
  587 + */
  588 + @Test
  589 + public void testMatches_onlyValue_noOperator() {
  590 + // 准备参数
  591 + String serviceIdentifier = "testService";
  592 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  593 + .put("identifier", serviceIdentifier)
  594 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  595 + .put("level", 5)
  596 + .build())
  597 + .build();
  598 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  599 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  600 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  601 + trigger.setIdentifier(serviceIdentifier);
  602 + trigger.setOperator(null); // 没有 operator
  603 + trigger.setValue("3"); // 只有 value
  604 +
  605 + // 调用
  606 + boolean result = matcher.matches(message, trigger);
  607 +
  608 + // 断言:只有 value 没有 operator 时,不触发参数条件匹配,标识符匹配即成功
  609 + assertTrue(result);
  610 + }
  611 +
  612 + /**
  613 + * 测试使用 inputParams 字段(替代 inputData)
  614 + * **Property 4: 服务调用触发器参数匹配逻辑**
  615 + * **Validates: Requirements 5.1**
  616 + */
  617 + @Test
  618 + public void testMatches_withInputParams_success() {
  619 + // 准备参数
  620 + String serviceIdentifier = "level";
  621 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  622 + .put("identifier", serviceIdentifier)
  623 + .put("inputParams", MapUtil.builder(new HashMap<String, Object>()) // 使用 inputParams 而不是 inputData
  624 + .put("level", 5)
  625 + .build())
  626 + .build();
  627 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  628 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  629 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  630 + trigger.setIdentifier(serviceIdentifier);
  631 + trigger.setOperator(">"); // 大于操作符
  632 + trigger.setValue("3");
  633 +
  634 + // 调用
  635 + boolean result = matcher.matches(message, trigger);
  636 +
  637 + // 断言
  638 + assertTrue(result);
  639 + }
  640 +
381 // ========== 辅助方法 ========== 641 // ========== 辅助方法 ==========
382 642
383 /** 643 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -207,6 +207,44 @@ public class IotDeviceMessageUtils { @@ -207,6 +207,44 @@ public class IotDeviceMessageUtils {
207 return null; 207 return null;
208 } 208 }
209 209
  210 + /**
  211 + * 从服务调用消息中提取输入参数
  212 + * <p>
  213 + * 服务调用消息的 params 结构通常为:
  214 + * {
  215 + * "identifier": "serviceIdentifier",
  216 + * "inputData": { ... } 或 "inputParams": { ... }
  217 + * }
  218 + *
  219 + * @param message 设备消息
  220 + * @return 输入参数 Map,如果未找到则返回 null
  221 + */
  222 + @SuppressWarnings("unchecked")
  223 + public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) {
  224 + Object params = message.getParams();
  225 + if (params == null) {
  226 + return null;
  227 + }
  228 + if (!(params instanceof Map)) {
  229 + return null;
  230 + }
  231 + Map<String, Object> paramsMap = (Map<String, Object>) params;
  232 +
  233 + // 尝试从 inputData 字段获取
  234 + Object inputData = paramsMap.get("inputData");
  235 + if (inputData instanceof Map) {
  236 + return (Map<String, Object>) inputData;
  237 + }
  238 +
  239 + // 尝试从 inputParams 字段获取
  240 + Object inputParams = paramsMap.get("inputParams");
  241 + if (inputParams instanceof Map) {
  242 + return (Map<String, Object>) inputParams;
  243 + }
  244 +
  245 + return null;
  246 + }
  247 +
210 // ========== Topic 相关 ========== 248 // ========== Topic 相关 ==========
211 249
212 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { 250 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {