From 5754c9a21ddb0dcaa7dfc989d55a04d9026eb8b7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 08:43:29 +0800 Subject: [PATCH] ✨ feat(iot):实现场景联动告警触发的消息发送 --- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java | 20 +++++++++++++++++--- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java | 7 ++++++- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java | 16 +++++----------- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 373 insertions(+), 24 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java index d70aea5..c29ff25 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.enums.alert; +import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -15,12 +16,21 @@ import java.util.Arrays; @Getter public enum IotAlertReceiveTypeEnum implements ArrayValuable { - SMS(1), // 短信 - MAIL(2), // 邮箱 - NOTIFY(3); // 站内信 + SMS(1, "iot_alert_sms"), // 短信 + MAIL(2, "iot_alert_mail"), // 邮箱 + NOTIFY(3, "iot_alert_notify"); // 站内信 // TODO 待实现(欢迎 pull request):webhook 4 + /** + * 接收方式 + */ private final Integer type; + /** + * 模板编号 + * + * 关联 SmsTemplateDO / MailTemplateDO / NotifyTemplateDO 的 code 属性 + */ + private final String templateCode; public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotAlertReceiveTypeEnum::getType).toArray(Integer[]::new); @@ -29,4 +39,8 @@ public enum IotAlertReceiveTypeEnum implements ArrayValuable { return ARRAYS; } + public static IotAlertReceiveTypeEnum of(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java index 68a2da9..8733b34 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertReco import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; import java.util.Collection; @@ -58,8 +60,11 @@ public interface IotAlertRecordService { * @param config 告警配置 * @param sceneRuleId 场景规则编号 * @param deviceMessage 设备消息,可为空 + * @param device 设备信息,可为空;用于回填产品编号、设备编号 * @return 告警记录编号 */ - Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, IotDeviceMessage deviceMessage); + @SuppressWarnings("UnusedReturnValue") + Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, + @Nullable IotDeviceMessage deviceMessage, @Nullable IotDeviceDO device); } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java index 34a673a..1a4e353 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.mysql.alert.IotAlertRecordMapper; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -28,9 +27,6 @@ public class IotAlertRecordServiceImpl implements IotAlertRecordService { @Resource private IotAlertRecordMapper alertRecordMapper; - @Resource - private IotDeviceService deviceService; - @Override public IotAlertRecordDO getAlertRecord(Long id) { return alertRecordMapper.selectById(id); @@ -57,20 +53,18 @@ public class IotAlertRecordServiceImpl implements IotAlertRecordService { } @Override - public Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, IotDeviceMessage message) { + public Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, + IotDeviceMessage message, IotDeviceDO device) { // 构建告警记录 IotAlertRecordDO.IotAlertRecordDOBuilder builder = IotAlertRecordDO.builder() .configId(config.getId()).configName(config.getName()).configLevel(config.getLevel()) .sceneRuleId(sceneRuleId).processStatus(false); if (message != null) { builder.deviceMessage(message); - // 填充设备信息 - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); - if (device != null) { - builder.productId(device.getProductId()).deviceId(device.getId()); - } } - + if (device != null) { + builder.productId(device.getProductId()).deviceId(device.getId()); + } // 插入记录 IotAlertRecordDO record = builder.build(); alertRecordMapper.insert(record); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java index 5ff4a61..bc26436 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java @@ -1,21 +1,34 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.action; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; +import cn.iocoder.yudao.module.iot.enums.alert.IotAlertReceiveTypeEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; import cn.iocoder.yudao.module.iot.service.alert.IotAlertConfigService; import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.system.api.mail.MailSendApi; +import cn.iocoder.yudao.module.system.api.mail.dto.MailSendSingleToUserReqDTO; import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import jakarta.annotation.Nullable; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import javax.annotation.Nullable; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * IoT 告警触发的 {@link IotSceneRuleAction} 实现类 @@ -23,12 +36,15 @@ import java.util.List; * @author 芋道源码 */ @Component +@Slf4j public class IotAlertTriggerSceneRuleAction implements IotSceneRuleAction { @Resource private IotAlertConfigService alertConfigService; @Resource private IotAlertRecordService alertRecordService; + @Resource + private IotDeviceService deviceService; @Resource private SmsSendApi smsSendApi; @@ -45,19 +61,67 @@ public class IotAlertTriggerSceneRuleAction implements IotSceneRuleAction { if (CollUtil.isEmpty(alertConfigs)) { return; } + // 获得设备信息 + IotDeviceDO device = message != null ? deviceService.getDeviceFromCache(message.getDeviceId()) : null; alertConfigs.forEach(alertConfig -> { - // 记录告警记录,传递场景规则ID - alertRecordService.createAlertRecord(alertConfig, rule.getId(), message); + // 创建告警记录 + alertRecordService.createAlertRecord(alertConfig, rule.getId(), message, device); // 发送告警消息 - sendAlertMessage(alertConfig, message); + sendAlertMessage(alertConfig, message, device); }); } - private void sendAlertMessage(IotAlertConfigDO config, IotDeviceMessage deviceMessage) { - // TODO @芋艿:等场景联动开发完,再实现 - // TODO @芋艿:短信 - // TODO @芋艿:邮箱 - // TODO @芋艿:站内信 + private void sendAlertMessage(IotAlertConfigDO config, + @Nullable IotDeviceMessage deviceMessage, + @Nullable IotDeviceDO device) { + if (CollUtil.isEmpty(config.getReceiveUserIds()) || CollUtil.isEmpty(config.getReceiveTypes())) { + return; + } + Map templateParams = buildTemplateParams(config, deviceMessage, device); + config.getReceiveUserIds().forEach(userId -> + config.getReceiveTypes().forEach(receiveType -> sendAlertMessageToUser(userId, receiveType, templateParams))); + } + + /** + * 按指定接收方式,给单个用户发送告警消息 + */ + private void sendAlertMessageToUser(Long userId, Integer receiveType, Map templateParams) { + IotAlertReceiveTypeEnum typeEnum = IotAlertReceiveTypeEnum.of(receiveType); + if (typeEnum == null) { + return; + } + try { + switch (typeEnum) { + case SMS: + smsSendApi.sendSingleSmsToAdmin(new SmsSendSingleToUserReqDTO().setUserId(userId) + .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)); + break; + case MAIL: + mailSendApi.sendSingleMailToAdmin(new MailSendSingleToUserReqDTO().setUserId(userId) + .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)); + break; + case NOTIFY: + notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO().setUserId(userId) + .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)); + break; + } + } catch (Exception ex) { + log.error("[sendAlertMessageToUser][用户({}) 模板参数({}) 发送 {} 告警失败]", + userId, templateParams, typeEnum, ex); + } + } + + private Map buildTemplateParams(IotAlertConfigDO config, + @Nullable IotDeviceMessage deviceMessage, + @Nullable IotDeviceDO device) { + Map params = new HashMap<>(); + params.put("configName", config.getName()); + params.put("configDescription", config.getDescription()); + params.put("configLevel", DictFrameworkUtils.parseDictDataLabel(DictTypeConstants.ALERT_LEVEL, config.getLevel())); + params.put("deviceName", device != null ? device.getDeviceName() : null); + params.put("reportTime", deviceMessage != null + ? LocalDateTimeUtil.format(deviceMessage.getReportTime(), DatePattern.NORM_DATETIME_PATTERN) : null); + return params; } @Override diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java new file mode 100644 index 0000000..6bce069 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java @@ -0,0 +1,272 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.action; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.alert.IotAlertReceiveTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertConfigService; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.system.api.mail.MailSendApi; +import cn.iocoder.yudao.module.system.api.mail.dto.MailSendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link IotAlertTriggerSceneRuleAction} 的单元测试 + * + * @author 芋道源码 + */ +public class IotAlertTriggerSceneRuleActionTest extends BaseMockitoUnitTest { + + @InjectMocks + private IotAlertTriggerSceneRuleAction action; + + @Mock + private IotAlertConfigService alertConfigService; + @Mock + private IotAlertRecordService alertRecordService; + @Mock + private IotDeviceService deviceService; + + @Mock + private SmsSendApi smsSendApi; + @Mock + private MailSendApi mailSendApi; + @Mock + private NotifyMessageSendApi notifyMessageSendApi; + + @Test + public void testGetType() { + // 调用并断言 + assertEquals(IotSceneRuleActionTypeEnum.ALERT_TRIGGER, action.getType()); + } + + @Test + public void testExecute_noAlertConfigs() throws Exception { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + + // mock 行为:返回空列表 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.emptyList()); + + // 调用 + action.execute(message, rule, actionConfig); + + // 断言:不查设备、不创建记录、不发消息 + verify(deviceService, never()).getDeviceFromCache(anyLong()); + verify(alertRecordService, never()).createAlertRecord(any(), any(), any(), any()); + verify(smsSendApi, never()).sendSingleSmsToAdmin(any()); + verify(mailSendApi, never()).sendSingleMailToAdmin(any()); + verify(notifyMessageSendApi, never()).sendSingleMessageToAdmin(any()); + } + + @Test + public void testExecute_deviceTrigger_sendAllChannels() throws Exception { + // 准备参数 + Long userId = randomLongId(); + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + IotAlertConfigDO config = randomPojo(IotAlertConfigDO.class, c -> { + c.setReceiveUserIds(Collections.singletonList(userId)); + c.setReceiveTypes(Arrays.asList( + IotAlertReceiveTypeEnum.SMS.getType(), + IotAlertReceiveTypeEnum.MAIL.getType(), + IotAlertReceiveTypeEnum.NOTIFY.getType())); + }); + IotDeviceDO device = randomPojo(IotDeviceDO.class); + + // mock 行为 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.singletonList(config)); + when(deviceService.getDeviceFromCache(message.getDeviceId())).thenReturn(device); + + // 调用(mockStatic 需包住整个调用链;buildTemplateParams 内有 DictFrameworkUtils 静态调用) + try (MockedStatic dictMock = mockStatic(DictFrameworkUtils.class)) { + dictMock.when(() -> DictFrameworkUtils.parseDictDataLabel(any(), any(Integer.class))) + .thenReturn("WARN"); + action.execute(message, rule, actionConfig); + } + + // 断言:设备只查一次 + verify(deviceService, times(1)).getDeviceFromCache(message.getDeviceId()); + // 断言:告警记录创建一次,参数透传 + verify(alertRecordService, times(1)) + .createAlertRecord(eq(config), eq(rule.getId()), eq(message), eq(device)); + // 断言:三条通道各发一次,模板编号匹配 + ArgumentCaptor smsCaptor = ArgumentCaptor.forClass(SmsSendSingleToUserReqDTO.class); + verify(smsSendApi, times(1)).sendSingleSmsToAdmin(smsCaptor.capture()); + assertEquals(userId, smsCaptor.getValue().getUserId()); + assertEquals(IotAlertReceiveTypeEnum.SMS.getTemplateCode(), smsCaptor.getValue().getTemplateCode()); + ArgumentCaptor mailCaptor = ArgumentCaptor.forClass(MailSendSingleToUserReqDTO.class); + verify(mailSendApi, times(1)).sendSingleMailToAdmin(mailCaptor.capture()); + assertEquals(IotAlertReceiveTypeEnum.MAIL.getTemplateCode(), mailCaptor.getValue().getTemplateCode()); + ArgumentCaptor notifyCaptor = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); + verify(notifyMessageSendApi, times(1)).sendSingleMessageToAdmin(notifyCaptor.capture()); + assertEquals(IotAlertReceiveTypeEnum.NOTIFY.getTemplateCode(), notifyCaptor.getValue().getTemplateCode()); + } + + @Test + public void testExecute_timerTrigger_skipDeviceLookup() throws Exception { + // 准备参数:定时触发,message 为 null + Long userId = randomLongId(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + IotAlertConfigDO config = randomPojo(IotAlertConfigDO.class, c -> { + c.setReceiveUserIds(Collections.singletonList(userId)); + c.setReceiveTypes(Collections.singletonList(IotAlertReceiveTypeEnum.NOTIFY.getType())); + }); + + // mock 行为 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.singletonList(config)); + + // 调用 + try (MockedStatic dictMock = mockStatic(DictFrameworkUtils.class)) { + dictMock.when(() -> DictFrameworkUtils.parseDictDataLabel(any(), any(Integer.class))) + .thenReturn("INFO"); + action.execute(null, rule, actionConfig); + } + + // 断言:跳过设备查询;message 与 device 都用 null 创建告警记录 + verify(deviceService, never()).getDeviceFromCache(anyLong()); + verify(alertRecordService, times(1)) + .createAlertRecord(eq(config), eq(rule.getId()), eq(null), eq(null)); + verify(notifyMessageSendApi, times(1)).sendSingleMessageToAdmin(any(NotifySendSingleToUserReqDTO.class)); + } + + @Test + public void testExecute_emptyReceiveUsers_skipSend() throws Exception { + // 准备参数:接收用户为空 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + IotAlertConfigDO config = randomPojo(IotAlertConfigDO.class, c -> { + c.setReceiveUserIds(Collections.emptyList()); + c.setReceiveTypes(Collections.singletonList(IotAlertReceiveTypeEnum.SMS.getType())); + }); + IotDeviceDO device = randomPojo(IotDeviceDO.class); + + // mock 行为 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.singletonList(config)); + when(deviceService.getDeviceFromCache(message.getDeviceId())).thenReturn(device); + + // 调用 + action.execute(message, rule, actionConfig); + + // 断言:告警记录仍然创建,但不发送任何消息 + verify(alertRecordService, times(1)) + .createAlertRecord(eq(config), eq(rule.getId()), eq(message), eq(device)); + verify(smsSendApi, never()).sendSingleSmsToAdmin(any()); + } + + @Test + public void testExecute_unknownReceiveType_skipSend() throws Exception { + // 准备参数:接收类型为未知值 + Long userId = randomLongId(); + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + IotAlertConfigDO config = randomPojo(IotAlertConfigDO.class, c -> { + c.setReceiveUserIds(Collections.singletonList(userId)); + c.setReceiveTypes(Collections.singletonList(99)); + }); + IotDeviceDO device = randomPojo(IotDeviceDO.class); + + // mock 行为 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.singletonList(config)); + when(deviceService.getDeviceFromCache(message.getDeviceId())).thenReturn(device); + + // 调用 + try (MockedStatic dictMock = mockStatic(DictFrameworkUtils.class)) { + dictMock.when(() -> DictFrameworkUtils.parseDictDataLabel(any(), any(Integer.class))) + .thenReturn("WARN"); + action.execute(message, rule, actionConfig); + } + + // 断言:未知类型不发送 + verify(smsSendApi, never()).sendSingleSmsToAdmin(any()); + verify(mailSendApi, never()).sendSingleMailToAdmin(any()); + verify(notifyMessageSendApi, never()).sendSingleMessageToAdmin(any()); + } + + @Test + public void testExecute_smsFailure_doesNotBlockOthers() throws Exception { + // 准备参数 + Long userId = randomLongId(); + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO rule = randomPojo(IotSceneRuleDO.class); + IotSceneRuleDO.Action actionConfig = randomPojo(IotSceneRuleDO.Action.class); + IotAlertConfigDO config = randomPojo(IotAlertConfigDO.class, c -> { + c.setReceiveUserIds(Collections.singletonList(userId)); + c.setReceiveTypes(Arrays.asList( + IotAlertReceiveTypeEnum.SMS.getType(), + IotAlertReceiveTypeEnum.MAIL.getType())); + }); + IotDeviceDO device = randomPojo(IotDeviceDO.class); + + // mock 行为:sms 抛异常 + when(alertConfigService.getAlertConfigListBySceneRuleIdAndStatus(rule.getId(), CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(Collections.singletonList(config)); + when(deviceService.getDeviceFromCache(message.getDeviceId())).thenReturn(device); + when(smsSendApi.sendSingleSmsToAdmin(any())).thenThrow(new RuntimeException("sms 渠道异常")); + + // 调用 + try (MockedStatic dictMock = mockStatic(DictFrameworkUtils.class)) { + dictMock.when(() -> DictFrameworkUtils.parseDictDataLabel(any(), any(Integer.class))) + .thenReturn("ERROR"); + action.execute(message, rule, actionConfig); + } + + // 断言:sms 抛错时邮件依旧发送 + verify(smsSendApi, times(1)).sendSingleSmsToAdmin(any()); + verify(mailSendApi, times(1)).sendSingleMailToAdmin(any()); + } + + /** + * 创建带 reportTime 的设备消息 + */ + private IotDeviceMessage createDeviceMessage() { + IotDeviceMessage message = new IotDeviceMessage(); + message.setId(randomString()); + message.setDeviceId(randomLongId()); + message.setReportTime(LocalDateTime.now()); + return message; + } + +} -- libgit2 0.21.4