Commit 824a801b39225d356bf5a7d6745e62d084b93a83
1 parent
71b45a29
【功能完善】IoT: 添加 Webhook 处理器以处理设备连接和断开事件,更新设备状态管理逻辑
Showing
2 changed files
with
158 additions
and
0 deletions
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java
| ... | ... | @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler; |
| 8 | +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceWebhookVertxHandler; | |
| 8 | 9 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 9 | 10 | import io.vertx.core.Future; |
| 10 | 11 | import io.vertx.core.Vertx; |
| ... | ... | @@ -70,6 +71,9 @@ public class IotDeviceUpstreamServer { |
| 70 | 71 | // TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀? |
| 71 | 72 | // 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式 |
| 72 | 73 | .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); |
| 74 | + // 添加 Webhook 处理器,用于处理设备连接和断开连接事件 | |
| 75 | + router.post(IotDeviceWebhookVertxHandler.PATH) | |
| 76 | + .handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi)); | |
| 73 | 77 | // 创建 HttpServer 实例 |
| 74 | 78 | this.server = vertx.createHttpServer().requestHandler(router); |
| 75 | 79 | this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client); | ... | ... |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 5 | +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; | |
| 6 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; | |
| 9 | +import io.vertx.core.Handler; | |
| 10 | +import io.vertx.core.json.JsonObject; | |
| 11 | +import io.vertx.ext.web.RoutingContext; | |
| 12 | +import lombok.RequiredArgsConstructor; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | + | |
| 15 | +import java.time.LocalDateTime; | |
| 16 | +import java.util.Collections; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * IoT Emqx Webhook 事件处理的 Vert.x Handler | |
| 20 | + * <a href= | |
| 21 | + * "https://docs.emqx.com/zh/emqx/latest/data-integration/webhook.html">EMQX | |
| 22 | + * Webhook</a> | |
| 23 | + * | |
| 24 | + * @author haohao | |
| 25 | + */ | |
| 26 | +@RequiredArgsConstructor | |
| 27 | +@Slf4j | |
| 28 | +public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> { | |
| 29 | + | |
| 30 | + public static final String PATH = "/mqtt/webhook"; | |
| 31 | + | |
| 32 | + private final IotDeviceUpstreamApi deviceUpstreamApi; | |
| 33 | + | |
| 34 | + @Override | |
| 35 | + public void handle(RoutingContext routingContext) { | |
| 36 | + try { | |
| 37 | + // 解析请求体 | |
| 38 | + JsonObject json = routingContext.body().asJsonObject(); | |
| 39 | + String event = json.getString("event"); | |
| 40 | + String clientId = json.getString("clientid"); | |
| 41 | + String username = json.getString("username"); | |
| 42 | + | |
| 43 | + // 处理不同的事件类型 | |
| 44 | + switch (event) { | |
| 45 | + case "client.connected": | |
| 46 | + handleClientConnected(clientId, username); | |
| 47 | + break; | |
| 48 | + case "client.disconnected": | |
| 49 | + handleClientDisconnected(clientId, username); | |
| 50 | + break; | |
| 51 | + default: | |
| 52 | + log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, | |
| 53 | + username); | |
| 54 | + break; | |
| 55 | + } | |
| 56 | + | |
| 57 | + // 返回成功响应 | |
| 58 | + IotPluginCommonUtils.writeJson(routingContext, Collections.singletonMap("result", "success")); | |
| 59 | + } catch (Exception e) { | |
| 60 | + log.error("[handle][处理 Webhook 事件异常]", e); | |
| 61 | + IotPluginCommonUtils.writeJson(routingContext, Collections.singletonMap("result", "error")); | |
| 62 | + } | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * 处理客户端连接事件 | |
| 67 | + * | |
| 68 | + * @param clientId 客户端ID | |
| 69 | + * @param username 用户名 | |
| 70 | + */ | |
| 71 | + private void handleClientConnected(String clientId, String username) { | |
| 72 | + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { | |
| 73 | + log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId); | |
| 74 | + return; | |
| 75 | + } | |
| 76 | + | |
| 77 | + // 解析产品标识和设备名称 | |
| 78 | + String[] parts = parseUsername(username); | |
| 79 | + if (parts == null) { | |
| 80 | + return; | |
| 81 | + } | |
| 82 | + | |
| 83 | + // 更新设备状态为在线 | |
| 84 | + IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO(); | |
| 85 | + updateReqDTO.setProductKey(parts[1]); | |
| 86 | + updateReqDTO.setDeviceName(parts[0]); | |
| 87 | + updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState()); | |
| 88 | + updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); | |
| 89 | + updateReqDTO.setReportTime(LocalDateTime.now()); | |
| 90 | + | |
| 91 | + CommonResult<Boolean> result = deviceUpstreamApi.updateDeviceState(updateReqDTO); | |
| 92 | + if (result.getCode() != 0 || !result.getData()) { | |
| 93 | + log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}", | |
| 94 | + clientId, username, result.getCode(), result.getMsg()); | |
| 95 | + } else { | |
| 96 | + log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username); | |
| 97 | + } | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * 处理客户端断开连接事件 | |
| 102 | + * | |
| 103 | + * @param clientId 客户端ID | |
| 104 | + * @param username 用户名 | |
| 105 | + */ | |
| 106 | + private void handleClientDisconnected(String clientId, String username) { | |
| 107 | + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { | |
| 108 | + log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId); | |
| 109 | + return; | |
| 110 | + } | |
| 111 | + | |
| 112 | + // 解析产品标识和设备名称 | |
| 113 | + String[] parts = parseUsername(username); | |
| 114 | + if (parts == null) { | |
| 115 | + return; | |
| 116 | + } | |
| 117 | + | |
| 118 | + // 更新设备状态为离线 | |
| 119 | + IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO(); | |
| 120 | + offlineReqDTO.setProductKey(parts[1]); | |
| 121 | + offlineReqDTO.setDeviceName(parts[0]); | |
| 122 | + offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState()); | |
| 123 | + offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); | |
| 124 | + offlineReqDTO.setReportTime(LocalDateTime.now()); | |
| 125 | + | |
| 126 | + CommonResult<Boolean> offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO); | |
| 127 | + if (offlineResult.getCode() != 0 || !offlineResult.getData()) { | |
| 128 | + log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}", | |
| 129 | + clientId, username, offlineResult.getCode(), offlineResult.getMsg()); | |
| 130 | + } else { | |
| 131 | + log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username); | |
| 132 | + } | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 解析用户名,格式为 deviceName&productKey | |
| 137 | + * | |
| 138 | + * @param username 用户名 | |
| 139 | + * @return 解析结果,[0]为deviceName,[1]为productKey,解析失败返回null | |
| 140 | + */ | |
| 141 | + private String[] parseUsername(String username) { | |
| 142 | + if (StrUtil.isEmpty(username)) { | |
| 143 | + return null; | |
| 144 | + } | |
| 145 | + | |
| 146 | + String[] parts = username.split("&"); | |
| 147 | + if (parts.length != 2) { | |
| 148 | + log.warn("[parseUsername][用户名格式不正确,无法解析产品标识和设备名称] username={}", username); | |
| 149 | + return null; | |
| 150 | + } | |
| 151 | + | |
| 152 | + return parts; | |
| 153 | + } | |
| 154 | +} | |
| 0 | 155 | \ No newline at end of file | ... | ... |