From 5c04cf0da5dacfadd82cbb7c39f0eb2973d80944 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 29 Jan 2026 23:43:05 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E7=B2=BE=E7=AE=80=E7=89=88=E6=9C=AC=EF=BC=81(?= =?UTF-8?q?=E3=80=83'=E2=96=BD'=E3=80=83)=20v2026.01=20=E5=8F=91=E5=B8=83?= =?UTF-8?q?=EF=BC=9A=E5=A4=A7=E5=A4=A7=E5=A4=A7=E5=A4=A7=E5=AE=8C=E5=96=84?= =?UTF-8?q?=20vben5=20=E7=9A=84=20antd=E3=80=81vben=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=20IoT=20?= =?UTF-8?q?=E5=90=84=E7=A7=8D=E6=8E=A5=E5=85=A5=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/device/IotDeviceBindGatewayReqVO.java | 22 - .../device/IotDeviceUnbindGatewayReqVO.java | 22 - .../action/IotWebSocketDataRuleAction.java | 130 ---- .../action/websocket/IotWebSocketClient.java | 209 ------ .../rule/scene/IotSceneRuleTimeHelper.java | 219 ------- .../timer/IotTimerConditionEvaluator.java | 187 ------ .../data/action/tcp/IotTcpClientTest.java | 151 ----- .../websocket/IotWebSocketClientTest.java | 257 -------- ...ceneRuleTimerConditionIntegrationTest.java | 610 ------------------ .../dto/IotSubDeviceRegisterFullReqDTO.java | 38 -- .../iot/core/topic/IotDeviceIdentity.java | 33 - .../topic/auth/IotDeviceRegisterReqDTO.java | 36 -- .../topic/auth/IotDeviceRegisterRespDTO.java | 35 - .../auth/IotSubDeviceRegisterReqDTO.java | 32 - .../auth/IotSubDeviceRegisterRespDTO.java | 35 - .../topic/event/IotDeviceEventPostReqDTO.java | 54 -- .../module/iot/core/topic/package-info.java | 8 - .../IotDevicePropertyPackPostReqDTO.java | 88 --- .../property/IotDevicePropertyPostReqDTO.java | 36 -- .../topic/topo/IotDeviceTopoAddReqDTO.java | 28 - .../topic/topo/IotDeviceTopoChangeReqDTO.java | 44 -- .../topic/topo/IotDeviceTopoDeleteReqDTO.java | 28 - .../topic/topo/IotDeviceTopoGetReqDTO.java | 16 - .../topic/topo/IotDeviceTopoGetRespDTO.java | 24 - .../coap/IotCoapDownstreamSubscriber.java | 47 -- .../coap/IotCoapUpstreamProtocol.java | 90 --- .../gateway/protocol/coap/package-info.java | 13 - .../coap/router/IotCoapAuthHandler.java | 117 ---- .../coap/router/IotCoapAuthResource.java | 37 -- .../coap/router/IotCoapRegisterHandler.java | 98 --- .../coap/router/IotCoapRegisterResource.java | 33 - .../coap/router/IotCoapUpstreamHandler.java | 110 ---- .../router/IotCoapUpstreamTopicResource.java | 67 -- .../protocol/coap/util/IotCoapUtils.java | 84 --- .../gateway/protocol/emqx/package-info.java | 1 - .../gateway/protocol/http/package-info.java | 6 - .../http/router/IotHttpRegisterHandler.java | 63 -- .../router/IotHttpRegisterSubHandler.java | 67 -- .../gateway/protocol/tcp/package-info.java | 6 - .../udp/IotUdpDownstreamSubscriber.java | 65 -- .../protocol/udp/IotUdpUpstreamProtocol.java | 171 ----- .../udp/manager/IotUdpSessionManager.java | 203 ------ .../gateway/protocol/udp/package-info.java | 6 - .../udp/router/IotUdpDownstreamHandler.java | 70 -- .../udp/router/IotUdpUpstreamHandler.java | 542 ---------------- .../IotWebSocketDownstreamSubscriber.java | 65 -- .../IotWebSocketUpstreamProtocol.java | 111 ---- .../IotWebSocketConnectionManager.java | 149 ----- .../router/IotWebSocketDownstreamHandler.java | 56 -- .../router/IotWebSocketUpstreamHandler.java | 471 -------------- ...rectDeviceCoapProtocolIntegrationTest.java | 227 ------- ...ewayDeviceCoapProtocolIntegrationTest.java | 376 ----------- ...ySubDeviceCoapProtocolIntegrationTest.java | 199 ------ ...rectDeviceHttpProtocolIntegrationTest.java | 182 ------ ...ewayDeviceHttpProtocolIntegrationTest.java | 312 --------- ...ySubDeviceHttpProtocolIntegrationTest.java | 162 ----- ...rectDeviceMqttProtocolIntegrationTest.java | 408 ------------ ...ewayDeviceMqttProtocolIntegrationTest.java | 499 -------------- ...ySubDeviceMqttProtocolIntegrationTest.java | 332 ---------- ...irectDeviceTcpProtocolIntegrationTest.java | 278 -------- ...tewayDeviceTcpProtocolIntegrationTest.java | 398 ------------ ...aySubDeviceTcpProtocolIntegrationTest.java | 245 ------- ...irectDeviceUdpProtocolIntegrationTest.java | 262 -------- ...tewayDeviceUdpProtocolIntegrationTest.java | 351 ---------- ...aySubDeviceUdpProtocolIntegrationTest.java | 206 ------ ...eviceWebSocketProtocolIntegrationTest.java | 322 --------- ...eviceWebSocketProtocolIntegrationTest.java | 452 ------------- ...eviceWebSocketProtocolIntegrationTest.java | 288 --------- 68 files changed, 10589 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClientTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClientTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java deleted file mode 100644 index d94332b116..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.Set; - -@Schema(description = "管理后台 - IoT 设备绑定网关 Request VO") -@Data -public class IotDeviceBindGatewayReqVO { - - @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") - @NotEmpty(message = "子设备编号列表不能为空") - private Set subIds; - - @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") - @NotNull(message = "网关设备编号不能为空") - private Long gatewayId; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java deleted file mode 100644 index a4398c683b..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.Set; - -@Schema(description = "管理后台 - IoT 设备解绑网关 Request VO") -@Data -public class IotDeviceUnbindGatewayReqVO { - - @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") - @NotEmpty(message = "子设备编号列表不能为空") - private Set subIds; - - @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "网关设备编号不能为空") - private Long gatewayId; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java deleted file mode 100644 index 7471642434..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java +++ /dev/null @@ -1,130 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkWebSocketConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.data.action.websocket.IotWebSocketClient; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - -/** - * WebSocket 的 {@link IotDataRuleAction} 实现类 - *

- * 负责将设备消息发送到外部 WebSocket 服务器 - * 支持 ws:// 和 wss:// 协议,支持 JSON 和 TEXT 数据格式 - * 使用连接池管理 WebSocket 连接,提高性能和资源利用率 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotWebSocketDataRuleAction extends - IotDataRuleCacheableAction { - - /** - * 锁等待超时时间(毫秒) - */ - private static final long LOCK_WAIT_TIME_MS = 5000; - - /** - * 重连锁,key 为 WebSocket 服务器地址 - *

- * WebSocket 连接是与特定服务器实例绑定的,使用单机锁即可保证重连的线程安全 - */ - private final ConcurrentHashMap reconnectLocks = new ConcurrentHashMap<>(); - - @Override - public Integer getType() { - return IotDataSinkTypeEnum.WEBSOCKET.getType(); - } - - @Override - protected IotWebSocketClient initProducer(IotDataSinkWebSocketConfig config) throws Exception { - // 1. 参数校验 - if (StrUtil.isBlank(config.getServerUrl())) { - throw new IllegalArgumentException("WebSocket 服务器地址不能为空"); - } - if (!StrUtil.startWithAny(config.getServerUrl(), "ws://", "wss://")) { - throw new IllegalArgumentException("WebSocket 服务器地址必须以 ws:// 或 wss:// 开头"); - } - - // 2.1 创建 WebSocket 客户端 - IotWebSocketClient webSocketClient = new IotWebSocketClient( - config.getServerUrl(), - config.getConnectTimeoutMs(), - config.getSendTimeoutMs(), - config.getDataFormat() - ); - // 2.2 连接服务器 - webSocketClient.connect(); - log.info("[initProducer][WebSocket 客户端创建并连接成功,服务器: {},数据格式: {}]", - config.getServerUrl(), config.getDataFormat()); - return webSocketClient; - } - - @Override - protected void closeProducer(IotWebSocketClient producer) throws Exception { - if (producer != null) { - producer.close(); - } - } - - @Override - protected void execute(IotDeviceMessage message, IotDataSinkWebSocketConfig config) throws Exception { - try { - // 1.1 获取或创建 WebSocket 客户端 - IotWebSocketClient webSocketClient = getProducer(config); - - // 1.2 检查连接状态,如果断开则使用分布式锁保证重连的线程安全 - if (!webSocketClient.isConnected()) { - reconnectWithLock(webSocketClient, config); - } - - // 2.1 发送消息 - webSocketClient.sendMessage(message); - // 2.2 记录发送成功日志 - log.info("[execute][message({}) config({}) 发送成功,WebSocket 服务器: {}]", - message, config, config.getServerUrl()); - } catch (Exception e) { - log.error("[execute][message({}) config({}) 发送失败,WebSocket 服务器: {}]", - message, config, config.getServerUrl(), e); - throw e; - } - } - - // TODO @puhui999:为什么这里要加锁呀? - /** - * 使用锁进行重连,保证同一服务器地址的重连操作线程安全 - * - * @param webSocketClient WebSocket 客户端 - * @param config 配置信息 - */ - private void reconnectWithLock(IotWebSocketClient webSocketClient, IotDataSinkWebSocketConfig config) throws Exception { - ReentrantLock lock = reconnectLocks.computeIfAbsent(config.getServerUrl(), k -> new ReentrantLock()); - boolean acquired = false; - try { - acquired = lock.tryLock(LOCK_WAIT_TIME_MS, TimeUnit.MILLISECONDS); - if (!acquired) { - throw new RuntimeException("获取 WebSocket 重连锁超时,服务器: " + config.getServerUrl()); - } - // 双重检查:获取锁后再次检查连接状态,避免重复连接 - if (!webSocketClient.isConnected()) { - log.warn("[reconnectWithLock][WebSocket 连接已断开,尝试重新连接,服务器: {}]", config.getServerUrl()); - webSocketClient.connect(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("获取 WebSocket 重连锁被中断,服务器: " + config.getServerUrl(), e); - } finally { - if (acquired && lock.isHeldByCurrentThread()) { - lock.unlock(); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java deleted file mode 100644 index 8eba723733..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java +++ /dev/null @@ -1,209 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action.websocket; - -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.IotDataSinkWebSocketConfig; -import lombok.extern.slf4j.Slf4j; -import okhttp3.*; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * IoT WebSocket 客户端 - *

- * 负责与外部 WebSocket 服务器建立连接并发送设备消息 - * 支持 ws:// 和 wss:// 协议,支持 JSON 和 TEXT 数据格式 - * 基于 OkHttp WebSocket 实现,兼容 JDK 8+ - *

- * 注意:该类的线程安全由调用方(IotWebSocketDataRuleAction)通过分布式锁保证 - * - * @author HUIHUI - */ -@Slf4j -public class IotWebSocketClient { - - private final String serverUrl; - private final Integer connectTimeoutMs; - private final Integer sendTimeoutMs; - private final String dataFormat; - - private OkHttpClient okHttpClient; - private volatile WebSocket webSocket; - private final AtomicBoolean connected = new AtomicBoolean(false); - - public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) { - this.serverUrl = serverUrl; - this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkWebSocketConfig.DEFAULT_CONNECT_TIMEOUT_MS; - this.sendTimeoutMs = sendTimeoutMs != null ? sendTimeoutMs : IotDataSinkWebSocketConfig.DEFAULT_SEND_TIMEOUT_MS; - this.dataFormat = dataFormat != null ? dataFormat : IotDataSinkWebSocketConfig.DEFAULT_DATA_FORMAT; - } - - /** - * 连接到 WebSocket 服务器 - *

- * 注意:调用方需要通过分布式锁保证并发安全 - */ - public void connect() throws Exception { - if (connected.get()) { - log.warn("[connect][WebSocket 客户端已经连接,无需重复连接]"); - return; - } - - try { - // 创建 OkHttpClient - okHttpClient = new OkHttpClient.Builder() - .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS) - .readTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS) - .writeTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS) - .build(); - - // 创建 WebSocket 请求 - Request request = new Request.Builder() - .url(serverUrl) - .build(); - - // 使用 CountDownLatch 等待连接完成 - CountDownLatch connectLatch = new CountDownLatch(1); - AtomicBoolean connectSuccess = new AtomicBoolean(false); - // 创建 WebSocket 连接 - webSocket = okHttpClient.newWebSocket(request, new IotWebSocketListener(connectLatch, connectSuccess)); - - // 等待连接完成 - boolean await = connectLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS); - if (!await || !connectSuccess.get()) { - close(); - throw new Exception("WebSocket 连接超时或失败,服务器地址: " + serverUrl); - } - log.info("[connect][WebSocket 客户端连接成功,服务器地址: {}]", serverUrl); - } catch (Exception e) { - close(); - log.error("[connect][WebSocket 客户端连接失败,服务器地址: {}]", serverUrl, e); - throw e; - } - } - - /** - * 发送设备消息 - * - * @param message 设备消息 - * @throws Exception 发送异常 - */ - public void sendMessage(IotDeviceMessage message) throws Exception { - WebSocket ws = this.webSocket; - if (!connected.get() || ws == null) { - throw new IllegalStateException("WebSocket 客户端未连接"); - } - - try { - String messageData; - if (IotDataSinkWebSocketConfig.DEFAULT_DATA_FORMAT.equalsIgnoreCase(dataFormat)) { - messageData = JsonUtils.toJsonString(message); - } else { - messageData = message.toString(); - } - - // 发送消息 - boolean success = ws.send(messageData); - if (!success) { - throw new Exception("WebSocket 发送消息失败,消息队列已满或连接已关闭"); - } - log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]", - message.getDeviceId(), messageData.length()); - } catch (Exception e) { - log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e); - throw e; - } - } - - /** - * 关闭连接 - */ - public void close() { - try { - if (webSocket != null) { - // 发送正常关闭帧,状态码 1000 表示正常关闭 - // TODO @puhui999:有没 1000 的枚举哈?在 okhttp 里 - webSocket.close(1000, "客户端主动关闭"); - webSocket = null; - } - if (okHttpClient != null) { - // 关闭连接池和调度器 - okHttpClient.dispatcher().executorService().shutdown(); - okHttpClient.connectionPool().evictAll(); - okHttpClient = null; - } - connected.set(false); - log.info("[close][WebSocket 客户端连接已关闭,服务器地址: {}]", serverUrl); - } catch (Exception e) { - log.error("[close][关闭 WebSocket 客户端连接异常]", e); - } - } - - /** - * 检查连接状态 - * - * @return 是否已连接 - */ - public boolean isConnected() { - return connected.get() && webSocket != null; - } - - @Override - public String toString() { - return "IotWebSocketClient{" + - "serverUrl='" + serverUrl + '\'' + - ", dataFormat='" + dataFormat + '\'' + - ", connected=" + connected.get() + - '}'; - } - - /** - * OkHttp WebSocket 监听器 - */ - @SuppressWarnings("NullableProblems") - private class IotWebSocketListener extends WebSocketListener { - - private final CountDownLatch connectLatch; - private final AtomicBoolean connectSuccess; - - public IotWebSocketListener(CountDownLatch connectLatch, AtomicBoolean connectSuccess) { - this.connectLatch = connectLatch; - this.connectSuccess = connectSuccess; - } - - @Override - public void onOpen(WebSocket webSocket, Response response) { - connected.set(true); - connectSuccess.set(true); - connectLatch.countDown(); - log.info("[onOpen][WebSocket 连接已打开,服务器: {}]", serverUrl); - } - - @Override - public void onMessage(WebSocket webSocket, String text) { - log.debug("[onMessage][收到消息: {}]", text); - } - - @Override - public void onClosing(WebSocket webSocket, int code, String reason) { - connected.set(false); - log.info("[onClosing][WebSocket 正在关闭,code: {}, reason: {}]", code, reason); - } - - @Override - public void onClosed(WebSocket webSocket, int code, String reason) { - connected.set(false); - log.info("[onClosed][WebSocket 已关闭,code: {}, reason: {}]", code, reason); - } - - @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { - connected.set(false); - connectLatch.countDown(); // 确保连接失败时也释放等待 - log.error("[onFailure][WebSocket 连接失败]", t); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java deleted file mode 100644 index df1ac239b3..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java +++ /dev/null @@ -1,219 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.text.CharPool; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher; -import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.List; - -/** - * IoT 场景规则时间匹配工具类 - *

- * 提供时间条件匹配的通用方法,供 {@link IotCurrentTimeConditionMatcher} 和 {@link IotTimerConditionEvaluator} 共同使用。 - * - * @author HUIHUI - */ -@Slf4j -public class IotSceneRuleTimeHelper { - - /** - * 时间格式化器 - HH:mm:ss - */ - private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); - - /** - * 时间格式化器 - HH:mm - */ - private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm"); - - // TODO @puhui999:可以使用 lombok 简化 - private IotSceneRuleTimeHelper() { - // 工具类,禁止实例化 - } - - /** - * 判断是否为日期时间操作符 - * - * @param operatorEnum 操作符枚举 - * @return 是否为日期时间操作符 - */ - public static boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { - return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN - || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN - || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN; - } - - /** - * 判断是否为时间操作符(包括日期时间操作符和当日时间操作符) - * - * @param operatorEnum 操作符枚举 - * @return 是否为时间操作符 - */ - public static boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { - return operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN - && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN - && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_BETWEEN - && !isDateTimeOperator(operatorEnum); - } - - /** - * 执行时间匹配逻辑 - * - * @param operatorEnum 操作符枚举 - * @param param 参数值 - * @return 是否匹配 - */ - public static boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) { - try { - LocalDateTime now = LocalDateTime.now(); - if (isDateTimeOperator(operatorEnum)) { - // 日期时间匹配(时间戳,秒级) - long currentTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond(); - return matchDateTime(currentTimestamp, operatorEnum, param); - } else { - // 当日时间匹配(HH:mm:ss) - return matchTime(now.toLocalTime(), operatorEnum, param); - } - } catch (Exception e) { - log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e); - return false; - } - } - - /** - * 匹配日期时间(时间戳,秒级) - * - * @param currentTimestamp 当前时间戳 - * @param operatorEnum 操作符枚举 - * @param param 参数值 - * @return 是否匹配 - */ - @SuppressWarnings("EnhancedSwitchMigration") - public static boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, - String param) { - try { - // DATE_TIME_BETWEEN 需要解析两个时间戳,单独处理 - if (operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN) { - return matchDateTimeBetween(currentTimestamp, param); - } - // 其他操作符只需要解析一个时间戳 - long targetTimestamp = Long.parseLong(param); - switch (operatorEnum) { - case DATE_TIME_GREATER_THAN: - return currentTimestamp > targetTimestamp; - case DATE_TIME_LESS_THAN: - return currentTimestamp < targetTimestamp; - default: - log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum); - return false; - } - } catch (Exception e) { - log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e); - return false; - } - } - - /** - * 匹配日期时间区间 - * - * @param currentTimestamp 当前时间戳 - * @param param 参数值(格式:startTimestamp,endTimestamp) - * @return 是否匹配 - */ - public static boolean matchDateTimeBetween(long currentTimestamp, String param) { - List timestampRange = StrUtil.splitTrim(param, CharPool.COMMA); - if (timestampRange.size() != 2) { - log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param); - return false; - } - long startTimestamp = Long.parseLong(timestampRange.get(0).trim()); - long endTimestamp = Long.parseLong(timestampRange.get(1).trim()); - // TODO @puhui999:hutool 里,看看有没 between 方法 - return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp; - } - - /** - * 匹配当日时间(HH:mm:ss 或 HH:mm) - * - * @param currentTime 当前时间 - * @param operatorEnum 操作符枚举 - * @param param 参数值 - * @return 是否匹配 - */ - @SuppressWarnings("EnhancedSwitchMigration") - public static boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, - String param) { - try { - // TIME_BETWEEN 需要解析两个时间,单独处理 - if (operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN) { - return matchTimeBetween(currentTime, param); - } - // 其他操作符只需要解析一个时间 - LocalTime targetTime = parseTime(param); - switch (operatorEnum) { - case TIME_GREATER_THAN: - return currentTime.isAfter(targetTime); - case TIME_LESS_THAN: - return currentTime.isBefore(targetTime); - default: - log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum); - return false; - } - } catch (Exception e) { - log.error("[matchTime][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e); - return false; - } - } - - /** - * 匹配时间区间 - * - * @param currentTime 当前时间 - * @param param 参数值(格式:startTime,endTime) - * @return 是否匹配 - */ - public static boolean matchTimeBetween(LocalTime currentTime, String param) { - List timeRange = StrUtil.splitTrim(param, CharPool.COMMA); - if (timeRange.size() != 2) { - log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param); - return false; - } - LocalTime startTime = parseTime(timeRange.get(0).trim()); - LocalTime endTime = parseTime(timeRange.get(1).trim()); - // TODO @puhui999:hutool 里,看看有没 between 方法 - return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime); - } - - /** - * 解析时间字符串 - * 支持 HH:mm 和 HH:mm:ss 两种格式 - * - * @param timeStr 时间字符串 - * @return 解析后的 LocalTime - */ - public static LocalTime parseTime(String timeStr) { - Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空"); - try { - // 尝试不同的时间格式 - if (timeStr.length() == 5) { // HH:mm - return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT); - } else if (timeStr.length() == 8) { // HH:mm:ss - return LocalTime.parse(timeStr, TIME_FORMATTER); - } else { - throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式"); - } - } catch (Exception e) { - log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e); - throw new IllegalArgumentException("时间格式无效: " + timeStr, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java deleted file mode 100644 index 2ff0417c76..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java +++ /dev/null @@ -1,187 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.timer; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.Map; - -/** - * IoT 定时触发器条件评估器 - *

- * 与设备触发器不同,定时触发器没有设备消息上下文, - * 需要主动查询设备属性和状态来评估条件。 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotTimerConditionEvaluator { - - @Resource - private IotDevicePropertyService devicePropertyService; - - @Resource - private IotDeviceService deviceService; - - /** - * 评估条件 - * - * @param condition 条件配置 - * @return 是否满足条件 - */ - @SuppressWarnings("EnhancedSwitchMigration") - public boolean evaluate(IotSceneRuleDO.TriggerCondition condition) { - // 1.1 基础参数校验 - if (condition == null || condition.getType() == null) { - log.warn("[evaluate][条件为空或类型为空]"); - return false; - } - // 1.2 根据条件类型分发到具体的评估方法 - IotSceneRuleConditionTypeEnum conditionType = - IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); - if (conditionType == null) { - log.warn("[evaluate][未知的条件类型: {}]", condition.getType()); - return false; - } - - // 2. 分发评估 - switch (conditionType) { - case DEVICE_PROPERTY: - return evaluateDevicePropertyCondition(condition); - case DEVICE_STATE: - return evaluateDeviceStateCondition(condition); - case CURRENT_TIME: - return evaluateCurrentTimeCondition(condition); - default: - log.warn("[evaluate][未知的条件类型: {}]", conditionType); - return false; - } - } - - /** - * 评估设备属性条件 - * - * @param condition 条件配置 - * @return 是否满足条件 - */ - private boolean evaluateDevicePropertyCondition(IotSceneRuleDO.TriggerCondition condition) { - // 1. 校验必要参数 - if (condition.getDeviceId() == null) { - log.debug("[evaluateDevicePropertyCondition][设备ID为空]"); - return false; - } - if (StrUtil.isBlank(condition.getIdentifier())) { - log.debug("[evaluateDevicePropertyCondition][属性标识符为空]"); - return false; - } - if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { - log.debug("[evaluateDevicePropertyCondition][操作符或参数无效]"); - return false; - } - - // 2.1 获取设备最新属性值 - Map properties = - devicePropertyService.getLatestDeviceProperties(condition.getDeviceId()); - if (CollUtil.isEmpty(properties)) { - log.debug("[evaluateDevicePropertyCondition][设备({}) 无属性数据]", condition.getDeviceId()); - return false; - } - // 2.2 获取指定属性 - IotDevicePropertyDO property = properties.get(condition.getIdentifier()); - if (property == null || property.getValue() == null) { - log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 不存在或值为空]", - condition.getDeviceId(), condition.getIdentifier()); - return false; - } - - // 3. 使用现有的条件评估逻辑进行比较 - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition( - property.getValue(), condition.getOperator(), condition.getParam()); - log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 值({}) 操作符({}) 参数({}) 匹配结果: {}]", - condition.getDeviceId(), condition.getIdentifier(), property.getValue(), - condition.getOperator(), condition.getParam(), matched); - return matched; - } - - /** - * 评估设备状态条件 - * - * @param condition 条件配置 - * @return 是否满足条件 - */ - private boolean evaluateDeviceStateCondition(IotSceneRuleDO.TriggerCondition condition) { - // 1. 校验必要参数 - if (condition.getDeviceId() == null) { - log.debug("[evaluateDeviceStateCondition][设备ID为空]"); - return false; - } - if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { - log.debug("[evaluateDeviceStateCondition][操作符或参数无效]"); - return false; - } - - // 2.1 获取设备信息 - IotDeviceDO device = deviceService.getDevice(condition.getDeviceId()); - if (device == null) { - log.debug("[evaluateDeviceStateCondition][设备({}) 不存在]", condition.getDeviceId()); - return false; - } - // 2.2 获取设备状态 - Integer state = device.getState(); - if (state == null) { - log.debug("[evaluateDeviceStateCondition][设备({}) 状态为空]", condition.getDeviceId()); - return false; - } - - // 3. 比较状态 - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition( - state.toString(), condition.getOperator(), condition.getParam()); - log.debug("[evaluateDeviceStateCondition][设备({}) 状态({}) 操作符({}) 参数({}) 匹配结果: {}]", - condition.getDeviceId(), state, condition.getOperator(), condition.getParam(), matched); - return matched; - } - - /** - * 评估当前时间条件 - * - * @param condition 条件配置 - * @return 是否满足条件 - */ - private boolean evaluateCurrentTimeCondition(IotSceneRuleDO.TriggerCondition condition) { - // 1.1 校验必要参数 - if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { - log.debug("[evaluateCurrentTimeCondition][操作符或参数无效]"); - return false; - } - // 1.2 验证操作符是否为支持的时间操作符 - IotSceneRuleConditionOperatorEnum operatorEnum = - IotSceneRuleConditionOperatorEnum.operatorOf(condition.getOperator()); - if (operatorEnum == null) { - log.debug("[evaluateCurrentTimeCondition][无效的操作符: {}]", condition.getOperator()); - return false; - } - if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) { - log.debug("[evaluateCurrentTimeCondition][不支持的时间操作符: {}]", condition.getOperator()); - return false; - } - - // 2. 执行时间匹配 - boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam()); - log.debug("[evaluateCurrentTimeCondition][操作符({}) 参数({}) 匹配结果: {}]", - condition.getOperator(), condition.getParam(), matched); - return matched; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClientTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClientTest.java deleted file mode 100644 index cd28f8f54e..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClientTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp; - -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * {@link IotTcpClient} 的单元测试 - *

- * 测试 dataFormat 默认值行为 - * Property 1: TCP 客户端 dataFormat 默认值行为 - * Validates: Requirements 1.1, 1.2 - * - * @author HUIHUI - */ -class IotTcpClientTest { - - @Test - public void testConstructor_dataFormatNull() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, null); - - // 断言:dataFormat 为 null 时应使用默认值 - assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT, - ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testConstructor_dataFormatEmpty() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, ""); - - // 断言:dataFormat 为空字符串时应使用默认值 - assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT, - ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testConstructor_dataFormatBlank() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, " "); - - // 断言:dataFormat 为纯空白字符串时应使用默认值 - assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT, - ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testConstructor_dataFormatValid() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - String dataFormat = "BINARY"; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, dataFormat); - - // 断言:dataFormat 为有效值时应保持原值 - assertEquals(dataFormat, ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testConstructor_defaultValues() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, null); - - // 断言:验证所有默认值 - assertEquals(host, ReflectUtil.getFieldValue(client, "host")); - assertEquals(port, ReflectUtil.getFieldValue(client, "port")); - assertEquals(IotDataSinkTcpConfig.DEFAULT_CONNECT_TIMEOUT_MS, - ReflectUtil.getFieldValue(client, "connectTimeoutMs")); - assertEquals(IotDataSinkTcpConfig.DEFAULT_READ_TIMEOUT_MS, - ReflectUtil.getFieldValue(client, "readTimeoutMs")); - assertEquals(IotDataSinkTcpConfig.DEFAULT_SSL, - ReflectUtil.getFieldValue(client, "ssl")); - assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT, - ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testConstructor_customValues() { - // 准备参数 - String host = "192.168.1.100"; - Integer port = 9090; - Integer connectTimeoutMs = 3000; - Integer readTimeoutMs = 8000; - Boolean ssl = true; - String dataFormat = "BINARY"; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, connectTimeoutMs, readTimeoutMs, ssl, dataFormat); - - // 断言:验证自定义值 - assertEquals(host, ReflectUtil.getFieldValue(client, "host")); - assertEquals(port, ReflectUtil.getFieldValue(client, "port")); - assertEquals(connectTimeoutMs, ReflectUtil.getFieldValue(client, "connectTimeoutMs")); - assertEquals(readTimeoutMs, ReflectUtil.getFieldValue(client, "readTimeoutMs")); - assertEquals(ssl, ReflectUtil.getFieldValue(client, "ssl")); - assertEquals(dataFormat, ReflectUtil.getFieldValue(client, "dataFormat")); - } - - @Test - public void testIsConnected_initialState() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, null); - - // 断言:初始状态应为未连接 - assertFalse(client.isConnected()); - } - - @Test - public void testToString() { - // 准备参数 - String host = "localhost"; - Integer port = 8080; - - // 调用 - IotTcpClient client = new IotTcpClient(host, port, null, null, null, null); - String result = client.toString(); - - // 断言 - assertNotNull(result); - assertTrue(result.contains("host='localhost'")); - assertTrue(result.contains("port=8080")); - assertTrue(result.contains("dataFormat='JSON'")); - assertTrue(result.contains("connected=false")); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClientTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClientTest.java deleted file mode 100644 index d3568db8b9..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClientTest.java +++ /dev/null @@ -1,257 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.data.action.websocket; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * {@link IotWebSocketClient} 的单元测试 - * - * @author HUIHUI - */ -class IotWebSocketClientTest { - - private MockWebServer mockWebServer; - - @BeforeEach - public void setUp() throws Exception { - mockWebServer = new MockWebServer(); - mockWebServer.start(); - } - - @AfterEach - public void tearDown() throws Exception { - if (mockWebServer != null) { - mockWebServer.shutdown(); - } - } - - /** - * 简单的 WebSocket 监听器,用于测试 - */ - private static class TestWebSocketListener extends WebSocketListener { - @Override - public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { - // 连接打开 - } - - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { - // 收到消息 - } - - @Override - public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { - webSocket.close(code, reason); - } - - @Override - public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { - // 连接失败 - } - } - - @Test - public void testConstructor_defaultValues() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - - // 调用 - IotWebSocketClient client = new IotWebSocketClient(serverUrl, null, null, null); - - // 断言:验证默认值被正确设置 - assertNotNull(client); - assertFalse(client.isConnected()); - } - - @Test - public void testConstructor_customValues() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - Integer connectTimeoutMs = 3000; - Integer sendTimeoutMs = 5000; - String dataFormat = "TEXT"; - - // 调用 - IotWebSocketClient client = new IotWebSocketClient(serverUrl, connectTimeoutMs, sendTimeoutMs, dataFormat); - - // 断言 - assertNotNull(client); - assertFalse(client.isConnected()); - } - - @Test - public void testConnect_success() throws Exception { - // 准备参数:使用 MockWebServer 的 WebSocket 端点 - String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort(); - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // mock:设置 MockWebServer 响应 WebSocket 升级请求 - mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener())); - - // 调用 - client.connect(); - - // 断言 - assertTrue(client.isConnected()); - - // 清理 - client.close(); - } - - @Test - public void testConnect_alreadyConnected() throws Exception { - // 准备参数 - String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort(); - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // mock - mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener())); - - // 调用:第一次连接 - client.connect(); - assertTrue(client.isConnected()); - - // 调用:第二次连接(应该不会重复连接) - client.connect(); - assertTrue(client.isConnected()); - - // 清理 - client.close(); - } - - @Test - public void testSendMessage_success() throws Exception { - // 准备参数 - String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort(); - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - IotDeviceMessage message = IotDeviceMessage.builder() - .deviceId(123L) - .method("thing.property.report") - .params("{\"temperature\": 25.5}") - .build(); - - // mock - mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener())); - - // 调用 - client.connect(); - client.sendMessage(message); - - // 断言:消息发送成功不抛异常 - assertTrue(client.isConnected()); - - // 清理 - client.close(); - } - - @Test - public void testSendMessage_notConnected() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - IotDeviceMessage message = IotDeviceMessage.builder() - .deviceId(123L) - .method("thing.property.report") - .params("{\"temperature\": 25.5}") - .build(); - - // 调用 & 断言:未连接时发送消息应抛出异常 - assertThrows(IllegalStateException.class, () -> client.sendMessage(message)); - } - - @Test - public void testClose_success() throws Exception { - // 准备参数 - String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort(); - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // mock - mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener())); - - // 调用 - client.connect(); - assertTrue(client.isConnected()); - - client.close(); - - // 断言 - assertFalse(client.isConnected()); - } - - @Test - public void testClose_notConnected() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // 调用:关闭未连接的客户端不应抛异常 - assertDoesNotThrow(client::close); - assertFalse(client.isConnected()); - } - - @Test - public void testIsConnected_initialState() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // 断言:初始状态应为未连接 - assertFalse(client.isConnected()); - } - - @Test - public void testToString() { - // 准备参数 - String serverUrl = "ws://localhost:8080"; - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON"); - - // 调用 - String result = client.toString(); - - // 断言 - assertNotNull(result); - assertTrue(result.contains("serverUrl='ws://localhost:8080'")); - assertTrue(result.contains("dataFormat='JSON'")); - assertTrue(result.contains("connected=false")); - } - - @Test - public void testSendMessage_textFormat() throws Exception { - // 准备参数 - String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort(); - IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "TEXT"); - - IotDeviceMessage message = IotDeviceMessage.builder() - .deviceId(123L) - .method("thing.property.report") - .params("{\"temperature\": 25.5}") - .build(); - - // mock - mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener())); - - // 调用 - client.connect(); - client.sendMessage(message); - - // 断言:消息发送成功不抛异常 - assertTrue(client.isConnected()); - - // 清理 - client.close(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java deleted file mode 100644 index 7fcae15713..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java +++ /dev/null @@ -1,610 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene; - -import cn.hutool.core.collection.ListUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; -import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; -import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; -import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; -import org.junit.jupiter.api.*; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.lang.reflect.Field; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -/** - * {@link IotSceneRuleServiceImpl} 定时触发器条件组集成测试 - *

- * 测试定时触发器的条件组评估功能: - * - 空条件组直接执行动作 - * - 条件组评估后决定是否执行动作 - * - 条件组之间的 OR 逻辑 - * - 条件组内的 AND 逻辑 - * - 所有条件组不满足时跳过执行 - *

- * Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5 - * - * @author HUIHUI - */ -@Disabled // TODO @puhui999:单测有报错,先屏蔽 -public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { - - @InjectMocks - private IotSceneRuleServiceImpl sceneRuleService; - - @Mock - private IotSceneRuleMapper sceneRuleMapper; - - @Mock - private IotDeviceService deviceService; - - @Mock - private IotDevicePropertyService devicePropertyService; - - @Mock - private List sceneRuleActions; - - @Mock - private IotSceneRuleTimerHandler timerHandler; - - private IotTimerConditionEvaluator timerConditionEvaluator; - - // 测试常量 - private static final Long SCENE_RULE_ID = 1L; - private static final Long TENANT_ID = 1L; - private static final Long DEVICE_ID = 100L; - private static final String PROPERTY_IDENTIFIER = "temperature"; - - @BeforeEach - void setUp() { - // 创建并注入 timerConditionEvaluator 的依赖 - timerConditionEvaluator = new IotTimerConditionEvaluator(); - try { - Field devicePropertyServiceField = IotTimerConditionEvaluator.class.getDeclaredField("devicePropertyService"); - devicePropertyServiceField.setAccessible(true); - devicePropertyServiceField.set(timerConditionEvaluator, devicePropertyService); - - Field deviceServiceField = IotTimerConditionEvaluator.class.getDeclaredField("deviceService"); - deviceServiceField.setAccessible(true); - deviceServiceField.set(timerConditionEvaluator, deviceService); - - Field evaluatorField = IotSceneRuleServiceImpl.class.getDeclaredField("timerConditionEvaluator"); - evaluatorField.setAccessible(true); - evaluatorField.set(sceneRuleService, timerConditionEvaluator); - } catch (Exception e) { - throw new RuntimeException("Failed to inject dependencies", e); - } - } - - // ========== 辅助方法 ========== - - private IotSceneRuleDO createBaseSceneRule() { - IotSceneRuleDO sceneRule = new IotSceneRuleDO(); - sceneRule.setId(SCENE_RULE_ID); - sceneRule.setTenantId(TENANT_ID); - sceneRule.setName("测试定时触发器"); - sceneRule.setStatus(CommonStatusEnum.ENABLE.getStatus()); - sceneRule.setActions(Collections.emptyList()); - return sceneRule; - } - - private IotSceneRuleDO.Trigger createTimerTrigger(String cronExpression, - List> conditionGroups) { - IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); - trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); - trigger.setCronExpression(cronExpression); - trigger.setConditionGroups(conditionGroups); - return trigger; - } - - private IotSceneRuleDO.TriggerCondition createDevicePropertyCondition(Long deviceId, String identifier, - String operator, String param) { - IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); - condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); - condition.setDeviceId(deviceId); - condition.setIdentifier(identifier); - condition.setOperator(operator); - condition.setParam(param); - return condition; - } - - private IotSceneRuleDO.TriggerCondition createDeviceStateCondition(Long deviceId, String operator, String param) { - IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); - condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); - condition.setDeviceId(deviceId); - condition.setOperator(operator); - condition.setParam(param); - return condition; - } - - private void mockDeviceProperty(Long deviceId, String identifier, Object value) { - Map properties = new HashMap<>(); - IotDevicePropertyDO property = new IotDevicePropertyDO(); - property.setValue(value); - properties.put(identifier, property); - when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); - } - - private void mockDeviceState(Long deviceId, Integer state) { - IotDeviceDO device = new IotDeviceDO(); - device.setId(deviceId); - device.setState(state); - when(deviceService.getDevice(deviceId)).thenReturn(device); - } - - /** - * 创建单条件的条件组列表 - */ - private List> createSingleConditionGroups( - IotSceneRuleDO.TriggerCondition condition) { - List group = new ArrayList<>(); - group.add(condition); - List> groups = new ArrayList<>(); - groups.add(group); - return groups; - } - - /** - * 创建两个单条件组的条件组列表 - */ - private List> createTwoSingleConditionGroups( - IotSceneRuleDO.TriggerCondition cond1, IotSceneRuleDO.TriggerCondition cond2) { - List group1 = new ArrayList<>(); - group1.add(cond1); - List group2 = new ArrayList<>(); - group2.add(cond2); - List> groups = new ArrayList<>(); - groups.add(group1); - groups.add(group2); - return groups; - } - - /** - * 创建单个多条件组的条件组列表 - */ - private List> createSingleGroupWithMultipleConditions( - IotSceneRuleDO.TriggerCondition... conditions) { - List group = new ArrayList<>(Arrays.asList(conditions)); - List> groups = new ArrayList<>(); - groups.add(group); - return groups; - } - - // ========== 测试用例 ========== - - @Nested - @DisplayName("空条件组测试 - Validates: Requirement 2.1") - class EmptyConditionGroupsTest { - - @Test - @DisplayName("定时触发器无条件组时,应直接执行动作") - void testTimerTrigger_withNullConditionGroups_shouldExecuteActions() { - // 准备数据 - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", null); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID); - verify(devicePropertyService, never()).getLatestDeviceProperties(any()); - verify(deviceService, never()).getDevice(any()); - } - - @Test - @DisplayName("定时触发器条件组为空列表时,应直接执行动作") - void testTimerTrigger_withEmptyConditionGroups_shouldExecuteActions() { - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", Collections.emptyList()); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID); - verify(devicePropertyService, never()).getLatestDeviceProperties(any()); - } - } - - @Nested - @DisplayName("条件组 OR 逻辑测试 - Validates: Requirements 2.2, 2.3") - class ConditionGroupOrLogicTest { - - @Test - @DisplayName("多个条件组中第一个满足时,应执行动作") - void testMultipleConditionGroups_firstGroupMatches_shouldExecuteActions() { - IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( - DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); - - List> conditionGroups = - createTwoSingleConditionGroups(condition1, condition2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - - @Test - @DisplayName("多个条件组中第二个满足时,应执行动作") - void testMultipleConditionGroups_secondGroupMatches_shouldExecuteActions() { - IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); - IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( - DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - - List> conditionGroups = - createTwoSingleConditionGroups(condition1, condition2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1); - } - } - - @Nested - @DisplayName("条件组内 AND 逻辑测试 - Validates: Requirement 2.4") - class ConditionGroupAndLogicTest { - - @Test - @DisplayName("条件组内所有条件都满足时,该组应匹配成功") - void testSingleConditionGroup_allConditionsMatch_shouldPass() { - IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( - DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( - DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "80"); - - List> conditionGroups = - createSingleGroupWithMultipleConditions(condition1, condition2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - Map properties = new HashMap<>(); - IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); - tempProperty.setValue(30); - properties.put("temperature", tempProperty); - IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); - humidityProperty.setValue(60); - properties.put("humidity", humidityProperty); - when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - - @Test - @DisplayName("条件组内有一个条件不满足时,该组应匹配失败") - void testSingleConditionGroup_oneConditionFails_shouldFail() { - IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( - DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( - DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50"); - - List> conditionGroups = - createSingleGroupWithMultipleConditions(condition1, condition2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - Map properties = new HashMap<>(); - IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); - tempProperty.setValue(30); - properties.put("temperature", tempProperty); - IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); - humidityProperty.setValue(60); // 不满足 < 50 - properties.put("humidity", humidityProperty); - when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - } - - @Nested - @DisplayName("所有条件组不满足测试 - Validates: Requirement 2.5") - class AllConditionGroupsFailTest { - - @Test - @DisplayName("所有条件组都不满足时,应跳过动作执行") - void testAllConditionGroups_allFail_shouldSkipExecution() { - IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); - IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( - DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); - - List> conditionGroups = - createTwoSingleConditionGroups(condition1, condition2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1); - } - } - - @Nested - @DisplayName("设备状态条件测试 - Validates: Requirements 4.1, 4.2") - class DeviceStateConditionTest { - - @Test - @DisplayName("设备在线状态条件满足时,应匹配成功") - void testDeviceStateCondition_online_shouldMatch() { - IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition( - DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), - String.valueOf(IotDeviceStateEnum.ONLINE.getState())); - - List> conditionGroups = createSingleConditionGroups(condition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); - } - - @Test - @DisplayName("设备不存在时,条件应不匹配") - void testDeviceStateCondition_deviceNotExists_shouldNotMatch() { - IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition( - DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), - String.valueOf(IotDeviceStateEnum.ONLINE.getState())); - - List> conditionGroups = createSingleConditionGroups(condition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - when(deviceService.getDevice(DEVICE_ID)).thenReturn(null); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); - } - } - - @Nested - @DisplayName("设备属性条件测试 - Validates: Requirements 3.1, 3.2, 3.3") - class DevicePropertyConditionTest { - - @Test - @DisplayName("设备属性条件满足时,应匹配成功") - void testDevicePropertyCondition_match_shouldPass() { - IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25"); - - List> conditionGroups = createSingleConditionGroups(condition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - - @Test - @DisplayName("设备属性不存在时,条件应不匹配") - void testDevicePropertyCondition_propertyNotExists_shouldNotMatch() { - IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( - DEVICE_ID, "nonexistent", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25"); - - List> conditionGroups = createSingleConditionGroups(condition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(Collections.emptyMap()); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - - @Test - @DisplayName("设备属性等于条件测试") - void testDevicePropertyCondition_equals_shouldMatch() { - IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), "30"); - - List> conditionGroups = createSingleConditionGroups(condition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - } - - @Nested - @DisplayName("场景规则状态测试") - class SceneRuleStatusTest { - - @Test - @DisplayName("场景规则不存在时,应直接返回") - void testSceneRule_notExists_shouldReturn() { - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(null); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, never()).getLatestDeviceProperties(any()); - } - - @Test - @DisplayName("场景规则已禁用时,应直接返回") - void testSceneRule_disabled_shouldReturn() { - IotSceneRuleDO sceneRule = createBaseSceneRule(); - sceneRule.setStatus(CommonStatusEnum.DISABLE.getStatus()); - - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, never()).getLatestDeviceProperties(any()); - } - - @Test - @DisplayName("场景规则无定时触发器时,应直接返回") - void testSceneRule_noTimerTrigger_shouldReturn() { - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger(); - deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); - sceneRule.setTriggers(ListUtil.toList(deviceTrigger)); - - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, never()).getLatestDeviceProperties(any()); - } - } - - @Nested - @DisplayName("复杂条件组合测试") - class ComplexConditionCombinationTest { - - @Test - @DisplayName("混合条件类型测试:设备属性 + 设备状态") - void testMixedConditionTypes_propertyAndState() { - IotSceneRuleDO.TriggerCondition propertyCondition = createDevicePropertyCondition( - DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - IotSceneRuleDO.TriggerCondition stateCondition = createDeviceStateCondition( - DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), - String.valueOf(IotDeviceStateEnum.ONLINE.getState())); - - List> conditionGroups = - createSingleGroupWithMultipleConditions(propertyCondition, stateCondition); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); - mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); - } - - @Test - @DisplayName("多条件组 OR 逻辑 + 组内 AND 逻辑综合测试") - void testComplexOrAndLogic() { - // 条件组1:温度 > 30 AND 湿度 < 50(不满足) - // 条件组2:温度 > 20 AND 设备在线(满足) - IotSceneRuleDO.TriggerCondition group1Cond1 = createDevicePropertyCondition( - DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "30"); - IotSceneRuleDO.TriggerCondition group1Cond2 = createDevicePropertyCondition( - DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50"); - - IotSceneRuleDO.TriggerCondition group2Cond1 = createDevicePropertyCondition( - DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); - IotSceneRuleDO.TriggerCondition group2Cond2 = createDeviceStateCondition( - DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), - String.valueOf(IotDeviceStateEnum.ONLINE.getState())); - - // 创建两个条件组 - List group1 = new ArrayList<>(); - group1.add(group1Cond1); - group1.add(group1Cond2); - List group2 = new ArrayList<>(); - group2.add(group2Cond1); - group2.add(group2Cond2); - List> conditionGroups = new ArrayList<>(); - conditionGroups.add(group1); - conditionGroups.add(group2); - - IotSceneRuleDO sceneRule = createBaseSceneRule(); - IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); - sceneRule.setTriggers(ListUtil.toList(trigger)); - - // Mock:温度 25,湿度 60,设备在线 - Map properties = new HashMap<>(); - IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); - tempProperty.setValue(25); - properties.put("temperature", tempProperty); - IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); - humidityProperty.setValue(60); - properties.put("humidity", humidityProperty); - when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); - mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); - when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); - - assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); - - verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java deleted file mode 100644 index dfa18a2752..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.biz.dto; - -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import lombok.Data; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.List; - -/** - * IoT 子设备动态注册 Request DTO - *

- * 额外包含了网关设备的标识信息 - * - * @author 芋道源码 - */ -@Data -public class IotSubDeviceRegisterFullReqDTO { - - /** - * 网关设备 ProductKey - */ - @NotEmpty(message = "网关产品标识不能为空") - private String gatewayProductKey; - - /** - * 网关设备 DeviceName - */ - @NotEmpty(message = "网关设备名称不能为空") - private String gatewayDeviceName; - - /** - * 子设备注册列表 - */ - @NotNull(message = "子设备注册列表不能为空") - private List subDevices; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java deleted file mode 100644 index 53f6958358..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.validation.constraints.NotEmpty; - -/** - * IoT 设备标识 - * - * 用于标识一个设备的基本信息(productKey + deviceName) - * - * @author 芋道源码 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class IotDeviceIdentity { - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java deleted file mode 100644 index 598a73a431..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.auth; - -import lombok.Data; - -import javax.validation.constraints.NotEmpty; - -/** - * IoT 设备动态注册 Request DTO - *

- * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - */ -@Data -public class IotDeviceRegisterReqDTO { - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - - /** - * 产品密钥 - */ - @NotEmpty(message = "产品密钥不能为空") - private String productSecret; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java deleted file mode 100644 index 707f79890b..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.auth; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * IoT 设备动态注册 Response DTO - *

- * 用于直连设备/网关的一型一密动态注册响应 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class IotDeviceRegisterRespDTO { - - /** - * 产品标识 - */ - private String productKey; - - /** - * 设备名称 - */ - private String deviceName; - - /** - * 设备密钥 - */ - private String deviceSecret; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java deleted file mode 100644 index dfe1073fe8..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.auth; - -import lombok.Data; - -import javax.validation.constraints.NotEmpty; - -/** - * IoT 子设备动态注册 Request DTO - *

- * 用于 thing.auth.register.sub 消息的 params 数组元素 - * - * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 - * - * @author 芋道源码 - * @see 阿里云 - 动态注册子设备 - */ -@Data -public class IotSubDeviceRegisterReqDTO { - - /** - * 子设备 ProductKey - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - - /** - * 子设备 DeviceName - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java deleted file mode 100644 index a45f14defe..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.auth; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * IoT 子设备动态注册 Response DTO - *

- * 用于 thing.auth.register.sub 响应的设备信息 - * - * @author 芋道源码 - * @see 阿里云 - 动态注册子设备 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class IotSubDeviceRegisterRespDTO { - - /** - * 子设备 ProductKey - */ - private String productKey; - - /** - * 子设备 DeviceName - */ - private String deviceName; - - /** - * 分配的 DeviceSecret - */ - private String deviceSecret; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java deleted file mode 100644 index 3b6a7a7d4c..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.event; - -import lombok.Data; - -/** - * IoT 设备事件上报 Request DTO - *

- * 用于 thing.event.post 消息的 params 参数 - * - * @author 芋道源码 - * @see 阿里云 - 设备上报事件 - */ -@Data -public class IotDeviceEventPostReqDTO { - - /** - * 事件标识符 - */ - private String identifier; - - /** - * 事件输出参数 - */ - private Object value; - - /** - * 上报时间(毫秒时间戳,可选) - */ - private Long time; - - /** - * 创建事件上报 DTO - * - * @param identifier 事件标识符 - * @param value 事件值 - * @return DTO 对象 - */ - public static IotDeviceEventPostReqDTO of(String identifier, Object value) { - return of(identifier, value, null); - } - - /** - * 创建事件上报 DTO(带时间) - * - * @param identifier 事件标识符 - * @param value 事件值 - * @param time 上报时间 - * @return DTO 对象 - */ - public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) { - return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java deleted file mode 100644 index bc97dd944a..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * IoT Topic 消息体 DTO 定义 - *

- * 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范 - * - * @see 阿里云 Alink 协议 - */ -package cn.iocoder.yudao.module.iot.core.topic; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java deleted file mode 100644 index 24494984eb..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java +++ /dev/null @@ -1,88 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.property; - -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import lombok.Data; - -import java.util.List; -import java.util.Map; - -/** - * IoT 设备属性批量上报 Request DTO - *

- * 用于 thing.event.property.pack.post 消息的 params 参数 - * - * @author 芋道源码 - * @see 阿里云 - 网关批量上报数据 - */ -@Data -public class IotDevicePropertyPackPostReqDTO { - - /** - * 网关自身属性 - *

- * key: 属性标识符 - * value: 属性值 - */ - private Map properties; - - /** - * 网关自身事件 - *

- * key: 事件标识符 - * value: 事件值对象(包含 value 和 time) - */ - private Map events; - - /** - * 子设备数据列表 - */ - private List subDevices; - - /** - * 事件值对象 - */ - @Data - public static class EventValue { - - /** - * 事件参数 - */ - private Object value; - - /** - * 上报时间(毫秒时间戳) - */ - private Long time; - - } - - /** - * 子设备数据 - */ - @Data - public static class SubDeviceData { - - /** - * 子设备标识 - */ - private IotDeviceIdentity identity; - - /** - * 子设备属性 - *

- * key: 属性标识符 - * value: 属性值 - */ - private Map properties; - - /** - * 子设备事件 - *

- * key: 事件标识符 - * value: 事件值对象(包含 value 和 time) - */ - private Map events; - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java deleted file mode 100644 index 2e537442d7..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.property; - -import java.util.HashMap; -import java.util.Map; - -/** - * IoT 设备属性上报 Request DTO - *

- * 用于 thing.property.post 消息的 params 参数 - *

- * 本质是一个 Map,key 为属性标识符,value 为属性值 - * - * @author 芋道源码 - * @see 阿里云 - 设备上报属性 - */ -public class IotDevicePropertyPostReqDTO extends HashMap { - - public IotDevicePropertyPostReqDTO() { - super(); - } - - public IotDevicePropertyPostReqDTO(Map properties) { - super(properties); - } - - /** - * 创建属性上报 DTO - * - * @param properties 属性数据 - * @return DTO 对象 - */ - public static IotDevicePropertyPostReqDTO of(Map properties) { - return new IotDevicePropertyPostReqDTO(properties); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java deleted file mode 100644 index 7f7e85fd11..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.topo; - -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import lombok.Data; - -import javax.validation.constraints.NotEmpty; -import java.util.List; - -/** - * IoT 设备拓扑添加 Request DTO - *

- * 用于 thing.topo.add 消息的 params 参数 - * - * @author 芋道源码 - * @see 阿里云 - 添加拓扑关系 - */ -@Data -public class IotDeviceTopoAddReqDTO { - - /** - * 子设备认证信息列表 - *

- * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password - */ - @NotEmpty(message = "子设备认证信息列表不能为空") - private List subDevices; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java deleted file mode 100644 index 0198206fe3..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.topo; - -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * IoT 设备拓扑关系变更通知 Request DTO - *

- * 用于 thing.topo.change 下行消息的 params 参数 - * - * @author 芋道源码 - * @see 阿里云 - 通知网关拓扑关系变化 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class IotDeviceTopoChangeReqDTO { - - public static final Integer STATUS_CREATE = 0; - public static final Integer STATUS_DELETE = 1; - - /** - * 拓扑关系状态 - */ - private Integer status; - - /** - * 子设备列表 - */ - private List subList; - - public static IotDeviceTopoChangeReqDTO ofCreate(List subList) { - return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList); - } - - public static IotDeviceTopoChangeReqDTO ofDelete(List subList) { - return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java deleted file mode 100644 index 6e0be865bc..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.topo; - -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import lombok.Data; - -import javax.validation.Valid; -import javax.validation.constraints.NotEmpty; -import java.util.List; - -/** - * IoT 设备拓扑删除 Request DTO - *

- * 用于 thing.topo.delete 消息的 params 参数 - * - * @author 芋道源码 - * @see 阿里云 - 删除拓扑关系 - */ -@Data -public class IotDeviceTopoDeleteReqDTO { - - /** - * 子设备标识列表 - */ - @Valid - @NotEmpty(message = "子设备标识列表不能为空") - private List subDevices; - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java deleted file mode 100644 index 7a61af0a58..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.topo; - -import lombok.Data; - -/** - * IoT 设备拓扑关系获取 Request DTO - *

- * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展) - * - * @author 芋道源码 - * @see 阿里云 - 获取拓扑关系 - */ -@Data -public class IotDeviceTopoGetReqDTO { - -} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java deleted file mode 100644 index 69c9b1555e..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.topic.topo; - -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import lombok.Data; - -import java.util.List; - -/** - * IoT 设备拓扑关系获取 Response DTO - *

- * 用于 thing.topo.get 响应 - * - * @author 芋道源码 - * @see 阿里云 - 获取拓扑关系 - */ -@Data -public class IotDeviceTopoGetRespDTO { - - /** - * 子设备列表 - */ - private List subDevices; - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java deleted file mode 100644 index d991d5ff11..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.PostConstruct; - -/** - * IoT 网关 CoAP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber { - - private final IotCoapUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted file mode 100644 index 8f883f5c3d..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.util.concurrent.TimeUnit; - -/** - * IoT 网关 CoAP 协议:接收设备上行消息 - * - * 基于 Eclipse Californium 实现,支持: - * 1. 认证:POST /auth - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamProtocol { - - private final IotGatewayProperties.CoapProperties coapProperties; - - private CoapServer coapServer; - - @Getter - private final String serverId; - - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { - this.coapProperties = coapProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); - } - - @PostConstruct - public void start() { - try { - // 1.1 创建网络配置(Californium 3.x API) - Configuration config = Configuration.createStandardWithoutFile(); - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); - // 1.2 创建 CoAP 服务器 - coapServer = new CoapServer(config); - - // 2.1 添加 /auth 认证资源 - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); - coapServer.add(authResource); - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); - authResource.add(new CoapResource("register") {{ - add(registerResource); - }}); - // 2.3 添加 /topic 根资源(用于上行消息) - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); - coapServer.add(topicResource); - - // 3. 启动服务器 - coapServer.start(); - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (coapServer != null) { - try { - coapServer.stop(); - log.info("[stop][IoT 网关 CoAP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java deleted file mode 100644 index 94536a6439..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java +++ /dev/null @@ -1,13 +0,0 @@ -/** - * CoAP 协议实现包 - *

- * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能 - *

- * URI 路径: - * - 认证:POST /auth - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - *

- * Token 通过 CoAP Option 2088 携带 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted file mode 100644 index 43fb77608a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【认证】处理器 - * - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler} - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapAuthHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceCommonApi deviceApi; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapAuthHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理认证请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - // 1.2 解析参数 - String clientId = MapUtil.getStr(body, "clientId"); - if (StrUtil.isEmpty(clientId)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); - return; - } - String username = MapUtil.getStr(body, "username"); - if (StrUtil.isEmpty(username)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); - return; - } - String password = MapUtil.getStr(body, "password"); - if (StrUtil.isEmpty(password)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); - return; - } - - // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - if (result.isError()) { - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); - return; - } - if (!BooleanUtil.isTrue(result.getData())) { - log.warn("[handle][认证失败,clientId: {}]", clientId); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); - return; - } - // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); - Assert.notNull(deviceInfo, "设备信息不能为空"); - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - Assert.notBlank(token, "生成 token 不能为空"); - - // 3. 执行上线 - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); - - // 4. 返回成功响应 - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); - } catch (Exception e) { - log.error("[handle][认证处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java deleted file mode 100644 index 9d0d90cb3e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.server.resources.CoapExchange; - -/** - * IoT 网关 CoAP 协议的认证资源(/auth) - * - * 设备通过此资源进行认证,获取 Token - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapAuthResource extends CoapResource { - - public static final String PATH = "auth"; - - private final IotCoapUpstreamProtocol protocol; - private final IotCoapAuthHandler authHandler; - - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol, - IotCoapAuthHandler authHandler) { - super(PATH); - this.protocol = protocol; - this.authHandler = authHandler; - log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH); - } - - @Override - public void handlePOST(CoapExchange exchange) { - log.debug("[handlePOST][收到 /auth POST 请求]"); - authHandler.handle(exchange, protocol); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted file mode 100644 index 8ffbe4f677..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【设备动态注册】处理器 - *

- * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler - */ -@Slf4j -public class IotCoapRegisterHandler { - - private final IotDeviceCommonApi deviceApi; - - public IotCoapRegisterHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - /** - * 处理设备动态注册请求 - * - * @param exchange CoAP 交换对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - - // 1.2 解析参数 - String productKey = MapUtil.getStr(body, "productKey"); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - String deviceName = MapUtil.getStr(body, "deviceName"); - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - String productSecret = MapUtil.getStr(body, "productSecret"); - if (StrUtil.isEmpty(productSecret)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); - return; - } - - // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey) - .setDeviceName(deviceName) - .setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); - if (result.isError()) { - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", - productKey, deviceName, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, - "设备动态注册失败:" + result.getMsg()); - return; - } - - // 3. 返回成功响应 - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); - IotCoapUtils.respondSuccess(exchange, result.getData()); - } catch (Exception e) { - log.error("[handle][设备动态注册处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java deleted file mode 100644 index 05fd1ec89d..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.server.resources.CoapExchange; - -/** - * IoT 网关 CoAP 协议的设备动态注册资源(/auth/register/device) - *

- * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapRegisterResource extends CoapResource { - - public static final String PATH = "device"; - - private final IotCoapRegisterHandler registerHandler; - - public IotCoapRegisterResource(IotCoapRegisterHandler registerHandler) { - super(PATH); - this.registerHandler = registerHandler; - log.info("[IotCoapRegisterResource][创建 CoAP 设备动态注册资源: /auth/register/{}]", PATH); - } - - @Override - public void handlePOST(CoapExchange exchange) { - log.debug("[handlePOST][收到设备动态注册请求]"); - registerHandler.handle(exchange); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted file mode 100644 index d33eb464bb..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.StrPool; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.List; - -/** - * IoT 网关 CoAP 协议的【上行】处理器 - * - * 处理设备通过 CoAP 协议发送的上行消息,包括: - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * Token 通过自定义 CoAP Option 2088 携带 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapUpstreamHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理 CoAP 请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1. 解析通用参数 - List uriPath = exchange.getRequestOptions().getUriPath(); - String productKey = CollUtil.get(uriPath, 2); - String deviceName = CollUtil.get(uriPath, 3); - byte[] payload = exchange.getRequestPayload(); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - if (ArrayUtil.isEmpty(payload)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - - // 2. 认证:从自定义 Option 获取 token - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN); - if (StrUtil.isEmpty(token)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空"); - return; - } - // 验证 token - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); - return; - } - // 验证设备信息匹配 - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); - return; - } - - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接 - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post] - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); - - // 2.2 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (ObjUtil.notEqual(method, message.getMethod())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配"); - return; - } - // 2.3 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); - - // 3. 返回成功响应 - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId())); - } catch (Exception e) { - log.error("[handle][CoAP 请求处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java deleted file mode 100644 index 1c694483fa..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java +++ /dev/null @@ -1,67 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.server.resources.CoapExchange; -import org.eclipse.californium.core.server.resources.Resource; - -/** - * IoT 网关 CoAP 协议的【上行】Topic 资源 - * - * 支持任意深度的路径匹配: - * - /topic/sys/{productKey}/{deviceName}/thing/property/post - * - /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamTopicResource extends CoapResource { - - public static final String PATH = "topic"; - - private final IotCoapUpstreamProtocol protocol; - private final IotCoapUpstreamHandler upstreamHandler; - - /** - * 创建根资源(/topic) - */ - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol, - IotCoapUpstreamHandler upstreamHandler) { - this(PATH, protocol, upstreamHandler); - log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH); - } - - /** - * 创建子资源(动态路径) - */ - private IotCoapUpstreamTopicResource(String name, - IotCoapUpstreamProtocol protocol, - IotCoapUpstreamHandler upstreamHandler) { - super(name); - this.protocol = protocol; - this.upstreamHandler = upstreamHandler; - } - - @Override - public Resource getChild(String name) { - // 递归创建动态子资源,支持任意深度路径 - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler); - } - - @Override - public void handleGET(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); - } - - @Override - public void handlePOST(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); - } - - @Override - public void handlePUT(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java deleted file mode 100644 index 9d5cdf3ffb..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; - -/** - * IoT CoAP 协议工具类 - * - * @author 芋道源码 - */ -public class IotCoapUtils { - - /** - * 自定义 CoAP Option 编号,用于携带 Token - *

- * CoAP Option 范围 2048-65535 属于实验/自定义范围 - */ - public static final int OPTION_TOKEN = 2088; - - /** - * 返回成功响应 - * - * @param exchange CoAP 交换对象 - * @param data 响应数据 - */ - public static void respondSuccess(CoapExchange exchange, Object data) { - CommonResult result = CommonResult.success(data); - String json = JsonUtils.toJsonString(result); - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 返回错误响应 - * - * @param exchange CoAP 交换对象 - * @param code CoAP 响应码 - * @param message 错误消息 - */ - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) { - int errorCode = mapCoapCodeToErrorCode(code); - CommonResult result = CommonResult.error(errorCode, message); - String json = JsonUtils.toJsonString(result); - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 从自定义 CoAP Option 中获取 Token - * - * @param exchange CoAP 交换对象 - * @param optionNumber Option 编号 - * @return Token 值,如果不存在则返回 null - */ - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) { - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), - o -> o.getNumber() == optionNumber); - return option != null ? new String(option.getValue()) : null; - } - - /** - * 将 CoAP 响应码映射到业务错误码 - * - * @param code CoAP 响应码 - * @return 业务错误码 - */ - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) { - if (code == CoAP.ResponseCode.BAD_REQUEST) { - return BAD_REQUEST.getCode(); - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) { - return UNAUTHORIZED.getCode(); - } else if (code == CoAP.ResponseCode.FORBIDDEN) { - return FORBIDDEN.getCode(); - } else { - return INTERNAL_SERVER_ERROR.getCode(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java deleted file mode 100644 index b64dd122bb..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java deleted file mode 100644 index 20124f8d07..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * HTTP 协议实现包 - *

- * 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java deleted file mode 100644 index 51459dfa26..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -/** - * IoT 网关 HTTP 协议的【设备动态注册】处理器 - *

- * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - */ -public class IotHttpRegisterHandler extends IotHttpAbstractHandler { - - public static final String PATH = "/auth/register/device"; - - private final IotDeviceCommonApi deviceApi; - - public IotHttpRegisterHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - @Override - public CommonResult handle0(RoutingContext context) { - // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String productKey = body.getString("productKey"); - if (StrUtil.isEmpty(productKey)) { - throw invalidParamException("productKey 不能为空"); - } - String deviceName = body.getString("deviceName"); - if (StrUtil.isEmpty(deviceName)) { - throw invalidParamException("deviceName 不能为空"); - } - String productSecret = body.getString("productSecret"); - if (StrUtil.isEmpty(productSecret)) { - throw invalidParamException("productSecret 不能为空"); - } - - // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); - result.checkError(); - - // 3. 返回结果 - return success(result.getData()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java deleted file mode 100644 index 32a6144b7a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java +++ /dev/null @@ -1,67 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; - -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -/** - * IoT 网关 HTTP 协议的【子设备动态注册】处理器 - *

- * 用于子设备的动态注册,需要网关认证 - * - * @author 芋道源码 - * @see 阿里云 - 动态注册子设备 - */ -public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { - - /** - * 路径:/auth/register/sub-device/:productKey/:deviceName - *

- * productKey 和 deviceName 是网关设备的标识 - */ - public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName"; - - private final IotDeviceCommonApi deviceApi; - - public IotHttpRegisterSubHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - @Override - public CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 - String productKey = context.pathParam("productKey"); - String deviceName = context.pathParam("deviceName"); - - // 2. 解析子设备列表 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - if (body.getJsonArray("params") == null) { - throw invalidParamException("params 不能为空"); - } - List subDevices = JsonUtils.parseArray( - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); - - // 3. 调用子设备动态注册 - IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); - CommonResult> result = deviceApi.registerSubDevices(reqDTO); - result.checkError(); - - // 4. 返回结果 - return success(result.getData()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java deleted file mode 100644 index 1b59f5446e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * TCP 协议实现包 - *

- * 提供基于 Vert.x TCP Server 的 IoT 设备连接和消息处理功能 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java deleted file mode 100644 index 6809eceb70..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.PostConstruct; - -/** - * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotUdpUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final IotMessageBus messageBus; - - private IotUdpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted file mode 100644 index 07708f51c8..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java +++ /dev/null @@ -1,171 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.datagram.DatagramSocket; -import io.vertx.core.datagram.DatagramSocketOptions; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.util.List; - -/** - * IoT 网关 UDP 协议:接收设备上行消息 - *

- * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: - * 1. 监听 UDP 端口,接收设备消息 - * 2. 定期清理不活跃的设备地址映射 - * 3. 提供 UDP Socket 用于下行消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamProtocol { - - private final IotGatewayProperties.UdpProperties udpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - @Getter - private DatagramSocket udpSocket; - - /** - * 会话清理定时器 ID - */ - private Long cleanTimerId; - - private IotUdpUpstreamHandler upstreamHandler; - - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - Vertx vertx) { - this.udpProperties = udpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.sessionManager = sessionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); - } - - @PostConstruct - public void start() { - // 1. 初始化上行消息处理器 - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); - - // 2. 创建 UDP Socket 选项 - DatagramSocketOptions options = new DatagramSocketOptions() - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) - .setSendBufferSize(udpProperties.getSendBufferSize()) - .setReuseAddress(true); - - // 3. 创建 UDP Socket - udpSocket = vertx.createDatagramSocket(options); - - // 4. 监听端口 - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { - if (result.failed()) { - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); - return; - } - // 设置数据包处理器 - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), - udpProperties.getSendBufferSize()); - - // 5. 启动会话清理定时器 - startSessionCleanTimer(); - }); - } - - @PreDestroy - public void stop() { - // 1. 取消会话清理定时器 - if (cleanTimerId != null) { - vertx.cancelTimer(cleanTimerId); - cleanTimerId = null; - log.info("[stop][会话清理定时器已取消]"); - } - - // 2. 关闭 UDP Socket - if (udpSocket != null) { - try { - udpSocket.close().result(); - log.info("[stop][IoT 网关 UDP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); - } - } - } - - /** - * 启动会话清理定时器 - */ - private void startSessionCleanTimer() { - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { - try { - // 1. 清理超时的设备地址映射,并获取离线设备列表 - List offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); - - // 2. 为每个离线设备发送离线消息 - for (Long deviceId : offlineDeviceIds) { - sendOfflineMessage(deviceId); - } - if (CollUtil.isNotEmpty(offlineDeviceIds)) { - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); - } - } catch (Exception e) { - log.error("[cleanExpiredMappings][清理超时会话失败]", e); - } - }); - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); - } - - /** - * 发送设备离线消息 - * - * @param deviceId 设备 ID - */ - private void sendOfflineMessage(Long deviceId) { - try { - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); - if (device == null) { - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); - return; - } - - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", - deviceId, device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java deleted file mode 100644 index 79a5bf0245..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ /dev/null @@ -1,203 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramSocket; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.net.InetSocketAddress; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * IoT 网关 UDP 会话管理器 - *

- * 采用无状态设计,SessionManager 主要用于: - * 1. 管理设备地址映射(用于下行消息发送) - * 2. 定期清理不活跃的设备地址映射 - *

- * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotUdpSessionManager { - - /** - * 设备 ID -> 会话信息(包含地址和 codecType) - */ - private final Map deviceSessionMap = new ConcurrentHashMap<>(); - - /** - * 设备地址 Key -> 最后活跃时间(用于清理) - */ - private final Map lastActiveTimeMap = new ConcurrentHashMap<>(); - - /** - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) - */ - private final Map addressDeviceMap = new ConcurrentHashMap<>(); - - /** - * 更新设备会话(每次收到上行消息时调用) - * - * @param deviceId 设备 ID - * @param address 设备地址 - * @param codecType 消息编解码类型 - */ - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { - String addressKey = buildAddressKey(address); - // 更新设备会话映射 - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); - addressDeviceMap.put(addressKey, deviceId); - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); - } - - /** - * 更新设备地址(兼容旧接口,默认不更新 codecType) - * - * @param deviceId 设备 ID - * @param address 设备地址 - */ - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; - updateDeviceSession(deviceId, address, codecType); - } - - /** - * 获取设备会话信息 - * - * @param deviceId 设备 ID - * @return 会话信息 - */ - public SessionInfo getSessionInfo(Long deviceId) { - return deviceSessionMap.get(deviceId); - } - - /** - * 检查设备是否在线(即是否有地址映射) - * - * @param deviceId 设备 ID - * @return 是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - return deviceSessionMap.containsKey(deviceId); - } - - /** - * 检查设备是否离线 - * - * @param deviceId 设备 ID - * @return 是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); - } - - /** - * 发送消息到设备 - * - * @param deviceId 设备 ID - * @param data 数据 - * @param socket UDP Socket - * @return 是否发送成功 - */ - public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - if (sessionInfo == null || sessionInfo.getAddress() == null) { - log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); - return false; - } - - InetSocketAddress address = sessionInfo.getAddress(); - try { - socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { - if (result.succeeded()) { - log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", - deviceId, buildAddressKey(address), data.length); - } else { - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", - deviceId, buildAddressKey(address), result.cause()); - } - }); - return true; - } catch (Exception e) { - log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); - return false; - } - } - - /** - * 定期清理不活跃的设备地址映射 - * - * @param timeoutMs 超时时间(毫秒) - * @return 清理的设备 ID 列表(用于发送离线消息) - */ - public List cleanExpiredMappings(long timeoutMs) { - List offlineDeviceIds = new ArrayList<>(); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); - Iterator> iterator = lastActiveTimeMap.entrySet().iterator(); - while (iterator.hasNext()) { - // 未过期,跳过 - Map.Entry entry = iterator.next(); - if (entry.getValue().isAfter(expireTime)) { - continue; - } - // 过期处理:记录离线设备 ID - String addressKey = entry.getKey(); - Long deviceId = addressDeviceMap.remove(addressKey); - if (deviceId == null) { - iterator.remove(); - continue; - } - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); - if (sessionInfo == null) { - iterator.remove(); - continue; - } - offlineDeviceIds.add(deviceId); - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", - deviceId, addressKey, entry.getValue()); - iterator.remove(); - } - return offlineDeviceIds; - } - - /** - * 构建地址 Key - * - * @param address 地址 - * @return 地址 Key - */ - public String buildAddressKey(InetSocketAddress address) { - return address.getHostString() + ":" + address.getPort(); - } - - /** - * 会话信息 - */ - @Data - public static class SessionInfo { - - /** - * 设备地址 - */ - private InetSocketAddress address; - - /** - * 消息编解码类型 - */ - private String codecType; - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/package-info.java deleted file mode 100644 index b1fcaa3f9d..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * UDP 协议实现包 - *

- * 提供基于 Vert.x DatagramSocket 的 IoT 设备连接和消息处理功能 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java deleted file mode 100644 index 6aeb2cb7aa..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 UDP 下行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotUdpSessionManager sessionManager; - - private final IotUdpUpstreamProtocol protocol; - - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotUdpSessionManager sessionManager, - IotUdpUpstreamProtocol protocol) { - this.deviceMessageService = deviceMessageService; - this.sessionManager = sessionManager; - this.protocol = protocol; - } - - /** - * 处理下行消息 - * - * @param message 下行消息 - */ - public void handle(IotDeviceMessage message) { - try { - log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取会话信息(包含 codecType) - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); - if (sessionInfo == null) { - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); - return; - } - - // 2. 使用会话中的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); - DatagramSocket socket = protocol.getUdpSocket(); - if (socket == null) { - log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); - return; - } - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - } - } catch (Exception e) { - log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", - message.getDeviceId(), message.getMethod(), message, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted file mode 100644 index 872a615a6f..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java +++ /dev/null @@ -1,542 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramPacket; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -import java.net.InetSocketAddress; -import java.util.Map; - -/** - * UDP 上行消息处理器 - *

- * 采用无状态 Token 机制(每次请求携带 token): - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password - * 2. 返回 Token:服务端验证后返回 JWT token - * 3. 后续请求:每次请求在 params 中携带 token - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamHandler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - /** - * Token 参数 Key - */ - private static final String PARAM_KEY_TOKEN = "token"; - /** - * Body 参数 Key(实际请求内容) - */ - private static final String PARAM_KEY_BODY = "body"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotUdpSessionManager sessionManager; - - private final IotDeviceTokenService deviceTokenService; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotUdpSessionManager sessionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - /** - * 处理 UDP 数据包 - * - * @param packet 数据包 - * @param socket UDP Socket - */ - public void handle(DatagramPacket packet, DatagramSocket socket) { - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); - Buffer data = packet.data(); - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", - sessionManager.buildAddressKey(senderAddress), data.length()); - try { - processMessage(data, senderAddress, socket); - } catch (Exception e) { - log.error("[handle][处理消息失败,来源: {},错误: {}]", - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); - // UDP 无连接,不需要断开连接,只记录错误 - } - } - - /** - * 处理消息 - * - * @param buffer 消息 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); - return; - } - } catch (Exception e) { - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); - return; - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(message, codecType, senderAddress, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(message, codecType, senderAddress, socket); - } else { - // 业务消息 - handleBusinessRequest(message, codecType, senderAddress, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); - } - } - - /** - * 处理认证请求 - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", - addressKey, authParams.getUsername()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 生成 JWT Token(无状态) - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); - - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 3.3 发送上线消息 - sendOnlineMessage(device); - - // 3.4 发送成功响应(包含 token) - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", - device.getId(), device.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 Token) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", - params.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - *

- * 请求参数格式: - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - @SuppressWarnings("unchecked") - private void handleBusinessRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) - String token = null; - Object body = null; - if (message.getParams() instanceof Map) { - Map paramsMap = (Map) message.getParams(); - token = (String) paramsMap.get(PARAM_KEY_TOKEN); - body = paramsMap.get(PARAM_KEY_BODY); - } - if (StrUtil.isBlank(token)) { - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); - return; - } - // 1.2 验证 token,获取设备信息 - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); - return; - } - - // 2. 获取设备详细信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3. 更新设备会话信息(保持最新,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 4. 将 body 设置为实际的 params,发送消息到消息总线 - message.setParams(body); - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), - device.getDeviceName(), serverId); - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", - device.getId(), message.getMethod(), addressKey); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer) { - // 检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送认证成功响应(包含 token) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param token JWT Token - * @param codecType 消息编解码类型 - */ - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String token, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", true) - .put("token", token) - .put("message", "认证成功") - .build(); - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - // 发送响应 - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, IotDeviceRegisterRespDTO registerResp, - String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送错误响应 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String errorMessage, String codecType) { - sendResponse(socket, address, false, errorMessage, requestId, codecType); - } - - /** - * 发送响应消息 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, - String message, String requestId, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - "response", responseData, code, message); - - // 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { - if (ar.failed()) { - log.error("[sendResponse][发送响应失败,地址: {}]", - sessionManager.buildAddressKey(address), ar.cause()); - } - }); - } catch (Exception e) { - log.error("[sendResponse][发送响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java deleted file mode 100644 index e7831e7086..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.PostConstruct; - -/** - * IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber { - - private final IotWebSocketUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotWebSocketDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java deleted file mode 100644 index df1748cb6e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java +++ /dev/null @@ -1,111 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; - -/** - * IoT 网关 WebSocket 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamProtocol { - - private final IotGatewayProperties.WebSocketProperties wsProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private HttpServer httpServer; - - public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - Vertx vertx) { - this.wsProperties = wsProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort()); - } - - @PostConstruct - @SuppressWarnings("deprecation") - public void start() { - // 1.1 创建服务器选项 - HttpServerOptions options = new HttpServerOptions() - .setPort(wsProperties.getPort()) - .setIdleTimeout(wsProperties.getIdleTimeoutSeconds()) - .setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize()) - .setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize()); - // 1.2 配置 SSL(如果启用) - if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsProperties.getSslKeyPath()) - .setCertPath(wsProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 2. 创建服务器并设置 WebSocket 处理器 - httpServer = vertx.createHttpServer(options); - httpServer.webSocketHandler(socket -> { - // 验证路径 - if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", - socket.path(), wsProperties.getPath()); - socket.reject(); - return; - } - // 创建上行处理器 - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, - messageService, deviceService, connectionManager); - handler.handle(socket); - }); - - // 3. 启动服务器 - try { - httpServer.listen().result(); - log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath()); - } catch (Exception e) { - log.error("[start][IoT 网关 WebSocket 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 WebSocket 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java deleted file mode 100644 index 128b360086..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ /dev/null @@ -1,149 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager; - -import io.vertx.core.http.ServerWebSocket; -import lombok.Data; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * IoT 网关 WebSocket 连接管理器 - *

- * 统一管理 WebSocket 连接的认证状态、设备会话和消息发送功能: - * 1. 管理 WebSocket 连接的认证状态 - * 2. 管理设备会话和在线状态 - * 3. 管理消息发送到设备 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotWebSocketConnectionManager { - - /** - * 连接信息映射:ServerWebSocket -> 连接信息 - */ - private final Map connectionMap = new ConcurrentHashMap<>(); - - /** - * 设备 ID -> ServerWebSocket 的映射 - */ - private final Map deviceSocketMap = new ConcurrentHashMap<>(); - - /** - * 注册设备连接(包含认证信息) - * - * @param socket WebSocket 连接 - * @param deviceId 设备 ID - * @param connectionInfo 连接信息 - */ - public void registerConnection(ServerWebSocket socket, Long deviceId, ConnectionInfo connectionInfo) { - // 如果设备已有其他连接,先清理旧连接 - ServerWebSocket oldSocket = deviceSocketMap.get(deviceId); - if (oldSocket != null && oldSocket != socket) { - log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", - deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - // 清理旧连接的映射 - connectionMap.remove(oldSocket); - } - - // 注册新连接 - connectionMap.put(socket, connectionInfo); - deviceSocketMap.put(deviceId, socket); - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", - deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); - } - - /** - * 注销设备连接 - * - * @param socket WebSocket 连接 - */ - public void unregisterConnection(ServerWebSocket socket) { - ConnectionInfo connectionInfo = connectionMap.remove(socket); - if (connectionInfo == null) { - return; - } - Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); - log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", - deviceId, socket.remoteAddress()); - } - - /** - * 获取连接信息 - */ - public ConnectionInfo getConnectionInfo(ServerWebSocket socket) { - return connectionMap.get(socket); - } - - /** - * 根据设备 ID 获取连接信息 - */ - public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { - ServerWebSocket socket = deviceSocketMap.get(deviceId); - return socket != null ? connectionMap.get(socket) : null; - } - - /** - * 发送消息到设备(文本消息) - * - * @param deviceId 设备 ID - * @param message JSON 消息 - * @return 是否发送成功 - */ - public boolean sendToDevice(Long deviceId, String message) { - ServerWebSocket socket = deviceSocketMap.get(deviceId); - if (socket == null) { - log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); - return false; - } - - try { - socket.writeTextMessage(message); - log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, message.length()); - return true; - } catch (Exception e) { - log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); - // 发送失败时清理连接 - unregisterConnection(socket); - return false; - } - } - - /** - * 连接信息(包含认证信息) - */ - @Data - @Accessors(chain = true) - public static class ConnectionInfo { - - /** - * 设备 ID - */ - private Long deviceId; - /** - * 产品 Key - */ - private String productKey; - /** - * 设备名称 - */ - private String deviceName; - - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java deleted file mode 100644 index 05e3c8c91f..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 下行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotWebSocketDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotWebSocketConnectionManager connectionManager; - - /** - * 处理下行消息 - */ - public void handle(IotDeviceMessage message) { - try { - log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取连接信息 - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( - message.getDeviceId()); - if (connectionInfo == null) { - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); - return; - } - - // 2. 编码消息并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - String jsonMessage = StrUtil.utf8Str(bytes); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - } - } catch (Exception e) { - log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", - message.getDeviceId(), message.getMethod(), message, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java deleted file mode 100644 index 630246afa3..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java +++ /dev/null @@ -1,471 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Handler; -import io.vertx.core.http.ServerWebSocket; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - - -/** - * WebSocket 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamHandler implements Handler { - - /** - * 默认消息编解码类型 - */ - private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotWebSocketConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - @Override - public void handle(ServerWebSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - - // 1. 设置异常和关闭处理器 - socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - - // 2. 设置文本消息处理器 - socket.textMessageHandler(message -> { - try { - processMessage(clientId, message, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param message 消息(JSON 字符串) - * @param socket WebSocket 连接 - * @throws Exception 消息解码失败时抛出异常 - */ - private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception { - // 1.1 基础检查 - if (StrUtil.isBlank(message)) { - return; - } - // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE; - IotDeviceMessage deviceMessage; - try { - deviceMessage = deviceMessageService.decodeDeviceMessage( - StrUtil.utf8Bytes(message), codecType); - if (deviceMessage == null) { - throw new Exception("解码后消息为空"); - } - } catch (Exception e) { - throw new Exception("消息解码失败: " + e.getMessage(), e); - } - - // 2. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(deviceMessage.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, deviceMessage, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, deviceMessage, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, deviceMessage, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, deviceMessage.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败"); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } - } - } - - /** - * 处理认证请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败"); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在"); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功"); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要认证) - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null - || StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 处理业务请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 获取认证信息并处理业务消息 - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo == null) { - log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "连接未认证"); - return; - } - - // 2. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); - } - } - - /** - * 注册连接信息 - * - * @param socket WebSocket 连接 - * @param device 设备 - * @param clientId 客户端 ID - */ - private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) { - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(CODEC_TYPE); - // 注册连接 - connectionManager.registerConnection(socket, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 清理连接 - * - * @param socket WebSocket 连接 - */ - private void cleanupConnection(ServerWebSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); - } - } - - /** - * 发送响应消息 - * - * @param socket WebSocket 连接 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - */ - private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) { - sendResponse(socket, false, errorMessage, requestId); - } - - /** - * 发送成功响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param message 消息 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) { - sendResponse(socket, true, message, requestId); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(ServerWebSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java deleted file mode 100644 index 583763e22c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapClient; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; -import org.eclipse.californium.elements.config.UdpConfig; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * IoT 直连设备 CoAP 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 CoAP 协议直接连接平台 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)
  2. - *
  3. 运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)
  4. - *
  5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    - *
  8. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotDirectDeviceCoapProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 5683; - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - /** - * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk5MjgxOSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.UHLCXsoGNsKbtJcbTV3n1psp03G75hVcVpV4wwd39r4"; - - @BeforeAll - public static void initCaliforniumConfig() { - // 注册 Californium 配置定义 - CoapConfig.register(); - UdpConfig.register(); - // 创建默认配置 - Configuration.setStandard(Configuration.createStandardWithoutFile()); - } - - // ===================== 认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URI: {}]", uri); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON); - // 2.2 输出结果 - log.info("[testAuth][响应码: {}]", response.getCode()); - log.info("[testAuth][响应体: {}]", response.getResponseText()); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } finally { - client.shutdown(); - } - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - @SuppressWarnings("deprecation") - public void testPropertyPost() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()) - ) - .build()); - // 1.2 输出请求 - log.info("[testPropertyPost][请求 URI: {}]", uri); - log.info("[testPropertyPost][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testPropertyPost][响应码: {}]", response.getCode()); - log.info("[testPropertyPost][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - @SuppressWarnings("deprecation") - public void testEventPost() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()) - ) - .build()); - // 1.2 输出请求 - log.info("[testEventPost][请求 URI: {}]", uri); - log.info("[testEventPost][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testEventPost][响应码: {}]", response.getCode()); - log.info("[testEventPost][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - // ===================== 动态注册测试 ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要 Token 认证 - */ - @Test - public void testDeviceRegister() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); - // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); - String payload = JsonUtils.toJsonString(reqDTO); - // 1.3 输出请求 - log.info("[testDeviceRegister][请求 URI: {}]", uri); - log.info("[testDeviceRegister][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON); - // 2.2 输出结果 - log.info("[testDeviceRegister][响应码: {}]", response.getCode()); - log.info("[testDeviceRegister][响应体: {}]", response.getResponseText()); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } finally { - client.shutdown(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java deleted file mode 100644 index ca581cb960..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java +++ /dev/null @@ -1,376 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapClient; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; -import org.eclipse.californium.elements.config.UdpConfig; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Map; - -/** - * IoT 网关设备 CoAP 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 CoAP 协议管理子设备拓扑关系 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)
  2. - *
  3. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  6. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewayDeviceCoapProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 5683; - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - /** - * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - @BeforeAll - public static void initCaliforniumConfig() { - // 注册 Californium 配置定义 - CoapConfig.register(); - UdpConfig.register(); - // 创建默认配置 - Configuration.setStandard(Configuration.createStandardWithoutFile()); - } - - // ===================== 认证测试 ===================== - - /** - * 网关设备认证测试:获取网关设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URI: {}]", uri); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON); - // 2.2 输出结果 - log.info("[testAuth][响应码: {}]", response.getCode()); - log.info("[testAuth][响应体: {}]", response.getResponseText()); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } finally { - client.shutdown(); - } - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - *

- * 网关设备向平台上报需要绑定的子设备信息 - */ - @Test - @SuppressWarnings("deprecation") - public void testTopoAdd() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/add", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - // 1.3 构建请求参数 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.4 输出请求 - log.info("[testTopoAdd][请求 URI: {}]", uri); - log.info("[testTopoAdd][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testTopoAdd][响应码: {}]", response.getCode()); - log.info("[testTopoAdd][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - /** - * 删除子设备拓扑关系测试 - *

- * 网关设备向平台上报需要解绑的子设备信息 - */ - @Test - @SuppressWarnings("deprecation") - public void testTopoDelete() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/delete", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.3 输出请求 - log.info("[testTopoDelete][请求 URI: {}]", uri); - log.info("[testTopoDelete][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testTopoDelete][响应码: {}]", response.getCode()); - log.info("[testTopoDelete][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - /** - * 获取子设备拓扑关系测试 - *

- * 网关设备向平台查询已绑定的子设备列表 - */ - @Test - @SuppressWarnings("deprecation") - public void testTopoGet() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/get", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数(目前为空,预留扩展) - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.3 输出请求 - log.info("[testTopoGet][请求 URI: {}]", uri); - log.info("[testTopoGet][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testTopoGet][响应码: {}]", response.getCode()); - log.info("[testTopoGet][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - // ===================== 子设备注册测试 ===================== - - /** - * 子设备动态注册测试 - *

- * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

- * 注意:此接口需要网关 Token 认证 - */ - @Test - @SuppressWarnings("deprecation") - public void testSubDeviceRegister() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/auth/register/sub-device/%s/%s", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) - .put("version", "1.0") - .put("params", Collections.singletonList(subDevice)) - .build()); - // 1.3 输出请求 - log.info("[testSubDeviceRegister][请求 URI: {}]", uri); - log.info("[testSubDeviceRegister][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testSubDeviceRegister][响应码: {}]", response.getCode()); - log.info("[testSubDeviceRegister][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - *

- * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 - */ - @Test - @SuppressWarnings("deprecation") - public void testPropertyPackPost() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - // 1.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil.builder() - .put("statusReport", gatewayEvent) - .build(); - // 1.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - // 1.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil.builder() - .put("healthCheck", subDeviceEvent) - .build(); - // 1.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - // 1.7 构建请求参数 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.8 输出请求 - log.info("[testPropertyPackPost][请求 URI: {}]", uri); - log.info("[testPropertyPackPost][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testPropertyPackPost][响应码: {}]", response.getCode()); - log.info("[testPropertyPackPost][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java deleted file mode 100644 index 7aed8ecb65..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapClient; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; -import org.eclipse.californium.elements.config.UdpConfig; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * IoT 网关子设备 CoAP 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - *

网关设备转发子设备请求时,Token 使用子设备自己的信息。 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceCoapProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  8. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewaySubDeviceCoapProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 5683; - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - /** - * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTk1NDY3OSwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.jfbUAoU0xkJl4UvO-NUvcJ6yITPRgUjQ4MKATPuwneg"; - - @BeforeAll - public static void initCaliforniumConfig() { - // 注册 Californium 配置定义 - CoapConfig.register(); - UdpConfig.register(); - // 创建默认配置 - Configuration.setStandard(Configuration.createStandardWithoutFile()); - } - - // ===================== 认证测试 ===================== - - /** - * 子设备认证测试:获取子设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URI: {}]", uri); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON); - // 2.2 输出结果 - log.info("[testAuth][响应码: {}]", response.getCode()); - log.info("[testAuth][响应体: {}]", response.getResponseText()); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } finally { - client.shutdown(); - } - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - @SuppressWarnings("deprecation") - public void testPropertyPost() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build())) - .build()); - // 1.2 输出请求 - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][请求 URI: {}]", uri); - log.info("[testPropertyPost][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testPropertyPost][响应码: {}]", response.getCode()); - log.info("[testPropertyPost][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - @SuppressWarnings("deprecation") - public void testEventPost() throws Exception { - // 1.1 构建请求 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis())) - .build()); - // 1.2 输出请求 - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][请求 URI: {}]", uri); - log.info("[testEventPost][请求体: {}]", payload); - - // 2.1 发送请求 - CoapClient client = new CoapClient(uri); - try { - Request request = Request.newPost(); - request.setURI(uri); - request.setPayload(payload); - request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); - - CoapResponse response = client.advanced(request); - // 2.2 输出结果 - log.info("[testEventPost][响应码: {}]", response.getCode()); - log.info("[testEventPost][响应体: {}]", response.getResponseText()); - } finally { - client.shutdown(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java deleted file mode 100644 index 8dd36cc635..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java +++ /dev/null @@ -1,182 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - - -/** - * IoT 直连设备 HTTP 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 HTTP 协议直接连接平台 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. - *
  3. 运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)
  4. - *
  5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    - *
  8. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -@SuppressWarnings("HttpUrlsUsage") -public class IotDirectDeviceHttpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8092; - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - /** - * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k"; - - // ===================== 认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URL: {}]", url); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - String response = HttpUtil.post(url, payload); - // 2.2 输出结果 - log.info("[testAuth][响应体: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - public void testPropertyPost() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()) - ) - .build()); - // 1.2 输出请求 - log.info("[testPropertyPost][请求 URL: {}]", url); - log.info("[testPropertyPost][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testPropertyPost][响应体: {}]", httpResponse.body()); - } - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - public void testEventPost() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()) - ) - .build()); - // 1.2 输出请求 - log.info("[testEventPost][请求 URL: {}]", url); - log.info("[testEventPost][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEventPost][响应体: {}]", httpResponse.body()); - } - } - - // ===================== 动态注册测试 ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要 Token 认证 - */ - @Test - public void testDeviceRegister() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); - // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); - String payload = JsonUtils.toJsonString(reqDTO); - // 1.3 输出请求 - log.info("[testDeviceRegister][请求 URL: {}]", url); - log.info("[testDeviceRegister][请求体: {}]", payload); - - // 2.1 发送请求 - String response = HttpUtil.post(url, payload); - // 2.2 输出结果 - log.info("[testDeviceRegister][响应体: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java deleted file mode 100644 index 4ca6cec270..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java +++ /dev/null @@ -1,312 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Map; - - -/** - * IoT 网关设备 HTTP 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 HTTP 协议管理子设备拓扑关系 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. - *
  3. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  6. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -@SuppressWarnings("HttpUrlsUsage") -public class IotGatewayDeviceHttpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8092; - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - /** - * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - // ===================== 认证测试 ===================== - - /** - * 网关设备认证测试:获取网关设备 Token - */ - @Test - public void testAuth() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URL: {}]", url); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - String response = HttpUtil.post(url, payload); - // 2.2 输出结果 - log.info("[testAuth][响应体: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - *

- * 网关设备向平台上报需要绑定的子设备信息 - */ - @Test - public void testTopoAdd() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - // 1.3 构建请求参数 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.4 输出请求 - log.info("[testTopoAdd][请求 URL: {}]", url); - log.info("[testTopoAdd][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", GATEWAY_TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testTopoAdd][响应体: {}]", httpResponse.body()); - } - } - - /** - * 删除子设备拓扑关系测试 - *

- * 网关设备向平台上报需要解绑的子设备信息 - */ - @Test - public void testTopoDelete() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.3 输出请求 - log.info("[testTopoDelete][请求 URL: {}]", url); - log.info("[testTopoDelete][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", GATEWAY_TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testTopoDelete][响应体: {}]", httpResponse.body()); - } - } - - /** - * 获取子设备拓扑关系测试 - *

- * 网关设备向平台查询已绑定的子设备列表 - */ - @Test - public void testTopoGet() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数(目前为空,预留扩展) - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.3 输出请求 - log.info("[testTopoGet][请求 URL: {}]", url); - log.info("[testTopoGet][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", GATEWAY_TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testTopoGet][响应体: {}]", httpResponse.body()); - } - } - - // ===================== 子设备注册测试 ===================== - - // TODO @芋艿:待测试 - - /** - * 子设备动态注册测试 - *

- * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

- * 注意:此接口需要网关 Token 认证 - */ - @Test - public void testSubDeviceRegister() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) - .put("version", "1.0") - .put("params", Collections.singletonList(subDevice)) - .build()); - // 1.3 输出请求 - log.info("[testSubDeviceRegister][请求 URL: {}]", url); - log.info("[testSubDeviceRegister][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", GATEWAY_TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body()); - } - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - *

- * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 - */ - @Test - public void testPropertyPackPost() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post", - SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - // 1.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - // 1.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil.builder() - .put("statusReport", gatewayEvent) - .build(); - // 1.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - // 1.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil.builder() - .put("healthCheck", subDeviceEvent) - .build(); - // 1.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - // 1.7 构建请求参数 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) - .put("version", "1.0") - .put("params", params) - .build()); - // 1.8 输出请求 - log.info("[testPropertyPackPost][请求 URL: {}]", url); - log.info("[testPropertyPackPost][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", GATEWAY_TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testPropertyPackPost][响应体: {}]", httpResponse.body()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java deleted file mode 100644 index cfebdbe3f8..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - - -/** - * IoT 网关子设备 HTTP 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - *

网关设备转发子设备请求时,URL 和 Token 都使用子设备自己的信息。 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceHttpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  8. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -@SuppressWarnings("HttpUrlsUsage") -public class IotGatewaySubDeviceHttpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8092; - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - /** - * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTg3MTI3NCwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.99sAlRalzMU3CqRlGStDzCwWSBJq6u3PJw48JQ3NpzQ"; - - // ===================== 认证测试 ===================== - - /** - * 子设备认证测试:获取子设备 Token - */ - @Test - public void testAuth() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - String payload = JsonUtils.toJsonString(authReqDTO); - // 1.2 输出请求 - log.info("[testAuth][请求 URL: {}]", url); - log.info("[testAuth][请求体: {}]", payload); - - // 2.1 发送请求 - String response = HttpUtil.post(url, payload); - // 2.2 输出结果 - log.info("[testAuth][响应体: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - public void testPropertyPost() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()) - ) - .build()); - // 1.2 输出请求 - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][请求 URL: {}]", url); - log.info("[testPropertyPost][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testPropertyPost][响应体: {}]", httpResponse.body()); - } - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - public void testEventPost() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", - SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") - .put("params", IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()) - ) - .build()); - // 1.2 输出请求 - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][请求 URL: {}]", url); - log.info("[testEventPost][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Authorization", TOKEN) - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEventPost][响应体: {}]", httpResponse.body()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java deleted file mode 100644 index 67a8ced4dd..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ /dev/null @@ -1,408 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * IoT 直连设备 MQTT 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 MQTT 协议直接连接平台 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)
  2. - *
  3. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 设备连接认证
    • - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    • {@link #testSubscribe()} - 订阅下行消息
    • - *
    - *
  4. - *
- * - *

注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成, - * 认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotDirectDeviceMqttProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 1883; - private static final int TIMEOUT_SECONDS = 10; - - private static Vertx vertx; - - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 连接认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - - // 1. 构建认证信息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", - authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); - - // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); - } - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testPropertyPost][连接认证成功]"); - - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testEventPost][连接认证成功]"); - - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); - } - - // ===================== 设备动态注册测试(一型一密) ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要认证 - */ - @Test - public void testDeviceRegister() throws Exception { - // 1. 连接并认证(使用已有设备连接) - MqttClient client = connectAndAuth(); - log.info("[testDeviceRegister][连接认证成功]"); - - // 2.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 2.2 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - subscribeReply(client, replyTopic); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/register", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - - // 4. 断开连接 - disconnect(client); - } - - // ===================== 订阅下行消息测试 ===================== - - /** - * 订阅下行消息测试:订阅服务端下发的消息 - */ - @Test - public void testSubscribe() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testSubscribe][连接认证成功]"); - - // 2. 设置消息处理器 - client.publishHandler(message -> { - log.info("[testSubscribe][收到消息: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - }); - - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); - log.info("[testSubscribe][订阅主题: {}]", topic); - - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(subscribeAr -> { - if (subscribeAr.succeeded()) { - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - // 保持连接 30 秒等待消息 - vertx.setTimer(30000, id -> { - client.disconnect() - .onComplete(disconnectAr -> { - log.info("[testSubscribe][断开连接]"); - latch.countDown(); - }); - }); - } else { - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); - latch.countDown(); - } - }); - - // 4. 等待测试完成 - boolean completed = latch.await(60, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testSubscribe][测试超时]"); - } - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 MQTT 客户端 - * - * @param authInfo 认证信息 - * @return MQTT 客户端 - */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { - MqttClientOptions options = new MqttClientOptions() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()) - .setCleanSession(true) - .setKeepAliveInterval(60); - return MqttClient.create(vertx, options); - } - - /** - * 连接并认证设备 - * - * @return 已认证的 MQTT 客户端 - */ - private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 订阅响应主题 - * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 - */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发布消息并等待响应 - * - * @param client MQTT 客户端 - * @param topic 发布主题 - * @param request 请求消息 - * @return 响应消息 - */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { - // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); - client.publishHandler(message -> { - log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); - }); - - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) - try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("[publishAndWaitReply][等待响应超时或失败]"); - return null; - } - } - - /** - * 断开连接 - * - * @param client MQTT 客户端 - */ - private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); - client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java deleted file mode 100644 index 517206734c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java +++ /dev/null @@ -1,499 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * IoT 网关设备 MQTT 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 MQTT 协议管理子设备拓扑关系 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)
  2. - *
  3. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 网关设备连接认证
    • - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  4. - *
- * - *

注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成, - * 认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewayDeviceMqttProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 1883; - private static final int TIMEOUT_SECONDS = 10; - - private static Vertx vertx; - - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 连接认证测试 ===================== - - /** - * 网关设备认证测试:获取网关设备 Token - */ - @Test - public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - - // 1. 构建认证信息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", - authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); - - // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); - } - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - *

- * 网关设备向平台上报需要绑定的子设备信息 - */ - @Test - public void testTopoAdd() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testTopoAdd][连接认证成功]"); - - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - - // 2.3 构建请求消息 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/add", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoAdd][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); - } - - /** - * 删除子设备拓扑关系测试 - *

- * 网关设备向平台上报需要解绑的子设备信息 - */ - @Test - public void testTopoDelete() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testTopoDelete][连接认证成功]"); - - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/delete", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoDelete][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); - } - - /** - * 获取子设备拓扑关系测试 - *

- * 网关设备向平台查询已绑定的子设备列表 - */ - @Test - public void testTopoGet() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testTopoGet][连接认证成功]"); - - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/get", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoGet][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); - } - - // ===================== 子设备注册测试 ===================== - - /** - * 子设备动态注册测试 - *

- * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

- * 注意:此接口需要网关认证 - */ - @Test - public void testSubDeviceRegister() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testSubDeviceRegister][连接认证成功]"); - - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建请求消息 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei-mqtt"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - *

- * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 - */ - @Test - public void testPropertyPackPost() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testPropertyPackPost][连接认证成功]"); - - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 2.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - - // 2.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil - .builder() - .put("statusReport", gatewayEvent) - .build(); - - // 2.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - - // 2.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil - .builder() - .put("healthCheck", subDeviceEvent) - .build(); - - // 2.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - - // 2.7 构建请求消息 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPackPost][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 MQTT 客户端 - * - * @param authInfo 认证信息 - * @return MQTT 客户端 - */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { - MqttClientOptions options = new MqttClientOptions() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()) - .setCleanSession(true) - .setKeepAliveInterval(60); - return MqttClient.create(vertx, options); - } - - /** - * 连接并认证网关设备 - * - * @return 已认证的 MQTT 客户端 - */ - private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 订阅响应主题 - * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 - */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发布消息并等待响应 - * - * @param client MQTT 客户端 - * @param topic 发布主题 - * @param request 请求消息 - * @return 响应消息 - */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { - // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); - client.publishHandler(message -> { - log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); - }); - - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) - try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("[publishAndWaitReply][等待响应超时或失败]"); - return null; - } - } - - /** - * 断开连接 - * - * @param client MQTT 客户端 - */ - private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); - client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java deleted file mode 100644 index c14d2c676b..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java +++ /dev/null @@ -1,332 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * IoT 网关子设备 MQTT 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - *

网关设备转发子设备请求时,使用子设备自己的认证信息连接。 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceMqttProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 子设备连接认证
    • - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  6. - *
- * - *

注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成, - * 认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewaySubDeviceMqttProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 1883; - private static final int TIMEOUT_SECONDS = 10; - - private static Vertx vertx; - - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 连接认证测试 ===================== - - /** - * 子设备认证测试:获取子设备 Token - */ - @Test - public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - - // 1. 构建认证信息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", - authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); - - // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); - } - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testPropertyPost][连接认证成功]"); - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1. 连接并认证 - MqttClient client = connectAndAuth(); - log.info("[testEventPost][连接认证成功]"); - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); - - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); - - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 MQTT 客户端 - * - * @param authInfo 认证信息 - * @return MQTT 客户端 - */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { - MqttClientOptions options = new MqttClientOptions() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()) - .setCleanSession(true) - .setKeepAliveInterval(60); - return MqttClient.create(vertx, options); - } - - /** - * 连接并认证子设备 - * - * @return 已认证的 MQTT 客户端 - */ - private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 订阅响应主题 - * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 - */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发布消息并等待响应 - * - * @param client MQTT 客户端 - * @param topic 发布主题 - * @param request 请求消息 - * @return 响应消息 - */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { - // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); - client.publishHandler(message -> { - log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); - }); - - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); - - // 3. 等待响应(超时返回 null) - try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("[publishAndWaitReply][等待响应超时或失败]"); - return null; - } - } - - /** - * 断开连接 - * - * @param client MQTT 客户端 - */ - private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); - client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java deleted file mode 100644 index 4b6936c63c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ /dev/null @@ -1,278 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.HexUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -/** - * IoT 直连设备 TCP 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 设备认证
    • - *
    • {@link #testDeviceRegister()} - 设备动态注册(一型一密)
    • - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    - *
  6. - *
- * - *

注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotDirectDeviceTcpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8091; - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - -// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); - private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - // ===================== 认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 动态注册测试 ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要认证 - */ - @Test - public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - } - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testPropertyPost][认证响应: {}]", authResponse); - - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testEventPost][认证响应: {}]", authResponse); - - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 执行设备认证 - * - * @param socket TCP 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]", - responseBytes.length, - String.format("%02X", responseBytes[0]), - HexUtil.encodeHexStr(responseBytes)); - return CODEC.decode(responseBytes); - } - return null; - } - - /** - * 发送 TCP 请求并接收响应 - * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 - */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; - } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java deleted file mode 100644 index b417ceb9fa..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ /dev/null @@ -1,398 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * IoT 网关设备 TCP 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 网关设备认证
    • - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  6. - *
- * - *

注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewayDeviceTcpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8091; - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - // ===================== 认证测试 ===================== - - /** - * 网关设备认证测试:获取网关设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - */ - @Test - public void testTopoAdd() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testTopoAdd][认证响应: {}]", authResponse); - - // 2.1 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - // 2.2 构建请求参数 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - } - } - - /** - * 删除子设备拓扑关系测试 - */ - @Test - public void testTopoDelete() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testTopoDelete][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - } - } - - /** - * 获取子设备拓扑关系测试 - */ - @Test - public void testTopoGet() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testTopoGet][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - } - } - - // ===================== 子设备注册测试 ===================== - - /** - * 子设备动态注册测试 - */ - @Test - public void testSubDeviceRegister() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - } - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - */ - @Test - public void testPropertyPackPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testPropertyPackPost][认证响应: {}]", authResponse); - - // 2.1 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - // 2.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil.builder() - .put("statusReport", gatewayEvent) - .build(); - // 2.3 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - // 2.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil.builder() - .put("healthCheck", subDeviceEvent) - .build(); - // 2.5 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - // 2.6 构建请求参数 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 执行网关设备认证 - * - * @param socket TCP 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; - } - - /** - * 发送 TCP 请求并接收响应 - * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 - */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; - } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java deleted file mode 100644 index c918b474c3..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ /dev/null @@ -1,245 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -/** - * IoT 网关子设备 TCP 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 修改 {@link #CODEC} 选择测试的编解码格式
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 子设备认证
    • - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  8. - *
- * - *

注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewaySubDeviceTcpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8091; - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - // ===================== 认证测试 ===================== - - /** - * 子设备认证测试:获取子设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testPropertyPost][认证响应: {}]", authResponse); - - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - - // 1. 先进行认证 - IotDeviceMessage authResponse = authenticate(socket); - log.info("[testEventPost][认证响应: {}]", authResponse); - - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 执行子设备认证 - * - * @param socket TCP 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; - } - - /** - * 发送 TCP 请求并接收响应 - * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 - */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; - } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java deleted file mode 100644 index 9d507cc036..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ /dev/null @@ -1,262 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; - -/** - * IoT 直连设备 UDP 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. - *
  5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    - *
  8. - *
- * - *

注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同) - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotDirectDeviceUdpProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8093; - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - /** - * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo"; - - // ===================== 认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 动态注册测试 ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要认证 - */ - @Test - public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - } - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - withToken(IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 构建带 token 的 params - *

- * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map - */ - private Map withToken(Object params) { - Map result = new HashMap<>(); - result.put("token", TOKEN); - result.put("body", params); - return result; - } - - /** - * 发送 UDP 请求并接收响应 - * - * @param socket UDP Socket - * @param payload 请求数据 - * @return 响应数据 - */ - public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { - InetAddress address = InetAddress.getByName(SERVER_HOST); - - // 发送请求 - DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); - socket.send(sendPacket); - - // 接收响应 - byte[] receiveData = new byte[4096]; - DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); - try { - socket.receive(receivePacket); - byte[] response = new byte[receivePacket.getLength()]; - System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); - return response; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java deleted file mode 100644 index c0dfaa8053..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java +++ /dev/null @@ -1,351 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.net.DatagramSocket; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - -/** - * IoT 网关设备 UDP 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. - *
  5. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
  6. - *
  7. 运行以下测试方法: - *
      - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  8. - *
- * - *

注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同) - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewayDeviceUdpProtocolIntegrationTest { - - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - /** - * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTk1NDcxNSwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.Vg5iateNrpg0FVQI2eJomggxrYXGpwug8wsz9BsVr5w"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - // ===================== 认证测试 ===================== - - /** - * 网关设备认证测试:获取网关设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - *

- * 网关设备向平台上报需要绑定的子设备信息 - */ - @Test - public void testTopoAdd() throws Exception { - // 1.1 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - // 1.2 构建请求参数 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - withToken(params), - null, null, null); - // 1.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - } - } - - /** - * 删除子设备拓扑关系测试 - *

- * 网关设备向平台上报需要解绑的子设备信息 - */ - @Test - public void testTopoDelete() throws Exception { - // 1.1 构建请求参数 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - } - } - - /** - * 获取子设备拓扑关系测试 - *

- * 网关设备向平台查询已绑定的子设备列表 - */ - @Test - public void testTopoGet() throws Exception { - // 1.1 构建请求参数(目前为空,预留扩展) - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - } - } - - // ===================== 子设备注册测试 ===================== - - /** - * 子设备动态注册测试 - *

- * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

- * 注意:此接口需要网关 Token 认证 - */ - @Test - public void testSubDeviceRegister() throws Exception { - // 1.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - withToken(Collections.singletonList(subDevice)), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - } - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - *

- * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 - */ - @Test - public void testPropertyPackPost() throws Exception { - // 1.1 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - // 1.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil.builder() - .put("statusReport", gatewayEvent) - .build(); - // 1.3 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - // 1.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil.builder() - .put("healthCheck", subDeviceEvent) - .build(); - // 1.5 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - // 1.6 构建请求参数 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - withToken(params), - null, null, null); - // 1.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 构建带 token 的 params - *

- * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map - */ - private Map withToken(Object params) { - Map result = new HashMap<>(); - result.put("token", GATEWAY_TOKEN); - result.put("body", params); - return result; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java deleted file mode 100644 index 100c276de2..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java +++ /dev/null @@ -1,206 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.net.DatagramSocket; -import java.util.HashMap; -import java.util.Map; - -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - -/** - * IoT 网关子设备 UDP 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - *

网关设备转发子设备请求时,Token 使用子设备自己的信息。 - * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 修改 {@link #CODEC} 选择测试的编解码格式
  6. - *
  7. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  8. - *
  9. 运行以下测试方法: - *
      - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  10. - *
- * - *

注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同) - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewaySubDeviceUdpProtocolIntegrationTest { - - private static final int TIMEOUT_MS = 5000; - - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - /** - * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 - */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTk1NDY3OSwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.jfbUAoU0xkJl4UvO-NUvcJ6yITPRgUjQ4MKATPuwneg"; - - // ===================== 认证测试 ===================== - - /** - * 子设备认证测试:获取子设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - withToken(IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } - } - - // ===================== 辅助方法 ===================== - - /** - * 构建带 token 的 params - *

- * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map - */ - private Map withToken(Object params) { - Map result = new HashMap<>(); - result.put("token", TOKEN); - result.put("body", params); - return result; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java deleted file mode 100644 index ca79c4220c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java +++ /dev/null @@ -1,322 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.vertx.core.Vertx; -import io.vertx.core.http.WebSocket; -import io.vertx.core.http.WebSocketClient; -import io.vertx.core.http.WebSocketConnectOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -/** - * IoT 直连设备 WebSocket 协议集成测试(手动测试) - * - *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 WebSocket 协议直接连接平台 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(WebSocket 端口 8094)
  2. - *
  3. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 设备认证
    • - *
    • {@link #testDeviceRegister()} - 设备动态注册(一型一密)
    • - *
    • {@link #testPropertyPost()} - 设备属性上报
    • - *
    • {@link #testEventPost()} - 设备事件上报
    • - *
    - *
  4. - *
- * - *

注意:WebSocket 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotDirectDeviceWebSocketProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8094; - private static final String WS_PATH = "/ws"; - private static final int TIMEOUT_SECONDS = 5; - - private static Vertx vertx; - - // ===================== 编解码器选择 ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 认证测试 ===================== - - /** - * 认证测试:获取设备 Token - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testAuth][WebSocket 连接成功]"); - - // 2.2 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - - // 3. 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testAuth][响应消息: {}]", responseMessage); - } else { - log.warn("[testAuth][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 动态注册测试 ===================== - - /** - * 直连设备动态注册测试(一型一密) - *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) - *

- * 注意:此接口不需要认证 - */ - @Test - public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testDeviceRegister][WebSocket 连接成功]"); - - // 2.2 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - - // 3. 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testDeviceRegister][响应消息: {}]", responseMessage); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 直连设备属性上报测试 ===================== - - /** - * 属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testPropertyPost][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testPropertyPost][认证响应: {}]", authResponse); - - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testPropertyPost][响应消息: {}]", responseMessage); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 直连设备事件上报测试 ===================== - - /** - * 事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testEventPost][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testEventPost][认证响应: {}]", authResponse); - - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testEventPost][响应消息: {}]", responseMessage); - } else { - log.warn("[testEventPost][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 WebSocket 连接(同步) - * - * @return WebSocket 连接 - */ - private WebSocket createWebSocketConnection() throws Exception { - WebSocketClient wsClient = vertx.createWebSocketClient(); - WebSocketConnectOptions options = new WebSocketConnectOptions() - .setHost(SERVER_HOST) - .setPort(SERVER_PORT) - .setURI(WS_PATH); - return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发送消息并等待响应(同步) - * - * @param ws WebSocket 连接 - * @param message 请求消息 - * @return 响应消息 - */ - public static String sendAndReceive(WebSocket ws, String message) throws Exception { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference responseRef = new AtomicReference<>(); - - // 设置消息处理器 - ws.textMessageHandler(response -> { - log.info("[sendAndReceive][收到响应: {}]", response); - responseRef.set(response); - latch.countDown(); - }); - - // 发送请求 - log.info("[sendAndReceive][发送请求: {}]", message); - ws.writeTextMessage(message); - - // 等待响应 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[sendAndReceive][等待响应超时]"); - } - return responseRef.get(); - } - - /** - * 执行设备认证(同步) - * - * @param ws WebSocket 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(WebSocket ws) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[authenticate][发送认证请求: {}]", jsonMessage); - - String response = sendAndReceive(ws, jsonMessage); - if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); - } - return null; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java deleted file mode 100644 index a44f2f6dd5..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java +++ /dev/null @@ -1,452 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.vertx.core.Vertx; -import io.vertx.core.http.WebSocket; -import io.vertx.core.http.WebSocketClient; -import io.vertx.core.http.WebSocketConnectOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -/** - * IoT 网关设备 WebSocket 协议集成测试(手动测试) - * - *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 WebSocket 协议管理子设备拓扑关系 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(WebSocket 端口 8094)
  2. - *
  3. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 网关设备认证
    • - *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • - *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • - *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • - *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • - *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • - *
    - *
  4. - *
- * - *

注意:WebSocket 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewayDeviceWebSocketProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8094; - private static final String WS_PATH = "/ws"; - private static final int TIMEOUT_SECONDS = 5; - - private static Vertx vertx; - - // ===================== 编解码器选择 ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== - - private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; - private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; - private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; - - // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String SUB_DEVICE_NAME = "chazuo-it"; - private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 认证测试 ===================== - - /** - * 网关设备认证测试 - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testAuth][WebSocket 连接成功]"); - - // 2.2 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - - // 3. 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testAuth][响应消息: {}]", responseMessage); - } else { - log.warn("[testAuth][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 拓扑管理测试 ===================== - - /** - * 添加子设备拓扑关系测试 - */ - @Test - public void testTopoAdd() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testTopoAdd][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testTopoAdd][认证响应: {}]", authResponse); - - // 2.1 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); - // 2.2 构建请求参数 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testTopoAdd][响应消息: {}]", responseMessage); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - /** - * 删除子设备拓扑关系测试 - */ - @Test - public void testTopoDelete() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testTopoDelete][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testTopoDelete][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testTopoDelete][响应消息: {}]", responseMessage); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - /** - * 获取子设备拓扑关系测试 - */ - @Test - public void testTopoGet() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testTopoGet][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testTopoGet][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testTopoGet][响应消息: {}]", responseMessage); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 子设备注册测试 ===================== - - /** - * 子设备动态注册测试 - */ - @Test - public void testSubDeviceRegister() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testSubDeviceRegister][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); - - // 2.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei-ws"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 批量上报测试 ===================== - - /** - * 批量上报属性测试(网关 + 子设备) - */ - @Test - public void testPropertyPackPost() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testPropertyPackPost][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testPropertyPackPost][认证响应: {}]", authResponse); - - // 2.1 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); - // 2.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil.builder() - .put("statusReport", gatewayEvent) - .build(); - // 2.3 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); - // 2.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil.builder() - .put("healthCheck", subDeviceEvent) - .build(); - // 2.5 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); - // 2.6 构建请求参数 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testPropertyPackPost][响应消息: {}]", responseMessage); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 WebSocket 连接(同步) - * - * @return WebSocket 连接 - */ - private WebSocket createWebSocketConnection() throws Exception { - WebSocketClient wsClient = vertx.createWebSocketClient(); - WebSocketConnectOptions options = new WebSocketConnectOptions() - .setHost(SERVER_HOST) - .setPort(SERVER_PORT) - .setURI(WS_PATH); - return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发送消息并等待响应(同步) - * - * @param ws WebSocket 连接 - * @param message 请求消息 - * @return 响应消息 - */ - private String sendAndReceive(WebSocket ws, String message) throws Exception { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference responseRef = new AtomicReference<>(); - - // 设置消息处理器 - ws.textMessageHandler(response -> { - log.info("[sendAndReceive][收到响应: {}]", response); - responseRef.set(response); - latch.countDown(); - }); - - // 发送请求 - log.info("[sendAndReceive][发送请求: {}]", message); - ws.writeTextMessage(message); - - // 等待响应 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[sendAndReceive][等待响应超时]"); - } - return responseRef.get(); - } - - /** - * 执行网关设备认证(同步) - * - * @param ws WebSocket 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(WebSocket ws) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[authenticate][发送认证请求: {}]", jsonMessage); - - String response = sendAndReceive(ws, jsonMessage); - if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); - } - return null; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java deleted file mode 100644 index 04bf3d5632..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java +++ /dev/null @@ -1,288 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -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.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.vertx.core.Vertx; -import io.vertx.core.http.WebSocket; -import io.vertx.core.http.WebSocketClient; -import io.vertx.core.http.WebSocketConnectOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -/** - * IoT 网关子设备 WebSocket 协议集成测试(手动测试) - * - *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 - * - *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 - * - *

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(WebSocket 端口 8094)
  2. - *
  3. 确保子设备已通过 {@link IotGatewayDeviceWebSocketProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 子设备认证
    • - *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • - *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • - *
    - *
  6. - *
- * - *

注意:WebSocket 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息 - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - private static final int SERVER_PORT = 8094; - private static final String WS_PATH = "/ws"; - private static final int TIMEOUT_SECONDS = 5; - - private static Vertx vertx; - - // ===================== 编解码器选择 ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== - - private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; - private static final String DEVICE_NAME = "chazuo-it"; - private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ===================== 认证测试 ===================== - - /** - * 子设备认证测试 - */ - @Test - public void testAuth() throws Exception { - // 1.1 构建认证消息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 2.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testAuth][WebSocket 连接成功]"); - - // 2.2 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - - // 3. 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testAuth][响应消息: {}]", responseMessage); - } else { - log.warn("[testAuth][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 子设备属性上报测试 ===================== - - /** - * 子设备属性上报测试 - */ - @Test - public void testPropertyPost() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testPropertyPost][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testPropertyPost][认证响应: {}]", authResponse); - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testPropertyPost][响应消息: {}]", responseMessage); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 子设备事件上报测试 ===================== - - /** - * 子设备事件上报测试 - */ - @Test - public void testEventPost() throws Exception { - // 1.1 创建 WebSocket 连接(同步) - WebSocket ws = createWebSocketConnection(); - log.info("[testEventPost][WebSocket 连接成功]"); - - // 1.2 先进行认证 - IotDeviceMessage authResponse = authenticate(ws); - log.info("[testEventPost][认证响应: {}]", authResponse); - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - - // 3.1 发送并等待响应 - String response = sendAndReceive(ws, jsonMessage); - // 3.2 解码响应 - if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); - log.info("[testEventPost][响应消息: {}]", responseMessage); - } else { - log.warn("[testEventPost][未收到响应]"); - } - - // 4. 关闭连接 - ws.close(); - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 WebSocket 连接(同步) - * - * @return WebSocket 连接 - */ - private WebSocket createWebSocketConnection() throws Exception { - WebSocketClient wsClient = vertx.createWebSocketClient(); - WebSocketConnectOptions options = new WebSocketConnectOptions() - .setHost(SERVER_HOST) - .setPort(SERVER_PORT) - .setURI(WS_PATH); - return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - - /** - * 发送消息并等待响应(同步) - * - * @param ws WebSocket 连接 - * @param message 请求消息 - * @return 响应消息 - */ - private String sendAndReceive(WebSocket ws, String message) throws Exception { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference responseRef = new AtomicReference<>(); - - // 设置消息处理器 - ws.textMessageHandler(response -> { - log.info("[sendAndReceive][收到响应: {}]", response); - responseRef.set(response); - latch.countDown(); - }); - - // 发送请求 - log.info("[sendAndReceive][发送请求: {}]", message); - ws.writeTextMessage(message); - - // 等待响应 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[sendAndReceive][等待响应超时]"); - } - return responseRef.get(); - } - - /** - * 执行子设备认证(同步) - * - * @param ws WebSocket 连接 - * @return 认证响应消息 - */ - private IotDeviceMessage authenticate(WebSocket ws) throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - - byte[] payload = CODEC.encode(request); - String jsonMessage = StrUtil.utf8Str(payload); - log.info("[authenticate][发送认证请求: {}]", jsonMessage); - - String response = sendAndReceive(ws, jsonMessage); - if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); - } - return null; - } - -}