diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java deleted file mode 100644 index 8f85da966a..0000000000 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; - -@Schema(description = "管理后台 - 流程实例的打印数据 Response VO") -@Data -public class BpmProcessPrintDataRespVO { - - @Schema(description = "流程实例数据") - private BpmProcessInstanceRespVO processInstance; - - @Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - private Boolean printTemplateEnable; - - @Schema(description = "自定义打印模板 HTML") - private String printTemplateHtml; - - @Schema(description = "审批任务列表") - private List tasks; - - @Schema(description = "流程任务") - @Data - public static class Task { - - @Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") - private String id; - - @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") - private String name; - - @Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png") - private String signPicUrl; - - @Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED) - private String description; // 该字段由后端拼接 - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java deleted file mode 100644 index 3d96f11ceb..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; - -import lombok.Data; - -/** - * IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类 - * - * @author HUIHUI - */ -@Data -public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig { - - /** - * TCP 服务器地址 - */ - private String host; - - /** - * TCP 服务器端口 - */ - private Integer port; - - /** - * 连接超时时间(毫秒) - */ - private Integer connectTimeoutMs = 5000; - - /** - * 读取超时时间(毫秒) - */ - private Integer readTimeoutMs = 10000; - - /** - * 是否启用 SSL - */ - private Boolean ssl = false; - - /** - * SSL 证书路径(当 ssl=true 时需要) - */ - private String sslCertPath; - - /** - * 数据格式:JSON 或 BINARY - */ - private String dataFormat = "JSON"; - - /** - * 心跳间隔时间(毫秒),0 表示不启用心跳 - */ - private Long heartbeatIntervalMs = 30000L; - - /** - * 重连间隔时间(毫秒) - */ - private Long reconnectIntervalMs = 5000L; - - /** - * 最大重连次数 - */ - private Integer maxReconnectAttempts = 3; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java deleted file mode 100644 index f1b7e86d86..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; - -import lombok.Data; - -/** - * IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类 - *

- * 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器 - * 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接 - * - * @author HUIHUI - */ -@Data -public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig { - - /** - * WebSocket 服务器地址 - * 例如:ws://localhost:8080/ws 或 wss://example.com/ws - */ - private String serverUrl; - - /** - * 连接超时时间(毫秒) - */ - private Integer connectTimeoutMs = 5000; - - /** - * 发送超时时间(毫秒) - */ - private Integer sendTimeoutMs = 10000; - - /** - * 心跳间隔时间(毫秒),0 表示不启用心跳 - */ - private Long heartbeatIntervalMs = 30000L; - - /** - * 心跳消息内容(JSON 格式) - */ - private String heartbeatMessage = "{\"type\":\"heartbeat\"}"; - - /** - * 子协议列表(逗号分隔) - */ - private String subprotocols; - - /** - * 自定义请求头(JSON 格式) - */ - private String customHeaders; - - /** - * 是否启用 SSL 证书验证(仅对 wss:// 生效) - */ - private Boolean verifySslCert = true; - - /** - * 数据格式:JSON 或 TEXT - */ - private String dataFormat = "JSON"; - - /** - * 重连间隔时间(毫秒) - */ - private Long reconnectIntervalMs = 5000L; - - /** - * 最大重连次数 - */ - private Integer maxReconnectAttempts = 3; - - /** - * 是否启用压缩 - */ - private Boolean enableCompression = false; - - /** - * 消息发送重试次数 - */ - private Integer sendRetryCount = 1; - - /** - * 消息发送重试间隔(毫秒) - */ - private Long sendRetryIntervalMs = 1000L; - -} \ 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/rule/data/action/IotTcpDataRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java deleted file mode 100644 index 4db6dc205a..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.time.Duration; - -/** - * TCP 的 {@link IotDataRuleAction} 实现类 - *

- * 负责将设备消息发送到外部 TCP 服务器 - * 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式 - * 使用连接池管理 TCP 连接,提高性能和资源利用率 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotTcpDataRuleAction extends - IotDataRuleCacheableAction { - - private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5); - private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10); - - @Override - public Integer getType() { - return IotDataSinkTypeEnum.TCP.getType(); - } - - @Override - protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception { - // 1.1 参数校验 - if (config.getHost() == null || config.getHost().trim().isEmpty()) { - throw new IllegalArgumentException("TCP 服务器地址不能为空"); - } - if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) { - throw new IllegalArgumentException("TCP 服务器端口无效"); - } - - // 2.1 创建 TCP 客户端 - IotTcpClient tcpClient = new IotTcpClient( - config.getHost(), - config.getPort(), - config.getConnectTimeoutMs(), - config.getReadTimeoutMs(), - config.getSsl(), - config.getSslCertPath(), - config.getDataFormat() - ); - // 2.2 连接服务器 - tcpClient.connect(); - log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]", - config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat()); - return tcpClient; - } - - @Override - protected void closeProducer(IotTcpClient producer) throws Exception { - if (producer != null) { - producer.close(); - } - } - - @Override - protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception { - try { - // 1.1 获取或创建 TCP 客户端 - IotTcpClient tcpClient = getProducer(config); - // 1.2 检查连接状态,如果断开则重新连接 - if (!tcpClient.isConnected()) { - log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort()); - tcpClient.connect(); - } - - // 2.1 发送消息并等待结果 - tcpClient.sendMessage(message); - // 2.2 记录发送成功日志 - log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]", - message, config, config.getHost(), config.getPort()); - } catch (Exception e) { - log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]", - message, config, config.getHost(), config.getPort(), e); - throw e; - } - } - -} \ 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/rule/data/action/tcp/IotTcpClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java deleted file mode 100644 index 1618532a4a..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java +++ /dev/null @@ -1,184 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp; - -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; - -import javax.net.ssl.SSLSocketFactory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * IoT TCP 客户端 - *

- * 负责与外部 TCP 服务器建立连接并发送设备消息 - * 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接 - * - * @author HUIHUI - */ -@Slf4j -public class IotTcpClient { - - private final String host; - private final Integer port; - private final Integer connectTimeoutMs; - private final Integer readTimeoutMs; - private final Boolean ssl; - private final String sslCertPath; - private final String dataFormat; - - private Socket socket; - private OutputStream outputStream; - private BufferedReader reader; - private final AtomicBoolean connected = new AtomicBoolean(false); - - // TODO @puhui999:default 值,IotDataSinkTcpConfig.java 枚举起来哈; - public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs, - Boolean ssl, String sslCertPath, String dataFormat) { - this.host = host; - this.port = port; - this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000; - this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000; - this.ssl = ssl != null ? ssl : false; - this.sslCertPath = sslCertPath; - this.dataFormat = dataFormat != null ? dataFormat : "JSON"; - } - - /** - * 连接到 TCP 服务器 - */ - public void connect() throws Exception { - if (connected.get()) { - log.warn("[connect][TCP 客户端已经连接,无需重复连接]"); - return; - } - - try { - if (ssl) { - // SSL 连接 - SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - socket = sslSocketFactory.createSocket(); - } else { - // 普通连接 - socket = new Socket(); - } - - // 连接服务器 - socket.connect(new InetSocketAddress(host, port), connectTimeoutMs); - socket.setSoTimeout(readTimeoutMs); - - // 获取输入输出流 - outputStream = socket.getOutputStream(); - reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); - - // 更新状态 - connected.set(true); - log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port); - } catch (Exception e) { - close(); - log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e); - throw e; - } - } - - /** - * 发送设备消息 - * - * @param message 设备消息 - * @throws Exception 发送异常 - */ - public void sendMessage(IotDeviceMessage message) throws Exception { - if (!connected.get()) { - throw new IllegalStateException("TCP 客户端未连接"); - } - - try { - // TODO @puhui999:枚举值 - String messageData; - if ("JSON".equalsIgnoreCase(dataFormat)) { - // JSON 格式 - messageData = JsonUtils.toJsonString(message); - } else { - // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议) - messageData = message.toString(); - } - - // 发送消息 - outputStream.write(messageData.getBytes(StandardCharsets.UTF_8)); - outputStream.write('\n'); // 添加换行符作为消息分隔符 - outputStream.flush(); - log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]", - message.getDeviceId(), messageData.length()); - } catch (Exception e) { - log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e); - throw e; - } - } - - /** - * 关闭连接 - */ - public void close() { - if (!connected.get()) { - return; - } - - try { - // 关闭资源 - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - log.warn("[close][关闭输入流失败]", e); - } - } - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - log.warn("[close][关闭输出流失败]", e); - } - } - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - log.warn("[close][关闭 Socket 失败]", e); - } - } - - // 更新状态 - connected.set(false); - log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port); - } catch (Exception e) { - log.error("[close][关闭 TCP 客户端连接异常]", e); - } - } - - /** - * 检查连接状态 - * - * @return 是否已连接 - */ - public boolean isConnected() { - return connected.get() && socket != null && !socket.isClosed(); - } - - @Override - public String toString() { - return "IotTcpClient{" + - "host='" + host + '\'' + - ", port=" + port + - ", ssl=" + ssl + - ", dataFormat='" + dataFormat + '\'' + - ", connected=" + connected.get() + - '}'; - } - -} \ 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/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java deleted file mode 100644 index d0b1ffea58..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java +++ /dev/null @@ -1,146 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.action; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -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.rule.IotSceneRuleActionTypeEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.Collections; -import java.util.List; - -/** - * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction { - - @Resource - private IotDeviceService deviceService; - @Resource - private IotDeviceMessageService deviceMessageService; - - @Override - public void execute(IotDeviceMessage message, - IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { - // 1. 参数校验 - if (actionConfig.getDeviceId() == null) { - log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig); - return; - } - if (StrUtil.isEmpty(actionConfig.getIdentifier())) { - log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig); - return; - } - - // 2. 判断是否为全部设备 - if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) { - executeForAllDevices(message, rule, actionConfig); - } else { - executeForSingleDevice(message, rule, actionConfig); - } - } - - /** - * 为单个设备执行服务调用 - */ - private void executeForSingleDevice(IotDeviceMessage message, - IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { - // 1. 获取设备信息 - IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId()); - if (device == null) { - log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]", - rule.getId(), actionConfig, actionConfig.getDeviceId()); - return; - } - - // 2. 执行服务调用 - executeServiceInvokeForDevice(rule, actionConfig, device); - } - - /** - * 为产品下的所有设备执行服务调用 - */ - private void executeForAllDevices(IotDeviceMessage message, - IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { - // 1. 参数校验 - if (actionConfig.getProductId() == null) { - log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig); - return; - } - - // 2. 获取产品下的所有设备 - List devices = deviceService.getDeviceListByProductId(actionConfig.getProductId()); - if (CollUtil.isEmpty(devices)) { - log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]", - rule.getId(), actionConfig, actionConfig.getProductId()); - return; - } - - // 3. 遍历所有设备执行服务调用 - for (IotDeviceDO device : devices) { - executeServiceInvokeForDevice(rule, actionConfig, device); - } - } - - /** - * 为指定设备执行服务调用 - */ - private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) { - // 1. 构建服务调用消息 - IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device); - if (downstreamMessage == null) { - log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]", - rule.getId(), actionConfig, device.getId()); - return; - } - - // 2. 发送设备消息 - try { - IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device); - log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]", - rule.getId(), actionConfig, device.getId(), result.getId()); - } catch (Exception e) { - log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]", - rule.getId(), actionConfig, device.getId(), e); - } - } - - /** - * 构建服务调用消息 - * - * @param actionConfig 动作配置 - * @param device 设备信息 - * @return 设备消息 - */ - private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) { - try { - // 服务调用参数格式: {"identifier": "serviceId", "params": {...}} - Object params = MapUtil.builder() - .put("identifier", actionConfig.getIdentifier()) - .put("params", actionConfig.getParams() != null ? actionConfig.getParams() : Collections.emptyMap()) - .build(); - return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params); - } catch (Exception e) { - log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e); - return null; - } - } - - @Override - public IotSceneRuleActionTypeEnum getType() { - return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java deleted file mode 100644 index d27ed66733..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.timer; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; -import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob; -import lombok.extern.slf4j.Slf4j; -import org.quartz.SchedulerException; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; - -/** - * IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotSceneRuleTimerHandler { - - @Resource(name = "iotSchedulerManager") - private IotSchedulerManager schedulerManager; - - /** - * 注册场景规则的定时触发器 - * - * @param sceneRule 场景规则 - */ - public void registerTimerTriggers(IotSceneRuleDO sceneRule) { - // 1. 过滤出定时触发器 - if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) { - return; - } - List timerTriggers = filterList(sceneRule.getTriggers(), - trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); - if (CollUtil.isEmpty(timerTriggers)) { - return; - } - - // 2. 注册每个定时触发器 - timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger)); - } - - /** - * 更新场景规则的定时触发器 - * - * @param sceneRule 场景规则 - */ - public void updateTimerTriggers(IotSceneRuleDO sceneRule) { - if (sceneRule == null) { - return; - } - - // 1. 先删除旧的定时任务 - unregisterTimerTriggers(sceneRule.getId()); - - // 2.1 如果场景规则已禁用,则不重新注册 - if (CommonStatusEnum.isDisable(sceneRule.getStatus())) { - log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId()); - return; - } - - // 2.2 重新注册定时触发器 - registerTimerTriggers(sceneRule); - } - - /** - * 注销场景规则的定时触发器 - * - * @param sceneRuleId 场景规则 ID - */ - public void unregisterTimerTriggers(Long sceneRuleId) { - if (sceneRuleId == null) { - return; - } - - String jobName = buildJobName(sceneRuleId); - try { - schedulerManager.deleteJob(jobName); - log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId); - } catch (SchedulerException e) { - log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e); - } - } - - /** - * 暂停场景规则的定时触发器 - * - * @param sceneRuleId 场景规则 ID - */ - public void pauseTimerTriggers(Long sceneRuleId) { - if (sceneRuleId == null) { - return; - } - - String jobName = buildJobName(sceneRuleId); - try { - schedulerManager.pauseJob(jobName); - log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId); - } catch (SchedulerException e) { - log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e); - } - } - - /** - * 注册单个定时触发器 - * - * @param sceneRule 场景规则 - * @param trigger 定时触发器配置 - */ - private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) { - // 1. 参数校验 - if (StrUtil.isBlank(trigger.getCronExpression())) { - log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId()); - return; - } - - try { - // 2.1 构建任务名称和数据 - String jobName = buildJobName(sceneRule.getId()); - // 2.2 注册定时任务 - schedulerManager.addOrUpdateJob( - IotSceneRuleJob.class, - jobName, - trigger.getCronExpression(), - IotSceneRuleJob.buildJobDataMap(sceneRule.getId()) - ); - log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功,CRON: {}]", - sceneRule.getId(), trigger.getCronExpression()); - } catch (SchedulerException e) { - log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败,CRON: {}]", - sceneRule.getId(), trigger.getCronExpression(), e); - } - } - - /** - * 构建任务名称 - * - * @param sceneRuleId 场景规则 ID - * @return 任务名称 - */ - private String buildJobName(Long sceneRuleId) { - return "iot_scene_rule_timer_" + sceneRuleId; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java deleted file mode 100644 index e37af78333..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action; - -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; -import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * {@link IotTcpDataRuleAction} 的单元测试 - * - * @author HUIHUI - */ -class IotTcpDataRuleActionTest { - - private IotTcpDataRuleAction tcpDataRuleAction; - - @Mock - private IotTcpClient mockTcpClient; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - tcpDataRuleAction = new IotTcpDataRuleAction(); - } - - @Test - public void testGetType() { - // 准备参数 - Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值 - - // 调用方法 - Integer actualType = tcpDataRuleAction.getType(); - - // 断言结果 - assertEquals(expectedType, actualType); - } - - // TODO @puhui999:_ 后面是小写哈,单测的命名规则。 - @Test - public void testInitProducer_Success() throws Exception { - // 准备参数 - IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); - config.setHost("localhost"); - config.setPort(8080); - config.setDataFormat("JSON"); - config.setSsl(false); - - // 调用方法 & 断言结果 - // 此测试需要实际的 TCP 连接,在单元测试中可能不可用 - // 目前我们只验证配置是否有效 - assertNotNull(config.getHost()); - assertTrue(config.getPort() > 0 && config.getPort() <= 65535); - } - - @Test - public void testInitProducer_InvalidHost() { - // 准备参数 - IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); - config.setHost(""); - config.setPort(8080); - - // 调用方法 & 断言结果 - IotTcpDataRuleAction action = new IotTcpDataRuleAction(); - - // 测试验证逻辑(通常在 initProducer 方法中) - assertThrows(IllegalArgumentException.class, () -> { - if (config.getHost() == null || config.getHost().trim().isEmpty()) { - throw new IllegalArgumentException("TCP 服务器地址不能为空"); - } - }); - } - - @Test - public void testInitProducer_InvalidPort() { - // 准备参数 - IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); - config.setHost("localhost"); - config.setPort(-1); - - // 调用方法 & 断言结果 - assertThrows(IllegalArgumentException.class, () -> { - if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) { - throw new IllegalArgumentException("TCP 服务器端口无效"); - } - }); - } - - @Test - public void testCloseProducer() throws Exception { - // 准备参数 - IotTcpClient client = mock(IotTcpClient.class); - - // 调用方法 - tcpDataRuleAction.closeProducer(client); - - // 断言结果 - verify(client, times(1)).close(); - } - - @Test - public void testExecute_WithValidConfig() { - // 准备参数 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report", - "{\"temperature\": 25.5, \"humidity\": 60}"); - - IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); - config.setHost("localhost"); - config.setPort(8080); - config.setDataFormat("JSON"); - - // 调用方法 & 断言结果 - // 通常这需要实际的 TCP 连接 - // 在单元测试中,我们只验证输入参数 - assertNotNull(message); - assertNotNull(config); - assertNotNull(config.getHost()); - assertTrue(config.getPort() > 0); - } - - @Test - public void testConfig_DefaultValues() { - // 准备参数 - IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); - - // 调用方法 & 断言结果 - // 验证默认值 - assertEquals("JSON", config.getDataFormat()); - assertEquals(5000, config.getConnectTimeoutMs()); - assertEquals(10000, config.getReadTimeoutMs()); - assertEquals(false, config.getSsl()); - assertEquals(30000L, config.getHeartbeatIntervalMs()); - assertEquals(5000L, config.getReconnectIntervalMs()); - assertEquals(3, config.getMaxReconnectAttempts()); - } - - @Test - public void testMessageSerialization() { - // 准备参数 - IotDeviceMessage message = IotDeviceMessage.builder() - .deviceId(123L) - .method("thing.property.report") - .params("{\"temperature\": 25.5}") - .build(); - - // 调用方法 - String json = JsonUtils.toJsonString(message); - - // 断言结果 - assertNotNull(json); - assertTrue(json.contains("\"deviceId\":123")); - assertTrue(json.contains("\"method\":\"thing.property.report\"")); - assertTrue(json.contains("\"temperature\":25.5")); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java deleted file mode 100644 index 5be63b57d2..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; - -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -/** - * Matcher 测试基类 - * 提供通用的 Spring 测试配置 - * - * @author HUIHUI - */ -@SpringJUnitConfig -public abstract class IotBaseConditionMatcherTest { - - /** - * 注入一下 SpringUtil,解析 EL 表达式时需要 - * {@link SpringExpressionUtils#parseExpression} - */ - @Configuration - static class TestConfig { - - @Bean - public SpringUtil springUtil() { - return new SpringUtil(); - } - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java deleted file mode 100644 index 3e38e93616..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.timer; - -import cn.hutool.core.collection.ListUtil; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; -import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.quartz.SchedulerException; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -/** - * {@link IotSceneRuleTimerHandler} 的单元测试类 - * - * @author HUIHUI - */ -@ExtendWith(MockitoExtension.class) -public class IotSceneRuleTimerHandlerTest { - - @Mock - private IotSchedulerManager schedulerManager; - - @InjectMocks - private IotSceneRuleTimerHandler timerHandler; - - @BeforeEach - void setUp() { - // 重置 Mock 对象 - reset(schedulerManager); - } - - @Test - public void testRegisterTimerTriggers_success() throws SchedulerException { - // 准备参数 - Long sceneRuleId = 1L; - IotSceneRuleDO sceneRule = new IotSceneRuleDO(); - sceneRule.setId(sceneRuleId); - sceneRule.setStatus(0); // 0 表示启用 - // 创建定时触发器 - IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger(); - timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); - timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点 - sceneRule.setTriggers(ListUtil.toList(timerTrigger)); - - // 调用 - timerHandler.registerTimerTriggers(sceneRule); - - // 验证 - verify(schedulerManager, times(1)).addOrUpdateJob( - eq(IotSceneRuleJob.class), - eq("iot_scene_rule_timer_" + sceneRuleId), - eq("0 0 12 * * ?"), - eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId)) - ); - } - - @Test - public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException { - // 准备参数 - 没有定时触发器 - IotSceneRuleDO sceneRule = new IotSceneRuleDO(); - sceneRule.setStatus(0); // 0 表示启用 - // 创建非定时触发器 - IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger(); - deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); - sceneRule.setTriggers(ListUtil.toList(deviceTrigger)); - - // 调用 - timerHandler.registerTimerTriggers(sceneRule); - - // 验证 - 不应该调用调度器 - verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any()); - } - - @Test - public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException { - // 准备参数 - CRON 表达式为空 - Long sceneRuleId = 2L; - IotSceneRuleDO sceneRule = new IotSceneRuleDO(); - sceneRule.setId(sceneRuleId); - sceneRule.setStatus(0); // 0 表示启用 - // 创建定时触发器但没有 CRON 表达式 - IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger(); - timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); - timerTrigger.setCronExpression(""); // 空的 CRON 表达式 - sceneRule.setTriggers(ListUtil.toList(timerTrigger)); - - // 调用 - timerHandler.registerTimerTriggers(sceneRule); - - // 验证 - 不应该调用调度器 - verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any()); - } - - @Test - public void testUnregisterTimerTriggers_success() throws SchedulerException { - // 准备参数 - Long sceneRuleId = 3L; - - // 调用 - timerHandler.unregisterTimerTriggers(sceneRuleId); - - // 验证 - verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId); - } - - @Test - public void testPauseTimerTriggers_success() throws SchedulerException { - // 准备参数 - Long sceneRuleId = 4L; - - // 调用 - timerHandler.pauseTimerTriggers(sceneRuleId); - - // 验证 - verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java deleted file mode 100644 index a6d669d170..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.util; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -/** - * {@link IotDeviceMessageUtils} 的单元测试 - * - * @author HUIHUI - */ -public class IotDeviceMessageUtilsTest { - - @Test - public void testExtractPropertyValue_directValue() { - // 测试直接值(非 Map) - IotDeviceMessage message = new IotDeviceMessage(); - message.setParams(25.5); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_directIdentifier() { - // 测试直接通过标识符获取 - IotDeviceMessage message = new IotDeviceMessage(); - Map params = new HashMap<>(); - params.put("temperature", 25.5); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_propertiesStructure() { - // 测试 properties 结构 - IotDeviceMessage message = new IotDeviceMessage(); - Map properties = new HashMap<>(); - properties.put("temperature", 25.5); - properties.put("humidity", 60); - - Map params = new HashMap<>(); - params.put("properties", properties); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_dataStructure() { - // 测试 data 结构 - IotDeviceMessage message = new IotDeviceMessage(); - Map data = new HashMap<>(); - data.put("temperature", 25.5); - - Map params = new HashMap<>(); - params.put("data", data); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_valueField() { - // 测试 value 字段 - IotDeviceMessage message = new IotDeviceMessage(); - Map params = new HashMap<>(); - params.put("identifier", "temperature"); - params.put("value", 25.5); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_singleValueMap() { - // 测试单值 Map(包含 identifier 和一个值) - IotDeviceMessage message = new IotDeviceMessage(); - Map params = new HashMap<>(); - params.put("identifier", "temperature"); - params.put("actualValue", 25.5); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); - } - - @Test - public void testExtractPropertyValue_notFound() { - // 测试未找到属性值 - IotDeviceMessage message = new IotDeviceMessage(); - Map params = new HashMap<>(); - params.put("humidity", 60); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertNull(result); - } - - @Test - public void testExtractPropertyValue_nullParams() { - // 测试 params 为 null - IotDeviceMessage message = new IotDeviceMessage(); - message.setParams(null); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertNull(result); - } - - @Test - public void testExtractPropertyValue_priorityOrder() { - // 测试优先级顺序:直接标识符 > properties > data > value - IotDeviceMessage message = new IotDeviceMessage(); - - Map properties = new HashMap<>(); - properties.put("temperature", 20.0); - - Map data = new HashMap<>(); - data.put("temperature", 30.0); - - Map params = new HashMap<>(); - params.put("temperature", 25.5); // 最高优先级 - params.put("properties", properties); - params.put("data", data); - params.put("value", 40.0); - message.setParams(params); - - Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); - assertEquals(25.5, result); // 应该返回直接标识符的值 - } -}