Commit a28a15295cdb60f340f7157a5bbd18a76fd4ccbb

Authored by YunaiV
1 parent 8e4b4cf2

feat(iot):【协议改造】coap 初步改造(100%)

Showing 30 changed files with 955 additions and 960 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
7 5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
8 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
9 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
... ... @@ -11,12 +9,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
11 9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
12 10 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
13 11 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
16   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
17   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler;
18 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
19   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
20 13 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
21 14 import io.vertx.core.Vertx;
22 15 import lombok.extern.slf4j.Slf4j;
... ... @@ -111,63 +104,4 @@ public class IotGatewayConfiguration {
111 104  
112 105 }
113 106  
114   - /**
115   - * IoT 网关 CoAP 协议配置类
116   - */
117   - @Configuration
118   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
119   - @Slf4j
120   - public static class CoapProtocolConfiguration {
121   -
122   - @Bean
123   - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
124   - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
125   - }
126   -
127   - @Bean
128   - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
129   - IotMessageBus messageBus) {
130   - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
131   - }
132   -
133   - }
134   -
135   - /**
136   - * IoT 网关 WebSocket 协议配置类
137   - */
138   - @Configuration
139   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
140   - @Slf4j
141   - public static class WebSocketProtocolConfiguration {
142   -
143   - @Bean(name = "websocketVertx", destroyMethod = "close")
144   - public Vertx websocketVertx() {
145   - return Vertx.vertx();
146   - }
147   -
148   - @Bean
149   - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
150   - IotDeviceService deviceService,
151   - IotDeviceMessageService messageService,
152   - IotWebSocketConnectionManager connectionManager,
153   - @Qualifier("websocketVertx") Vertx websocketVertx) {
154   - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
155   - deviceService, messageService, connectionManager, websocketVertx);
156   - }
157   -
158   - @Bean
159   - public IotWebSocketDownstreamHandler iotWebSocketDownstreamHandler(IotDeviceMessageService messageService,
160   - IotWebSocketConnectionManager connectionManager) {
161   - return new IotWebSocketDownstreamHandler(messageService, connectionManager);
162   - }
163   -
164   - @Bean
165   - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
166   - IotWebSocketDownstreamHandler downstreamHandler,
167   - IotMessageBus messageBus) {
168   - return new IotWebSocketDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus);
169   - }
170   -
171   - }
172   -
173 107 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
1 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
4 5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
6 7 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
7 9 import io.vertx.core.net.KeyCertOptions;
8 10 import io.vertx.core.net.TrustOptions;
9 11 import jakarta.validation.Valid;
... ... @@ -90,16 +92,6 @@ public class IotGatewayProperties {
90 92 */
91 93 private MqttProperties mqtt;
92 94  
93   - /**
94   - * CoAP 组件配置
95   - */
96   - private CoapProperties coap;
97   -
98   - /**
99   - * WebSocket 组件配置
100   - */
101   - private WebSocketProperties websocket;
102   -
103 95 }
104 96  
105 97 @Data
... ... @@ -344,93 +336,6 @@ public class IotGatewayProperties {
344 336  
345 337 }
346 338  
347   - @Data
348   - public static class CoapProperties {
349   -
350   - /**
351   - * 是否开启
352   - */
353   - @NotNull(message = "是否开启不能为空")
354   - private Boolean enabled;
355   -
356   - /**
357   - * 服务端口(CoAP 默认端口 5683)
358   - */
359   - @NotNull(message = "服务端口不能为空")
360   - private Integer port = 5683;
361   -
362   - /**
363   - * 最大消息大小(字节)
364   - */
365   - @NotNull(message = "最大消息大小不能为空")
366   - private Integer maxMessageSize = 1024;
367   -
368   - /**
369   - * ACK 超时时间(毫秒)
370   - */
371   - @NotNull(message = "ACK 超时时间不能为空")
372   - private Integer ackTimeout = 2000;
373   -
374   - /**
375   - * 最大重传次数
376   - */
377   - @NotNull(message = "最大重传次数不能为空")
378   - private Integer maxRetransmit = 4;
379   -
380   - }
381   -
382   - @Data
383   - public static class WebSocketProperties {
384   -
385   - /**
386   - * 是否开启
387   - */
388   - @NotNull(message = "是否开启不能为空")
389   - private Boolean enabled;
390   -
391   - /**
392   - * 服务器端口(默认:8094)
393   - */
394   - private Integer port = 8094;
395   -
396   - /**
397   - * WebSocket 路径(默认:/ws)
398   - */
399   - @NotEmpty(message = "WebSocket 路径不能为空")
400   - private String path = "/ws";
401   -
402   - /**
403   - * 最大消息大小(字节,默认 64KB)
404   - */
405   - private Integer maxMessageSize = 65536;
406   -
407   - /**
408   - * 最大帧大小(字节,默认 64KB)
409   - */
410   - private Integer maxFrameSize = 65536;
411   -
412   - /**
413   - * 空闲超时时间(秒,默认 60)
414   - */
415   - private Integer idleTimeoutSeconds = 60;
416   -
417   - /**
418   - * 是否启用 SSL(wss://)
419   - */
420   - private Boolean sslEnabled = false;
421   -
422   - /**
423   - * SSL 证书路径
424   - */
425   - private String sslCertPath;
426   -
427   - /**
428   - * SSL 私钥路径
429   - */
430   - private String sslKeyPath;
431   -
432   - }
433   -
434 339 // TODO @AI:【暂时忽略】改成 ProtocolProperties
435 340 /**
436 341 * 协议实例配置
... ... @@ -489,6 +394,18 @@ public class IotGatewayProperties {
489 394 @Valid
490 395 private IotUdpConfig udp;
491 396  
  397 + /**
  398 + * CoAP 协议配置
  399 + */
  400 + @Valid
  401 + private IotCoapConfig coap;
  402 +
  403 + /**
  404 + * WebSocket 协议配置
  405 + */
  406 + @Valid
  407 + private IotWebSocketConfig websocket;
  408 +
492 409 }
493 410  
494 411 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
... ... @@ -4,9 +4,11 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.BooleanUtil;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
7 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
8 9 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
9 10 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
10 12 import lombok.Getter;
11 13 import lombok.extern.slf4j.Slf4j;
12 14 import org.springframework.context.SmartLifecycle;
... ... @@ -100,6 +102,10 @@ public class IotProtocolManager implements SmartLifecycle {
100 102 return createTcpProtocol(config);
101 103 case UDP:
102 104 return createUdpProtocol(config);
  105 + case COAP:
  106 + return createCoapProtocol(config);
  107 + case WEBSOCKET:
  108 + return createWebSocketProtocol(config);
103 109 default:
104 110 throw new IllegalArgumentException(String.format(
105 111 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
... ... @@ -136,4 +142,24 @@ public class IotProtocolManager implements SmartLifecycle {
136 142 return new IotUdpProtocol(config);
137 143 }
138 144  
  145 + /**
  146 + * 创建 CoAP 协议实例
  147 + *
  148 + * @param config 协议实例配置
  149 + * @return CoAP 协议实例
  150 + */
  151 + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  152 + return new IotCoapProtocol(config);
  153 + }
  154 +
  155 + /**
  156 + * 创建 WebSocket 协议实例
  157 + *
  158 + * @param config 协议实例配置
  159 + * @return WebSocket 协议实例
  160 + */
  161 + private IotWebSocketUpstreamProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  162 + return new IotWebSocketUpstreamProtocol(config);
  163 + }
  164 +
139 165 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT CoAP 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotCoapConfig {
  14 +
  15 + /**
  16 + * 最大消息大小(字节)
  17 + */
  18 + @NotNull(message = "最大消息大小不能为空")
  19 + @Min(value = 64, message = "最大消息大小必须大于 64 字节")
  20 + private Integer maxMessageSize = 1024;
  21 +
  22 + /**
  23 + * ACK 超时时间(毫秒)
  24 + */
  25 + @NotNull(message = "ACK 超时时间不能为空")
  26 + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒")
  27 + private Integer ackTimeoutMs = 2000;
  28 +
  29 + /**
  30 + * 最大重传次数
  31 + */
  32 + @NotNull(message = "最大重传次数不能为空")
  33 + @Min(value = 0, message = "最大重传次数必须大于等于 0")
  34 + private Integer maxRetransmit = 4;
  35 +
  36 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthHandler;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthResource;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterResource;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubResource;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamTopicResource;
  18 +import lombok.Getter;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.eclipse.californium.core.CoapResource;
  21 +import org.eclipse.californium.core.CoapServer;
  22 +import org.eclipse.californium.core.config.CoapConfig;
  23 +import org.eclipse.californium.elements.config.Configuration;
  24 +import org.springframework.util.Assert;
  25 +
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +/**
  29 + * IoT CoAP 协议实现
  30 + * <p>
  31 + * 基于 Eclipse Californium 实现,支持:
  32 + * 1. 认证:POST /auth
  33 + * 2. 设备动态注册:POST /auth/register/device
  34 + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName}
  35 + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  36 + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  37 + *
  38 + * @author 芋道源码
  39 + */
  40 +@Slf4j
  41 +public class IotCoapProtocol implements IotProtocol {
  42 +
  43 + /**
  44 + * 协议配置
  45 + */
  46 + private final ProtocolInstanceProperties properties;
  47 + /**
  48 + * 服务器 ID(用于消息追踪,全局唯一)
  49 + */
  50 + @Getter
  51 + private final String serverId;
  52 +
  53 + /**
  54 + * 运行状态
  55 + */
  56 + @Getter
  57 + private volatile boolean running = false;
  58 +
  59 + /**
  60 + * CoAP 服务器
  61 + */
  62 + private CoapServer coapServer;
  63 +
  64 + /**
  65 + * 下行消息订阅者
  66 + */
  67 + private final IotCoapDownstreamSubscriber downstreamSubscriber;
  68 +
  69 + public IotCoapProtocol(ProtocolInstanceProperties properties) {
  70 + IotCoapConfig coapConfig = properties.getCoap();
  71 + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
  72 + this.properties = properties;
  73 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  74 +
  75 + // 初始化下行消息订阅者
  76 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  77 + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
  78 + }
  79 +
  80 + @Override
  81 + public String getId() {
  82 + return properties.getId();
  83 + }
  84 +
  85 + @Override
  86 + public IotProtocolTypeEnum getType() {
  87 + return IotProtocolTypeEnum.COAP;
  88 + }
  89 +
  90 + @Override
  91 + public void start() {
  92 + if (running) {
  93 + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId());
  94 + return;
  95 + }
  96 +
  97 + IotCoapConfig coapConfig = properties.getCoap();
  98 + try {
  99 + // 1.1 创建 CoAP 配置
  100 + Configuration config = Configuration.createStandardWithoutFile();
  101 + config.set(CoapConfig.COAP_PORT, properties.getPort());
  102 + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
  103 + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS);
  104 + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit());
  105 + // 1.2 创建 CoAP 服务器
  106 + coapServer = new CoapServer(config);
  107 +
  108 + // 2.1 添加 /auth 认证资源
  109 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId);
  110 + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler);
  111 + coapServer.add(authResource);
  112 + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
  113 + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
  114 + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
  115 + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源
  116 + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler();
  117 + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler);
  118 + authResource.add(new CoapResource("register") {{
  119 + add(registerResource);
  120 + add(registerSubResource);
  121 + }});
  122 + // 2.4 添加 /topic 根资源(用于上行消息)
  123 + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId);
  124 + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler);
  125 + coapServer.add(topicResource);
  126 +
  127 + // 3. 启动服务器
  128 + coapServer.start();
  129 + running = true;
  130 + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]",
  131 + getId(), properties.getPort(), serverId);
  132 +
  133 + // 4. 启动下行消息订阅者
  134 + this.downstreamSubscriber.start();
  135 + } catch (Exception e) {
  136 + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
  137 + if (coapServer != null) {
  138 + coapServer.destroy();
  139 + coapServer = null;
  140 + }
  141 + throw e;
  142 + }
  143 + }
  144 +
  145 + @Override
  146 + public void stop() {
  147 + if (!running) {
  148 + return;
  149 + }
  150 + // 1. 停止下行消息订阅者
  151 + try {
  152 + downstreamSubscriber.stop();
  153 + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
  154 + } catch (Exception e) {
  155 + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  156 + }
  157 +
  158 + // 2. 关闭 CoAP 服务器
  159 + if (coapServer != null) {
  160 + try {
  161 + coapServer.stop();
  162 + coapServer.destroy();
  163 + coapServer = null;
  164 + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId());
  165 + } catch (Exception e) {
  166 + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e);
  167 + }
  168 + }
  169 + running = false;
  170 + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId());
  171 + }
  172 +
  173 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
2   -
3   -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler;
13   -import jakarta.annotation.PostConstruct;
14   -import jakarta.annotation.PreDestroy;
15   -import lombok.Getter;
16   -import lombok.extern.slf4j.Slf4j;
17   -import org.eclipse.californium.core.CoapResource;
18   -import org.eclipse.californium.core.CoapServer;
19   -import org.eclipse.californium.core.config.CoapConfig;
20   -import org.eclipse.californium.elements.config.Configuration;
21   -
22   -import java.util.concurrent.TimeUnit;
23   -
24   -/**
25   - * IoT 网关 CoAP 协议:接收设备上行消息
26   - *
27   - * 基于 Eclipse Californium 实现,支持:
28   - * 1. 认证:POST /auth
29   - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
30   - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
31   - *
32   - * @author 芋道源码
33   - */
34   -@Slf4j
35   -public class IotCoapUpstreamProtocol implements IotProtocol {
36   -
37   - private static final String ID = "coap";
38   -
39   - private final IotGatewayProperties.CoapProperties coapProperties;
40   -
41   - private CoapServer coapServer;
42   -
43   - @Getter
44   - private final String serverId;
45   -
46   - private volatile boolean running = false;
47   -
48   - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
49   - this.coapProperties = coapProperties;
50   - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
51   - }
52   -
53   - @Override
54   - public String getId() {
55   - return ID;
56   - }
57   -
58   - @Override
59   - public IotProtocolTypeEnum getType() {
60   - return IotProtocolTypeEnum.COAP;
61   - }
62   -
63   - @Override
64   - @PostConstruct
65   - public void start() {
66   - try {
67   - // 1.1 创建网络配置(Californium 3.x API)
68   - Configuration config = Configuration.createStandardWithoutFile();
69   - config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
70   - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
71   - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
72   - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
73   - // 1.2 创建 CoAP 服务器
74   - coapServer = new CoapServer(config);
75   -
76   - // 2.1 添加 /auth 认证资源
77   - IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
78   - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
79   - coapServer.add(authResource);
80   - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
81   - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
82   - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
83   - authResource.add(new CoapResource("register") {{
84   - add(registerResource);
85   - }});
86   - // 2.3 添加 /topic 根资源(用于上行消息)
87   - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
88   - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
89   - coapServer.add(topicResource);
90   -
91   - // 3. 启动服务器
92   - coapServer.start();
93   - running = true;
94   - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
95   - } catch (Exception e) {
96   - log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
97   - throw e;
98   - }
99   - }
100   -
101   - @Override
102   - @PreDestroy
103   - public void stop() {
104   - if (coapServer != null) {
105   - try {
106   - coapServer.stop();
107   - running = false;
108   - log.info("[stop][IoT 网关 CoAP 协议已停止]");
109   - } catch (Exception e) {
110   - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
111   - }
112   - }
113   - }
114   -
115   - @Override
116   - public boolean isRunning() {
117   - return running;
118   - }
119   -
120   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
6 7 import lombok.extern.slf4j.Slf4j;
7 8  
8 9 /**
... ... @@ -13,7 +14,7 @@ import lombok.extern.slf4j.Slf4j;
13 14 @Slf4j
14 15 public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
15 16  
16   - public IotCoapDownstreamSubscriber(IotCoapUpstreamProtocol protocol, IotMessageBus messageBus) {
  17 + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
17 18 super(protocol, messageBus);
18 19 }
19 20  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.extra.spring.SpringUtil;
  8 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  9 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  10 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  12 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.eclipse.californium.core.coap.CoAP;
  15 +import org.eclipse.californium.core.coap.MediaTypeRegistry;
  16 +import org.eclipse.californium.core.coap.Option;
  17 +import org.eclipse.californium.core.server.resources.CoapExchange;
  18 +
  19 +import java.util.List;
  20 +
  21 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +
  24 +/**
  25 + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public abstract class IotCoapAbstractHandler {
  31 +
  32 + /**
  33 + * 自定义 CoAP Option 编号,用于携带 Token
  34 + * <p>
  35 + * CoAP Option 范围 2048-65535 属于实验/自定义范围
  36 + */
  37 + public static final int OPTION_TOKEN = 2088;
  38 +
  39 + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  40 +
  41 + /**
  42 + * 处理 CoAP 请求(模板方法)
  43 + *
  44 + * @param exchange CoAP 交换对象
  45 + */
  46 + public final void handle(CoapExchange exchange) {
  47 + try {
  48 + // 1. 前置处理
  49 + beforeHandle(exchange);
  50 +
  51 + // 2. 执行业务逻辑
  52 + CommonResult<Object> result = handle0(exchange);
  53 + writeResponse(exchange, result);
  54 + } catch (ServiceException e) {
  55 + // 业务异常,返回对应的错误码和消息
  56 + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage()));
  57 + } catch (IllegalArgumentException e) {
  58 + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST
  59 + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
  60 + } catch (Exception e) {
  61 + // 其他未知异常,返回 INTERNAL_SERVER_ERROR
  62 + log.error("[handle][CoAP 请求处理异常]", e);
  63 + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR));
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * 处理 CoAP 请求(子类实现)
  69 + *
  70 + * @param exchange CoAP 交换对象
  71 + * @return 处理结果
  72 + */
  73 + protected abstract CommonResult<Object> handle0(CoapExchange exchange);
  74 +
  75 + /**
  76 + * 前置处理:认证等
  77 + *
  78 + * @param exchange CoAP 交换对象
  79 + */
  80 + private void beforeHandle(CoapExchange exchange) {
  81 + // 1.1 如果不需要认证,则不走前置处理
  82 + if (!requiresAuthentication()) {
  83 + return;
  84 + }
  85 + // 1.2 从自定义 Option 获取 token
  86 + String token = getTokenFromOption(exchange);
  87 + if (StrUtil.isEmpty(token)) {
  88 + throw exception(UNAUTHORIZED);
  89 + }
  90 + // 1.3 校验 token
  91 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
  92 + if (deviceInfo == null) {
  93 + throw exception(UNAUTHORIZED);
  94 + }
  95 +
  96 + // 2.1 解析 productKey 和 deviceName
  97 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  98 + String productKey = getProductKey(uriPath);
  99 + String deviceName = getDeviceName(uriPath);
  100 + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) {
  101 + throw exception(BAD_REQUEST);
  102 + }
  103 + // 2.2 校验设备信息是否匹配
  104 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
  105 + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
  106 + throw exception(FORBIDDEN);
  107 + }
  108 + }
  109 +
  110 + // ========== Token 相关方法 ==========
  111 +
  112 + /**
  113 + * 是否需要认证(子类可覆盖)
  114 + * <p>
  115 + * 默认不需要认证
  116 + *
  117 + * @return 是否需要认证
  118 + */
  119 + protected boolean requiresAuthentication() {
  120 + return false;
  121 + }
  122 +
  123 + /**
  124 + * 从 URI 路径中获取 productKey(子类实现)
  125 + * <p>
  126 + * 默认抛出异常,需要认证的子类必须实现此方法
  127 + *
  128 + * @param uriPath URI 路径
  129 + * @return productKey
  130 + */
  131 + protected String getProductKey(List<String> uriPath) {
  132 + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法");
  133 + }
  134 +
  135 + /**
  136 + * 从 URI 路径中获取 deviceName(子类实现)
  137 + * <p>
  138 + * 默认抛出异常,需要认证的子类必须实现此方法
  139 + *
  140 + * @param uriPath URI 路径
  141 + * @return deviceName
  142 + */
  143 + protected String getDeviceName(List<String> uriPath) {
  144 + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法");
  145 + }
  146 +
  147 + /**
  148 + * 从自定义 CoAP Option 中获取 Token
  149 + *
  150 + * @param exchange CoAP 交换对象
  151 + * @return Token 值,如果不存在则返回 null
  152 + */
  153 + protected String getTokenFromOption(CoapExchange exchange) {
  154 + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
  155 + o -> o.getNumber() == OPTION_TOKEN);
  156 + return option != null ? new String(option.getValue()) : null;
  157 + }
  158 +
  159 + // ========== 序列化相关方法 ==========
  160 +
  161 + /**
  162 + * 解析请求体为指定类型
  163 + *
  164 + * @param exchange CoAP 交换对象
  165 + * @param clazz 目标类型
  166 + * @param <T> 目标类型泛型
  167 + * @return 解析后的对象,解析失败返回 null
  168 + */
  169 + protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) {
  170 + byte[] payload = exchange.getRequestPayload();
  171 + if (ArrayUtil.isEmpty(payload)) {
  172 + return null;
  173 + }
  174 + return JsonUtils.parseObject(payload, clazz);
  175 + }
  176 +
  177 + private static String serializeResponse(Object data) {
  178 + return JsonUtils.toJsonString(data);
  179 + }
  180 +
  181 + protected void writeResponse(CoapExchange exchange, CommonResult<?> data) {
  182 + String json = serializeResponse(data);
  183 + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
  184 + }
  185 +
  186 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.extra.spring.SpringUtil;
  7 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  12 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.eclipse.californium.core.server.resources.CoapExchange;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  18 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  19 +
  20 +/**
  21 + * IoT 网关 CoAP 协议的【认证】处理器
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Slf4j
  26 +public class IotCoapAuthHandler extends IotCoapAbstractHandler {
  27 +
  28 + private final String serverId;
  29 +
  30 + private final IotDeviceTokenService deviceTokenService;
  31 + private final IotDeviceCommonApi deviceApi;
  32 + private final IotDeviceMessageService deviceMessageService;
  33 +
  34 + public IotCoapAuthHandler(String serverId) {
  35 + this.serverId = serverId;
  36 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  37 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  38 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  39 + }
  40 +
  41 + @Override
  42 + @SuppressWarnings("DuplicatedCode")
  43 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  44 + // 1. 解析参数
  45 + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class);
  46 + Assert.notNull(request, "请求体不能为空");
  47 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  48 + Assert.notBlank(request.getUsername(), "username 不能为空");
  49 + Assert.notBlank(request.getPassword(), "password 不能为空");
  50 +
  51 + // 2.1 执行认证
  52 + CommonResult<Boolean> result = deviceApi.authDevice(request);
  53 + result.checkError();
  54 + if (BooleanUtil.isFalse(result.getData())) {
  55 + throw exception(DEVICE_AUTH_FAIL);
  56 + }
  57 + // 2.2 生成 Token
  58 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
  59 + Assert.notNull(deviceInfo, "设备信息不能为空");
  60 + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  61 + Assert.notBlank(token, "生成 token 不能为空");
  62 +
  63 + // 3. 执行上线
  64 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
  65 + deviceMessageService.sendDeviceMessage(message,
  66 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
  67 +
  68 + // 4. 构建响应数据
  69 + return CommonResult.success(MapUtil.of("token", token));
  70 + }
  71 +
  72 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
2 2  
3   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
4 3 import lombok.extern.slf4j.Slf4j;
5 4 import org.eclipse.californium.core.CoapResource;
6 5 import org.eclipse.californium.core.server.resources.CoapExchange;
... ... @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource {
17 16  
18 17 public static final String PATH = "auth";
19 18  
20   - private final IotCoapUpstreamProtocol protocol;
21 19 private final IotCoapAuthHandler authHandler;
22 20  
23   - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
24   - IotCoapAuthHandler authHandler) {
  21 + public IotCoapAuthResource(IotCoapAuthHandler authHandler) {
25 22 super(PATH);
26   - this.protocol = protocol;
27 23 this.authHandler = authHandler;
28 24 log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
29 25 }
... ... @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource {
31 27 @Override
32 28 public void handlePOST(CoapExchange exchange) {
33 29 log.debug("[handlePOST][收到 /auth POST 请求]");
34   - authHandler.handle(exchange, protocol);
  30 + authHandler.handle(exchange);
35 31 }
36 32  
37 33 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.eclipse.californium.core.server.resources.CoapExchange;
  11 +
  12 +/**
  13 + * IoT 网关 CoAP 协议的【设备动态注册】处理器
  14 + * <p>
  15 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  16 + *
  17 + * @author 芋道源码
  18 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  19 + */
  20 +@Slf4j
  21 +public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
  22 +
  23 + private final IotDeviceCommonApi deviceApi;
  24 +
  25 + public IotCoapRegisterHandler() {
  26 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  27 + }
  28 +
  29 + @Override
  30 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  31 + // 1. 解析参数
  32 + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class);
  33 + Assert.notNull(request, "请求体不能为空");
  34 + Assert.notBlank(request.getProductKey(), "productKey 不能为空");
  35 + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
  36 + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
  37 +
  38 + // 2. 调用动态注册
  39 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
  40 + result.checkError();
  41 +
  42 + // 3. 构建响应数据
  43 + return CommonResult.success(result.getData());
  44 + }
  45 +
  46 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
2 2  
3 3 import lombok.extern.slf4j.Slf4j;
4 4 import org.eclipse.californium.core.CoapResource;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  11 +import lombok.Data;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.eclipse.californium.core.server.resources.CoapExchange;
  14 +
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  18 +
  19 +/**
  20 + * IoT 网关 CoAP 协议的【子设备动态注册】处理器
  21 + * <p>
  22 + * 用于子设备的动态注册,需要网关认证
  23 + *
  24 + * @author 芋道源码
  25 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  26 + */
  27 +@Slf4j
  28 +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler {
  29 +
  30 + private final IotDeviceCommonApi deviceApi;
  31 +
  32 + public IotCoapRegisterSubHandler() {
  33 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  34 + }
  35 +
  36 + @Override
  37 + @SuppressWarnings("DuplicatedCode")
  38 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  39 + // 1.1 解析通用参数(从 URI 路径获取网关设备信息)
  40 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  41 + String productKey = getProductKey(uriPath);
  42 + String deviceName = getDeviceName(uriPath);
  43 + // 1.2 解析子设备列表
  44 + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class);
  45 + Assert.notNull(request, "请求参数不能为空");
  46 + Assert.notEmpty(request.getParams(), "params 不能为空");
  47 +
  48 + // 2. 调用子设备动态注册
  49 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
  50 + .setGatewayProductKey(productKey)
  51 + .setGatewayDeviceName(deviceName)
  52 + .setSubDevices(request.getParams());
  53 + CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
  54 + result.checkError();
  55 +
  56 + // 3. 返回结果
  57 + return success(result.getData());
  58 + }
  59 +
  60 + @Override
  61 + protected boolean requiresAuthentication() {
  62 + return true;
  63 + }
  64 +
  65 + @Override
  66 + protected String getProductKey(List<String> uriPath) {
  67 + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
  68 + return CollUtil.get(uriPath, 3);
  69 + }
  70 +
  71 + @Override
  72 + protected String getDeviceName(List<String> uriPath) {
  73 + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
  74 + return CollUtil.get(uriPath, 4);
  75 + }
  76 +
  77 + @Data
  78 + public static class SubDeviceRegisterRequest {
  79 +
  80 + private List<IotSubDeviceRegisterReqDTO> params;
  81 +
  82 + }
  83 +
  84 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.eclipse.californium.core.CoapResource;
  5 +import org.eclipse.californium.core.server.resources.CoapExchange;
  6 +import org.eclipse.californium.core.server.resources.Resource;
  7 +
  8 +/**
  9 + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName})
  10 + * <p>
  11 + * 用于子设备的动态注册,需要网关认证
  12 + * <p>
  13 + * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +public class IotCoapRegisterSubResource extends CoapResource {
  19 +
  20 + public static final String PATH = "sub-device";
  21 +
  22 + private final IotCoapRegisterSubHandler registerSubHandler;
  23 +
  24 + /**
  25 + * 创建根资源(/auth/register/sub-device)
  26 + */
  27 + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) {
  28 + this(PATH, registerSubHandler);
  29 + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH);
  30 + }
  31 +
  32 + /**
  33 + * 创建子资源(动态路径)
  34 + */
  35 + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) {
  36 + super(name);
  37 + this.registerSubHandler = registerSubHandler;
  38 + }
  39 +
  40 + @Override
  41 + public Resource getChild(String name) {
  42 + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径
  43 + return new IotCoapRegisterSubResource(name, registerSubHandler);
  44 + }
  45 +
  46 + @Override
  47 + public void handlePOST(CoapExchange exchange) {
  48 + log.debug("[handlePOST][收到子设备动态注册请求]");
  49 + registerSubHandler.handle(exchange);
  50 + }
  51 +
  52 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.text.StrPool;
  7 +import cn.hutool.extra.spring.SpringUtil;
  8 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.eclipse.californium.core.server.resources.CoapExchange;
  13 +
  14 +import java.util.List;
  15 +
  16 +/**
  17 + * IoT 网关 CoAP 协议的【上行】处理器
  18 + *
  19 + * 处理设备通过 CoAP 协议发送的上行消息,包括:
  20 + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  21 + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  22 + *
  23 + * Token 通过自定义 CoAP Option 2088 携带
  24 + *
  25 + * @author 芋道源码
  26 + */
  27 +@Slf4j
  28 +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler {
  29 +
  30 + private final String serverId;
  31 +
  32 + private final IotDeviceMessageService deviceMessageService;
  33 +
  34 + public IotCoapUpstreamHandler(String serverId) {
  35 + this.serverId = serverId;
  36 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  37 + }
  38 +
  39 + @Override
  40 + @SuppressWarnings("DuplicatedCode")
  41 + protected CommonResult<Object> handle0(CoapExchange exchange) {
  42 + // 1.1 解析通用参数
  43 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  44 + String productKey = getProductKey(uriPath);
  45 + String deviceName = getDeviceName(uriPath);
  46 + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
  47 + // 1.2 解析消息
  48 + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class);
  49 + Assert.notNull(message, "请求参数不能为空");
  50 + Assert.equals(method, message.getMethod(), "method 不匹配");
  51 +
  52 + // 2. 发送消息
  53 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
  54 +
  55 + // 3. 返回结果
  56 + return CommonResult.success(MapUtil.of("messageId", message.getId()));
  57 + }
  58 +
  59 + @Override
  60 + protected boolean requiresAuthentication() {
  61 + return true;
  62 + }
  63 +
  64 + @Override
  65 + protected String getProductKey(List<String> uriPath) {
  66 + // 路径格式:/topic/sys/{productKey}/{deviceName}/...
  67 + return CollUtil.get(uriPath, 2);
  68 + }
  69 +
  70 + @Override
  71 + protected String getDeviceName(List<String> uriPath) {
  72 + // 路径格式:/topic/sys/{productKey}/{deviceName}/...
  73 + return CollUtil.get(uriPath, 3);
  74 + }
  75 +
  76 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem;
2 2  
3   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
4 3 import lombok.extern.slf4j.Slf4j;
5 4 import org.eclipse.californium.core.CoapResource;
6 5 import org.eclipse.californium.core.server.resources.CoapExchange;
... ... @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
20 19  
21 20 public static final String PATH = "topic";
22 21  
23   - private final IotCoapUpstreamProtocol protocol;
  22 + private final String serverId;
24 23 private final IotCoapUpstreamHandler upstreamHandler;
25 24  
26 25 /**
27 26 * 创建根资源(/topic)
28 27 */
29   - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
  28 + public IotCoapUpstreamTopicResource(String serverId,
30 29 IotCoapUpstreamHandler upstreamHandler) {
31   - this(PATH, protocol, upstreamHandler);
  30 + this(PATH, serverId, upstreamHandler);
32 31 log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
33 32 }
34 33  
... ... @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
36 35 * 创建子资源(动态路径)
37 36 */
38 37 private IotCoapUpstreamTopicResource(String name,
39   - IotCoapUpstreamProtocol protocol,
  38 + String serverId,
40 39 IotCoapUpstreamHandler upstreamHandler) {
41 40 super(name);
42   - this.protocol = protocol;
  41 + this.serverId = serverId;
43 42 this.upstreamHandler = upstreamHandler;
44 43 }
45 44  
46 45 @Override
47 46 public Resource getChild(String name) {
48 47 // 递归创建动态子资源,支持任意深度路径
49   - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
  48 + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler);
50 49 }
51 50  
52 51 @Override
53 52 public void handleGET(CoapExchange exchange) {
54   - upstreamHandler.handle(exchange, protocol);
  53 + upstreamHandler.handle(exchange);
55 54 }
56 55  
57 56 @Override
58 57 public void handlePOST(CoapExchange exchange) {
59   - upstreamHandler.handle(exchange, protocol);
  58 + upstreamHandler.handle(exchange);
60 59 }
61 60  
62 61 @Override
63 62 public void handlePUT(CoapExchange exchange) {
64   - upstreamHandler.handle(exchange, protocol);
  63 + upstreamHandler.handle(exchange);
65 64 }
66 65  
67 66 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
... ... @@ -2,12 +2,5 @@
2 2 * CoAP 协议实现包
3 3 * <p>
4 4 * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
5   - * <p>
6   - * URI 路径:
7   - * - 认证:POST /auth
8   - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
9   - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
10   - * <p>
11   - * Token 通过 CoAP Option 2088 携带
12 5 */
13 6 package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7   -import cn.hutool.extra.spring.SpringUtil;
8   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
13   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
16   -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
17   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
18   -import lombok.extern.slf4j.Slf4j;
19   -import org.eclipse.californium.core.coap.CoAP;
20   -import org.eclipse.californium.core.server.resources.CoapExchange;
21   -
22   -import java.util.Map;
23   -
24   -/**
25   - * IoT 网关 CoAP 协议的【认证】处理器
26   - *
27   - * @author 芋道源码
28   - */
29   -@Slf4j
30   -public class IotCoapAuthHandler {
31   -
32   - private final IotDeviceTokenService deviceTokenService;
33   - private final IotDeviceCommonApi deviceApi;
34   - private final IotDeviceMessageService deviceMessageService;
35   -
36   - public IotCoapAuthHandler() {
37   - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
38   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
39   - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
40   - }
41   -
42   - /**
43   - * 处理认证请求
44   - *
45   - * @param exchange CoAP 交换对象
46   - * @param protocol 协议对象
47   - */
48   - @SuppressWarnings("unchecked")
49   - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
50   - try {
51   - // 1.1 解析请求体
52   - byte[] payload = exchange.getRequestPayload();
53   - if (payload == null || payload.length == 0) {
54   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
55   - return;
56   - }
57   - Map<String, Object> body;
58   - try {
59   - body = JsonUtils.parseObject(new String(payload), Map.class);
60   - } catch (Exception e) {
61   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
62   - return;
63   - }
64   - // 1.2 解析参数
65   - String clientId = MapUtil.getStr(body, "clientId");
66   - if (StrUtil.isEmpty(clientId)) {
67   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
68   - return;
69   - }
70   - String username = MapUtil.getStr(body, "username");
71   - if (StrUtil.isEmpty(username)) {
72   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
73   - return;
74   - }
75   - String password = MapUtil.getStr(body, "password");
76   - if (StrUtil.isEmpty(password)) {
77   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
78   - return;
79   - }
80   -
81   - // 2.1 执行认证
82   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
83   - .setClientId(clientId).setUsername(username).setPassword(password));
84   - if (result.isError()) {
85   - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
86   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
87   - return;
88   - }
89   - if (!BooleanUtil.isTrue(result.getData())) {
90   - log.warn("[handle][认证失败,clientId: {}]", clientId);
91   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
92   - return;
93   - }
94   - // 2.2 生成 Token
95   - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
96   - Assert.notNull(deviceInfo, "设备信息不能为空");
97   - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
98   - Assert.notBlank(token, "生成 token 不能为空");
99   -
100   - // 3. 执行上线
101   - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
102   - deviceMessageService.sendDeviceMessage(message,
103   - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
104   -
105   - // 4. 返回成功响应
106   - log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
107   - deviceInfo.getProductKey(), deviceInfo.getDeviceName());
108   - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
109   - } catch (Exception e) {
110   - log.error("[handle][认证处理异常]", e);
111   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
112   - }
113   - }
114   -
115   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.hutool.extra.spring.SpringUtil;
6   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
9   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
12   -import lombok.extern.slf4j.Slf4j;
13   -import org.eclipse.californium.core.coap.CoAP;
14   -import org.eclipse.californium.core.server.resources.CoapExchange;
15   -
16   -import java.util.Map;
17   -
18   -/**
19   - * IoT 网关 CoAP 协议的【设备动态注册】处理器
20   - * <p>
21   - * 用于直连设备/网关的一型一密动态注册,不需要认证
22   - *
23   - * @author 芋道源码
24   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
25   - */
26   -@Slf4j
27   -public class IotCoapRegisterHandler {
28   -
29   - private final IotDeviceCommonApi deviceApi;
30   -
31   - public IotCoapRegisterHandler() {
32   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
33   - }
34   -
35   - /**
36   - * 处理设备动态注册请求
37   - *
38   - * @param exchange CoAP 交换对象
39   - */
40   - @SuppressWarnings("unchecked")
41   - public void handle(CoapExchange exchange) {
42   - try {
43   - // 1.1 解析请求体
44   - byte[] payload = exchange.getRequestPayload();
45   - if (payload == null || payload.length == 0) {
46   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
47   - return;
48   - }
49   - Map<String, Object> body;
50   - try {
51   - body = JsonUtils.parseObject(new String(payload), Map.class);
52   - } catch (Exception e) {
53   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
54   - return;
55   - }
56   -
57   - // 1.2 解析参数
58   - String productKey = MapUtil.getStr(body, "productKey");
59   - if (StrUtil.isEmpty(productKey)) {
60   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
61   - return;
62   - }
63   - String deviceName = MapUtil.getStr(body, "deviceName");
64   - if (StrUtil.isEmpty(deviceName)) {
65   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
66   - return;
67   - }
68   - String productSecret = MapUtil.getStr(body, "productSecret");
69   - if (StrUtil.isEmpty(productSecret)) {
70   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
71   - return;
72   - }
73   -
74   - // 2. 调用动态注册
75   - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
76   - .setProductKey(productKey)
77   - .setDeviceName(deviceName)
78   - .setProductSecret(productSecret);
79   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
80   - if (result.isError()) {
81   - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]",
82   - productKey, deviceName, result.getMsg());
83   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
84   - "设备动态注册失败:" + result.getMsg());
85   - return;
86   - }
87   -
88   - // 3. 返回成功响应
89   - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName);
90   - IotCoapUtils.respondSuccess(exchange, result.getData());
91   - } catch (Exception e) {
92   - log.error("[handle][设备动态注册处理异常]", e);
93   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
94   - }
95   - }
96   -
97   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2   -
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.text.StrPool;
6   -import cn.hutool.core.util.ArrayUtil;
7   -import cn.hutool.core.util.ObjUtil;
8   -import cn.hutool.core.util.StrUtil;
9   -import cn.hutool.extra.spring.SpringUtil;
10   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
11   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
14   -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
16   -import lombok.extern.slf4j.Slf4j;
17   -import org.eclipse.californium.core.coap.CoAP;
18   -import org.eclipse.californium.core.server.resources.CoapExchange;
19   -
20   -import java.util.List;
21   -
22   -/**
23   - * IoT 网关 CoAP 协议的【上行】处理器
24   - *
25   - * 处理设备通过 CoAP 协议发送的上行消息,包括:
26   - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
27   - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
28   - *
29   - * Token 通过自定义 CoAP Option 2088 携带
30   - *
31   - * @author 芋道源码
32   - */
33   -@Slf4j
34   -public class IotCoapUpstreamHandler {
35   -
36   - private final IotDeviceTokenService deviceTokenService;
37   - private final IotDeviceMessageService deviceMessageService;
38   -
39   - public IotCoapUpstreamHandler() {
40   - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
41   - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
42   - }
43   -
44   - /**
45   - * 处理 CoAP 请求
46   - *
47   - * @param exchange CoAP 交换对象
48   - * @param protocol 协议对象
49   - */
50   - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
51   - try {
52   - // 1. 解析通用参数
53   - List<String> uriPath = exchange.getRequestOptions().getUriPath();
54   - String productKey = CollUtil.get(uriPath, 2);
55   - String deviceName = CollUtil.get(uriPath, 3);
56   - byte[] payload = exchange.getRequestPayload();
57   - if (StrUtil.isEmpty(productKey)) {
58   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
59   - return;
60   - }
61   - if (StrUtil.isEmpty(deviceName)) {
62   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
63   - return;
64   - }
65   - if (ArrayUtil.isEmpty(payload)) {
66   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
67   - return;
68   - }
69   -
70   - // 2. 认证:从自定义 Option 获取 token
71   - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
72   - if (StrUtil.isEmpty(token)) {
73   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
74   - return;
75   - }
76   - // 验证 token
77   - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
78   - if (deviceInfo == null) {
79   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
80   - return;
81   - }
82   - // 验证设备信息匹配
83   - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
84   - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
85   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
86   - return;
87   - }
88   -
89   - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接
90   - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
91   - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
92   -
93   - // 2.2 解码消息
94   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
95   - if (ObjUtil.notEqual(method, message.getMethod())) {
96   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
97   - return;
98   - }
99   - // 2.3 发送消息到消息总线
100   - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
101   -
102   - // 3. 返回成功响应
103   - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
104   - } catch (Exception e) {
105   - log.error("[handle][CoAP 请求处理异常]", e);
106   - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
107   - }
108   - }
109   -
110   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import org.eclipse.californium.core.coap.CoAP;
7   -import org.eclipse.californium.core.coap.MediaTypeRegistry;
8   -import org.eclipse.californium.core.coap.Option;
9   -import org.eclipse.californium.core.server.resources.CoapExchange;
10   -
11   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAbstractHandler;
12 4  
13 5 /**
14 6 * IoT CoAP 协议工具类
... ... @@ -22,63 +14,6 @@ public class IotCoapUtils {
22 14 * <p>
23 15 * CoAP Option 范围 2048-65535 属于实验/自定义范围
24 16 */
25   - public static final int OPTION_TOKEN = 2088;
26   -
27   - /**
28   - * 返回成功响应
29   - *
30   - * @param exchange CoAP 交换对象
31   - * @param data 响应数据
32   - */
33   - public static void respondSuccess(CoapExchange exchange, Object data) {
34   - CommonResult<Object> result = CommonResult.success(data);
35   - String json = JsonUtils.toJsonString(result);
36   - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
37   - }
38   -
39   - /**
40   - * 返回错误响应
41   - *
42   - * @param exchange CoAP 交换对象
43   - * @param code CoAP 响应码
44   - * @param message 错误消息
45   - */
46   - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
47   - int errorCode = mapCoapCodeToErrorCode(code);
48   - CommonResult<Object> result = CommonResult.error(errorCode, message);
49   - String json = JsonUtils.toJsonString(result);
50   - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
51   - }
52   -
53   - /**
54   - * 从自定义 CoAP Option 中获取 Token
55   - *
56   - * @param exchange CoAP 交换对象
57   - * @param optionNumber Option 编号
58   - * @return Token 值,如果不存在则返回 null
59   - */
60   - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
61   - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
62   - o -> o.getNumber() == optionNumber);
63   - return option != null ? new String(option.getValue()) : null;
64   - }
65   -
66   - /**
67   - * 将 CoAP 响应码映射到业务错误码
68   - *
69   - * @param code CoAP 响应码
70   - * @return 业务错误码
71   - */
72   - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
73   - if (code == CoAP.ResponseCode.BAD_REQUEST) {
74   - return BAD_REQUEST.getCode();
75   - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
76   - return UNAUTHORIZED.getCode();
77   - } else if (code == CoAP.ResponseCode.FORBIDDEN) {
78   - return FORBIDDEN.getCode();
79   - } else {
80   - return INTERNAL_SERVER_ERROR.getCode();
81   - }
82   - }
  17 + public static final int OPTION_TOKEN = IotCoapAbstractHandler.OPTION_TOKEN;
83 18  
84 19 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.util.ArrayUtil;
... ... @@ -10,9 +10,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
10 10 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
11 11 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
12 12 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler;
15   -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
16 13 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
17 14 import io.vertx.core.Handler;
18 15 import io.vertx.core.http.HttpHeaders;
... ... @@ -20,8 +17,7 @@ import io.vertx.ext.web.RoutingContext;
20 17 import lombok.extern.slf4j.Slf4j;
21 18 import org.springframework.http.MediaType;
22 19  
23   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
24   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  20 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
25 21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
26 22 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
27 23  
... ... @@ -35,8 +31,6 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
35 31  
36 32 private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
37 33  
38   - private final IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class);
39   -
40 34 @Override
41 35 public final void handle(RoutingContext context) {
42 36 try {
... ... @@ -47,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
47 41 CommonResult<Object> result = handle0(context);
48 42 writeResponse(context, result);
49 43 } catch (ServiceException e) {
  44 + // 已知异常,返回对应的错误码和错误信息
50 45 writeResponse(context, CommonResult.error(e.getCode(), e.getMessage()));
  46 + } catch (IllegalArgumentException e) {
  47 + // 参数校验异常,返回 400 错误
  48 + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
51 49 } catch (Exception e) {
  50 + // 其他未知异常,返回 500 错误
52 51 log.error("[handle][path({}) 处理异常]", context.request().path(), e);
53 52 writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR));
54 53 }
55 54 }
56 55  
  56 + /**
  57 + * 处理 HTTP 请求(子类实现)
  58 + *
  59 + * @param context RoutingContext 对象
  60 + * @return 处理结果
  61 + */
57 62 protected abstract CommonResult<Object> handle0(RoutingContext context);
58 63  
  64 + /**
  65 + * 前置处理:认证等
  66 + *
  67 + * @param context RoutingContext 对象
  68 + */
59 69 private void beforeHandle(RoutingContext context) {
60 70 // 如果不需要认证,则不走前置处理
61 71 String path = context.request().path();
... ... @@ -102,7 +112,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
102 112 }
103 113  
104 114 @SuppressWarnings("deprecation")
105   - public static void writeResponse(RoutingContext context, Object data) {
  115 + public static void writeResponse(RoutingContext context, CommonResult<?> data) {
106 116 context.response()
107 117 .setStatusCode(200)
108 118 .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7 6 import cn.hutool.extra.spring.SpringUtil;
8 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
... ... @@ -11,13 +10,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 11 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13 12 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler;
15 13 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
16 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
17 15 import io.vertx.ext.web.RoutingContext;
18 16  
19 17 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
20   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
21 18 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
22 19 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
23 20  
... ... @@ -48,23 +45,19 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
48 45 }
49 46  
50 47 @Override
  48 + @SuppressWarnings("DuplicatedCode")
51 49 public CommonResult<Object> handle0(RoutingContext context) {
52 50 // 1. 解析参数
53 51 IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class);
54   - if (StrUtil.isEmpty(request.getClientId())) {
55   - throw invalidParamException("clientId 不能为空");
56   - }
57   - if (StrUtil.isEmpty(request.getUsername())) {
58   - throw invalidParamException("username 不能为空");
59   - }
60   - if (StrUtil.isEmpty(request.getPassword())) {
61   - throw invalidParamException("password 不能为空");
62   - }
  52 + Assert.notNull(request, "请求参数不能为空");
  53 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  54 + Assert.notBlank(request.getUsername(), "username 不能为空");
  55 + Assert.notBlank(request.getPassword(), "password 不能为空");
63 56  
64 57 // 2.1 执行认证
65 58 CommonResult<Boolean> result = deviceApi.authDevice(request);
66 59 result.checkError();
67   - if (!BooleanUtil.isTrue(result.getData())) {
  60 + if (BooleanUtil.isFalse(result.getData())) {
68 61 throw exception(DEVICE_AUTH_FAIL);
69 62 }
70 63 // 2.2 生成 Token
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3   -import cn.hutool.core.util.StrUtil;
  3 +import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 7 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler;
10 9 import io.vertx.ext.web.RoutingContext;
11 10  
12   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
13 11 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
14 12  
15 13 /**
... ... @@ -34,15 +32,10 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
34 32 public CommonResult<Object> handle0(RoutingContext context) {
35 33 // 1. 解析参数
36 34 IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class);
37   - if (StrUtil.isEmpty(request.getProductKey())) {
38   - throw invalidParamException("productKey 不能为空");
39   - }
40   - if (StrUtil.isEmpty(request.getDeviceName())) {
41   - throw invalidParamException("deviceName 不能为空");
42   - }
43   - if (StrUtil.isEmpty(request.getProductSecret())) {
44   - throw invalidParamException("productSecret 不能为空");
45   - }
  35 + Assert.notNull(request, "请求参数不能为空");
  36 + Assert.notBlank(request.getProductKey(), "productKey 不能为空");
  37 + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
  38 + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
46 39  
47 40 // 2. 调用动态注册
48 41 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
  3 +import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler;
11 10 import io.vertx.ext.web.RoutingContext;
12 11 import lombok.Data;
13 12  
14 13 import java.util.List;
15 14  
16   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
17 15 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
18 16  
19 17 /**
... ... @@ -46,9 +44,8 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
46 44 String deviceName = context.pathParam("deviceName");
47 45 // 1.2 解析子设备列表
48 46 SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class);
49   - if (CollUtil.isEmpty(request.getParams())) {
50   - throw invalidParamException("params 不能为空");
51   - }
  47 + Assert.notNull(request, "请求参数不能为空");
  48 + Assert.notEmpty(request.getParams(), "params 不能为空");
52 49  
53 50 // 2. 调用子设备动态注册
54 51 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java
... ... @@ -7,7 +7,6 @@ import cn.hutool.extra.spring.SpringUtil;
7 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler;
11 10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
12 11 import io.vertx.ext.web.RoutingContext;
13 12 import lombok.extern.slf4j.Slf4j;
... ... @@ -33,15 +32,16 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
33 32  
34 33 @Override
35 34 protected CommonResult<Object> handle0(RoutingContext context) {
36   - // 1. 解析通用参数
  35 + // 1.1 解析通用参数
37 36 String productKey = context.pathParam("productKey");
38 37 String deviceName = context.pathParam("deviceName");
39 38 String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
40   -
41   - // 2.1 根据 Content-Type 反序列化消息
  39 + // 1.2 根据 Content-Type 反序列化消息
42 40 IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class);
  41 + Assert.notNull(message, "请求参数不能为空");
43 42 Assert.equals(method, message.getMethod(), "method 不匹配");
44   - // 2.2 发送消息
  43 +
  44 + // 2. 发送消息
45 45 deviceMessageService.sendDeviceMessage(message,
46 46 productKey, deviceName, serverId);
47 47  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream;
2 2  
  3 +import cn.hutool.core.util.BooleanUtil;
3 4 import cn.hutool.core.util.IdUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
5 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
7 9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
... ... @@ -26,7 +28,8 @@ import lombok.extern.slf4j.Slf4j;
26 28 import org.springframework.util.Assert;
27 29  
28 30 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
29   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
  31 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  32 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
30 33  
31 34 /**
32 35 * TCP 上行消息处理器
... ... @@ -132,6 +135,12 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
132 135 // 业务消息
133 136 handleBusinessRequest(clientId, message, socket);
134 137 }
  138 + } catch (ServiceException e) {
  139 + // 业务异常,返回对应的错误码和错误信息
  140 + log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", clientId, e.getMessage());
  141 + String requestId = message != null ? message.getRequestId() : null;
  142 + String method = message != null ? message.getMethod() : null;
  143 + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage());
135 144 } catch (IllegalArgumentException e) {
136 145 // 参数校验失败,返回 400
137 146 log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
... ... @@ -166,10 +175,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
166 175  
167 176 // 2.1 执行认证
168 177 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
169   - if (authResult.isError()) {
170   - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", clientId, authParams.getUsername());
171   - sendErrorResponse(socket, message.getRequestId(), AUTH_METHOD, authResult.getCode(), authResult.getMsg());
172   - return;
  178 + authResult.checkError();
  179 + if (BooleanUtil.isFalse(authResult.getData())) {
  180 + throw exception(DEVICE_AUTH_FAIL);
173 181 }
174 182 // 2.2 解析设备信息
175 183 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
... ... @@ -205,12 +213,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
205 213  
206 214 // 2. 调用动态注册
207 215 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
208   - if (result.isError()) {
209   - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
210   - sendErrorResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(),
211   - result.getCode(), result.getMsg());
212   - return;
213   - }
  216 + result.checkError();
214 217  
215 218 // 3. 发送成功响应
216 219 sendSuccessResponse(socket, message.getRequestId(),
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream;
2 2  
  3 +import cn.hutool.core.map.MapUtil;
3 4 import cn.hutool.core.util.ArrayUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
4 6 import cn.hutool.core.util.IdUtil;
5 7 import cn.hutool.core.util.StrUtil;
6 8 import cn.hutool.extra.spring.SpringUtil;
  9 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
7 10 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 11 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 12 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
... ... @@ -27,10 +30,11 @@ import lombok.extern.slf4j.Slf4j;
27 30 import org.springframework.util.Assert;
28 31  
29 32 import java.net.InetSocketAddress;
30   -import java.time.LocalDateTime;
31 33 import java.util.Map;
32 34  
33 35 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  36 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  37 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
34 38  
35 39 /**
36 40 * UDP 上行消息处理器
... ... @@ -87,7 +91,7 @@ public class IotUdpUpstreamHandler {
87 91 this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
88 92 }
89 93  
90   - // TODO @AI:vertx 有 udp 的实现么?
  94 + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现
91 95 /**
92 96 * 处理 UDP 数据包
93 97 *
... ... @@ -99,18 +103,7 @@ public class IotUdpUpstreamHandler {
99 103 Buffer data = packet.data();
100 104 String addressKey = sessionManager.buildAddressKey(senderAddress);
101 105 log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length());
102   - try {
103   - processMessage(data, senderAddress, socket);
104   - } catch (IllegalArgumentException e) {
105   - // 参数校验失败,返回 400
106   - log.warn("[handle][参数校验失败,来源: {},错误: {}]", addressKey, e.getMessage());
107   - sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), e.getMessage());
108   - } catch (Exception e) {
109   - // 其他异常,返回 500
110   - log.error("[handle][处理消息失败,来源: {}]", addressKey, e);
111   - sendErrorResponse(socket, senderAddress, null, null,
112   - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
113   - }
  106 + processMessage(data, senderAddress, socket);
114 107 }
115 108  
116 109 /**
... ... @@ -121,6 +114,7 @@ public class IotUdpUpstreamHandler {
121 114 * @param socket UDP Socket
122 115 */
123 116 private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) {
  117 + String addressKey = sessionManager.buildAddressKey(senderAddress);
124 118 // 1.1 基础检查
125 119 if (ArrayUtil.isEmpty(buffer)) {
126 120 return;
... ... @@ -133,15 +127,35 @@ public class IotUdpUpstreamHandler {
133 127 }
134 128  
135 129 // 2. 根据消息类型路由处理
136   - if (AUTH_METHOD.equals(message.getMethod())) {
137   - // 认证请求
138   - handleAuthenticationRequest(message, senderAddress, socket);
139   - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
140   - // 设备动态注册请求
141   - handleRegisterRequest(message, senderAddress, socket);
142   - } else {
143   - // 业务消息
144   - handleBusinessRequest(message, senderAddress, socket);
  130 + try {
  131 + if (AUTH_METHOD.equals(message.getMethod())) {
  132 + // 认证请求
  133 + handleAuthenticationRequest(message, senderAddress, socket);
  134 + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
  135 + // 设备动态注册请求
  136 + handleRegisterRequest(message, senderAddress, socket);
  137 + } else {
  138 + // 业务消息
  139 + handleBusinessRequest(message, senderAddress, socket);
  140 + }
  141 + } catch (ServiceException e) {
  142 + // 业务异常,返回对应的错误码和错误信息
  143 + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]",
  144 + addressKey, message.getRequestId(), message.getMethod(), e.getMessage());
  145 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  146 + e.getCode(), e.getMessage());
  147 + } catch (IllegalArgumentException e) {
  148 + // 参数校验失败,返回 400
  149 + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]",
  150 + addressKey, message.getRequestId(), message.getMethod(), e.getMessage());
  151 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  152 + BAD_REQUEST.getCode(), e.getMessage());
  153 + } catch (Exception e) {
  154 + // 其他异常,返回 500
  155 + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]",
  156 + addressKey, message.getRequestId(), message.getMethod(), e);
  157 + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(),
  158 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
145 159 }
146 160 }
147 161  
... ... @@ -164,12 +178,9 @@ public class IotUdpUpstreamHandler {
164 178  
165 179 // 2.1 执行认证
166 180 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
167   - if (authResult.isError()) {
168   - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
169   - clientId, authParams.getUsername());
170   - sendErrorResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD,
171   - authResult.getCode(), authResult.getMsg());
172   - return;
  181 + authResult.checkError();
  182 + if (!BooleanUtil.isTrue(authResult.getData())) {
  183 + throw exception(DEVICE_AUTH_FAIL);
173 184 }
174 185 // 2.2 解析设备信息
175 186 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
... ... @@ -187,7 +198,8 @@ public class IotUdpUpstreamHandler {
187 198 // 4.2 发送上线消息
188 199 sendOnlineMessage(device);
189 200 // 4.3 发送成功响应(包含 token)
190   - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token);
  201 + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD,
  202 + MapUtil.of("token", token));
191 203 log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]",
192 204 device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress));
193 205 }
... ... @@ -211,13 +223,7 @@ public class IotUdpUpstreamHandler {
211 223  
212 224 // 2. 调用动态注册
213 225 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
214   - if (result.isError()) {
215   - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]",
216   - sessionManager.buildAddressKey(senderAddress), result.getMsg());
217   - sendErrorResponse(socket, senderAddress, message.getRequestId(),
218   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getCode(), result.getMsg());
219   - return;
220   - }
  226 + result.checkError();
221 227  
222 228 // 3. 发送成功响应
223 229 sendSuccessResponse(socket, senderAddress, message.getRequestId(),
... ... @@ -274,17 +280,8 @@ public class IotUdpUpstreamHandler {
274 280 return;
275 281 }
276 282  
277   - // 2. 更新会话活跃时间和地址
278   - // TODO @AI:是不是合并到 sessionManager 里面更好?
279   - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(device.getId());
280   - if (sessionInfo != null) {
281   - // 检查地址是否变化,变化则更新
282   - if (!senderAddress.equals(sessionInfo.getAddress())) {
283   - sessionManager.updateSessionAddress(device.getId(), senderAddress);
284   - } else {
285   - sessionManager.updateSessionActivity(device.getId());
286   - }
287   - }
  283 + // 2. 更新会话地址(如有变化)
  284 + sessionManager.updateSessionAddress(device.getId(), senderAddress);
288 285  
289 286 // 3. 将 body 设置为实际的 params,发送消息到消息总线
290 287 message.setParams(body);
... ... @@ -306,8 +303,7 @@ public class IotUdpUpstreamHandler {
306 303 .setDeviceId(device.getId())
307 304 .setProductKey(device.getProductKey())
308 305 .setDeviceName(device.getDeviceName())
309   - .setAddress(address)
310   - .setLastActiveTime(LocalDateTime.now());
  306 + .setAddress(address);
311 307 sessionManager.registerSession(device.getId(), sessionInfo);
312 308 }
313 309  
... ... @@ -325,21 +321,6 @@ public class IotUdpUpstreamHandler {
325 321 // ===================== 发送响应消息 =====================
326 322  
327 323 /**
328   - * 发送认证成功响应(包含 token)
329   - *
330   - * @param socket UDP Socket
331   - * @param address 目标地址
332   - * @param requestId 请求 ID
333   - * @param token JWT Token
334   - */
335   - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address,
336   - String requestId, String token) {
337   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, token,
338   - SUCCESS.getCode(), null);
339   - writeResponse(socket, address, responseMessage);
340   - }
341   -
342   - /**
343 324 * 发送成功响应
344 325 *
345 326 * @param socket UDP Socket
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import com.google.common.cache.Cache;
  5 +import com.google.common.cache.CacheBuilder;
3 6 import io.vertx.core.buffer.Buffer;
4 7 import io.vertx.core.datagram.DatagramSocket;
5 8 import lombok.Data;
6 9 import lombok.extern.slf4j.Slf4j;
7 10  
8 11 import java.net.InetSocketAddress;
9   -import java.time.LocalDateTime;
10   -import java.util.ArrayList;
11   -import java.util.Iterator;
12   -import java.util.List;
13   -import java.util.Map;
14   -import java.util.concurrent.ConcurrentHashMap;
  12 +import java.util.concurrent.TimeUnit;
15 13  
16 14 /**
17 15 * IoT 网关 UDP 会话管理器
18 16 * <p>
19   - * 统一管理 UDP 会话的认证状态、设备会话和消息发送功能:
20   - * 1. 管理 UDP 会话的认证状态
21   - * 2. 管理设备会话和在线状态
22   - * 3. 管理消息发送到设备
  17 + * 基于 Guava Cache 实现会话的自动过期清理:
  18 + * 1. 管理设备会话信息(设备 ID -> 地址映射)
  19 + * 2. 自动清理超时会话(expireAfterAccess)
  20 + * 3. 限制最大会话数(maximumSize)
23 21 *
24 22 * @author 芋道源码
25 23 */
... ... @@ -27,109 +25,76 @@ import java.util.concurrent.ConcurrentHashMap;
27 25 public class IotUdpSessionManager {
28 26  
29 27 /**
30   - * 最大会话数
  28 + * 设备会话缓存:设备 ID -> 会话信息
  29 + * <p>
  30 + * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间
31 31 */
32   - private final int maxSessions;
  32 + private final Cache<Long, SessionInfo> deviceSessionCache;
33 33  
34   - /**
35   - * 设备 ID -> 会话信息
36   - */
37   - private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>();
38   -
39   - /**
40   - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步)
41   - */
42   - // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须?
43   - private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>();
  34 + private final int maxSessions;
44 35  
45   - public IotUdpSessionManager(int maxSessions) {
  36 + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) {
46 37 this.maxSessions = maxSessions;
  38 + this.deviceSessionCache = CacheBuilder.newBuilder()
  39 + .maximumSize(maxSessions)
  40 + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS)
  41 + .build();
47 42 }
48 43  
49 44 /**
50   - * 注册设备会话(包含认证信息)
  45 + * 注册设备会话
51 46 *
52 47 * @param deviceId 设备 ID
53 48 * @param sessionInfo 会话信息
54 49 */
55 50 public void registerSession(Long deviceId, SessionInfo sessionInfo) {
56   - // 检查会话数是否已达上限
57   - if (deviceSessionMap.size() >= maxSessions) {
  51 + // 检查是否为新设备,且会话数已达上限
  52 + if (deviceSessionCache.getIfPresent(deviceId) == null
  53 + && deviceSessionCache.size() >= maxSessions) {
58 54 throw new IllegalStateException("会话数已达上限: " + maxSessions);
59 55 }
60   - // 如果设备已有其他会话,先清理旧会话
61   - SessionInfo oldSessionInfo = deviceSessionMap.get(deviceId);
62   - if (oldSessionInfo != null) {
63   - String oldAddressKey = buildAddressKey(oldSessionInfo.getAddress());
64   - addressDeviceMap.remove(oldAddressKey, deviceId);
65   - log.info("[registerSession][设备已有其他会话,清理旧会话,设备 ID: {},旧地址: {}]",
66   - deviceId, oldAddressKey);
67   - }
68   -
69   - // 注册新会话
70   - String addressKey = buildAddressKey(sessionInfo.getAddress());
71   - deviceSessionMap.put(deviceId, sessionInfo);
72   - addressDeviceMap.put(addressKey, deviceId);
73   - log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},product key: {},device name: {}]",
74   - deviceId, addressKey, sessionInfo.getProductKey(), sessionInfo.getDeviceName());
  56 + // 注册会话
  57 + deviceSessionCache.put(deviceId, sessionInfo);
  58 + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]",
  59 + deviceId, buildAddressKey(sessionInfo.getAddress()),
  60 + sessionInfo.getProductKey(), sessionInfo.getDeviceName());
75 61 }
76 62  
77 63 /**
78   - * 注销设备会话
  64 + * 获取会话信息
  65 + * <p>
  66 + * 注意:调用此方法会自动刷新会话的过期时间
79 67 *
80 68 * @param deviceId 设备 ID
  69 + * @return 会话信息,不存在则返回 null
81 70 */
82   - public void unregisterSession(Long deviceId) {
83   - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId);
84   - if (sessionInfo == null) {
85   - return;
86   - }
87   - String addressKey = buildAddressKey(sessionInfo.getAddress());
88   - // 仅当 addressDeviceMap 中的 deviceId 是当前 deviceId 时才移除,避免误删新会话
89   - addressDeviceMap.remove(addressKey, deviceId);
90   - log.info("[unregisterSession][注销设备会话,设备 ID: {},地址: {}]", deviceId, addressKey);
91   - }
92   -
93   - /**
94   - * 更新会话活跃时间(每次收到上行消息时调用)
95   - *
96   - * @param deviceId 设备 ID
97   - */
98   - public void updateSessionActivity(Long deviceId) {
99   - SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
100   - if (sessionInfo != null) {
101   - sessionInfo.setLastActiveTime(LocalDateTime.now());
102   - }
  71 + public SessionInfo getSession(Long deviceId) {
  72 + return deviceSessionCache.getIfPresent(deviceId);
103 73 }
104 74  
105 75 /**
106 76 * 更新设备会话地址(设备地址变更时调用)
  77 + * <p>
  78 + * 注意:getIfPresent 已自动刷新过期时间,无需重新 put
107 79 *
108 80 * @param deviceId 设备 ID
109 81 * @param newAddress 新地址
110 82 */
111 83 public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) {
112   - SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
  84 + // 地址未变化,无需更新
  85 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
113 86 if (sessionInfo == null) {
114 87 return;
115 88 }
116   - // 清理旧地址映射
117   - String oldAddressKey = buildAddressKey(sessionInfo.getAddress());
118   - addressDeviceMap.remove(oldAddressKey, deviceId);
  89 + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) {
  90 + return;
  91 + }
119 92  
120   - // 更新新地址
121   - String newAddressKey = buildAddressKey(newAddress);
  93 + // 更新地址
  94 + String oldAddressKey = buildAddressKey(sessionInfo.getAddress());
122 95 sessionInfo.setAddress(newAddress);
123   - sessionInfo.setLastActiveTime(LocalDateTime.now());
124   - addressDeviceMap.put(newAddressKey, deviceId);
125   - log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},新地址: {}]", deviceId, newAddressKey);
126   - }
127   -
128   - /**
129   - * 获取会话信息
130   - */
131   - public SessionInfo getSessionInfo(Long deviceId) {
132   - return deviceSessionMap.get(deviceId);
  96 + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]",
  97 + deviceId, oldAddressKey, buildAddressKey(newAddress));
133 98 }
134 99  
135 100 /**
... ... @@ -141,7 +106,7 @@ public class IotUdpSessionManager {
141 106 * @return 是否发送成功
142 107 */
143 108 public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) {
144   - SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
  109 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
145 110 if (sessionInfo == null || sessionInfo.getAddress() == null) {
146 111 log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
147 112 return false;
... ... @@ -165,38 +130,7 @@ public class IotUdpSessionManager {
165 130 }
166 131  
167 132 /**
168   - * 定期清理不活跃的设备会话
169   - *
170   - * @param timeoutMs 超时时间(毫秒)
171   - * @return 清理的设备 ID 列表(用于发送离线消息)
172   - */
173   - public List<Long> cleanExpiredSessions(long timeoutMs) {
174   - List<Long> offlineDeviceIds = new ArrayList<>();
175   - LocalDateTime now = LocalDateTime.now();
176   - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000);
177   - Iterator<Map.Entry<Long, SessionInfo>> iterator = deviceSessionMap.entrySet().iterator();
178   - // TODO @AI:改成 for each 会不会更好?
179   - while (iterator.hasNext()) {
180   - Map.Entry<Long, SessionInfo> entry = iterator.next();
181   - SessionInfo sessionInfo = entry.getValue();
182   - // 未过期,跳过
183   - if (sessionInfo.getLastActiveTime().isAfter(expireTime)) {
184   - continue;
185   - }
186   - // 过期处理:记录离线设备 ID
187   - Long deviceId = entry.getKey();
188   - String addressKey = buildAddressKey(sessionInfo.getAddress());
189   - addressDeviceMap.remove(addressKey, deviceId);
190   - offlineDeviceIds.add(deviceId);
191   - log.debug("[cleanExpiredSessions][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]",
192   - deviceId, addressKey, sessionInfo.getLastActiveTime());
193   - iterator.remove();
194   - }
195   - return offlineDeviceIds;
196   - }
197   -
198   - /**
199   - * 构建地址 Key
  133 + * 构建地址 Key(用于日志输出)
200 134 *
201 135 * @param address 地址
202 136 * @return 地址 Key
... ... @@ -206,7 +140,7 @@ public class IotUdpSessionManager {
206 140 }
207 141  
208 142 /**
209   - * 会话信息(包含认证信息)
  143 + * 会话信息
210 144 */
211 145 @Data
212 146 public static class SessionInfo {
... ... @@ -228,10 +162,6 @@ public class IotUdpSessionManager {
228 162 * 设备地址
229 163 */
230 164 private InetSocketAddress address;
231   - /**
232   - * 最后活跃时间
233   - */
234   - private LocalDateTime lastActiveTime;
235 165  
236 166 }
237 167  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -81,13 +81,38 @@ yudao:
81 81 - id: udp-json
82 82 type: udp
83 83 port: 8093
84   - enabled: true
  84 + enabled: false
85 85 serialize: json
86 86 udp:
87 87 max-sessions: 1000 # 最大会话数
88 88 session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期
89 89 receive-buffer-size: 65536 # 接收缓冲区大小(字节)
90 90 send-buffer-size: 65536 # 发送缓冲区大小(字节)
  91 + # ====================================
  92 + # 针对引入的 WebSocket 组件的配置
  93 + # ====================================
  94 + - id: websocket-json
  95 + type: websocket
  96 + port: 8094
  97 + enabled: false
  98 + serialize: json
  99 + websocket:
  100 + path: /ws
  101 + max-message-size: 65536 # 最大消息大小(字节,默认 64KB)
  102 + max-frame-size: 65536 # 最大帧大小(字节,默认 64KB)
  103 + idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60)
  104 + ssl-enabled: false # 是否启用 SSL(wss://)
  105 + # ====================================
  106 + # 针对引入的 CoAP 组件的配置
  107 + # ====================================
  108 + - id: coap-json
  109 + type: coap
  110 + port: 5683
  111 + enabled: true
  112 + coap:
  113 + max-message-size: 1024 # 最大消息大小(字节)
  114 + ack-timeout-ms: 2000 # ACK 超时时间(毫秒)
  115 + max-retransmit: 4 # 最大重传次数
91 116  
92 117 # 协议配置(旧版,保持兼容)
93 118 protocol:
... ... @@ -134,27 +159,6 @@ yudao:
134 159 max-message-size: 8192
135 160 connect-timeout-seconds: 60
136 161 ssl-enabled: false
137   - # ====================================
138   - # 针对引入的 CoAP 组件的配置
139   - # ====================================
140   - coap:
141   - enabled: false # 是否启用 CoAP 协议
142   - port: 5683 # CoAP 服务端口(默认 5683)
143   - max-message-size: 1024 # 最大消息大小(字节)
144   - ack-timeout: 2000 # ACK 超时时间(毫秒)
145   - max-retransmit: 4 # 最大重传次数
146   - # ====================================
147   - # 针对引入的 WebSocket 组件的配置
148   - # ====================================
149   - websocket:
150   - enabled: false # 是否启用 WebSocket 协议
151   - port: 8094 # WebSocket 服务端口(默认 8094)
152   - path: /ws # WebSocket 路径(默认 /ws)
153   - max-message-size: 65536 # 最大消息大小(字节,默认 64KB)
154   - max-frame-size: 65536 # 最大帧大小(字节,默认 64KB)
155   - idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60)
156   - ssl-enabled: false # 是否启用 SSL(wss://)
157   -
158 162 --- #################### 日志相关配置 ####################
159 163  
160 164 # 基础日志配置
... ...