Commit b945970ce6b5a6d7e37a29261ecd78b355791a81

Authored by 芋道源码
Committed by Gitee
2 parents 21945e6b 1cdae71d

!1417 feat: 【IoT 物联网】实现场景规则定时触发器的注册、重构设备属性设置执行器、新增设备服务调用实现

Merge pull request !1417 from puhui999/feature/iot
Showing 20 changed files with 964 additions and 170 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
@@ -17,11 +17,11 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -17,11 +17,11 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; 18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
19 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 19 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
20 -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;  
21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 20 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
22 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 21 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 22 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; 23 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
  24 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
25 import jakarta.annotation.Resource; 25 import jakarta.annotation.Resource;
26 import lombok.extern.slf4j.Slf4j; 26 import lombok.extern.slf4j.Slf4j;
27 import org.springframework.stereotype.Service; 27 import org.springframework.stereotype.Service;
@@ -47,9 +47,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -47,9 +47,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
47 @Resource 47 @Resource
48 private IotSceneRuleMapper sceneRuleMapper; 48 private IotSceneRuleMapper sceneRuleMapper;
49 49
50 - // TODO @puhui999:定时任务,基于它调度;  
51 - @Resource(name = "iotSchedulerManager")  
52 - private IotSchedulerManager schedulerManager;  
53 @Resource 50 @Resource
54 private IotProductService productService; 51 private IotProductService productService;
55 @Resource 52 @Resource
@@ -59,11 +56,17 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -59,11 +56,17 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
59 private IotSceneRuleMatcherManager sceneRuleMatcherManager; 56 private IotSceneRuleMatcherManager sceneRuleMatcherManager;
60 @Resource 57 @Resource
61 private List<IotSceneRuleAction> sceneRuleActions; 58 private List<IotSceneRuleAction> sceneRuleActions;
  59 + @Resource
  60 + private IotSceneRuleTimerHandler timerHandler;
62 61
63 @Override 62 @Override
64 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { 63 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
65 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); 64 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
66 sceneRuleMapper.insert(sceneRule); 65 sceneRuleMapper.insert(sceneRule);
  66 +
  67 + // 注册定时触发器
  68 + timerHandler.registerTimerTriggers(sceneRule);
  69 +
67 return sceneRule.getId(); 70 return sceneRule.getId();
68 } 71 }
69 72
@@ -74,6 +77,9 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -74,6 +77,9 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
74 // 更新 77 // 更新
75 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class); 78 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
76 sceneRuleMapper.updateById(updateObj); 79 sceneRuleMapper.updateById(updateObj);
  80 +
  81 + // 更新定时触发器
  82 + timerHandler.updateTimerTriggers(updateObj);
77 } 83 }
78 84
79 @Override 85 @Override
@@ -83,12 +89,26 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -83,12 +89,26 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
83 // 更新状态 89 // 更新状态
84 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status); 90 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
85 sceneRuleMapper.updateById(updateObj); 91 sceneRuleMapper.updateById(updateObj);
  92 +
  93 + // 根据状态管理定时触发器
  94 + if (CommonStatusEnum.isEnable(status)) {
  95 + // 启用时,获取完整的场景规则信息并注册定时触发器
  96 + IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
  97 + if (sceneRule != null) {
  98 + timerHandler.registerTimerTriggers(sceneRule);
  99 + }
  100 + } else {
  101 + // 禁用时,暂停定时触发器
  102 + timerHandler.pauseTimerTriggers(id);
  103 + }
86 } 104 }
87 105
88 @Override 106 @Override
89 public void deleteSceneRule(Long id) { 107 public void deleteSceneRule(Long id) {
90 // 校验存在 108 // 校验存在
91 validateSceneRuleExists(id); 109 validateSceneRuleExists(id);
  110 + // 删除定时触发器
  111 + timerHandler.unregisterTimerTriggers(id);
92 // 删除 112 // 删除
93 sceneRuleMapper.deleteById(id); 113 sceneRuleMapper.deleteById(id);
94 } 114 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.action; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.action;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 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.dal.dataobject.device.IotDeviceDO;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 10 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -9,8 +13,11 @@ import jakarta.annotation.Resource; @@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
9 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.stereotype.Component; 14 import org.springframework.stereotype.Component;
11 15
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
12 /** 19 /**
13 - * IoT 设备控制的 {@link IotSceneRuleAction} 实现类 20 + * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
14 * 21 *
15 * @author 芋道源码 22 * @author 芋道源码
16 */ 23 */
@@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { @@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
23 @Resource 30 @Resource
24 private IotDeviceMessageService deviceMessageService; 31 private IotDeviceMessageService deviceMessageService;
25 32
26 - // TODO @puhui999:这里  
27 @Override 33 @Override
28 public void execute(IotDeviceMessage message, 34 public void execute(IotDeviceMessage message,
29 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { 35 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
30 - //IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();  
31 - //Assert.notNull(control, "设备控制配置不能为空");  
32 - //// 遍历每个设备,下发消息  
33 - //control.getDeviceNames().forEach(deviceName -> {  
34 - // IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);  
35 - // if (device == null) {  
36 - // log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);  
37 - // return;  
38 - // }  
39 - // try {  
40 - // // TODO @芋艿:@puhui999:这块可能要改,从 type => method  
41 - // IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(  
42 - // control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));  
43 - // log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);  
44 - // } catch (Exception e) {  
45 - // log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);  
46 - // }  
47 - //}); 36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行属性设置
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行属性设置
  68 + executePropertySetForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行属性设置
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行属性设置
  91 + for (IotDeviceDO device : devices) {
  92 + executePropertySetForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行属性设置
  98 + */
  99 + private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建属性设置消息
  101 + IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建属性设置消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 属性设置参数格式: {"properties": {"identifier": value}}
  129 + Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
  130 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
  131 + } catch (Exception e) {
  132 + log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
  133 + return null;
  134 + }
48 } 135 }
49 136
50 @Override 137 @Override
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.action;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
  10 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  11 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
  12 +import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Component
  25 +@Slf4j
  26 +public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
  27 +
  28 + @Resource
  29 + private IotDeviceService deviceService;
  30 + @Resource
  31 + private IotDeviceMessageService deviceMessageService;
  32 +
  33 + @Override
  34 + public void execute(IotDeviceMessage message,
  35 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行服务调用
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行服务调用
  68 + executeServiceInvokeForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行服务调用
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行服务调用
  91 + for (IotDeviceDO device : devices) {
  92 + executeServiceInvokeForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行服务调用
  98 + */
  99 + private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建服务调用消息
  101 + IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建服务调用消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
  129 + Object params = Map.of(
  130 + "identifier", actionConfig.getIdentifier(),
  131 + "params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
  132 + );
  133 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
  134 + } catch (Exception e) {
  135 + log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
  136 + return null;
  137 + }
  138 + }
  139 +
  140 + @Override
  141 + public IotSceneRuleActionTypeEnum getType() {
  142 + return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
  143 + }
  144 +
  145 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
@@ -91,7 +91,7 @@ public final class IotSceneRuleMatcherHelper { @@ -91,7 +91,7 @@ public final class IotSceneRuleMatcherHelper {
91 Map<String, Object> springExpressionVariables = new HashMap<>(); 91 Map<String, Object> springExpressionVariables = new HashMap<>();
92 92
93 // 设置源值 93 // 设置源值
94 - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); 94 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
95 95
96 // 处理参数值 96 // 处理参数值
97 if (StrUtil.isNotBlank(paramValue)) { 97 if (StrUtil.isNotBlank(paramValue)) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.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 +
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -7,6 +8,7 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; @@ -7,6 +8,7 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
9 10
  11 +
10 /** 12 /**
11 * 设备属性条件匹配器 13 * 设备属性条件匹配器
12 * <p> 14 * <p>
@@ -43,10 +45,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc @@ -43,10 +45,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
43 return false; 45 return false;
44 } 46 }
45 47
46 - // 2.1. 获取属性值  
47 - Object propertyValue = message.getParams(); 48 + // 2.1. 获取属性值 - 使用工具类方法正确提取属性值
  49 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
48 if (propertyValue == null) { 50 if (propertyValue == null) {
49 - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); 51 + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
50 return false; 52 return false;
51 } 53 }
52 54
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.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.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
@@ -35,8 +36,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher @@ -35,8 +36,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
35 return false; 36 return false;
36 } 37 }
37 38
38 - // 2.1 获取设备状态值  
39 - Object stateValue = message.getParams(); 39 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  40 + // 对于设备状态条件,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  41 + String stateValue = IotDeviceMessageUtils.getIdentifier(message);
40 if (stateValue == null) { 42 if (stateValue == null) {
41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); 43 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
42 return false; 44 return false;
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java
@@ -52,10 +52,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc @@ -52,10 +52,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
52 return false; 52 return false;
53 } 53 }
54 54
55 - // 2.1 获取属性值  
56 - Object propertyValue = message.getParams(); 55 + // 2.1 获取属性值 - 使用工具类方法正确提取属性值
  56 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
57 if (propertyValue == null) { 57 if (propertyValue == null) {
58 - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); 58 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
59 return false; 59 return false;
60 } 60 }
61 61
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
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.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
@@ -43,16 +44,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch @@ -43,16 +44,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
43 return false; 44 return false;
44 } 45 }
45 46
46 - // 2.1 获取设备状态值  
47 - Object stateValue = message.getParams();  
48 - if (stateValue == null) { 47 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  48 + // 对于状态更新消息,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  49 + String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  50 + if (stateIdentifier == null) {
49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); 51 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
50 return false; 52 return false;
51 } 53 }
52 54
53 // 2.2 使用条件评估器进行匹配 55 // 2.2 使用条件评估器进行匹配
54 - // TODO @puhui999: 状态匹配重新实现  
55 - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); 56 + // 状态值通常是字符串或数字,直接使用标识符作为状态值
  57 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
56 if (matched) { 58 if (matched) {
57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 59 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
58 } else { 60 } else {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
  10 +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
  11 +import jakarta.annotation.Resource;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.quartz.SchedulerException;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
  19 +
  20 +/**
  21 + * IoT 场景规则定时触发器处理器
  22 + * <p>
  23 + * 负责管理定时触发器的注册、更新、删除等操作
  24 + *
  25 + * @author HUIHUI
  26 + */
  27 +@Component
  28 +@Slf4j
  29 +public class IotSceneRuleTimerHandler {
  30 +
  31 + @Resource(name = "iotSchedulerManager")
  32 + private IotSchedulerManager schedulerManager;
  33 +
  34 + /**
  35 + * 注册场景规则的定时触发器
  36 + *
  37 + * @param sceneRule 场景规则
  38 + */
  39 + public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
  40 + if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
  41 + return;
  42 + }
  43 +
  44 + // 过滤出定时触发器
  45 + List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
  46 + trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
  47 +
  48 + if (CollUtil.isEmpty(timerTriggers)) {
  49 + return;
  50 + }
  51 +
  52 + // 注册每个定时触发器
  53 + timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
  54 + }
  55 +
  56 + /**
  57 + * 更新场景规则的定时触发器
  58 + *
  59 + * @param sceneRule 场景规则
  60 + */
  61 + public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
  62 + if (sceneRule == null) {
  63 + return;
  64 + }
  65 +
  66 + // 先删除旧的定时任务
  67 + unregisterTimerTriggers(sceneRule.getId());
  68 +
  69 + // 如果场景规则已禁用,则不重新注册
  70 + if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
  71 + log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
  72 + return;
  73 + }
  74 +
  75 + // 重新注册定时触发器
  76 + registerTimerTriggers(sceneRule);
  77 + }
  78 +
  79 + /**
  80 + * 注销场景规则的定时触发器
  81 + *
  82 + * @param sceneRuleId 场景规则ID
  83 + */
  84 + public void unregisterTimerTriggers(Long sceneRuleId) {
  85 + if (sceneRuleId == null) {
  86 + return;
  87 + }
  88 +
  89 + String jobName = buildJobName(sceneRuleId);
  90 + try {
  91 + schedulerManager.deleteJob(jobName);
  92 + log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
  93 + } catch (SchedulerException e) {
  94 + log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
  95 + }
  96 + }
  97 +
  98 + /**
  99 + * 暂停场景规则的定时触发器
  100 + *
  101 + * @param sceneRuleId 场景规则ID
  102 + */
  103 + public void pauseTimerTriggers(Long sceneRuleId) {
  104 + if (sceneRuleId == null) {
  105 + return;
  106 + }
  107 +
  108 + String jobName = buildJobName(sceneRuleId);
  109 + try {
  110 + schedulerManager.pauseJob(jobName);
  111 + log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
  112 + } catch (SchedulerException e) {
  113 + log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
  114 + }
  115 + }
  116 +
  117 + /**
  118 + * 恢复场景规则的定时触发器
  119 + *
  120 + * @param sceneRuleId 场景规则ID
  121 + */
  122 + public void resumeTimerTriggers(Long sceneRuleId) {
  123 + if (sceneRuleId == null) {
  124 + return;
  125 + }
  126 +
  127 + String jobName = buildJobName(sceneRuleId);
  128 + try {
  129 + schedulerManager.resumeJob(jobName);
  130 + log.info("[resumeTimerTriggers][场景规则({}) 定时触发器恢复成功]", sceneRuleId);
  131 + } catch (SchedulerException e) {
  132 + log.error("[resumeTimerTriggers][场景规则({}) 定时触发器恢复失败]", sceneRuleId, e);
  133 + }
  134 + }
  135 +
  136 + /**
  137 + * 注册单个定时触发器
  138 + *
  139 + * @param sceneRule 场景规则
  140 + * @param trigger 定时触发器配置
  141 + */
  142 + private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
  143 + // 1. 参数校验
  144 + if (StrUtil.isBlank(trigger.getCronExpression())) {
  145 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
  146 + return;
  147 + }
  148 +
  149 + // 2. 构建任务名称和数据
  150 + String jobName = buildJobName(sceneRule.getId());
  151 +
  152 + try {
  153 + // 3. 注册定时任务
  154 + schedulerManager.addOrUpdateJob(
  155 + IotSceneRuleJob.class,
  156 + jobName,
  157 + trigger.getCronExpression(),
  158 + IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
  159 + );
  160 +
  161 + log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功,CRON: {}]",
  162 + sceneRule.getId(), trigger.getCronExpression());
  163 + } catch (SchedulerException e) {
  164 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败,CRON: {}]",
  165 + sceneRule.getId(), trigger.getCronExpression(), e);
  166 + }
  167 + }
  168 +
  169 + /**
  170 + * 构建任务名称
  171 + *
  172 + * @param sceneRuleId 场景规则ID
  173 + * @return 任务名称
  174 + */
  175 + private String buildJobName(Long sceneRuleId) {
  176 + return "iot_scene_rule_timer_" + sceneRuleId;
  177 + }
  178 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/BaseMatcherTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
  8 +
  9 +/**
  10 + * Matcher 测试基类
  11 + * 提供通用的 Spring 测试配置
  12 + *
  13 + * @author HUIHUI
  14 + */
  15 +@SpringJUnitConfig
  16 +public abstract class BaseMatcherTest {
  17 +
  18 + /**
  19 + * 注入一下 SpringUtil,解析 EL 表达式时需要
  20 + * {@link SpringExpressionUtils#parseExpression}
  21 + */
  22 + @Configuration
  23 + static class TestConfig {
  24 +
  25 + @Bean
  26 + public SpringUtil springUtil() {
  27 + return new SpringUtil();
  28 + }
  29 +
  30 + }
  31 +
  32 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.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.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 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.BaseMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
8 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
9 -import org.mockito.InjectMocks;  
10 10
11 import java.time.LocalDateTime; 11 import java.time.LocalDateTime;
12 import java.time.ZoneOffset; 12 import java.time.ZoneOffset;
@@ -20,11 +20,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -20,11 +20,15 @@ import static org.junit.jupiter.api.Assertions.*;
20 * 20 *
21 * @author HUIHUI 21 * @author HUIHUI
22 */ 22 */
23 -public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest { 23 +public class CurrentTimeConditionMatcherTest extends BaseMatcherTest {
24 24
25 - @InjectMocks  
26 private CurrentTimeConditionMatcher matcher; 25 private CurrentTimeConditionMatcher matcher;
27 26
  27 + @BeforeEach
  28 + public void setUp() {
  29 + matcher = new CurrentTimeConditionMatcher();
  30 + }
  31 +
28 @Test 32 @Test
29 public void testGetSupportedConditionType() { 33 public void testGetSupportedConditionType() {
30 // 调用 34 // 调用
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.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.hutool.core.map.MapUtil;  
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 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.BaseMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 10
12 import java.util.HashMap; 11 import java.util.HashMap;
13 import java.util.Map; 12 import java.util.Map;
14 13
15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 14 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
16 -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;  
17 import static org.junit.jupiter.api.Assertions.*; 15 import static org.junit.jupiter.api.Assertions.*;
18 16
19 /** 17 /**
@@ -21,11 +19,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -21,11 +19,15 @@ import static org.junit.jupiter.api.Assertions.*;
21 * 19 *
22 * @author HUIHUI 20 * @author HUIHUI
23 */ 21 */
24 -public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { 22 +public class DevicePropertyConditionMatcherTest extends BaseMatcherTest {
25 23
26 - @InjectMocks  
27 private DevicePropertyConditionMatcher matcher; 24 private DevicePropertyConditionMatcher matcher;
28 25
  26 + @BeforeEach
  27 + public void setUp() {
  28 + matcher = new DevicePropertyConditionMatcher();
  29 + }
  30 +
29 @Test 31 @Test
30 public void testGetSupportedConditionType() { 32 public void testGetSupportedConditionType() {
31 // 调用 33 // 调用
@@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
41 int result = matcher.getPriority(); 43 int result = matcher.getPriority();
42 44
43 // 断言 45 // 断言
44 - assertEquals(20, result);  
45 - }  
46 -  
47 - @Test  
48 - public void testIsEnabled() {  
49 - // 调用  
50 - boolean result = matcher.isEnabled();  
51 -  
52 - // 断言  
53 - assertTrue(result); 46 + assertEquals(25, result); // 修正:实际返回值是 25
54 } 47 }
55 48
56 @Test 49 @Test
57 public void testMatches_temperatureEquals_success() { 50 public void testMatches_temperatureEquals_success() {
58 - // 准备参数  
59 - String propertyName = "temperature"; 51 + // 准备参数:创建属性上报消息
  52 + String propertyIdentifier = "temperature";
60 Double propertyValue = 25.5; 53 Double propertyValue = 25.5;
61 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
62 - IotDeviceMessage message = createDeviceMessage(properties); 54 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
63 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 55 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
64 - propertyName, 56 + propertyIdentifier,
65 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 57 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
66 String.valueOf(propertyValue) 58 String.valueOf(propertyValue)
67 ); 59 );
@@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
75 67
76 @Test 68 @Test
77 public void testMatches_humidityGreaterThan_success() { 69 public void testMatches_humidityGreaterThan_success() {
78 - // 准备参数  
79 - String propertyName = "humidity"; 70 + // 准备参数:创建属性上报消息
  71 + String propertyIdentifier = "humidity";
80 Integer propertyValue = 75; 72 Integer propertyValue = 75;
81 Integer compareValue = 70; 73 Integer compareValue = 70;
82 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
83 - IotDeviceMessage message = createDeviceMessage(properties); 74 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
84 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 75 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
85 - propertyName, 76 + propertyIdentifier,
86 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 77 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
87 String.valueOf(compareValue) 78 String.valueOf(compareValue)
88 ); 79 );
@@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
96 87
97 @Test 88 @Test
98 public void testMatches_pressureLessThan_success() { 89 public void testMatches_pressureLessThan_success() {
99 - // 准备参数  
100 - String propertyName = "pressure"; 90 + // 准备参数:创建属性上报消息
  91 + String propertyIdentifier = "pressure";
101 Double propertyValue = 1010.5; 92 Double propertyValue = 1010.5;
102 Integer compareValue = 1020; 93 Integer compareValue = 1020;
103 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
104 - IotDeviceMessage message = createDeviceMessage(properties); 94 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
105 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 95 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
106 - propertyName, 96 + propertyIdentifier,
107 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), 97 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
108 String.valueOf(compareValue) 98 String.valueOf(compareValue)
109 ); 99 );
@@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
117 107
118 @Test 108 @Test
119 public void testMatches_statusNotEquals_success() { 109 public void testMatches_statusNotEquals_success() {
120 - // 准备参数  
121 - String propertyName = "status"; 110 + // 准备参数:创建属性上报消息
  111 + String propertyIdentifier = "status";
122 String propertyValue = "active"; 112 String propertyValue = "active";
123 String compareValue = "inactive"; 113 String compareValue = "inactive";
124 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
125 - IotDeviceMessage message = createDeviceMessage(properties); 114 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
126 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 115 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
127 - propertyName, 116 + propertyIdentifier,
128 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), 117 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
129 compareValue 118 compareValue
130 ); 119 );
@@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
138 127
139 @Test 128 @Test
140 public void testMatches_propertyMismatch_fail() { 129 public void testMatches_propertyMismatch_fail() {
141 - // 准备参数  
142 - String propertyName = "temperature"; 130 + // 准备参数:创建属性上报消息,值不满足条件
  131 + String propertyIdentifier = "temperature";
143 Double propertyValue = 15.0; 132 Double propertyValue = 15.0;
144 Integer compareValue = 20; 133 Integer compareValue = 20;
145 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
146 - IotDeviceMessage message = createDeviceMessage(properties); 134 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
147 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 135 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
148 - propertyName, 136 + propertyIdentifier,
149 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 137 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
150 String.valueOf(compareValue) 138 String.valueOf(compareValue)
151 ); 139 );
@@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
158 } 146 }
159 147
160 @Test 148 @Test
161 - public void testMatches_propertyNotFound_fail() {  
162 - // 准备参数  
163 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
164 - IotDeviceMessage message = createDeviceMessage(properties); 149 + public void testMatches_identifierMismatch_fail() {
  150 + // 准备参数:标识符不匹配
  151 + String messageIdentifier = "temperature";
  152 + String conditionIdentifier = "humidity";
  153 + Double propertyValue = 25.5;
  154 + IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
165 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 155 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
166 - randomString(), // 随机不存在的属性名  
167 - IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),  
168 - "50" 156 + conditionIdentifier,
  157 + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  158 + String.valueOf(propertyValue)
169 ); 159 );
170 160
171 // 调用 161 // 调用
@@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
178 @Test 168 @Test
179 public void testMatches_nullCondition_fail() { 169 public void testMatches_nullCondition_fail() {
180 // 准备参数 170 // 准备参数
181 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
182 - IotDeviceMessage message = createDeviceMessage(properties); 171 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
183 172
184 // 调用 173 // 调用
185 boolean result = matcher.matches(message, null); 174 boolean result = matcher.matches(message, null);
@@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
191 @Test 180 @Test
192 public void testMatches_nullConditionType_fail() { 181 public void testMatches_nullConditionType_fail() {
193 // 准备参数 182 // 准备参数
194 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
195 - IotDeviceMessage message = createDeviceMessage(properties); 183 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
196 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 184 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
197 condition.setType(null); 185 condition.setType(null);
198 186
@@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
206 @Test 194 @Test
207 public void testMatches_missingIdentifier_fail() { 195 public void testMatches_missingIdentifier_fail() {
208 // 准备参数 196 // 准备参数
209 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
210 - IotDeviceMessage message = createDeviceMessage(properties); 197 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
211 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 198 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
212 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 199 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
213 condition.setIdentifier(null); // 缺少标识符 200 condition.setIdentifier(null); // 缺少标识符
@@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
224 @Test 211 @Test
225 public void testMatches_missingOperator_fail() { 212 public void testMatches_missingOperator_fail() {
226 // 准备参数 213 // 准备参数
227 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
228 - IotDeviceMessage message = createDeviceMessage(properties); 214 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
229 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 215 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
230 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 216 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
231 condition.setIdentifier("temperature"); 217 condition.setIdentifier("temperature");
@@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
242 @Test 228 @Test
243 public void testMatches_missingParam_fail() { 229 public void testMatches_missingParam_fail() {
244 // 准备参数 230 // 准备参数
245 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
246 - IotDeviceMessage message = createDeviceMessage(properties); 231 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
247 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 232 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
248 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 233 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
249 condition.setIdentifier("temperature"); 234 condition.setIdentifier("temperature");
@@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
275 260
276 @Test 261 @Test
277 public void testMatches_nullDeviceProperties_fail() { 262 public void testMatches_nullDeviceProperties_fail() {
278 - // 准备参数 263 + // 准备参数:消息的 params 为 null
279 IotDeviceMessage message = new IotDeviceMessage(); 264 IotDeviceMessage message = new IotDeviceMessage();
280 message.setParams(null); 265 message.setParams(null);
281 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 266 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
292 } 277 }
293 278
294 @Test 279 @Test
  280 + public void testMatches_propertiesStructure_success() {
  281 + // 测试使用 properties 结构的消息(真实的属性上报场景)
  282 + String identifier = "temperature";
  283 + Double propertyValue = 25.5;
  284 + IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
  285 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  286 + identifier,
  287 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  288 + "20"
  289 + );
  290 +
  291 + // 调用
  292 + boolean result = matcher.matches(message, condition);
  293 +
  294 + // 断言:修复后的实现应该能正确从 properties 中提取属性值
  295 + assertTrue(result);
  296 + }
  297 +
  298 + @Test
  299 + public void testMatches_simpleValueMessage_success() {
  300 + // 测试简单值消息(params 直接是属性值)
  301 + Double propertyValue = 25.5;
  302 + IotDeviceMessage message = createSimpleValueMessage(propertyValue);
  303 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  304 + "any", // 对于简单值消息,标识符匹配会被跳过
  305 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  306 + "20"
  307 + );
  308 +
  309 + // 调用
  310 + boolean result = matcher.matches(message, condition);
  311 +
  312 + // 断言:修复后的实现应该能处理简单值消息
  313 + // 但由于标识符匹配失败,结果为 false
  314 + assertFalse(result);
  315 + }
  316 +
  317 + @Test
  318 + public void testMatches_valueFieldStructure_success() {
  319 + // 测试使用 value 字段的消息结构
  320 + String identifier = "temperature";
  321 + Double propertyValue = 25.5;
  322 +
  323 + IotDeviceMessage message = new IotDeviceMessage();
  324 + message.setDeviceId(randomLongId());
  325 + message.setMethod("thing.event.post");
  326 +
  327 + Map<String, Object> params = new HashMap<>();
  328 + params.put("identifier", identifier);
  329 + params.put("value", propertyValue);
  330 + message.setParams(params);
  331 +
  332 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  333 + identifier,
  334 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  335 + "20"
  336 + );
  337 +
  338 + // 调用
  339 + boolean result = matcher.matches(message, condition);
  340 +
  341 + // 断言:修复后的实现应该能从 value 字段提取属性值
  342 + assertTrue(result);
  343 + }
  344 +
  345 + @Test
295 public void testMatches_voltageGreaterThanOrEquals_success() { 346 public void testMatches_voltageGreaterThanOrEquals_success() {
296 - // 准备参数  
297 - String propertyName = "voltage"; 347 + // 准备参数:创建属性上报消息
  348 + String propertyIdentifier = "voltage";
298 Double propertyValue = 12.0; 349 Double propertyValue = 12.0;
299 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
300 - IotDeviceMessage message = createDeviceMessage(properties); 350 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
301 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 351 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
302 - propertyName, 352 + propertyIdentifier,
303 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), 353 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
304 String.valueOf(propertyValue) 354 String.valueOf(propertyValue)
305 ); 355 );
@@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
313 363
314 @Test 364 @Test
315 public void testMatches_currentLessThanOrEquals_success() { 365 public void testMatches_currentLessThanOrEquals_success() {
316 - // 准备参数  
317 - String propertyName = "current"; 366 + // 准备参数:创建属性上报消息
  367 + String propertyIdentifier = "current";
318 Double propertyValue = 2.5; 368 Double propertyValue = 2.5;
319 Double compareValue = 3.0; 369 Double compareValue = 3.0;
320 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
321 - IotDeviceMessage message = createDeviceMessage(properties); 370 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
322 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 371 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
323 - propertyName, 372 + propertyIdentifier,
324 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), 373 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
325 String.valueOf(compareValue) 374 String.valueOf(compareValue)
326 ); 375 );
@@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
334 383
335 @Test 384 @Test
336 public void testMatches_stringProperty_success() { 385 public void testMatches_stringProperty_success() {
337 - // 准备参数  
338 - String propertyName = "mode"; 386 + // 准备参数:创建属性上报消息
  387 + String propertyIdentifier = "mode";
339 String propertyValue = "auto"; 388 String propertyValue = "auto";
340 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
341 - IotDeviceMessage message = createDeviceMessage(properties); 389 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
342 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 390 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
343 - propertyName, 391 + propertyIdentifier,
344 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 392 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
345 propertyValue 393 propertyValue
346 ); 394 );
@@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
354 402
355 @Test 403 @Test
356 public void testMatches_booleanProperty_success() { 404 public void testMatches_booleanProperty_success() {
357 - // 准备参数  
358 - String propertyName = "enabled"; 405 + // 准备参数:创建属性上报消息
  406 + String propertyIdentifier = "enabled";
359 Boolean propertyValue = true; 407 Boolean propertyValue = true;
360 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
361 - IotDeviceMessage message = createDeviceMessage(properties); 408 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
362 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 409 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
363 - propertyName, 410 + propertyIdentifier,
364 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 411 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
365 String.valueOf(propertyValue) 412 String.valueOf(propertyValue)
366 ); 413 );
@@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
372 assertTrue(result); 419 assertTrue(result);
373 } 420 }
374 421
375 - @Test  
376 - public void testMatches_multipleProperties_success() {  
377 - // 准备参数  
378 - Map<String, Object> properties = MapUtil.builder(new HashMap<String, Object>())  
379 - .put("temperature", 25.5)  
380 - .put("humidity", 60)  
381 - .put("status", "active")  
382 - .put("enabled", true)  
383 - .build();  
384 - IotDeviceMessage message = createDeviceMessage(properties);  
385 - String targetProperty = "humidity";  
386 - Integer targetValue = 60;  
387 - IotSceneRuleDO.TriggerCondition condition = createValidCondition(  
388 - targetProperty,  
389 - IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),  
390 - String.valueOf(targetValue)  
391 - ); 422 + // ========== 辅助方法 ==========
392 423
393 - // 调用  
394 - boolean result = matcher.matches(message, condition); 424 + /**
  425 + * 创建设备消息用于测试
  426 + *
  427 + * 支持的消息格式:
  428 + * 1. 直接属性值:params 直接是属性值(适用于简单消息)
  429 + * 2. 标识符+值:params 包含 identifier 和对应的属性值
  430 + * 3. properties 结构:params.properties[identifier] = value
  431 + * 4. data 结构:params.data[identifier] = value
  432 + * 5. value 字段:params.value = value
  433 + */
  434 + private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
  435 + IotDeviceMessage message = new IotDeviceMessage();
  436 + message.setDeviceId(randomLongId());
  437 + message.setMethod("thing.event.post"); // 使用事件上报方法
395 438
396 - // 断言  
397 - assertTrue(result); 439 + // 创建符合修复后逻辑的 params 结构
  440 + Map<String, Object> params = new HashMap<>();
  441 + params.put("identifier", identifier);
  442 + // 直接将属性值放在标识符对应的字段中
  443 + params.put(identifier, value);
  444 + message.setParams(params);
  445 +
  446 + return message;
398 } 447 }
399 448
400 - // ========== 辅助方法 ========== 449 + /**
  450 + * 创建使用 properties 结构的消息(模拟真实的属性上报消息)
  451 + */
  452 + private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
  453 + IotDeviceMessage message = new IotDeviceMessage();
  454 + message.setDeviceId(randomLongId());
  455 + message.setMethod("thing.property.post"); // 属性上报方法
  456 +
  457 + Map<String, Object> properties = new HashMap<>();
  458 + properties.put(identifier, value);
  459 +
  460 + Map<String, Object> params = new HashMap<>();
  461 + params.put("properties", properties);
  462 + message.setParams(params);
  463 +
  464 + return message;
  465 + }
401 466
402 /** 467 /**
403 - * 创建设备消息 468 + * 创建简单值消息(params 直接是属性值)
404 */ 469 */
405 - private IotDeviceMessage createDeviceMessage(Map<String, Object> properties) { 470 + private IotDeviceMessage createSimpleValueMessage(Object value) {
406 IotDeviceMessage message = new IotDeviceMessage(); 471 IotDeviceMessage message = new IotDeviceMessage();
407 message.setDeviceId(randomLongId()); 472 message.setDeviceId(randomLongId());
408 - message.setParams(properties); 473 + message.setMethod("thing.property.post");
  474 + // 直接将属性值作为 params
  475 + message.setParams(value);
  476 +
409 return message; 477 return message;
410 } 478 }
411 479
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.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.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@@ -18,11 +18,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -18,11 +18,15 @@ import static org.junit.jupiter.api.Assertions.*;
18 * 18 *
19 * @author HUIHUI 19 * @author HUIHUI
20 */ 20 */
21 -public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest { 21 +public class DeviceStateConditionMatcherTest extends BaseMatcherTest {
22 22
23 - @InjectMocks  
24 private DeviceStateConditionMatcher matcher; 23 private DeviceStateConditionMatcher matcher;
25 24
  25 + @BeforeEach
  26 + public void setUp() {
  27 + matcher = new DeviceStateConditionMatcher();
  28 + }
  29 +
26 @Test 30 @Test
27 public void testGetSupportedConditionType() { 31 public void testGetSupportedConditionType() {
28 // 调用 32 // 调用
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.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.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 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.BaseMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*;
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class DeviceEventPostTriggerMatcherTest extends BaseMatcherTest {
26 26
27 - @InjectMocks  
28 private DeviceEventPostTriggerMatcher matcher; 27 private DeviceEventPostTriggerMatcher matcher;
29 28
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new DeviceEventPostTriggerMatcher();
  32 + }
  33 +
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
32 // 准备参数 36 // 准备参数
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.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.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 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.BaseMatcherTest;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks;  
12 12
13 import java.util.HashMap; 13 import java.util.HashMap;
14 import java.util.Map; 14 import java.util.Map;
@@ -24,11 +24,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -24,11 +24,15 @@ import static org.junit.jupiter.api.Assertions.*;
24 * 24 *
25 * @author HUIHUI 25 * @author HUIHUI
26 */ 26 */
27 -public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest { 27 +public class DevicePropertyPostTriggerMatcherTest extends BaseMatcherTest {
28 28
29 - @InjectMocks  
30 private DevicePropertyPostTriggerMatcher matcher; 29 private DevicePropertyPostTriggerMatcher matcher;
31 30
  31 + @BeforeEach
  32 + public void setUp() {
  33 + matcher = new DevicePropertyPostTriggerMatcher();
  34 + }
  35 +
32 @Test 36 @Test
33 public void testGetSupportedTriggerType_success() { 37 public void testGetSupportedTriggerType_success() {
34 // 准备参数 38 // 准备参数
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.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.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 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.BaseMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -22,11 +22,15 @@ import static org.junit.jupiter.api.Assertions.*;
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class DeviceServiceInvokeTriggerMatcherTest extends BaseMatcherTest {
26 26
27 - @InjectMocks  
28 private DeviceServiceInvokeTriggerMatcher matcher; 27 private DeviceServiceInvokeTriggerMatcher matcher;
29 28
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new DeviceServiceInvokeTriggerMatcher();
  32 + }
  33 +
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
32 // 准备参数 36 // 准备参数
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.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.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 3 +import cn.hutool.extra.spring.SpringUtil;
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.enums.IotDeviceStateEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
6 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.dal.dataobject.rule.IotSceneRuleDO; 7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks; 12 +import org.springframework.context.annotation.Bean;
  13 +import org.springframework.context.annotation.Configuration;
  14 +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
12 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 org.junit.jupiter.api.Assertions.*; 17 import static org.junit.jupiter.api.Assertions.*;
@@ -18,11 +21,24 @@ import static org.junit.jupiter.api.Assertions.*; @@ -18,11 +21,24 @@ import static org.junit.jupiter.api.Assertions.*;
18 * 21 *
19 * @author HUIHUI 22 * @author HUIHUI
20 */ 23 */
21 -public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest { 24 +@SpringJUnitConfig(DeviceStateUpdateTriggerMatcherTest.TestConfig.class)
  25 +public class DeviceStateUpdateTriggerMatcherTest {
  26 +
  27 + @Configuration
  28 + static class TestConfig {
  29 + @Bean
  30 + public SpringUtil springUtil() {
  31 + return new SpringUtil();
  32 + }
  33 + }
22 34
23 - @InjectMocks  
24 private DeviceStateUpdateTriggerMatcher matcher; 35 private DeviceStateUpdateTriggerMatcher matcher;
25 36
  37 + @BeforeEach
  38 + public void setUp() {
  39 + matcher = new DeviceStateUpdateTriggerMatcher();
  40 + }
  41 +
26 @Test 42 @Test
27 public void testGetSupportedTriggerType_success() { 43 public void testGetSupportedTriggerType_success() {
28 // 准备参数 44 // 准备参数
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.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.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 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.BaseMatcherTest;
  7 +import org.junit.jupiter.api.BeforeEach;
7 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
8 -import org.mockito.InjectMocks;  
9 9
10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@@ -16,11 +16,15 @@ import static org.junit.jupiter.api.Assertions.*; @@ -16,11 +16,15 @@ import static org.junit.jupiter.api.Assertions.*;
16 * 16 *
17 * @author HUIHUI 17 * @author HUIHUI
18 */ 18 */
19 -public class TimerTriggerMatcherTest extends BaseMockitoUnitTest { 19 +public class TimerTriggerMatcherTest extends BaseMatcherTest {
20 20
21 - @InjectMocks  
22 private TimerTriggerMatcher matcher; 21 private TimerTriggerMatcher matcher;
23 22
  23 + @BeforeEach
  24 + public void setUp() {
  25 + matcher = new TimerTriggerMatcher();
  26 + }
  27 +
24 @Test 28 @Test
25 public void testGetSupportedTriggerType_success() { 29 public void testGetSupportedTriggerType_success() {
26 // 准备参数 30 // 准备参数
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -69,6 +69,83 @@ public class IotDeviceMessageUtils { @@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
69 return null; 69 return null;
70 } 70 }
71 71
  72 + /**
  73 + * 从设备消息中提取指定标识符的属性值
  74 + * - 支持多种消息格式和属性值提取策略
  75 + * - 兼容现有的消息结构
  76 + * - 提供统一的属性值提取接口
  77 + * <p>
  78 + * 支持的提取策略(按优先级顺序):
  79 + * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息)
  80 + * 2. 标识符字段:从 params[identifier] 获取
  81 + * 3. properties 结构:从 params.properties[identifier] 获取(标准属性上报)
  82 + * 4. data 结构:从 params.data[identifier] 获取
  83 + * 5. value 字段:从 params.value 获取(单值消息)
  84 + * 6. 单值 Map:如果 Map 只包含 identifier 和一个值,返回该值
  85 + *
  86 + * @param message 设备消息
  87 + * @param identifier 属性标识符
  88 + * @return 属性值,如果未找到则返回 null
  89 + */
  90 + @SuppressWarnings("unchecked")
  91 + public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
  92 + Object params = message.getParams();
  93 + if (params == null) {
  94 + return null;
  95 + }
  96 +
  97 + // 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
  98 + if (!(params instanceof Map)) {
  99 + return params;
  100 + }
  101 +
  102 + Map<String, Object> paramsMap = (Map<String, Object>) params;
  103 +
  104 + // 策略2:直接通过标识符获取属性值
  105 + Object directValue = paramsMap.get(identifier);
  106 + if (directValue != null) {
  107 + return directValue;
  108 + }
  109 +
  110 + // 策略3:从 properties 字段中获取(适用于标准属性上报消息)
  111 + Object properties = paramsMap.get("properties");
  112 + if (properties instanceof Map) {
  113 + Map<String, Object> propertiesMap = (Map<String, Object>) properties;
  114 + Object propertyValue = propertiesMap.get(identifier);
  115 + if (propertyValue != null) {
  116 + return propertyValue;
  117 + }
  118 + }
  119 +
  120 + // 策略4:从 data 字段中获取(适用于某些消息格式)
  121 + Object data = paramsMap.get("data");
  122 + if (data instanceof Map) {
  123 + Map<String, Object> dataMap = (Map<String, Object>) data;
  124 + Object dataValue = dataMap.get(identifier);
  125 + if (dataValue != null) {
  126 + return dataValue;
  127 + }
  128 + }
  129 +
  130 + // 策略5:从 value 字段中获取(适用于单值消息)
  131 + Object value = paramsMap.get("value");
  132 + if (value != null) {
  133 + return value;
  134 + }
  135 +
  136 + // 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
  137 + if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
  138 + for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
  139 + if (!"identifier".equals(entry.getKey())) {
  140 + return entry.getValue();
  141 + }
  142 + }
  143 + }
  144 +
  145 + // 未找到对应的属性值
  146 + return null;
  147 + }
  148 +
72 // ========== Topic 相关 ========== 149 // ========== Topic 相关 ==========
73 150
74 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { 151 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import java.util.HashMap;
  7 +import java.util.Map;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.assertEquals;
  10 +import static org.junit.jupiter.api.Assertions.assertNull;
  11 +
  12 +/**
  13 + * {@link IotDeviceMessageUtils} 的单元测试
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +public class IotDeviceMessageUtilsTest {
  18 +
  19 + @Test
  20 + public void testExtractPropertyValue_directValue() {
  21 + // 测试直接值(非 Map)
  22 + IotDeviceMessage message = new IotDeviceMessage();
  23 + message.setParams(25.5);
  24 +
  25 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  26 + assertEquals(25.5, result);
  27 + }
  28 +
  29 + @Test
  30 + public void testExtractPropertyValue_directIdentifier() {
  31 + // 测试直接通过标识符获取
  32 + IotDeviceMessage message = new IotDeviceMessage();
  33 + Map<String, Object> params = new HashMap<>();
  34 + params.put("temperature", 25.5);
  35 + message.setParams(params);
  36 +
  37 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  38 + assertEquals(25.5, result);
  39 + }
  40 +
  41 + @Test
  42 + public void testExtractPropertyValue_propertiesStructure() {
  43 + // 测试 properties 结构
  44 + IotDeviceMessage message = new IotDeviceMessage();
  45 + Map<String, Object> properties = new HashMap<>();
  46 + properties.put("temperature", 25.5);
  47 + properties.put("humidity", 60);
  48 +
  49 + Map<String, Object> params = new HashMap<>();
  50 + params.put("properties", properties);
  51 + message.setParams(params);
  52 +
  53 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  54 + assertEquals(25.5, result);
  55 + }
  56 +
  57 + @Test
  58 + public void testExtractPropertyValue_dataStructure() {
  59 + // 测试 data 结构
  60 + IotDeviceMessage message = new IotDeviceMessage();
  61 + Map<String, Object> data = new HashMap<>();
  62 + data.put("temperature", 25.5);
  63 +
  64 + Map<String, Object> params = new HashMap<>();
  65 + params.put("data", data);
  66 + message.setParams(params);
  67 +
  68 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  69 + assertEquals(25.5, result);
  70 + }
  71 +
  72 + @Test
  73 + public void testExtractPropertyValue_valueField() {
  74 + // 测试 value 字段
  75 + IotDeviceMessage message = new IotDeviceMessage();
  76 + Map<String, Object> params = new HashMap<>();
  77 + params.put("identifier", "temperature");
  78 + params.put("value", 25.5);
  79 + message.setParams(params);
  80 +
  81 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  82 + assertEquals(25.5, result);
  83 + }
  84 +
  85 + @Test
  86 + public void testExtractPropertyValue_singleValueMap() {
  87 + // 测试单值 Map(包含 identifier 和一个值)
  88 + IotDeviceMessage message = new IotDeviceMessage();
  89 + Map<String, Object> params = new HashMap<>();
  90 + params.put("identifier", "temperature");
  91 + params.put("actualValue", 25.5);
  92 + message.setParams(params);
  93 +
  94 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  95 + assertEquals(25.5, result);
  96 + }
  97 +
  98 + @Test
  99 + public void testExtractPropertyValue_notFound() {
  100 + // 测试未找到属性值
  101 + IotDeviceMessage message = new IotDeviceMessage();
  102 + Map<String, Object> params = new HashMap<>();
  103 + params.put("humidity", 60);
  104 + message.setParams(params);
  105 +
  106 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  107 + assertNull(result);
  108 + }
  109 +
  110 + @Test
  111 + public void testExtractPropertyValue_nullParams() {
  112 + // 测试 params 为 null
  113 + IotDeviceMessage message = new IotDeviceMessage();
  114 + message.setParams(null);
  115 +
  116 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  117 + assertNull(result);
  118 + }
  119 +
  120 + @Test
  121 + public void testExtractPropertyValue_priorityOrder() {
  122 + // 测试优先级顺序:直接标识符 > properties > data > value
  123 + IotDeviceMessage message = new IotDeviceMessage();
  124 +
  125 + Map<String, Object> properties = new HashMap<>();
  126 + properties.put("temperature", 20.0);
  127 +
  128 + Map<String, Object> data = new HashMap<>();
  129 + data.put("temperature", 30.0);
  130 +
  131 + Map<String, Object> params = new HashMap<>();
  132 + params.put("temperature", 25.5); // 最高优先级
  133 + params.put("properties", properties);
  134 + params.put("data", data);
  135 + params.put("value", 40.0);
  136 + message.setParams(params);
  137 +
  138 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  139 + assertEquals(25.5, result); // 应该返回直接标识符的值
  140 + }
  141 +}