diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
index 300b2e48ec..9086480d3f 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
@@ -18,6 +18,8 @@ import org.springframework.stereotype.Component;
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
+ private static final String TYPE = "Alink";
+
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -62,6 +64,11 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
}
+ @Override
+ public String type() {
+ return TYPE;
+ }
+
@Override
public byte[] encode(IotDeviceMessage message) {
AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
@@ -79,9 +86,4 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
}
- @Override
- public String type() {
- return "Alink";
- }
-
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
new file mode 100644
index 0000000000..8279ca2471
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
@@ -0,0 +1,351 @@
+package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
+import io.vertx.core.buffer.Buffer;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * TCP 二进制格式 {@link IotDeviceMessage} 编解码器
+ *
+ * 二进制协议格式(所有数值使用大端序):
+ *
+ *
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4 字节) |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 消息 ID 长度(2 字节) | 消息 ID (变长字符串) |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 方法名长度(2 字节) | 方法名(变长字符串) |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 消息体数据(变长) |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ *
+ *
+ * 消息体格式:
+ * - 请求消息:params 数据(JSON)
+ * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON)
+ *
+ * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@Component
+public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
+
+ public static final String TYPE = "TCP_BINARY";
+
+ // ==================== 协议常量 ====================
+
+ /**
+ * 协议魔术字,用于协议识别
+ */
+ private static final byte MAGIC_NUMBER = (byte) 0x7E;
+
+ /**
+ * 协议版本号
+ */
+ private static final byte PROTOCOL_VERSION = (byte) 0x01;
+
+ // TODO @haohao:这个要不直接静态枚举,不用 MessageType
+ /**
+ * 消息类型常量
+ */
+ public static class MessageType {
+
+ /**
+ * 请求消息
+ */
+ public static final byte REQUEST = 0x01;
+ /**
+ * 响应消息
+ */
+ public static final byte RESPONSE = 0x02;
+
+ }
+
+ /**
+ * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息标志 + 消息长度)
+ */
+ private static final int HEADER_FIXED_LENGTH = 8;
+
+ /**
+ * 最小消息长度(头部 + 消息ID长度 + 方法名长度)
+ */
+ private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4;
+
+ @Override
+ public String type() {
+ return TYPE;
+ }
+
+ @Override
+ public byte[] encode(IotDeviceMessage message) {
+ Assert.notNull(message, "消息不能为空");
+ Assert.notBlank(message.getMethod(), "消息方法不能为空");
+ try {
+ // 1. 确定消息类型
+ byte messageType = determineMessageType(message);
+ // 2. 构建消息体
+ byte[] bodyData = buildMessageBody(message, messageType);
+ // 3. 构建完整消息(不包含deviceId,由连接上下文管理)
+ return buildCompleteMessage(message, messageType, bodyData);
+ } catch (Exception e) {
+ log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e);
+ throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public IotDeviceMessage decode(byte[] bytes) {
+ Assert.notNull(bytes, "待解码数据不能为空");
+ Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足");
+ try {
+ Buffer buffer = Buffer.buffer(bytes);
+ // 1. 解析协议头部
+ ProtocolHeader header = parseProtocolHeader(buffer);
+ // 2. 解析消息内容(不包含deviceId,由上层连接管理器设置)
+ return parseMessageContent(buffer, header);
+ } catch (Exception e) {
+ log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e);
+ throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e);
+ }
+ }
+
+ // ==================== 编码相关方法 ====================
+
+ /**
+ * 确定消息类型
+ * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息
+ */
+ private byte determineMessageType(IotDeviceMessage message) {
+ // 判断是否为响应消息:有响应码或响应消息时为响应
+ // TODO @haohao:感觉只判断 code 更稳妥点?msg 有可能空。。。
+ if (message.getCode() != null || StrUtil.isNotBlank(message.getMsg())) {
+ return MessageType.RESPONSE;
+ }
+ // 默认为请求消息
+ return MessageType.REQUEST;
+ }
+
+ /**
+ * 构建消息体
+ */
+ private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) {
+ Buffer bodyBuffer = Buffer.buffer();
+ if (messageType == MessageType.RESPONSE) {
+ // code
+ bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0);
+ // msg
+ String msg = message.getMsg() != null ? message.getMsg() : "";
+ byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
+ bodyBuffer.appendShort((short) msgBytes.length);
+ bodyBuffer.appendBytes(msgBytes);
+ // data
+ if (message.getData() != null) {
+ bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData()));
+ }
+ } else {
+ // params
+ // TODO @haohao:请求是不是只处理 message.getParams() 哈?
+ Object payload = message.getParams() != null ? message.getParams() : message.getData();
+ if (payload != null) {
+ bodyBuffer.appendBytes(JsonUtils.toJsonByte(payload));
+ }
+ }
+ return bodyBuffer.getBytes();
+ }
+
+ /**
+ * 构建完整消息
+ */
+ private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) {
+ Buffer buffer = Buffer.buffer();
+ // 1. 写入协议头部
+ buffer.appendByte(MAGIC_NUMBER);
+ buffer.appendByte(PROTOCOL_VERSION);
+ buffer.appendByte(messageType);
+ buffer.appendByte((byte) 0x00); // 消息标志,预留字段 TODO @haohao:这个标识的作用是啥呀?
+ // 2. 预留消息长度位置(在 6. 更新消息长度)
+ int lengthPosition = buffer.length();
+ buffer.appendInt(0);
+ // 3. 写入消息 ID
+ String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId()
+ // TODO @haohao:复用 IotDeviceMessageUtils 的 generateMessageId 哇?
+ : generateMessageId(message.getMethod());
+ // TODO @haohao:StrUtil.utf8Bytes()
+ byte[] messageIdBytes = messageId.getBytes(StandardCharsets.UTF_8);
+ buffer.appendShort((short) messageIdBytes.length);
+ buffer.appendBytes(messageIdBytes);
+ // 4. 写入方法名
+ byte[] methodBytes = message.getMethod().getBytes(StandardCharsets.UTF_8);
+ buffer.appendShort((short) methodBytes.length);
+ buffer.appendBytes(methodBytes);
+ // 5. 写入消息体
+ buffer.appendBytes(bodyData);
+ // 6. 更新消息长度
+ buffer.setInt(lengthPosition, buffer.length());
+ return buffer.getBytes();
+ }
+
+ /**
+ * 生成消息 ID
+ */
+ private String generateMessageId(String method) {
+ return method + "_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000);
+ }
+
+ // ==================== 解码相关方法 ====================
+
+ // TODO @haohao:是不是把 parseProtocolHeader、parseMessageContent 合并?
+ /**
+ * 解析协议头部
+ */
+ private ProtocolHeader parseProtocolHeader(Buffer buffer) {
+ int index = 0;
+ // 1. 验证魔术字
+ byte magic = buffer.getByte(index++);
+ Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic);
+ // 2. 验证版本号
+ byte version = buffer.getByte(index++);
+ Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version);
+
+ // 3. 读取消息类型
+ byte messageType = buffer.getByte(index++);
+ Assert.isTrue(isValidMessageType(messageType), "无效的消息类型: " + messageType);
+ // 4. 读取消息标志(暂时跳过)
+ byte messageFlags = buffer.getByte(index++);
+
+ // 5. 读取消息长度
+ int messageLength = buffer.getInt(index);
+ index += 4;
+
+ Assert.isTrue(messageLength == buffer.length(),
+ "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length());
+
+ return new ProtocolHeader(magic, version, messageType, messageFlags, messageLength, index);
+ }
+
+ /**
+ * 解析消息内容
+ */
+ private IotDeviceMessage parseMessageContent(Buffer buffer, ProtocolHeader header) {
+ int index = header.getNextIndex();
+
+ // 1. 读取消息 ID
+ short messageIdLength = buffer.getShort(index);
+ index += 2;
+ String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name());
+ index += messageIdLength;
+
+ // 2. 读取方法名
+ short methodLength = buffer.getShort(index);
+ index += 2;
+ String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name());
+ index += methodLength;
+
+ // 3. 解析消息体
+ return parseMessageBody(buffer, index, header.getMessageType(), messageId, method);
+ }
+
+ /**
+ * 解析消息体
+ */
+ private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType,
+ String messageId, String method) {
+ if (startIndex >= buffer.length()) {
+ // 空消息体
+ return IotDeviceMessage.of(messageId, method, null, null, null, null);
+ }
+
+ if (messageType == MessageType.RESPONSE) {
+ // 响应消息:解析 code + msg + data
+ return parseResponseMessage(buffer, startIndex, messageId, method);
+ } else {
+ // 请求消息:解析 payload(可能是 params 或 data)
+ Object payload = parseJsonData(buffer, startIndex, buffer.length());
+ return IotDeviceMessage.of(messageId, method, payload, null, null, null);
+ }
+ }
+
+ /**
+ * 解析响应消息
+ */
+ private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) {
+ int index = startIndex;
+
+ // 1. 读取响应码
+ Integer code = buffer.getInt(index);
+ index += 4;
+
+ // 2. 读取响应消息
+ short msgLength = buffer.getShort(index);
+ index += 2;
+ String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null;
+ index += msgLength;
+
+ // 3. 读取响应数据
+ Object data = null;
+ if (index < buffer.length()) {
+ data = parseJsonData(buffer, index, buffer.length());
+ }
+
+ return IotDeviceMessage.of(messageId, method, null, data, code, msg);
+ }
+
+ /**
+ * 解析JSON数据
+ */
+ private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) {
+ if (startIndex >= endIndex) {
+ return null;
+ }
+ try {
+ String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
+ return JsonUtils.parseObject(jsonStr, Object.class);
+ } catch (Exception e) {
+ log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e);
+ return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
+ }
+ }
+
+ // ==================== 辅助方法 ====================
+
+ // TODO @haohao:这个貌似只用一次,可以考虑不抽小方法哈;
+ /**
+ * 验证消息类型是否有效
+ */
+ private boolean isValidMessageType(byte messageType) {
+ return messageType == MessageType.REQUEST || messageType == MessageType.RESPONSE;
+ }
+
+ // ==================== 内部类 ====================
+
+ /**
+ * 协议头部信息
+ */
+ @Data
+ @AllArgsConstructor
+ private static class ProtocolHeader {
+
+ private byte magic;
+ private byte version;
+ private byte messageType;
+ private byte messageFlags;
+ private int messageLength;
+ /**
+ * 指向消息内容开始位置
+ */
+ private int nextIndex;
+
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java
deleted file mode 100644
index 0bcef2e0cb..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java
+++ /dev/null
@@ -1,390 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
-
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSONException;
-import cn.hutool.json.JSONObject;
-import cn.hutool.json.JSONUtil;
-import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage;
-import io.vertx.core.buffer.Buffer;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * TCP {@link IotDeviceMessage} 编解码器
- *
- * 参考 EMQX 设计理念:
- * 1. 高性能编解码
- * 2. 容错机制
- * 3. 缓存优化
- * 4. 监控统计
- * 5. 资源管理
- *
- * @author 芋道源码
- */
-@Component
-@Slf4j
-public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec {
-
- /**
- * 编解码器类型
- */
- public static final String TYPE = "tcp";
-
- // ==================== 方法映射 ====================
-
- /**
- * 消息方法到功能码的映射
- */
- private static final Map METHOD_TO_CODE_MAP = new ConcurrentHashMap<>();
-
- /**
- * 功能码到消息方法的映射
- */
- private static final Map CODE_TO_METHOD_MAP = new ConcurrentHashMap<>();
-
- static {
- // 初始化方法映射
- initializeMethodMappings();
- }
-
- // ==================== 缓存管理 ====================
-
- /**
- * JSON 缓存,提升编解码性能
- */
- private final Map jsonCache = new ConcurrentHashMap<>();
-
- /**
- * 缓存最大大小
- */
- private static final int MAX_CACHE_SIZE = 1000;
-
- // ==================== 常量定义 ====================
-
- /**
- * 负载字段名
- */
- public static class PayloadField {
- public static final String TIMESTAMP = "timestamp";
- public static final String MESSAGE_ID = "msgId";
- public static final String DEVICE_ID = "deviceId";
- public static final String PARAMS = "params";
- public static final String DATA = "data";
- public static final String CODE = "code";
- public static final String MESSAGE = "message";
- }
-
- /**
- * 消息方法映射
- */
- public static class MessageMethod {
- public static final String PROPERTY_POST = "thing.property.post";
- public static final String PROPERTY_SET = "thing.property.set";
- public static final String PROPERTY_GET = "thing.property.get";
- public static final String EVENT_POST = "thing.event.post";
- public static final String SERVICE_INVOKE = "thing.service.invoke";
- public static final String CONFIG_PUSH = "thing.config.push";
- public static final String OTA_UPGRADE = "thing.ota.upgrade";
- public static final String STATE_ONLINE = "thing.state.online";
- public static final String STATE_OFFLINE = "thing.state.offline";
- }
-
- // ==================== 初始化方法 ====================
-
- /**
- * 初始化方法映射
- */
- private static void initializeMethodMappings() {
- METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP);
- METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET);
- METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET);
- METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP);
- METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE);
- METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN);
- METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN);
- METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT);
- METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT);
-
- // 反向映射
- METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method));
- }
-
- // ==================== 编解码方法 ====================
-
- @Override
- public byte[] encode(IotDeviceMessage message) {
- validateEncodeParams(message);
-
- try {
- if (log.isDebugEnabled()) {
- log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}",
- message.getMethod(), message.getRequestId());
- }
-
- // 1. 获取功能码
- short code = getCodeByMethodSafely(message.getMethod());
-
- // 2. 构建负载
- String payload = buildPayloadOptimized(message);
-
- // 3. 构建 TCP 数据包
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr("") // 地址在发送时由调用方设置
- .code(code)
- .mid((short) 0) // 消息序号在发送时由调用方设置
- .payload(payload)
- .build();
-
- // 4. 编码为字节流
- Buffer buffer = TcpDataEncoder.encode(dataPackage);
- byte[] result = buffer.getBytes();
-
- // 5. 统计信息
- if (log.isDebugEnabled()) {
- log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}",
- message.getMethod(), result.length);
- }
-
- return result;
-
- } catch (Exception e) {
- log.error("[encode][TCP 消息编码失败] 消息: {}", message, e);
- throw new TcpCodecException("TCP 消息编码失败", e);
- }
- }
-
- @Override
- public IotDeviceMessage decode(byte[] bytes) {
- validateDecodeParams(bytes);
-
- try {
- if (log.isDebugEnabled()) {
- log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length);
- }
-
- // 1. 解码 TCP 数据包
- Buffer buffer = Buffer.buffer(bytes);
- TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
-
- // 2. 获取消息方法
- String method = getMethodByCodeSafely(dataPackage.getCode());
-
- // 3. 解析负载数据
- Object params = parsePayloadOptimized(dataPackage.getPayload());
-
- // 4. 构建 IoT 设备消息
- IotDeviceMessage message = IotDeviceMessage.requestOf(method, params);
-
- // 5. 统计信息
- if (log.isDebugEnabled()) {
- log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}",
- method, dataPackage.getCode());
- }
-
- return message;
-
- } catch (Exception e) {
- log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}",
- bytes.length, truncateData(bytes, 100), e);
- throw new TcpCodecException("TCP 消息解码失败", e);
- }
- }
-
- @Override
- public String type() {
- return TYPE;
- }
-
- // ==================== 内部辅助方法 ====================
-
- /**
- * 验证编码参数
- */
- private void validateEncodeParams(IotDeviceMessage message) {
- if (Objects.isNull(message)) {
- throw new IllegalArgumentException("IoT 设备消息不能为空");
- }
- if (StrUtil.isEmpty(message.getMethod())) {
- throw new IllegalArgumentException("消息方法不能为空");
- }
- }
-
- /**
- * 验证解码参数
- */
- private void validateDecodeParams(byte[] bytes) {
- if (Objects.isNull(bytes) || bytes.length == 0) {
- throw new IllegalArgumentException("待解码数据不能为空");
- }
- if (bytes.length > 1024 * 1024) { // 1MB 限制
- throw new IllegalArgumentException("数据包过大,超过1MB限制");
- }
- }
-
- /**
- * 安全获取功能码
- */
- private short getCodeByMethodSafely(String method) {
- Short code = METHOD_TO_CODE_MAP.get(method);
- if (code == null) {
- log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method);
- return TcpDataPackage.CODE_DATA_UP; // 默认为数据上报
- }
- return code;
- }
-
- /**
- * 安全获取消息方法
- */
- private String getMethodByCodeSafely(short code) {
- String method = CODE_TO_METHOD_MAP.get(code);
- if (method == null) {
- log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code);
- return MessageMethod.PROPERTY_POST; // 默认为属性上报
- }
- return method;
- }
-
- /**
- * 优化的负载构建
- */
- private String buildPayloadOptimized(IotDeviceMessage message) {
- // 使用缓存键
- String cacheKey = message.getMethod() + "_" + message.getRequestId();
- JSONObject cachedPayload = jsonCache.get(cacheKey);
-
- if (cachedPayload != null) {
- // 更新时间戳
- cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());
- return cachedPayload.toString();
- }
-
- // 创建新的负载
- JSONObject payload = new JSONObject();
-
- // 添加基础字段
- addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId());
- addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId());
- addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams());
- addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData());
- addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode());
- addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg());
-
- // 添加时间戳
- payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis());
-
- // 缓存管理
- if (jsonCache.size() < MAX_CACHE_SIZE) {
- jsonCache.put(cacheKey, payload);
- } else {
- cleanJsonCacheIfNeeded();
- }
-
- return payload.toString();
- }
-
- /**
- * 优化的负载解析
- */
- private Object parsePayloadOptimized(String payload) {
- if (StrUtil.isEmpty(payload)) {
- return null;
- }
-
- try {
- // 尝试从缓存获取
- JSONObject cachedJson = jsonCache.get(payload);
- if (cachedJson != null) {
- return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson;
- }
-
- // 解析 JSON 对象
- JSONObject jsonObject = JSONUtil.parseObj(payload);
-
- // 缓存解析结果
- if (jsonCache.size() < MAX_CACHE_SIZE) {
- jsonCache.put(payload, jsonObject);
- }
-
- return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject;
-
- } catch (JSONException e) {
- log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload);
- return payload;
- } catch (Exception e) {
- log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e);
- return payload;
- }
- }
-
- /**
- * 添加非空值到负载
- */
- private void addToPayloadIfNotNull(JSONObject json, String key, Object value) {
- if (ObjectUtil.isNotNull(value)) {
- json.set(key, value);
- }
- }
-
- /**
- * 添加非空字符串到负载
- */
- private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) {
- if (StrUtil.isNotEmpty(value)) {
- json.set(key, value);
- }
- }
-
- /**
- * 清理JSON缓存
- */
- private void cleanJsonCacheIfNeeded() {
- if (jsonCache.size() > MAX_CACHE_SIZE) {
- // 清理一半的缓存
- int clearCount = jsonCache.size() / 2;
- jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5);
-
- if (log.isDebugEnabled()) {
- log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size());
- }
- }
- }
-
- /**
- * 截断数据用于日志输出
- */
- private String truncateData(byte[] data, int maxLength) {
- if (data.length <= maxLength) {
- return new String(data, StandardCharsets.UTF_8);
- }
-
- byte[] truncated = new byte[maxLength];
- System.arraycopy(data, 0, truncated, 0, maxLength);
- return new String(truncated, StandardCharsets.UTF_8) + "...(截断)";
- }
-
- // ==================== 自定义异常 ====================
-
- /**
- * TCP 编解码异常
- */
- public static class TcpCodecException extends RuntimeException {
- public TcpCodecException(String message) {
- super(message);
- }
-
- public TcpCodecException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
new file mode 100644
index 0000000000..8f31305f17
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
@@ -0,0 +1,108 @@
+package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.stereotype.Component;
+
+/**
+ * TCP JSON 格式 {@link IotDeviceMessage} 编解码器
+ *
+ * 采用纯 JSON 格式传输,格式如下:
+ * {
+ * "id": "消息 ID",
+ * "method": "消息方法",
+ * "params": {...}, // 请求参数
+ * "data": {...}, // 响应结果
+ * "code": 200, // 响应错误码
+ * "msg": "success", // 响应提示
+ * "timestamp": 时间戳
+ * }
+ *
+ * @author 芋道源码
+ */
+@Component
+public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
+
+ public static final String TYPE = "TCP_JSON";
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ private static class TcpJsonMessage {
+
+ /**
+ * 消息 ID,且每个消息 ID 在当前设备具有唯一性
+ */
+ private String id;
+
+ /**
+ * 请求方法
+ */
+ private String method;
+
+ /**
+ * 请求参数
+ */
+ private Object params;
+
+ /**
+ * 响应结果
+ */
+ private Object data;
+
+ /**
+ * 响应错误码
+ */
+ private Integer code;
+
+ /**
+ * 响应提示
+ */
+ private String msg;
+
+ /**
+ * 时间戳
+ */
+ private Long timestamp;
+
+ }
+
+ @Override
+ public String type() {
+ return TYPE;
+ }
+
+ @Override
+ public byte[] encode(IotDeviceMessage message) {
+ TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
+ message.getRequestId(),
+ message.getMethod(),
+ message.getParams(),
+ message.getData(),
+ message.getCode(),
+ message.getMsg(),
+ System.currentTimeMillis());
+ return JsonUtils.toJsonByte(tcpJsonMessage);
+ }
+
+ @Override
+ @SuppressWarnings("DataFlowIssue")
+ public IotDeviceMessage decode(byte[] bytes) {
+ TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class);
+ Assert.notNull(tcpJsonMessage, "消息不能为空");
+ Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
+ return IotDeviceMessage.of(
+ tcpJsonMessage.getId(),
+ tcpJsonMessage.getMethod(),
+ tcpJsonMessage.getParams(),
+ tcpJsonMessage.getData(),
+ tcpJsonMessage.getCode(),
+ tcpJsonMessage.getMsg());
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
index de5f3426be..51af9bd3ce 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.gateway.config;
-import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
@@ -9,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
@@ -89,28 +88,24 @@ public class IotGatewayConfiguration {
return Vertx.vertx();
}
- @Bean
- public TcpDeviceConnectionManager tcpDeviceConnectionManager() {
- return new TcpDeviceConnectionManager();
- }
-
@Bean
public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
- TcpDeviceConnectionManager connectionManager,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
- IotDeviceCommonApi deviceApi,
+ IotTcpConnectionManager connectionManager,
Vertx tcpVertx) {
- return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager,
- deviceService, messageService, deviceApi, tcpVertx);
+ return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
+ deviceService, messageService, connectionManager, tcpVertx);
}
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
- TcpDeviceConnectionManager connectionManager,
IotDeviceMessageService messageService,
+ IotDeviceService deviceService,
+ IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
- return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus);
+ return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
+ messageBus);
}
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
index d5c916295c..e4d46b3af6 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
@@ -4,160 +4,64 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
+import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-
/**
* IoT 网关 TCP 下游订阅者:接收下行给设备的消息
- *
- * 参考 EMQX 设计理念:
- * 1. 高性能消息路由
- * 2. 容错机制
- * 3. 状态监控
- * 4. 资源管理
*
* @author 芋道源码
*/
-@RequiredArgsConstructor
@Slf4j
+@RequiredArgsConstructor
public class IotTcpDownstreamSubscriber implements IotMessageSubscriber {
- private final IotTcpUpstreamProtocol protocolHandler;
-
- private final TcpDeviceConnectionManager connectionManager;
+ private final IotTcpUpstreamProtocol protocol;
private final IotDeviceMessageService messageService;
+ private final IotDeviceService deviceService;
+
+ private final IotTcpConnectionManager connectionManager;
+
private final IotMessageBus messageBus;
- private volatile IotTcpDownstreamHandler downstreamHandler;
-
- private final AtomicBoolean initialized = new AtomicBoolean(false);
-
- private final AtomicLong processedMessages = new AtomicLong(0);
-
- private final AtomicLong failedMessages = new AtomicLong(0);
+ private IotTcpDownstreamHandler downstreamHandler;
@PostConstruct
public void init() {
- if (!initialized.compareAndSet(false, true)) {
- log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]");
- return;
- }
+ // 初始化下游处理器
+ this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager);
- try {
- // 初始化下游处理器
- downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService);
-
- // 注册到消息总线
- messageBus.register(this);
-
- log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}",
- getTopic(), getGroup());
- } catch (Exception e) {
- initialized.set(false);
- log.error("[init][TCP 下游消息订阅者初始化失败]", e);
- throw new RuntimeException("TCP 下游消息订阅者初始化失败", e);
- }
- }
-
- @PreDestroy
- public void destroy() {
- if (!initialized.get()) {
- return;
- }
-
- try {
- log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}",
- processedMessages.get(), failedMessages.get());
- } catch (Exception e) {
- log.error("[destroy][TCP 下游消息订阅者关闭失败]", e);
- } finally {
- initialized.set(false);
- }
+ messageBus.register(this);
+ log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
+ protocol.getServerId(), getTopic());
}
@Override
public String getTopic() {
- return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId());
+ return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
- return "tcp-downstream-" + protocolHandler.getServerId();
+ // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
+ return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
- if (!initialized.get()) {
- log.warn("[onMessage][订阅者未初始化,跳过消息处理]");
- return;
- }
-
- long startTime = System.currentTimeMillis();
-
try {
- processedMessages.incrementAndGet();
-
- if (log.isDebugEnabled()) {
- log.debug("[onMessage][收到下行消息] 设备ID: {}, 方法: {}, 消息ID: {}",
- message.getDeviceId(), message.getMethod(), message.getId());
- }
-
- // 参数校验
- if (message.getDeviceId() == null) {
- log.warn("[onMessage][下行消息设备ID为空,跳过处理] 消息: {}", message);
- return;
- }
-
- // 检查连接状态
- if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) {
- log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}",
- message.getDeviceId(), message.getMethod());
- return;
- }
-
- // 处理下行消息
downstreamHandler.handle(message);
-
- // 性能监控
- long processTime = System.currentTimeMillis() - startTime;
- if (processTime > 1000) { // 超过1秒的慢消息
- log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms",
- message.getDeviceId(), message.getMethod(), processTime);
- }
-
} catch (Exception e) {
- failedMessages.incrementAndGet();
- log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}",
- message.getDeviceId(), message.getMethod(), message, e);
+ log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
+ message.getDeviceId(), message.getMethod(), message.getId(), e);
}
}
- /**
- * 获取订阅者统计信息
- */
- public String getSubscriberStatistics() {
- return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%",
- processedMessages.get(),
- failedMessages.get(),
- processedMessages.get() > 0
- ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100
- : 0.0);
- }
-
- /**
- * 检查订阅者健康状态
- */
- public boolean isHealthy() {
- return initialized.get() && downstreamHandler != null;
- }
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
index c42fe19300..791c6cbfc2 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
@@ -1,9 +1,8 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
-import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -16,19 +15,8 @@ import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
/**
* IoT 网关 TCP 协议:接收设备上行消息
- *
- * 负责接收设备上行消息,支持:
- * 1. 设备注册
- * 2. 心跳保活
- * 3. 属性上报
- * 4. 事件上报
- * 5. 设备连接管理
*
* @author 芋道源码
*/
@@ -37,69 +25,41 @@ public class IotTcpUpstreamProtocol {
private final IotGatewayProperties.TcpProperties tcpProperties;
- private final TcpDeviceConnectionManager connectionManager;
-
private final IotDeviceService deviceService;
private final IotDeviceMessageService messageService;
- private final IotDeviceCommonApi deviceApi;
+ private final IotTcpConnectionManager connectionManager;
private final Vertx vertx;
@Getter
private final String serverId;
- private NetServer netServer;
+ private NetServer tcpServer;
public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
- TcpDeviceConnectionManager connectionManager,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
- IotDeviceCommonApi deviceApi,
+ IotTcpConnectionManager connectionManager,
Vertx vertx) {
this.tcpProperties = tcpProperties;
- this.connectionManager = connectionManager;
this.deviceService = deviceService;
this.messageService = messageService;
- this.deviceApi = deviceApi;
+ this.connectionManager = connectionManager;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort());
}
@PostConstruct
public void start() {
- // 1. 启动 TCP 服务器
- try {
- startTcpServer();
- log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId);
- } catch (Exception e) {
- log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e);
- // 抛出异常,中断 Spring 容器启动
- throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e);
- }
- }
-
- @PreDestroy
- public void stop() {
- if (netServer != null) {
- stopTcpServer();
- log.info("[stop][IoT 网关 TCP 协议处理器已停止]");
- }
- }
-
- /**
- * 启动 TCP 服务器
- */
- private void startTcpServer() {
- // 1. 创建服务器选项
+ // 创建服务器选项
NetServerOptions options = new NetServerOptions()
.setPort(tcpProperties.getPort())
.setTcpKeepAlive(true)
.setTcpNoDelay(true)
.setReuseAddress(true);
-
- // 2. 配置 SSL(如果启用)
+ // 配置 SSL(如果启用)
if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(tcpProperties.getSslKeyPath())
@@ -107,72 +67,33 @@ public class IotTcpUpstreamProtocol {
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
- // 3. 创建 TCP 服务器
- netServer = vertx.createNetServer(options);
-
- // 4. 设置连接处理器
- netServer.connectHandler(socket -> {
- log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress());
- IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(
- tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId);
+ // 创建服务器并设置连接处理器
+ tcpServer = vertx.createNetServer(options);
+ tcpServer.connectHandler(socket -> {
+ IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService,
+ connectionManager);
handler.handle(socket);
});
- // 5. 同步启动服务器,等待结果
- CountDownLatch latch = new CountDownLatch(1);
- AtomicReference failure = new AtomicReference<>();
- netServer.listen(result -> {
- if (result.succeeded()) {
- log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}",
- result.result().actualPort(), serverId);
- } else {
- log.error("[startTcpServer][TCP 服务器启动失败]", result.cause());
- failure.set(result.cause());
- }
- latch.countDown();
- });
-
- // 6. 等待启动结果,设置超时
+ // 启动服务器
try {
- if (!latch.await(10, TimeUnit.SECONDS)) {
- throw new RuntimeException("TCP 服务器启动超时");
- }
- if (failure.get() != null) {
- throw new RuntimeException("TCP 服务器启动失败", failure.get());
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("TCP 服务器启动被中断", e);
+ tcpServer.listen().result();
+ log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort());
+ } catch (Exception e) {
+ log.error("[start][IoT 网关 TCP 协议启动失败]", e);
+ throw e;
}
}
- /**
- * 停止 TCP 服务器
- */
- private void stopTcpServer() {
- if (netServer == null) {
- return;
- }
- log.info("[stopTcpServer][准备关闭 TCP 服务器]");
- CountDownLatch latch = new CountDownLatch(1);
- // 异步关闭,并使用 Latch 等待结果
- netServer.close(result -> {
- if (result.succeeded()) {
- log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]");
- } else {
- log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause());
+ @PreDestroy
+ public void stop() {
+ if (tcpServer != null) {
+ try {
+ tcpServer.close().result();
+ log.info("[stop][IoT 网关 TCP 协议已停止]");
+ } catch (Exception e) {
+ log.error("[stop][IoT 网关 TCP 协议停止失败]", e);
}
- latch.countDown();
- });
-
- try {
- // 等待关闭完成,设置超时
- if (!latch.await(10, TimeUnit.SECONDS)) {
- log.warn("[stopTcpServer][关闭 TCP 服务器超时]");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e);
}
}
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java
deleted file mode 100644
index eb353a457a..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client;
-
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.net.NetSocket;
-import io.vertx.core.parsetools.RecordParser;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * TCP 设备客户端
- *
- * 封装设备连接的基本信息和操作。
- * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。
- * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。
- *
- * @author 芋道源码
- */
-@Slf4j
-public class TcpDeviceClient {
-
- @Getter
- private final String clientId;
-
- @Getter
- @Setter
- private String deviceAddr; // 从 final 移除,因为在注册后才设置
-
- @Getter
- @Setter
- private String productKey;
-
- @Getter
- @Setter
- private String deviceName;
-
- @Getter
- @Setter
- private Long deviceId;
-
- @Getter
- private NetSocket socket;
-
- @Getter
- @Setter
- private RecordParser parser;
-
- @Getter
- private final long keepAliveTimeoutMs; // 改为 final,通过构造函数注入
-
- private volatile long lastKeepAliveTime;
-
- private final AtomicBoolean authenticated = new AtomicBoolean(false);
- private final AtomicBoolean closed = new AtomicBoolean(false);
-
- /**
- * 构造函数
- *
- * @param clientId 客户端ID,全局唯一
- * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取
- */
- public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) {
- this.clientId = clientId;
- this.keepAliveTimeoutMs = keepAliveTimeoutMs;
- this.lastKeepAliveTime = System.currentTimeMillis();
- }
-
- /**
- * 绑定网络套接字,并设置相关处理器。
- * 此方法应在 Vert.x Event Loop 线程中调用。
- *
- * @param socket 网络套接字
- */
- public void setSocket(NetSocket socket) {
- // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理
- if (this.socket != null && this.socket != socket) {
- log.warn("[setSocket][客户端({})] 正在用新的 socket 替换旧的,旧 socket 将被关闭。", clientId);
- this.socket.close();
- }
-
- this.socket = socket;
-
- if (socket != null) {
- // 1. 设置关闭处理器
- socket.closeHandler(v -> {
- log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId);
- shutdown(); // 统一调用 shutdown 进行资源清理
- });
-
- // 2. 设置异常处理器
- socket.exceptionHandler(e -> {
- log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e);
- shutdown(); // 出现异常时也关闭连接
- });
-
- // 3. 设置数据处理器
- socket.handler(buffer -> {
- // 任何数据往来都表示连接是活跃的
- keepAlive();
-
- if (parser != null) {
- parser.handle(buffer);
- } else {
- log.warn("[setSocket][设备客户端({})] 未设置解析器(parser),原始数据被忽略: {}", clientId, buffer.toString());
- }
- });
- }
- }
-
- /**
- * 更新心跳时间,表示设备仍然活跃。
- */
- public void keepAlive() {
- this.lastKeepAliveTime = System.currentTimeMillis();
- }
-
- /**
- * 检查连接是否在线。
- * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内。
- *
- * @return 是否在线
- */
- public boolean isOnline() {
- if (closed.get() || socket == null) {
- return false;
- }
- long idleTime = System.currentTimeMillis() - lastKeepAliveTime;
- return idleTime < keepAliveTimeoutMs;
- }
-
- public boolean isAuthenticated() {
- return authenticated.get();
- }
-
- public void setAuthenticated(boolean authenticated) {
- this.authenticated.set(authenticated);
- }
-
- /**
- * 向设备发送消息。
- *
- * @param buffer 消息内容
- */
- public void sendMessage(Buffer buffer) {
- if (closed.get() || socket == null) {
- log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId);
- return;
- }
-
- // Vert.x 的 write 是异步的,不会阻塞
- socket.write(buffer, result -> {
- if (result.succeeded()) {
- log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId);
- // 发送成功也更新心跳,表示连接活跃
- keepAlive();
- } else {
- log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause());
- // 发送失败可能意味着连接已断开,主动关闭
- shutdown();
- }
- });
- }
-
- /**
- * 关闭客户端连接并清理资源。
- * 这是一个幂等操作,可以被多次安全调用。
- */
- public void shutdown() {
- // 使用原子操作保证只执行一次关闭逻辑
- if (closed.getAndSet(true)) {
- return;
- }
-
- log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId);
-
- // 先将 socket 引用置空,再关闭,避免并发问题
- NetSocket socketToClose = this.socket;
- this.socket = null;
-
- if (socketToClose != null) {
- try {
- // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭
- socketToClose.close();
- } catch (Exception e) {
- log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e);
- }
- }
-
- // 重置认证状态
- authenticated.set(false);
- }
-
- public String getConnectionInfo() {
- NetSocket currentSocket = this.socket;
- if (currentSocket != null && currentSocket.remoteAddress() != null) {
- return currentSocket.remoteAddress().toString();
- }
- return "disconnected";
- }
-
- public long getLastKeepAliveTime() {
- return lastKeepAliveTime;
- }
-
- @Override
- public String toString() {
- return "TcpDeviceClient{" +
- "clientId='" + clientId + '\'' +
- ", deviceAddr='" + deviceAddr + '\'' +
- ", deviceId=" + deviceId +
- ", authenticated=" + authenticated.get() +
- ", online=" + isOnline() +
- ", connection=" + getConnectionInfo() +
- '}';
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
new file mode 100644
index 0000000000..520861e51e
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
@@ -0,0 +1,188 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
+
+import io.vertx.core.net.NetSocket;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * IoT 网关 TCP 连接管理器
+ *
+ * 统一管理 TCP 连接的认证状态、设备会话和消息发送功能:
+ * 1. 管理 TCP 连接的认证状态
+ * 2. 管理设备会话和在线状态
+ * 3. 管理消息发送到设备
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@Component
+public class IotTcpConnectionManager {
+
+ /**
+ * 连接信息映射:NetSocket -> 连接信息
+ */
+ private final Map connectionMap = new ConcurrentHashMap<>();
+
+ /**
+ * 设备 ID -> NetSocket 的映射(用于快速查找)
+ */
+ private final Map deviceSocketMap = new ConcurrentHashMap<>();
+
+ /**
+ * NetSocket -> 设备 ID 的映射(用于连接断开时清理)
+ */
+ private final Map socketDeviceMap = new ConcurrentHashMap<>();
+
+ /**
+ * 注册设备连接(包含认证信息)
+ *
+ * @param socket TCP 连接
+ * @param deviceId 设备 ID
+ * @param authInfo 认证信息
+ */
+ public void registerConnection(NetSocket socket, Long deviceId, AuthInfo authInfo) {
+ // 如果设备已有其他连接,先清理旧连接
+ NetSocket oldSocket = deviceSocketMap.get(deviceId);
+ if (oldSocket != null && oldSocket != socket) {
+ log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
+ deviceId, oldSocket.remoteAddress());
+ oldSocket.close();
+ // 清理所有相关映射
+ connectionMap.remove(oldSocket);
+ socketDeviceMap.remove(oldSocket);
+ }
+
+ // 注册新连接 - 更新所有映射关系
+ ConnectionInfo connectionInfo = new ConnectionInfo()
+ .setDeviceId(deviceId)
+ .setAuthInfo(authInfo)
+ .setAuthenticated(true);
+ connectionMap.put(socket, connectionInfo);
+ deviceSocketMap.put(deviceId, socket);
+ // TODO @haohao:socketDeviceMap 和 connectionMap 会重复哇?connectionMap.get(socket).getDeviceId
+ socketDeviceMap.put(socket, deviceId);
+
+ log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
+ deviceId, socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName());
+ }
+
+ /**
+ * 注销设备连接
+ *
+ * @param socket TCP 连接
+ */
+ public void unregisterConnection(NetSocket socket) {
+ ConnectionInfo connectionInfo = connectionMap.remove(socket);
+ Long deviceId = socketDeviceMap.remove(socket);
+ if (connectionInfo != null && deviceId != null) {
+ deviceSocketMap.remove(deviceId);
+ log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
+ deviceId, socket.remoteAddress());
+ }
+ }
+
+ // TODO @haohao:用不到,要不暂时清理哈。
+ /**
+ * 注销设备连接(通过设备 ID)
+ *
+ * @param deviceId 设备 ID
+ */
+ public void unregisterConnection(Long deviceId) {
+ NetSocket socket = deviceSocketMap.remove(deviceId);
+ if (socket != null) {
+ connectionMap.remove(socket);
+ socketDeviceMap.remove(socket);
+ log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress());
+ }
+ }
+
+ /**
+ * 检查连接是否已认证
+ */
+ public boolean isAuthenticated(NetSocket socket) {
+ ConnectionInfo info = connectionMap.get(socket);
+ return info != null && info.isAuthenticated();
+ }
+
+ /**
+ * 检查连接是否未认证
+ */
+ public boolean isNotAuthenticated(NetSocket socket) {
+ return !isAuthenticated(socket);
+ }
+
+ /**
+ * 获取连接的认证信息
+ */
+ public AuthInfo getAuthInfo(NetSocket socket) {
+ ConnectionInfo info = connectionMap.get(socket);
+ return info != null ? info.getAuthInfo() : null;
+ }
+
+ /**
+ * 检查设备是否在线
+ */
+ public boolean isDeviceOnline(Long deviceId) {
+ return deviceSocketMap.containsKey(deviceId);
+ }
+
+ /**
+ * 检查设备是否离线
+ */
+ public boolean isDeviceOffline(Long deviceId) {
+ return !isDeviceOnline(deviceId);
+ }
+
+ /**
+ * 发送消息到设备
+ */
+ public boolean sendToDevice(Long deviceId, byte[] data) {
+ NetSocket socket = deviceSocketMap.get(deviceId);
+ if (socket == null) {
+ log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId);
+ return false;
+ }
+
+ try {
+ socket.write(io.vertx.core.buffer.Buffer.buffer(data));
+ log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length);
+ return true;
+ } catch (Exception e) {
+ log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e);
+ // 发送失败时清理连接
+ unregisterConnection(socket);
+ return false;
+ }
+ }
+
+ // TODO @haohao:ConnectionInfo 和 AuthInfo 是不是可以融合哈?
+
+ /**
+ * 连接信息
+ */
+ @Data
+ public static class ConnectionInfo {
+
+ private Long deviceId;
+ private AuthInfo authInfo;
+ private boolean authenticated;
+
+ }
+
+ /**
+ * 认证信息
+ */
+ @Data
+ public static class AuthInfo {
+
+ private Long deviceId;
+ private String productKey;
+ private String deviceName;
+ private String clientId;
+
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java
deleted file mode 100644
index ce7fe4aa5c..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java
+++ /dev/null
@@ -1,503 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
-
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.net.NetSocket;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-/**
- * TCP 设备连接管理器
- *
- * 参考 EMQX 设计理念:
- * 1. 高性能连接管理
- * 2. 连接池和资源管理
- * 3. 流量控制
- * 4. 监控统计
- * 5. 自动清理和容错
- *
- * @author 芋道源码
- */
-@Component
-@Slf4j
-public class TcpDeviceConnectionManager {
-
- // ==================== 连接存储 ====================
-
- /**
- * 设备客户端映射
- * Key: 设备地址, Value: 设备客户端
- */
- private final ConcurrentMap clientMap = new ConcurrentHashMap<>();
-
- /**
- * 设备ID到设备地址的映射
- * Key: 设备ID, Value: 设备地址
- */
- private final ConcurrentMap deviceIdToAddrMap = new ConcurrentHashMap<>();
-
- /**
- * 套接字到客户端的映射,用于快速查找
- * Key: NetSocket, Value: 设备地址
- */
- private final ConcurrentMap socketToAddrMap = new ConcurrentHashMap<>();
-
- // ==================== 读写锁 ====================
-
- private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
- private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
- private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
-
- // ==================== 定时任务 ====================
-
- /**
- * 定时任务执行器
- */
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> {
- Thread t = new Thread(r, "tcp-connection-manager");
- t.setDaemon(true);
- return t;
- });
-
- // ==================== 统计信息 ====================
-
- private final AtomicLong totalConnections = new AtomicLong(0);
- private final AtomicLong totalDisconnections = new AtomicLong(0);
- private final AtomicLong totalMessages = new AtomicLong(0);
- private final AtomicLong totalFailedMessages = new AtomicLong(0);
- private final AtomicLong totalBytes = new AtomicLong(0);
-
- // ==================== 配置参数 ====================
-
- private static final int MAX_CONNECTIONS = 10000;
- private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒
- private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒
- private static final int STATS_LOG_INTERVAL = 300; // 秒
-
- /**
- * 构造函数,启动定时任务
- */
- public TcpDeviceConnectionManager() {
- startScheduledTasks();
- }
-
- /**
- * 启动定时任务
- */
- private void startScheduledTasks() {
- // 心跳检查任务
- scheduler.scheduleAtFixedRate(this::checkHeartbeat,
- HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS);
-
- // 连接清理任务
- scheduler.scheduleAtFixedRate(this::cleanupConnections,
- CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS);
-
- // 统计日志任务
- scheduler.scheduleAtFixedRate(this::logStatistics,
- STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS);
- }
-
- /**
- * 添加设备客户端
- */
- public boolean addClient(String deviceAddr, TcpDeviceClient client) {
- if (clientMap.size() >= MAX_CONNECTIONS) {
- log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr);
- return false;
- }
-
- writeLock.lock();
- try {
- log.info("[addClient][添加设备客户端: {}]", deviceAddr);
-
- // 关闭之前的连接(如果存在)
- TcpDeviceClient existingClient = clientMap.get(deviceAddr);
- if (existingClient != null) {
- log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr);
- removeClientInternal(deviceAddr, existingClient);
- }
-
- // 添加新连接
- clientMap.put(deviceAddr, client);
-
- // 添加套接字映射
- if (client.getSocket() != null) {
- socketToAddrMap.put(client.getSocket(), deviceAddr);
- }
-
- // 如果客户端已设置设备ID,更新映射
- if (client.getDeviceId() != null) {
- deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr);
- }
-
- totalConnections.incrementAndGet();
- return true;
-
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * 移除设备客户端
- */
- public void removeClient(String deviceAddr) {
- writeLock.lock();
- try {
- TcpDeviceClient client = clientMap.get(deviceAddr);
- if (client != null) {
- removeClientInternal(deviceAddr, client);
- }
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * 内部移除客户端方法(无锁)
- */
- private void removeClientInternal(String deviceAddr, TcpDeviceClient client) {
- log.info("[removeClient][移除设备客户端: {}]", deviceAddr);
-
- // 从映射中移除
- clientMap.remove(deviceAddr);
-
- // 移除套接字映射
- if (client.getSocket() != null) {
- socketToAddrMap.remove(client.getSocket());
- }
-
- // 移除设备ID映射
- if (client.getDeviceId() != null) {
- deviceIdToAddrMap.remove(client.getDeviceId());
- }
-
- // 关闭连接
- client.shutdown();
-
- totalDisconnections.incrementAndGet();
- }
-
- /**
- * 通过设备地址获取客户端
- */
- public TcpDeviceClient getClient(String deviceAddr) {
- readLock.lock();
- try {
- return clientMap.get(deviceAddr);
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 通过设备ID获取客户端
- */
- public TcpDeviceClient getClientByDeviceId(Long deviceId) {
- readLock.lock();
- try {
- String deviceAddr = deviceIdToAddrMap.get(deviceId);
- return deviceAddr != null ? clientMap.get(deviceAddr) : null;
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 通过网络连接获取客户端
- */
- public TcpDeviceClient getClientBySocket(NetSocket socket) {
- readLock.lock();
- try {
- String deviceAddr = socketToAddrMap.get(socket);
- return deviceAddr != null ? clientMap.get(deviceAddr) : null;
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 检查设备是否在线
- */
- public boolean isDeviceOnline(Long deviceId) {
- TcpDeviceClient client = getClientByDeviceId(deviceId);
- return client != null && client.isOnline();
- }
-
- /**
- * 设置设备ID映射
- */
- public void setDeviceIdMapping(String deviceAddr, Long deviceId) {
- writeLock.lock();
- try {
- TcpDeviceClient client = clientMap.get(deviceAddr);
- if (client != null) {
- client.setDeviceId(deviceId);
- deviceIdToAddrMap.put(deviceId, deviceAddr);
- log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId);
- }
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * 发送消息给设备
- */
- public boolean sendMessage(String deviceAddr, Buffer buffer) {
- TcpDeviceClient client = getClient(deviceAddr);
- if (client != null && client.isOnline()) {
- try {
- client.sendMessage(buffer);
- totalMessages.incrementAndGet();
- totalBytes.addAndGet(buffer.length());
- return true;
- } catch (Exception e) {
- totalFailedMessages.incrementAndGet();
- log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e);
- return false;
- }
- }
- log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr);
- return false;
- }
-
- /**
- * 通过设备ID发送消息
- */
- public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) {
- TcpDeviceClient client = getClientByDeviceId(deviceId);
- if (client != null && client.isOnline()) {
- try {
- client.sendMessage(buffer);
- totalMessages.incrementAndGet();
- totalBytes.addAndGet(buffer.length());
- return true;
- } catch (Exception e) {
- totalFailedMessages.incrementAndGet();
- log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e);
- return false;
- }
- }
- log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId);
- return false;
- }
-
- /**
- * 广播消息给所有在线设备
- */
- public int broadcastMessage(Buffer buffer) {
- int successCount = 0;
- readLock.lock();
- try {
- for (TcpDeviceClient client : clientMap.values()) {
- if (client.isOnline()) {
- try {
- client.sendMessage(buffer);
- successCount++;
- } catch (Exception e) {
- log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e);
- }
- }
- }
- } finally {
- readLock.unlock();
- }
-
- totalMessages.addAndGet(successCount);
- totalBytes.addAndGet((long) successCount * buffer.length());
- return successCount;
- }
-
- /**
- * 获取在线设备数量
- */
- public int getOnlineCount() {
- readLock.lock();
- try {
- return (int) clientMap.values().stream()
- .filter(TcpDeviceClient::isOnline)
- .count();
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 获取总连接数
- */
- public int getTotalCount() {
- return clientMap.size();
- }
-
- /**
- * 获取认证设备数量
- */
- public int getAuthenticatedCount() {
- readLock.lock();
- try {
- return (int) clientMap.values().stream()
- .filter(TcpDeviceClient::isAuthenticated)
- .count();
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 心跳检查任务
- */
- private void checkHeartbeat() {
- try {
- long currentTime = System.currentTimeMillis();
- int offlineCount = 0;
-
- readLock.lock();
- try {
- for (TcpDeviceClient client : clientMap.values()) {
- if (!client.isOnline()) {
- offlineCount++;
- }
- }
- } finally {
- readLock.unlock();
- }
-
- if (offlineCount > 0) {
- log.info("[checkHeartbeat][发现{}个离线设备,将在清理任务中处理]", offlineCount);
- }
- } catch (Exception e) {
- log.error("[checkHeartbeat][心跳检查任务异常]", e);
- }
- }
-
- /**
- * 连接清理任务
- */
- private void cleanupConnections() {
- try {
- int beforeSize = clientMap.size();
-
- writeLock.lock();
- try {
- clientMap.entrySet().removeIf(entry -> {
- TcpDeviceClient client = entry.getValue();
- if (!client.isOnline()) {
- log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey());
-
- // 清理相关映射
- if (client.getSocket() != null) {
- socketToAddrMap.remove(client.getSocket());
- }
- if (client.getDeviceId() != null) {
- deviceIdToAddrMap.remove(client.getDeviceId());
- }
-
- client.shutdown();
- totalDisconnections.incrementAndGet();
- return true;
- }
- return false;
- });
- } finally {
- writeLock.unlock();
- }
-
- int afterSize = clientMap.size();
- if (beforeSize != afterSize) {
- log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}",
- beforeSize, afterSize, beforeSize - afterSize);
- }
- } catch (Exception e) {
- log.error("[cleanupConnections][连接清理任务异常]", e);
- }
- }
-
- /**
- * 统计日志任务
- */
- private void logStatistics() {
- try {
- long totalConn = totalConnections.get();
- long totalDisconn = totalDisconnections.get();
- long totalMsg = totalMessages.get();
- long totalFailedMsg = totalFailedMessages.get();
- long totalBytesValue = totalBytes.get();
-
- log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " +
- "总消息: {}, 失败消息: {}, 总字节: {}",
- totalConn, totalDisconn, getOnlineCount(), getAuthenticatedCount(),
- totalMsg, totalFailedMsg, totalBytesValue);
- } catch (Exception e) {
- log.error("[logStatistics][统计日志任务异常]", e);
- }
- }
-
- /**
- * 关闭连接管理器
- */
- public void shutdown() {
- log.info("[shutdown][关闭TCP连接管理器]");
-
- // 关闭定时任务
- scheduler.shutdown();
- try {
- if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
- }
- } catch (InterruptedException e) {
- scheduler.shutdownNow();
- Thread.currentThread().interrupt();
- }
-
- // 关闭所有连接
- writeLock.lock();
- try {
- clientMap.values().forEach(TcpDeviceClient::shutdown);
- clientMap.clear();
- deviceIdToAddrMap.clear();
- socketToAddrMap.clear();
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * 获取连接状态信息
- */
- public String getConnectionStatus() {
- return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%",
- getTotalCount(), getOnlineCount(), getAuthenticatedCount(),
- totalMessages.get() > 0
- ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100
- : 0.0);
- }
-
- /**
- * 获取详细统计信息
- */
- public String getDetailedStatistics() {
- return String.format(
- "TCP连接管理器统计:\n" +
- "- 当前连接数: %d\n" +
- "- 在线设备数: %d\n" +
- "- 认证设备数: %d\n" +
- "- 历史总连接: %d\n" +
- "- 历史总断开: %d\n" +
- "- 总消息数: %d\n" +
- "- 失败消息数: %d\n" +
- "- 总字节数: %d\n" +
- "- 消息成功率: %.2f%%",
- getTotalCount(), getOnlineCount(), getAuthenticatedCount(),
- totalConnections.get(), totalDisconnections.get(),
- totalMessages.get(), totalFailedMessages.get(), totalBytes.get(),
- totalMessages.get() > 0
- ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100
- : 0.0);
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java
deleted file mode 100644
index 8e7baa37d8..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;
-
-import io.vertx.core.buffer.Buffer;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * TCP 数据解码器
- *
- * 负责将字节流解码为 TcpDataPackage 对象
- *
- * 数据包格式:
- * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长)
- *
- * @author 芋道源码
- */
-@Slf4j
-public class TcpDataDecoder {
-
- /**
- * 解码数据包
- *
- * @param buffer 数据缓冲区
- * @return 解码后的数据包
- * @throws IllegalArgumentException 如果数据包格式不正确
- */
- public static TcpDataPackage decode(Buffer buffer) {
- if (buffer == null || buffer.length() < 8) {
- throw new IllegalArgumentException("数据包长度不足");
- }
-
- try {
- int index = 0;
-
- // 1. 获取设备地址长度(2字节)
- short addrLength = buffer.getShort(index);
- index += 2;
-
- // 2. 校验数据包长度
- int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号
- if (buffer.length() < expectedLength) {
- throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节");
- }
-
- // 3. 获取设备地址
- String addr = buffer.getBuffer(index, index + addrLength).toString();
- index += addrLength;
-
- // 4. 获取功能码(2字节)
- short code = buffer.getShort(index);
- index += 2;
-
- // 5. 获取消息序号(2字节)
- short mid = buffer.getShort(index);
- index += 2;
-
- // 6. 获取包体数据
- String payload = "";
- if (index < buffer.length()) {
- payload = buffer.getString(index, buffer.length());
- }
-
- // 7. 构建数据包对象
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addrLength((int) addrLength)
- .addr(addr)
- .code(code)
- .mid(mid)
- .payload(payload)
- .build();
-
- log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}",
- addr, dataPackage.getCodeDescription(), mid, payload.length());
-
- return dataPackage;
-
- } catch (Exception e) {
- log.error("[decode][解码失败] 数据: {}", buffer.toString(), e);
- throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 校验数据包格式
- *
- * @param buffer 数据缓冲区
- * @return 校验结果
- */
- public static boolean validate(Buffer buffer) {
- try {
- decode(buffer);
- return true;
- } catch (Exception e) {
- log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage());
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java
deleted file mode 100644
index fb0a68c182..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;
-
-import io.vertx.core.buffer.Buffer;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * TCP 数据编码器
- *
- * 负责将 TcpDataPackage 对象编码为字节流
- *
- * 数据包格式:
- * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长)
- *
- * @author 芋道源码
- */
-@Slf4j
-public class TcpDataEncoder {
-
- /**
- * 编码数据包
- *
- * @param dataPackage 数据包对象
- * @return 编码后的字节流
- * @throws IllegalArgumentException 如果数据包对象不正确
- */
- public static Buffer encode(TcpDataPackage dataPackage) {
- if (dataPackage == null) {
- throw new IllegalArgumentException("数据包对象不能为空");
- }
-
- if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) {
- throw new IllegalArgumentException("设备地址不能为空");
- }
-
- if (dataPackage.getPayload() == null) {
- dataPackage.setPayload("");
- }
-
- try {
- Buffer buffer = Buffer.buffer();
-
- // 1. 计算包体长度(除了包头4字节)
- int payloadLength = dataPackage.getPayload().getBytes().length;
- int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength;
-
- // 2. 写入包头:总长度(4字节)
- buffer.appendInt(totalLength);
-
- // 3. 写入设备地址长度(2字节)
- buffer.appendShort((short) dataPackage.getAddr().length());
-
- // 4. 写入设备地址(不定长)
- buffer.appendBytes(dataPackage.getAddr().getBytes());
-
- // 5. 写入功能码(2字节)
- buffer.appendShort(dataPackage.getCode());
-
- // 6. 写入消息序号(2字节)
- buffer.appendShort(dataPackage.getMid());
-
- // 7. 写入包体数据(不定长)
- buffer.appendBytes(dataPackage.getPayload().getBytes());
-
- log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}",
- dataPackage.getAddr(), dataPackage.getCodeDescription(),
- dataPackage.getMid(), buffer.length());
-
- return buffer;
-
- } catch (Exception e) {
- log.error("[encode][编码失败] 数据包: {}", dataPackage, e);
- throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 创建注册回复数据包
- *
- * @param addr 设备地址
- * @param mid 消息序号
- * @param success 是否成功
- * @return 编码后的数据包
- */
- public static Buffer createRegisterReply(String addr, short mid, boolean success) {
- String payload = success ? "0" : "1"; // 0表示成功,1表示失败
-
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(addr)
- .code(TcpDataPackage.CODE_REGISTER_REPLY)
- .mid(mid)
- .payload(payload)
- .build();
-
- return encode(dataPackage);
- }
-
- /**
- * 创建数据下发数据包
- *
- * @param addr 设备地址
- * @param mid 消息序号
- * @param data 下发数据
- * @return 编码后的数据包
- */
- public static Buffer createDataDownPackage(String addr, short mid, String data) {
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(addr)
- .code(TcpDataPackage.CODE_DATA_DOWN)
- .mid(mid)
- .payload(data)
- .build();
-
- return encode(dataPackage);
- }
-
- /**
- * 创建服务调用数据包
- *
- * @param addr 设备地址
- * @param mid 消息序号
- * @param serviceData 服务数据
- * @return 编码后的数据包
- */
- public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) {
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(addr)
- .code(TcpDataPackage.CODE_SERVICE_INVOKE)
- .mid(mid)
- .payload(serviceData)
- .build();
-
- return encode(dataPackage);
- }
-
- /**
- * 创建属性设置数据包
- *
- * @param addr 设备地址
- * @param mid 消息序号
- * @param propertyData 属性数据
- * @return 编码后的数据包
- */
- public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) {
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(addr)
- .code(TcpDataPackage.CODE_PROPERTY_SET)
- .mid(mid)
- .payload(propertyData)
- .build();
-
- return encode(dataPackage);
- }
-
- /**
- * 创建属性获取数据包
- *
- * @param addr 设备地址
- * @param mid 消息序号
- * @param propertyNames 属性名称列表
- * @return 编码后的数据包
- */
- public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) {
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(addr)
- .code(TcpDataPackage.CODE_PROPERTY_GET)
- .mid(mid)
- .payload(propertyNames)
- .build();
-
- return encode(dataPackage);
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java
deleted file mode 100644
index 3b6f7df286..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/**
- * TCP 数据包协议定义
- *
- * 数据包格式:
- * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长)
- *
- * @author 芋道源码
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-@Builder
-public class TcpDataPackage {
-
- // ==================== 功能码定义 ====================
-
- /**
- * 设备注册
- */
- public static final short CODE_REGISTER = 10;
- /**
- * 注册回复
- */
- public static final short CODE_REGISTER_REPLY = 11;
- /**
- * 心跳
- */
- public static final short CODE_HEARTBEAT = 20;
- /**
- * 数据上报
- */
- public static final short CODE_DATA_UP = 30;
- /**
- * 事件上报
- */
- public static final short CODE_EVENT_UP = 40;
- /**
- * 数据下发
- */
- public static final short CODE_DATA_DOWN = 50;
- /**
- * 服务调用
- */
- public static final short CODE_SERVICE_INVOKE = 60;
- /**
- * 属性设置
- */
- public static final short CODE_PROPERTY_SET = 70;
- /**
- * 属性获取
- */
- public static final short CODE_PROPERTY_GET = 80;
-
- // ==================== 数据包字段 ====================
-
- /**
- * 设备地址长度
- */
- private Integer addrLength;
-
- /**
- * 设备地址
- */
- private String addr;
-
- /**
- * 功能码
- */
- private short code;
-
- /**
- * 消息序号
- */
- private short mid;
-
- /**
- * 包体数据
- */
- private String payload;
-
- // ==================== 辅助方法 ====================
-
- /**
- * 是否为注册消息
- */
- public boolean isRegisterMessage() {
- return code == CODE_REGISTER;
- }
-
- /**
- * 是否为心跳消息
- */
- public boolean isHeartbeatMessage() {
- return code == CODE_HEARTBEAT;
- }
-
- /**
- * 是否为数据上报消息
- */
- public boolean isDataUpMessage() {
- return code == CODE_DATA_UP;
- }
-
- /**
- * 是否为事件上报消息
- */
- public boolean isEventUpMessage() {
- return code == CODE_EVENT_UP;
- }
-
- /**
- * 是否为下行消息
- */
- public boolean isDownstreamMessage() {
- return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE ||
- code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET;
- }
-
- /**
- * 获取功能码描述
- */
- public String getCodeDescription() {
- switch (code) {
- case CODE_REGISTER:
- return "设备注册";
- case CODE_REGISTER_REPLY:
- return "注册回复";
- case CODE_HEARTBEAT:
- return "心跳";
- case CODE_DATA_UP:
- return "数据上报";
- case CODE_EVENT_UP:
- return "事件上报";
- case CODE_DATA_DOWN:
- return "数据下发";
- case CODE_SERVICE_INVOKE:
- return "服务调用";
- case CODE_PROPERTY_SET:
- return "属性设置";
- case CODE_PROPERTY_GET:
- return "属性获取";
- default:
- return "未知功能码";
- }
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java
deleted file mode 100644
index f796389907..0000000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol;
-
-import io.vertx.core.Handler;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.parsetools.RecordParser;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.function.Consumer;
-
-/**
- * TCP 数据读取器
- *
- * 负责从 TCP 流中读取完整的数据包
- *
- * 数据包格式:
- * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长)
- *
- * @author 芋道源码
- */
-@Slf4j
-public class TcpDataReader {
-
- /**
- * 创建数据包解析器
- *
- * @param receiveHandler 接收处理器
- * @return RecordParser 解析器
- */
- public static RecordParser createParser(Consumer receiveHandler) {
- // 首先读取4字节的长度信息
- RecordParser parser = RecordParser.newFixed(4);
-
- // 设置处理器
- parser.setOutput(new Handler() {
- // 当前数据包的长度,-1表示还没有读取到长度信息
- private int dataLength = -1;
-
- @Override
- public void handle(Buffer buffer) {
- try {
- // 如果还没有读取到长度信息
- if (dataLength == -1) {
- // 从包头中读取数据长度
- dataLength = buffer.getInt(0);
-
- // 校验数据长度
- if (dataLength <= 0 || dataLength > 1024 * 1024) { // 最大1MB
- log.error("[handle][无效的数据包长度: {}]", dataLength);
- reset();
- return;
- }
-
- // 切换到读取数据模式
- parser.fixedSizeMode(dataLength);
-
- log.debug("[handle][读取到数据包长度: {}]", dataLength);
- } else {
- // 读取到完整的数据包
- log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());
-
- // 处理数据包
- try {
- receiveHandler.accept(buffer);
- } catch (Exception e) {
- log.error("[handle][处理数据包失败]", e);
- }
-
- // 重置状态,准备读取下一个数据包
- reset();
- }
- } catch (Exception e) {
- log.error("[handle][数据包处理异常]", e);
- reset();
- }
- }
-
- /**
- * 重置解析器状态
- */
- private void reset() {
- dataLength = -1;
- parser.fixedSizeMode(4);
- }
- });
-
- return parser;
- }
-
- /**
- * 创建带异常处理的数据包解析器
- *
- * @param receiveHandler 接收处理器
- * @param exceptionHandler 异常处理器
- * @return RecordParser 解析器
- */
- public static RecordParser createParserWithExceptionHandler(
- Consumer receiveHandler,
- Consumer exceptionHandler) {
-
- RecordParser parser = RecordParser.newFixed(4);
-
- parser.setOutput(new Handler() {
- private int dataLength = -1;
-
- @Override
- public void handle(Buffer buffer) {
- try {
- if (dataLength == -1) {
- dataLength = buffer.getInt(0);
-
- if (dataLength <= 0 || dataLength > 1024 * 1024) {
- throw new IllegalArgumentException("无效的数据包长度: " + dataLength);
- }
-
- parser.fixedSizeMode(dataLength);
- log.debug("[handle][读取到数据包长度: {}]", dataLength);
- } else {
- log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length());
-
- try {
- receiveHandler.accept(buffer);
- } catch (Exception e) {
- exceptionHandler.accept(e);
- }
-
- reset();
- }
- } catch (Exception e) {
- exceptionHandler.accept(e);
- reset();
- }
- }
-
- private void reset() {
- dataLength = -1;
- parser.fixedSizeMode(4);
- }
- });
-
- return parser;
- }
-
- /**
- * 创建简单的数据包解析器(用于测试)
- *
- * @param receiveHandler 接收处理器
- * @return RecordParser 解析器
- */
- public static RecordParser createSimpleParser(Consumer receiveHandler) {
- return createParser(buffer -> {
- try {
- TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
- receiveHandler.accept(dataPackage);
- } catch (Exception e) {
- log.error("[createSimpleParser][解码数据包失败]", e);
- }
- });
- }
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
index 7c499fb974..3ee31d82e4 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
@@ -1,364 +1,63 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
-import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
-import com.alibaba.fastjson.JSON;
-import io.vertx.core.buffer.Buffer;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 TCP 下行消息处理器
- *
- * 负责处理从业务系统发送到设备的下行消息,包括:
- * 1. 属性设置
- * 2. 服务调用
- * 3. 属性获取
- * 4. 配置下发
- * 5. OTA 升级
*
* @author 芋道源码
*/
@Slf4j
+@RequiredArgsConstructor
public class IotTcpDownstreamHandler {
- private final TcpDeviceConnectionManager connectionManager;
+ private final IotDeviceMessageService deviceMessageService;
- private final IotDeviceMessageService messageService;
+ private final IotDeviceService deviceService;
- public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager,
- IotDeviceMessageService messageService) {
- this.connectionManager = connectionManager;
- this.messageService = messageService;
- }
+ private final IotTcpConnectionManager connectionManager;
/**
* 处理下行消息
- *
- * @param message 设备消息
*/
public void handle(IotDeviceMessage message) {
try {
- log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}",
+ log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
message.getDeviceId(), message.getMethod(), message.getId());
- // 1. 获取设备连接
- TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId());
- if (client == null || !client.isOnline()) {
- log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId());
+ // 1.1 获取设备信息
+ IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
+ if (deviceInfo == null) {
+ log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId());
+ return;
+ }
+ // 1.2 检查设备是否在线
+ if (connectionManager.isDeviceOffline(message.getDeviceId())) {
+ log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
return;
}
- // 2. 根据消息方法处理不同类型的下行消息
- switch (message.getMethod()) {
- case "thing.property.set":
- handlePropertySet(client, message);
- break;
- case "thing.property.get":
- handlePropertyGet(client, message);
- break;
- case "thing.service.invoke":
- handleServiceInvoke(client, message);
- break;
- case "thing.config.push":
- handleConfigPush(client, message);
- break;
- case "thing.ota.upgrade":
- handleOtaUpgrade(client, message);
- break;
- default:
- log.warn("[handle][未知的下行消息方法: {}]", message.getMethod());
- break;
+ // 2. 根据产品 Key 和设备名称编码消息并发送到设备
+ byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
+ deviceInfo.getDeviceName());
+ boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
+ if (success) {
+ log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
+ message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
+ } else {
+ log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
+ message.getDeviceId(), message.getMethod(), message.getId());
}
-
} catch (Exception e) {
- log.error("[handle][处理下行消息失败]", e);
+ log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
+ message.getDeviceId(), message.getMethod(), message, e);
}
}
- /**
- * 处理属性设置
- *
- * @param client 设备客户端
- * @param message 设备消息
- */
- private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) {
- try {
- log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}",
- client.getDeviceAddr(), message.getParams());
-
- // 使用编解码器发送消息,降级处理使用原始编码
- sendMessageWithCodec(client, message, "handlePropertySet", () -> {
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- Buffer buffer = TcpDataEncoder.createPropertySetPackage(
- client.getDeviceAddr(), mid, payload);
- client.sendMessage(buffer);
-
- log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}",
- client.getDeviceAddr(), mid);
- });
-
- } catch (Exception e) {
- log.error("[handlePropertySet][属性设置失败]", e);
- }
- }
-
- /**
- * 处理属性获取
- *
- * @param client 设备客户端
- * @param message 设备消息
- */
- private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) {
- try {
- log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}",
- client.getDeviceAddr(), message.getParams());
-
- // 使用编解码器发送消息,降级处理使用原始编码
- sendMessageWithCodec(client, message, "handlePropertyGet", () -> {
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- Buffer buffer = TcpDataEncoder.createPropertyGetPackage(
- client.getDeviceAddr(), mid, payload);
- client.sendMessage(buffer);
-
- log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}",
- client.getDeviceAddr(), mid);
- });
-
- } catch (Exception e) {
- log.error("[handlePropertyGet][属性获取失败]", e);
- }
- }
-
- /**
- * 处理服务调用
- *
- * @param client 设备客户端
- * @param message 设备消息
- */
- private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) {
- try {
- log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}",
- client.getDeviceAddr(), message.getParams());
-
- // 1. 构建服务调用数据包
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- Buffer buffer = TcpDataEncoder.createServiceInvokePackage(
- client.getDeviceAddr(), mid, payload);
-
- // 2. 发送消息
- client.sendMessage(buffer);
-
- log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}",
- client.getDeviceAddr(), mid);
-
- } catch (Exception e) {
- log.error("[handleServiceInvoke][服务调用失败]", e);
- }
- }
-
- /**
- * 处理配置推送
- *
- * @param client 设备客户端
- * @param message 设备消息
- */
- private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) {
- try {
- log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}",
- client.getDeviceAddr(), message.getParams());
-
- // 1. 构建配置推送数据包
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- Buffer buffer = TcpDataEncoder.createDataDownPackage(
- client.getDeviceAddr(), mid, payload);
-
- // 2. 发送消息
- client.sendMessage(buffer);
-
- log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}",
- client.getDeviceAddr(), mid);
-
- } catch (Exception e) {
- log.error("[handleConfigPush][配置推送失败]", e);
- }
- }
-
- /**
- * 处理 OTA 升级
- *
- * @param client 设备客户端
- * @param message 设备消息
- */
- private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) {
- try {
- log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}",
- client.getDeviceAddr(), message.getParams());
-
- // 1. 构建 OTA 升级数据包
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- Buffer buffer = TcpDataEncoder.createDataDownPackage(
- client.getDeviceAddr(), mid, payload);
-
- // 2. 发送消息
- client.sendMessage(buffer);
-
- log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}",
- client.getDeviceAddr(), mid);
-
- } catch (Exception e) {
- log.error("[handleOtaUpgrade][OTA升级失败]", e);
- }
- }
-
- /**
- * 处理自定义下行消息
- *
- * @param client 设备客户端
- * @param message 设备消息
- * @param code 功能码
- */
- private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) {
- try {
- log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}",
- client.getDeviceAddr(), code, message.getParams());
-
- // 1. 构建自定义数据包
- String payload = JSON.toJSONString(message.getParams());
- short mid = generateMessageId();
-
- TcpDataPackage dataPackage = TcpDataPackage.builder()
- .addr(client.getDeviceAddr())
- .code(code)
- .mid(mid)
- .payload(payload)
- .build();
-
- Buffer buffer = TcpDataEncoder.encode(dataPackage);
-
- // 2. 发送消息
- client.sendMessage(buffer);
-
- log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}",
- client.getDeviceAddr(), code, mid);
-
- } catch (Exception e) {
- log.error("[handleCustomMessage][自定义消息发送失败]", e);
- }
- }
-
- /**
- * 批量发送下行消息
- *
- * @param deviceIds 设备ID列表
- * @param message 设备消息
- */
- public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) {
- try {
- log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}",
- deviceIds.length, message.getMethod());
-
- for (Long deviceId : deviceIds) {
- // 创建副本消息(避免消息ID冲突)
- IotDeviceMessage copyMessage = IotDeviceMessage.of(
- message.getRequestId(),
- message.getMethod(),
- message.getParams(),
- message.getData(),
- message.getCode(),
- message.getMsg());
- copyMessage.setDeviceId(deviceId);
-
- // 处理单个设备消息
- handle(copyMessage);
- }
-
- } catch (Exception e) {
- log.error("[broadcastMessage][批量发送消息失败]", e);
- }
- }
-
- /**
- * 检查设备是否支持指定方法
- *
- * @param client 设备客户端
- * @param method 消息方法
- * @return 是否支持
- */
- private boolean isMethodSupported(TcpDeviceClient client, String method) {
- // TODO: 可以根据设备类型或产品信息判断是否支持特定方法
- return IotDeviceMessageMethodEnum.of(method) != null;
- }
-
- /**
- * 生成消息序号
- *
- * @return 消息序号
- */
- private short generateMessageId() {
- return (short) (System.currentTimeMillis() % Short.MAX_VALUE);
- }
-
- /**
- * 使用编解码器发送消息
- *
- * @param client 设备客户端
- * @param message 设备消息
- * @param methodName 方法名称
- * @param fallbackAction 降级处理逻辑
- */
- private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message,
- String methodName, Runnable fallbackAction) {
- try {
- // 1. 使用编解码器编码消息
- byte[] messageBytes = messageService.encodeDeviceMessage(
- message, client.getProductKey(), client.getDeviceName());
-
- // 2. 解析编码后的数据包并设置设备地址和消息序号
- Buffer buffer = Buffer.buffer(messageBytes);
- TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
- dataPackage.setAddr(client.getDeviceAddr());
- dataPackage.setMid(generateMessageId());
-
- // 3. 重新编码并发送
- Buffer finalBuffer = TcpDataEncoder.encode(dataPackage);
- client.sendMessage(finalBuffer);
-
- log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}",
- methodName, client.getDeviceAddr(), dataPackage.getMid());
-
- } catch (Exception e) {
- log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}",
- methodName, e.getMessage());
-
- // 执行降级处理
- if (fallbackAction != null) {
- fallbackAction.run();
- }
- }
- }
-
- /**
- * 获取连接统计信息
- *
- * @return 连接统计信息
- */
- public String getHandlerStatistics() {
- return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus());
- }
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
index 0067e72064..627daad680 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
@@ -1,393 +1,450 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
+import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage;
-import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader;
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
+import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
+import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
-import io.vertx.core.parsetools.RecordParser;
-import lombok.RequiredArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Data;
import lombok.extern.slf4j.Slf4j;
+import java.nio.charset.StandardCharsets;
+
/**
- * IoT 网关 TCP 上行消息处理器
- *
- * 核心负责:
- * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信
- * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态
- * 3. 【数据上报】接收设备数据上报和事件上报
- * 4. 【连接管理】管理连接的建立、维护和清理
+ * TCP 上行消息处理器
*
* @author 芋道源码
*/
-@RequiredArgsConstructor
@Slf4j
public class IotTcpUpstreamHandler implements Handler {
- private final IotGatewayProperties.TcpProperties tcpConfig;
+ private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
+ private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
- private final TcpDeviceConnectionManager connectionManager;
+ private static final String AUTH_METHOD = "auth";
+
+ private final IotDeviceMessageService deviceMessageService;
private final IotDeviceService deviceService;
- private final IotDeviceMessageService messageService;
+ private final IotTcpConnectionManager connectionManager;
private final IotDeviceCommonApi deviceApi;
private final String serverId;
+ public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol,
+ IotDeviceMessageService deviceMessageService,
+ IotDeviceService deviceService,
+ IotTcpConnectionManager connectionManager) {
+ this.deviceMessageService = deviceMessageService;
+ this.deviceService = deviceService;
+ this.connectionManager = connectionManager;
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ this.serverId = protocol.getServerId();
+ }
+
@Override
public void handle(NetSocket socket) {
- log.info("[handle][收到设备连接: {}]", socket.remoteAddress());
+ String clientId = IdUtil.simpleUUID();
+ log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
- // 创建客户端ID和设备客户端
- String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress();
- TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs());
+ // 设置异常和关闭处理器
+ socket.exceptionHandler(ex -> {
+ log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
+ cleanupConnection(socket);
+ });
+ socket.closeHandler(v -> {
+ log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
+ cleanupConnection(socket);
+ });
- try {
- // 设置连接异常和关闭处理
- socket.exceptionHandler(ex -> {
- log.error("[handle][连接({})异常]", socket.remoteAddress(), ex);
- handleConnectionClose(client);
- });
-
- socket.closeHandler(v -> {
- log.info("[handle][连接({})关闭]", socket.remoteAddress());
- handleConnectionClose(client);
- });
-
- // 设置网络连接
- client.setSocket(socket);
-
- // 创建数据解析器
- RecordParser parser = TcpDataReader.createParser(buffer -> {
- try {
- handleDataPackage(client, buffer);
- } catch (Exception e) {
- log.error("[handle][处理数据包异常]", e);
- }
- });
-
- // 设置解析器
- client.setParser(parser);
-
- log.info("[handle][设备连接处理器初始化完成: {}]", clientId);
-
- } catch (Exception e) {
- log.error("[handle][初始化连接处理器失败]", e);
- client.shutdown();
- }
+ // 设置消息处理器
+ socket.handler(buffer -> processMessage(clientId, buffer, socket));
}
/**
- * 处理数据包
- *
- * @param client 设备客户端
- * @param buffer 数据缓冲区
+ * 处理消息
*/
- private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) {
+ private void processMessage(String clientId, Buffer buffer, NetSocket socket) {
try {
- // 解码数据包
- TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer);
-
- log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}",
- dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid());
-
- // 根据功能码处理不同类型的消息
- switch (dataPackage.getCode()) {
- case TcpDataPackage.CODE_REGISTER:
- handleDeviceRegister(client, dataPackage);
- break;
- case TcpDataPackage.CODE_HEARTBEAT:
- handleHeartbeat(client, dataPackage);
- break;
- case TcpDataPackage.CODE_DATA_UP:
- handleDataUp(client, dataPackage);
- break;
- case TcpDataPackage.CODE_EVENT_UP:
- handleEventUp(client, dataPackage);
- break;
- default:
- log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode());
- break;
+ // 1.1 数据包基础检查
+ if (buffer.length() == 0) {
+ return;
+ }
+ // 1.2 解码消息
+ MessageInfo messageInfo = decodeMessage(buffer);
+ if (messageInfo == null) {
+ return;
}
+ // 2. 根据消息类型路由处理
+ if (isAuthRequest(messageInfo.message)) {
+ // 认证请求
+ handleAuthenticationRequest(clientId, messageInfo, socket);
+ } else {
+ // 业务消息
+ handleBusinessRequest(clientId, messageInfo, socket);
+ }
} catch (Exception e) {
- log.error("[handleDataPackage][处理数据包失败]", e);
+ log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e);
}
}
/**
- * 处理设备注册
- *
- * @param client 设备客户端
- * @param dataPackage 数据包
+ * 处理认证请求
*/
- private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) {
+ private void handleAuthenticationRequest(String clientId, MessageInfo messageInfo, NetSocket socket) {
try {
- String deviceAddr = dataPackage.getAddr();
- String productKey = dataPackage.getPayload();
+ // 1.1 解析认证参数
+ IotDeviceMessage message = messageInfo.message;
+ AuthParams authParams = parseAuthParams(message.getParams());
+ if (authParams == null) {
+ sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType);
+ return;
+ }
+ // 1.2 执行认证
+ if (!authenticateDevice(authParams)) {
+ log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
+ clientId, authParams.username);
+ sendError(socket, message.getRequestId(), "认证失败", messageInfo.codecType);
+ return;
+ }
- log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey);
-
- // 获取设备信息
- IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr);
+ // 2.1 解析设备信息
+ IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.username);
+ if (deviceInfo == null) {
+ sendError(socket, message.getRequestId(), "解析设备信息失败", messageInfo.codecType);
+ return;
+ }
+ // 2.2 获取设备信息
+ IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
+ deviceInfo.getDeviceName());
if (device == null) {
- log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr);
- sendRegisterReply(client, dataPackage, false);
+ sendError(socket, message.getRequestId(), "设备不存在", messageInfo.codecType);
return;
}
- // 更新客户端信息
- client.setProductKey(productKey);
- client.setDeviceName(deviceAddr);
- client.setDeviceId(device.getId());
- client.setAuthenticated(true);
+ // 3. 注册连接并发送成功响应
+ registerConnection(socket, device, deviceInfo, authParams.clientId);
+ sendOnlineMessage(deviceInfo);
+ sendSuccess(socket, message.getRequestId(), "认证成功", messageInfo.codecType);
+ log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
+ device.getId(), deviceInfo.getDeviceName());
+ } catch (Exception e) {
+ log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
+ sendError(socket, messageInfo.message.getRequestId(), "认证处理异常", messageInfo.codecType);
+ }
+ }
- // 添加到连接管理器
- connectionManager.addClient(deviceAddr, client);
- connectionManager.setDeviceIdMapping(deviceAddr, device.getId());
+ /**
+ * 处理业务请求
+ */
+ private void handleBusinessRequest(String clientId, MessageInfo messageInfo, NetSocket socket) {
+ try {
+ // 1. 检查认证状态
+ if (connectionManager.isNotAuthenticated(socket)) {
+ log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId);
+ sendError(socket, messageInfo.message.getRequestId(), "请先进行认证", messageInfo.codecType);
+ return;
+ }
- // 发送设备上线消息
+ // 2. 获取认证信息并处理业务消息
+ IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket);
+ processBusinessMessage(clientId, messageInfo.message, authInfo);
+ } catch (Exception e) {
+ log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
+ }
+ }
+
+ // TODO @haohao:processBusinessMessage 这个小方法,直接融合到 handleBusinessRequest 里?读起来更聚集点
+ /**
+ * 处理业务消息
+ */
+ private void processBusinessMessage(String clientId, IotDeviceMessage message,
+ IotTcpConnectionManager.AuthInfo authInfo) {
+ try {
+ message.setDeviceId(authInfo.getDeviceId());
+ message.setServerId(serverId);
+ // 发送到消息总线
+ deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(),
+ authInfo.getDeviceName(), serverId);
+ } catch (Exception e) {
+ log.error("[processBusinessMessage][业务消息处理失败,客户端 ID: {},消息 ID: {}]",
+ clientId, message.getId(), e);
+ }
+ }
+
+ /**
+ * 解码消息
+ *
+ * @param buffer 消息
+ */
+ private MessageInfo decodeMessage(Buffer buffer) {
+ if (buffer == null || buffer.length() == 0) {
+ return null;
+ }
+ // 1. 快速检测消息格式类型
+ // TODO @haohao:是不是进一步优化?socket 建立认证后,那条消息已经定义了所有消息的格式哈?
+ String codecType = detectMessageFormat(buffer);
+ try {
+ // 2. 使用检测到的格式进行解码
+ IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
+ if (message == null) {
+ return null;
+ }
+ return new MessageInfo(message, codecType);
+ } catch (Exception e) {
+ log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]",
+ codecType, buffer.length(), e.getMessage());
+ // TODO @haohao:一般消息格式不对,应该抛出异常,断开连接居多?
+ return null;
+ }
+ }
+
+ /**
+ * 检测消息格式类型
+ * 优化性能:避免不必要的字符串转换
+ */
+ private String detectMessageFormat(Buffer buffer) {
+ // TODO @haohao:是不是 IotTcpBinaryDeviceMessageCodec 提供一个 isBinaryFormat 方法哈?
+ // 默认使用 JSON
+ if (buffer.length() == 0) {
+ return CODEC_TYPE_JSON;
+ }
+
+ // 1. 优先检测二进制格式(检查魔术字节 0x7E)
+ if (isBinaryFormat(buffer)) {
+ return CODEC_TYPE_BINARY;
+ }
+
+ // 2. 检测 JSON 格式(检查前几个有效字符)
+ // TODO @haohao:这个检测去掉?直接 return CODEC_TYPE_JSON 更简洁一点。
+ if (isJsonFormat(buffer)) {
+ return CODEC_TYPE_JSON;
+ }
+
+ // 3. 默认尝试 JSON 格式
+ return CODEC_TYPE_JSON;
+ }
+
+ /**
+ * 检测二进制格式
+ * 通过检查魔术字节快速识别,避免完整字符串转换
+ */
+ private boolean isBinaryFormat(Buffer buffer) {
+ // 二进制协议最小长度检查
+ if (buffer.length() < 8) {
+ return false;
+ }
+
+ try {
+ // 检查魔术字节 0x7E(二进制协议的第一个字节)
+ byte firstByte = buffer.getByte(0);
+ return firstByte == (byte) 0x7E;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 检测 JSON 格式
+ * 只检查前几个有效字符,避免完整字符串转换
+ */
+ private boolean isJsonFormat(Buffer buffer) {
+ try {
+ // 检查前 64 个字节或整个缓冲区(取较小值)
+ int checkLength = Math.min(buffer.length(), 64);
+ String prefix = buffer.getString(0, checkLength, StandardCharsets.UTF_8.name());
+
+ if (StrUtil.isBlank(prefix)) {
+ return false;
+ }
+
+ String trimmed = prefix.trim();
+ // JSON 格式必须以 { 或 [ 开头
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
+
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 注册连接信息
+ */
+ private void registerConnection(NetSocket socket, IotDeviceRespDTO device,
+ IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) {
+ // TODO @haohao:AuthInfo 的创建,放在 connectionManager 里构建貌似会更收敛一点?
+ // 创建认证信息
+ IotTcpConnectionManager.AuthInfo authInfo = new IotTcpConnectionManager.AuthInfo()
+ .setDeviceId(device.getId())
+ .setProductKey(deviceInfo.getProductKey())
+ .setDeviceName(deviceInfo.getDeviceName())
+ .setClientId(clientId);
+ // 注册连接
+ connectionManager.registerConnection(socket, device.getId(), authInfo);
+ }
+
+ /**
+ * 发送设备上线消息
+ */
+ private void sendOnlineMessage(IotDeviceAuthUtils.DeviceInfo deviceInfo) {
+ try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
- messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId);
-
- // 发送注册成功回复
- sendRegisterReply(client, dataPackage, true);
-
- log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId());
-
+ deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(),
+ deviceInfo.getDeviceName(), serverId);
} catch (Exception e) {
- log.error("[handleDeviceRegister][设备注册失败]", e);
- sendRegisterReply(client, dataPackage, false);
+ log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", deviceInfo.getDeviceName(), e);
}
}
/**
- * 处理心跳
- *
- * @param client 设备客户端
- * @param dataPackage 数据包
+ * 清理连接
*/
- private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) {
+ private void cleanupConnection(NetSocket socket) {
try {
- String deviceAddr = dataPackage.getAddr();
-
- log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr);
-
- // 更新心跳时间
- client.keepAlive();
-
- // 发送心跳回复(可选)
- // sendHeartbeatReply(client, dataPackage);
-
- } catch (Exception e) {
- log.error("[handleHeartbeat][处理心跳失败]", e);
- }
- }
-
- /**
- * 处理数据上报
- *
- * @param client 设备客户端
- * @param dataPackage 数据包
- */
- private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) {
- try {
- String deviceAddr = dataPackage.getAddr();
- String payload = dataPackage.getPayload();
-
- log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload);
-
- // 检查设备是否已认证
- if (!client.isAuthenticated()) {
- log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr);
- return;
- }
-
- // 使用 IotDeviceMessageService 解码消息
- try {
- // 1. 将 TCP 数据包重新编码为字节数组
- Buffer buffer = TcpDataEncoder.encode(dataPackage);
- byte[] messageBytes = buffer.getBytes();
-
- // 2. 使用 messageService 解码消息
- IotDeviceMessage message = messageService.decodeDeviceMessage(
- messageBytes, client.getProductKey(), client.getDeviceName());
-
- // 3. 发送解码后的消息
- messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
-
- } catch (Exception e) {
- log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());
-
- // 降级处理:使用原始方式解析数据
- JSONObject dataJson = JSONUtil.parseObj(payload);
- IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson);
- messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
- }
-
- // 发送数据上报回复
- sendDataUpReply(client, dataPackage);
-
- } catch (Exception e) {
- log.error("[handleDataUp][处理数据上报失败]", e);
- }
- }
-
- /**
- * 处理事件上报
- *
- * @param client 设备客户端
- * @param dataPackage 数据包
- */
- private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) {
- try {
- String deviceAddr = dataPackage.getAddr();
- String payload = dataPackage.getPayload();
-
- log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload);
-
- // 检查设备是否已认证
- if (!client.isAuthenticated()) {
- log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr);
- return;
- }
-
- // 使用 IotDeviceMessageService 解码消息
- try {
- // 1. 将 TCP 数据包重新编码为字节数组
- Buffer buffer = TcpDataEncoder.encode(dataPackage);
- byte[] messageBytes = buffer.getBytes();
-
- // 2. 使用 messageService 解码消息
- IotDeviceMessage message = messageService.decodeDeviceMessage(
- messageBytes, client.getProductKey(), client.getDeviceName());
-
- // 3. 发送解码后的消息
- messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
-
- } catch (Exception e) {
- log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage());
-
- // 降级处理:使用原始方式解析数据
- JSONObject eventJson = JSONUtil.parseObj(payload);
- IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson);
- messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId);
- }
-
- // 发送事件上报回复
- sendEventUpReply(client, dataPackage);
-
- } catch (Exception e) {
- log.error("[handleEventUp][处理事件上报失败]", e);
- }
- }
-
- /**
- * 发送注册回复
- *
- * @param client 设备客户端
- * @param dataPackage 原始数据包
- * @param success 是否成功
- */
- private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) {
- try {
- io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply(
- dataPackage.getAddr(), dataPackage.getMid(), success);
- client.sendMessage(replyBuffer);
-
- log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}",
- dataPackage.getAddr(), success ? "成功" : "失败");
- } catch (Exception e) {
- log.error("[sendRegisterReply][发送注册回复失败]", e);
- }
- }
-
- /**
- * 发送数据上报回复
- *
- * @param client 设备客户端
- * @param dataPackage 原始数据包
- */
- private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) {
- try {
- TcpDataPackage replyPackage = TcpDataPackage.builder()
- .addr(dataPackage.getAddr())
- .code(TcpDataPackage.CODE_DATA_UP)
- .mid(dataPackage.getMid())
- .payload("0") // 0表示成功
- .build();
-
- io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);
- client.sendMessage(replyBuffer);
-
- } catch (Exception e) {
- log.error("[sendDataUpReply][发送数据上报回复失败]", e);
- }
- }
-
- /**
- * 发送事件上报回复
- *
- * @param client 设备客户端
- * @param dataPackage 原始数据包
- */
- private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) {
- try {
- TcpDataPackage replyPackage = TcpDataPackage.builder()
- .addr(dataPackage.getAddr())
- .code(TcpDataPackage.CODE_EVENT_UP)
- .mid(dataPackage.getMid())
- .payload("0") // 0表示成功
- .build();
-
- io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage);
- client.sendMessage(replyBuffer);
-
- } catch (Exception e) {
- log.error("[sendEventUpReply][发送事件上报回复失败]", e);
- }
- }
-
- /**
- * 处理连接关闭
- *
- * @param client 设备客户端
- */
- private void handleConnectionClose(TcpDeviceClient client) {
- try {
- String deviceAddr = client.getDeviceAddr();
-
- // 发送设备离线消息
- if (client.isAuthenticated()) {
+ // 发送离线消息(如果已认证)
+ IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket);
+ if (authInfo != null) {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
- messageService.sendDeviceMessage(offlineMessage,
- client.getProductKey(), client.getDeviceName(), serverId);
+ deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(),
+ authInfo.getDeviceName(), serverId);
}
- // 从连接管理器移除
- if (deviceAddr != null) {
- connectionManager.removeClient(deviceAddr);
- }
+ // 注销连接
+ connectionManager.unregisterConnection(socket);
+ } catch (Exception e) {
+ log.error("[cleanupConnection][清理连接失败]", e);
+ }
+ }
- log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr);
+ /**
+ * 发送响应消息
+ */
+ private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) {
+ try {
+ Object responseData = MapUtil.builder()
+ .put("success", success)
+ .put("message", message)
+ .build();
+
+ IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData,
+ success ? 0 : 401, message);
+
+ byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
+ socket.write(Buffer.buffer(encodedData));
} catch (Exception e) {
- log.error("[handleConnectionClose][处理连接关闭失败]", e);
+ log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
}
}
+
+ // ==================== 辅助方法 ====================
+
+ /**
+ * 判断是否为认证请求
+ */
+ private boolean isAuthRequest(IotDeviceMessage message) {
+ return AUTH_METHOD.equals(message.getMethod());
+ }
+
+ /**
+ * 解析认证参数
+ */
+ private AuthParams parseAuthParams(Object params) {
+ if (params == null) {
+ return null;
+ }
+ try {
+ JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params
+ : JSONUtil.parseObj(params.toString());
+ String clientId = paramsJson.getStr("clientId");
+ String username = paramsJson.getStr("username");
+ String password = paramsJson.getStr("password");
+ return StrUtil.hasBlank(clientId, username, password) ? null
+ : new AuthParams(clientId, username, password);
+ } catch (Exception e) {
+ log.warn("[parseAuthParams][解析认证参数失败]", e);
+ return null;
+ }
+ }
+
+ /**
+ * 认证设备
+ */
+ private boolean authenticateDevice(AuthParams authParams) {
+ try {
+ CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
+ .setClientId(authParams.clientId)
+ .setUsername(authParams.username)
+ .setPassword(authParams.password));
+ return result.isSuccess() && Boolean.TRUE.equals(result.getData());
+ } catch (Exception e) {
+ log.error("[authenticateDevice][设备认证异常,username: {}]", authParams.username, e);
+ return false;
+ }
+ }
+
+ // TODO @haohao:改成 sendErrorResponse sendSuccessResponse 更清晰点?
+
+ /**
+ * 发送错误响应
+ */
+ private void sendError(NetSocket socket, String requestId, String errorMessage, String codecType) {
+ sendResponse(socket, false, errorMessage, requestId, codecType);
+ }
+
+ /**
+ * 发送成功响应
+ */
+ private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) {
+ sendResponse(socket, true, message, requestId, codecType);
+ }
+
+ // ==================== 内部类 ====================
+
+ // TODO @haohao:IotDeviceAuthReqDTO 复用这个?
+ /**
+ * 认证参数
+ */
+ @Data
+ @AllArgsConstructor
+ private static class AuthParams {
+
+ private final String clientId;
+ private final String username;
+ private final String password;
+
+ }
+
+ /**
+ * 消息信息
+ */
+ @Data
+ @AllArgsConstructor
+ private static class MessageInfo {
+
+ private final IotDeviceMessage message;
+
+ private final String codecType;
+
+ }
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
index 24134ba94a..c86fc0983d 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
@@ -20,6 +20,16 @@ public interface IotDeviceMessageService {
byte[] encodeDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName);
+ /**
+ * 编码消息
+ *
+ * @param message 消息
+ * @param codecType 编解码器类型
+ * @return 编码后的消息内容
+ */
+ byte[] encodeDeviceMessage(IotDeviceMessage message,
+ String codecType);
+
/**
* 解码消息
*
@@ -31,13 +41,22 @@ public interface IotDeviceMessageService {
IotDeviceMessage decodeDeviceMessage(byte[] bytes,
String productKey, String deviceName);
+ /**
+ * 解码消息
+ *
+ * @param bytes 消息内容
+ * @param codecType 编解码器类型
+ * @return 解码后的消息内容
+ */
+ IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType);
+
/**
* 发送消息
*
- * @param message 消息
+ * @param message 消息
* @param productKey 产品 Key
* @param deviceName 设备名称
- * @param serverId 设备连接的 serverId
+ * @param serverId 设备连接的 serverId
*/
void sendDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName, String serverId);
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
index 6f1f731d29..014da9a5df 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
@@ -61,6 +61,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
return codec.encode(message);
}
+ @Override
+ public byte[] encodeDeviceMessage(IotDeviceMessage message,
+ String codecType) {
+ // 1. 获取编解码器
+ IotDeviceMessageCodec codec = codes.get(codecType);
+ if (codec == null) {
+ throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
+ }
+
+ // 2. 编码消息
+ return codec.encode(message);
+ }
+
@Override
public IotDeviceMessage decodeDeviceMessage(byte[] bytes,
String productKey, String deviceName) {
@@ -79,6 +92,18 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
return codec.decode(bytes);
}
+ @Override
+ public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
+ // 1. 获取编解码器
+ IotDeviceMessageCodec codec = codes.get(codecType);
+ if (codec == null) {
+ throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
+ }
+
+ // 2. 解码消息
+ return codec.decode(bytes);
+ }
+
@Override
public void sendDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName, String serverId) {
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
index 26376b6669..b306f0588c 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -4,6 +4,15 @@ spring:
profiles:
active: local # 默认激活本地开发环境
+ # Redis 配置
+ data:
+ redis:
+ host: 127.0.0.1 # Redis 服务器地址
+ port: 6379 # Redis 服务器端口
+ database: 0 # Redis 数据库索引
+ # password: # Redis 密码,如果有的话
+ timeout: 30000ms # 连接超时时间
+
--- #################### 消息队列相关 ####################
# rocketmq 配置项,对应 RocketMQProperties 配置类
@@ -45,7 +54,7 @@ yudao:
# 针对引入的 EMQX 组件的配置
# ====================================
emqx:
- enabled: false
+ enabled: true
http-port: 8090 # MQTT HTTP 服务端口
mqtt-host: 127.0.0.1 # MQTT Broker 地址
mqtt-port: 1883 # MQTT Broker 端口
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md
new file mode 100644
index 0000000000..d85d347f70
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md
@@ -0,0 +1,198 @@
+# TCP 二进制协议数据包格式说明
+
+## 1. 协议概述
+
+TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。
+
+### 1.1 协议特点
+
+- **高效传输**:完全二进制格式,减少数据传输量
+- **版本控制**:内置协议版本号,支持协议升级
+- **类型安全**:明确的消息类型标识
+- **扩展性**:预留标志位,支持未来功能扩展
+- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容
+
+## 2. 协议格式
+
+### 2.1 整体结构
+
+```
++--------+--------+--------+--------+--------+--------+--------+--------+
+| 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4字节) |
++--------+--------+--------+--------+--------+--------+--------+--------+
+| 消息 ID 长度(2字节) | 消息 ID (变长字符串) |
++--------+--------+--------+--------+--------+--------+--------+--------+
+| 方法名长度(2字节) | 方法名(变长字符串) |
++--------+--------+--------+--------+--------+--------+--------+--------+
+| 消息体数据(变长) |
++--------+--------+--------+--------+--------+--------+--------+--------+
+```
+
+### 2.2 字段详细说明
+
+| 字段 | 长度 | 类型 | 说明 |
+|------|------|------|------|
+| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 |
+| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 |
+| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 |
+| 消息标志 | 1字节 | byte | 预留字段,用于未来扩展 |
+| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) |
+| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 |
+| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) |
+| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 |
+| 方法名 | 变长 | string | 消息方法名(UTF-8编码) |
+| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 |
+
+**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
+
+### 2.3 协议常量定义
+
+```java
+// 协议标识
+private static final byte MAGIC_NUMBER = (byte) 0x7E;
+private static final byte PROTOCOL_VERSION = (byte) 0x01;
+
+// 消息类型
+public static class MessageType {
+ public static final byte REQUEST = 0x01; // 请求消息
+ public static final byte RESPONSE = 0x02; // 响应消息
+}
+
+// 协议长度
+private static final int HEADER_FIXED_LENGTH = 8; // 固定头部长度
+private static final int MIN_MESSAGE_LENGTH = 12; // 最小消息长度
+```
+
+## 3. 消息类型和格式
+
+### 3.1 请求消息 (REQUEST - 0x01)
+
+请求消息用于设备向服务器发送数据或请求。
+
+#### 3.1.1 消息体格式
+```
+消息体 = params 数据(JSON格式)
+```
+
+#### 3.1.2 示例:设备认证请求
+
+**消息内容:**
+- 消息 ID: `auth_1704067200000_123`
+- 方法名: `auth`
+- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}`
+
+**二进制数据包结构:**
+```
+7E // 魔术字 (0x7E)
+01 // 版本号 (0x01)
+01 // 消息类型 (REQUEST)
+00 // 消息标志 (预留)
+00 00 00 8A // 消息长度 (138字节)
+00 19 // 消息 ID 长度 (25字节)
+61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123"
+36 37 32 30 30 30 30 30 5F 31
+32 33
+00 04 // 方法名长度 (4字节)
+61 75 74 68 // 方法名: "auth"
+7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据
+22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001",
+30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName",
+6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"}
+64 75 63 74 4B 65 79 5F 64 65
+76 69 63 65 4E 61 6D 65 22 2C
+22 70 61 73 73 77 6F 72 64 22
+3A 22 64 65 76 69 63 65 5F 70
+61 73 73 77 6F 72 64 22 7D
+```
+
+#### 3.1.3 示例:属性数据上报
+
+**消息内容:**
+- 消息 ID: `property_1704067200000_456`
+- 方法名: `thing.property.post`
+- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}`
+
+### 3.2 响应消息 (RESPONSE - 0x02)
+
+响应消息用于服务器向设备回复请求结果。
+
+#### 3.2.1 消息体格式
+```
+消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON)
+```
+
+#### 3.2.2 字段说明
+
+| 字段 | 长度 | 类型 | 说明 |
+|------|------|------|------|
+| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 |
+| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 |
+| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) |
+| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) |
+
+#### 3.2.3 示例:认证成功响应
+
+**消息内容:**
+- 消息 ID: `auth_response_1704067200000_123`
+- 方法名: `auth`
+- 响应码: `0`
+- 响应消息: `认证成功`
+- 响应数据: `{"success":true,"message":"认证成功"}`
+
+**二进制数据包结构:**
+```
+7E // 魔术字 (0x7E)
+01 // 版本号 (0x01)
+02 // 消息类型 (RESPONSE)
+00 // 消息标志 (预留)
+00 00 00 A5 // 消息长度 (165字节)
+00 22 // 消息 ID 长度 (34字节)
+61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123"
+6E 73 65 5F 31 37 30 34 30 36
+37 32 30 30 30 30 30 5F 31 32
+33
+00 04 // 方法名长度 (4字节)
+61 75 74 68 // 方法名: "auth"
+00 00 00 00 // 响应码 (0 = 成功)
+00 0C // 响应消息长度 (12字节)
+E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8)
+8A 9F
+7B 22 73 75 63 63 65 73 73 22 // JSON响应数据
+3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"}
+73 61 67 65 22 3A 22 E8 AE A4
+E8 AF 81 E6 88 90 E5 8A 9F 22
+7D
+```
+
+## 4. 编解码器标识
+
+```java
+public static final String TYPE = "TCP_BINARY";
+```
+
+## 5. 协议优势
+
+- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量
+- **解析高效**:直接二进制操作,减少字符串转换开销
+- **类型安全**:明确的消息类型和字段定义
+- **扩展性强**:预留标志位支持未来功能扩展
+- **版本控制**:内置版本号支持协议升级
+
+## 6. 与 JSON 协议对比
+
+| 特性 | 二进制协议 | JSON协议 |
+|------|------------|----------|
+| 数据大小 | 小(节省30-50%) | 大 |
+| 解析性能 | 高 | 中等 |
+| 网络开销 | 低 | 高 |
+| 可读性 | 差 | 优秀 |
+| 调试难度 | 高 | 低 |
+| 扩展性 | 良好(有预留位) | 优秀 |
+
+**推荐场景**:
+- ✅ **高频数据传输**:传感器数据实时上报
+- ✅ **带宽受限环境**:移动网络、卫星通信
+- ✅ **性能要求高**:需要低延迟、高吞吐的场景
+- ✅ **设备资源有限**:嵌入式设备、低功耗设备
+- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议
+- ❌ **快速原型开发**:开发效率低
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md
new file mode 100644
index 0000000000..09ef50cfe5
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md
@@ -0,0 +1,191 @@
+# TCP JSON 格式协议说明
+
+## 1. 协议概述
+
+TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点:
+
+- **标准化**:使用标准 JSON 格式,易于解析和处理
+- **可读性**:人类可读,便于调试和维护
+- **扩展性**:可以轻松添加新字段,向后兼容
+- **跨平台**:JSON 格式支持所有主流编程语言
+- **安全优化**:移除冗余的 deviceId 字段,提高安全性
+
+## 2. 消息格式
+
+### 2.1 基础消息结构
+
+```json
+{
+ "id": "消息唯一标识",
+ "method": "消息方法",
+ "params": {
+ // 请求参数
+ },
+ "data": {
+ // 响应数据
+ },
+ "code": 响应码,
+ "msg": "响应消息",
+ "timestamp": 时间戳
+}
+```
+
+**⚠️ 重要说明**:
+- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID
+- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息
+
+### 2.2 字段详细说明
+
+| 字段名 | 类型 | 必填 | 用途 | 说明 |
+|--------|------|------|------|------|
+| id | String | 是 | 所有消息 | 消息唯一标识 |
+| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` |
+| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 |
+| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 |
+| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 |
+| msg | String | 否 | 响应消息 | 响应提示信息 |
+| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 |
+
+### 2.3 消息分类
+
+#### 2.3.1 请求消息(上行)
+- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段
+- **方向**:设备 → 服务器
+- **用途**:设备认证、数据上报、状态更新等
+
+#### 2.3.2 响应消息(下行)
+- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段
+- **方向**:服务器 → 设备
+- **用途**:认证结果、指令响应、错误提示等
+
+## 3. 消息示例
+
+### 3.1 设备认证 (auth)
+
+#### 认证请求格式
+**消息方向**:设备 → 服务器
+
+```json
+{
+ "id": "auth_1704067200000_123",
+ "method": "auth",
+ "params": {
+ "clientId": "device_001",
+ "username": "productKey_deviceName",
+ "password": "设备密码"
+ },
+ "timestamp": 1704067200000
+}
+```
+
+**认证参数说明:**
+
+| 字段名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| clientId | String | 是 | 客户端唯一标识,用于连接管理 |
+| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` |
+| password | String | 是 | 设备密码,在设备管理平台配置 |
+
+#### 认证响应格式
+**消息方向**:服务器 → 设备
+
+**认证成功响应:**
+```json
+{
+ "id": "response_auth_1704067200000_123",
+ "method": "auth",
+ "data": {
+ "success": true,
+ "message": "认证成功"
+ },
+ "code": 0,
+ "msg": "认证成功",
+ "timestamp": 1704067200001
+}
+```
+
+**认证失败响应:**
+```json
+{
+ "id": "response_auth_1704067200000_123",
+ "method": "auth",
+ "data": {
+ "success": false,
+ "message": "认证失败:用户名或密码错误"
+ },
+ "code": 401,
+ "msg": "认证失败",
+ "timestamp": 1704067200001
+}
+```
+
+### 3.2 属性数据上报 (thing.property.post)
+
+**消息方向**:设备 → 服务器
+
+**示例:温度传感器数据上报**
+```json
+{
+ "id": "property_1704067200000_456",
+ "method": "thing.property.post",
+ "params": {
+ "temperature": 25.5,
+ "humidity": 60.2,
+ "pressure": 1013.25,
+ "battery": 85,
+ "signal_strength": -65
+ },
+ "timestamp": 1704067200000
+}
+```
+
+### 3.3 设备状态更新 (thing.state.update)
+
+**消息方向**:设备 → 服务器
+
+**示例:心跳请求**
+```json
+{
+ "id": "heartbeat_1704067200000_321",
+ "method": "thing.state.update",
+ "params": {
+ "state": "online",
+ "uptime": 86400,
+ "memory_usage": 65.2,
+ "cpu_usage": 12.8
+ },
+ "timestamp": 1704067200000
+}
+```
+
+## 4. 编解码器标识
+
+```java
+public static final String TYPE = "TCP_JSON";
+```
+
+## 5. 协议优势
+
+- **开发效率高**:JSON 格式,开发和调试简单
+- **跨语言支持**:所有主流语言都支持 JSON
+- **可读性优秀**:可以直接查看消息内容
+- **扩展性强**:可以轻松添加新字段
+- **安全性高**:移除 deviceId 字段,防止伪造攻击
+
+## 6. 与二进制协议对比
+
+| 特性 | JSON协议 | 二进制协议 |
+|------|----------|------------|
+| 开发难度 | 低 | 高 |
+| 调试难度 | 低 | 高 |
+| 可读性 | 优秀 | 差 |
+| 数据大小 | 中等 | 小(节省30-50%) |
+| 解析性能 | 中等 | 高 |
+| 学习成本 | 低 | 高 |
+
+**推荐场景**:
+- ✅ **开发调试阶段**:调试友好,开发效率高
+- ✅ **快速原型开发**:实现简单,快速迭代
+- ✅ **多语言集成**:广泛的语言支持
+- ❌ **高频数据传输**:建议使用二进制协议
+- ❌ **带宽受限环境**:建议使用二进制协议
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
index df8ae8c5bd..56ebc283fe 100644
--- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
+++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.product.controller.admin.history.vo;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
index fbc75522a7..d962fe7469 100755
--- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
+++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
@@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
import cn.iocoder.yudao.module.product.enums.DictTypeConstants;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java
index 248160dd9c..059d6af579 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java
@@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
-import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 客服消息 Response VO")
@Data
@@ -42,4 +41,4 @@ public class KeFuMessageRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java
index d81b3d6902..14f1ee4ccd 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java
@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -69,4 +69,4 @@ public class PointActivityRespVO {
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
private Integer price;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java
index 8e8250b387..638ea61fb8 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java
@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -36,4 +36,4 @@ public class PointProductRespVO {
@Schema(description = "积分商城商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer activityStatus;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java
index ddcc23b9cb..83e00c202a 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java
@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.promotion.controller.app.point.vo;
-import com.alibaba.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java
index 91216bf184..59ee95fb12 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java
@@ -31,9 +31,13 @@ import java.util.List;
public class CouponTemplateDO extends BaseDO {
/**
- * 不限制领取数量
+ * 领取数量 - 不限制
*/
- public static final Integer TIME_LIMIT_COUNT_MAX = -1;
+ public static final Integer TAKE_LIMIT_COUNT_MAX = -1;
+ /**
+ * 发放数量 - 不限制
+ */
+ public static final Integer TOTAL_COUNT_MAX = -1;
// ========== 基本信息 BEGIN ==========
/**
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
index 3096a49f3c..84e98f3dbd 100755
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
@@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX {
.orderByDesc(CouponTemplateDO::getId));
}
- default void updateTakeCount(Long id, Integer incrCount) {
- update(null, new LambdaUpdateWrapper()
+ default int updateTakeCount(Long id, Integer incrCount) {
+ LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper()
.eq(CouponTemplateDO::getId, id)
- .setSql("take_count = take_count + " + incrCount));
+ .setSql("take_count = take_count + " + incrCount);
+ // 增加已领取的数量(incrCount 为正数),需要考虑发放数量 totalCount 的限制
+ if (incrCount > 0) {
+ updateWrapper.and(i -> i.apply("take_count < total_count")
+ .or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX));
+ }
+ return update(updateWrapper);
}
default List selectListByTakeType(Integer takeType) {
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
index 8e168f4f9f..f5b38a13cc 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
@@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId));
}
// 查找是否有其它活动,选择了该产品
- List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId));
+ List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId));
if (CollUtil.isNotEmpty(matchActivityList)) {
throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS);
}
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
index e6f82a69fc..e175807503 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
@@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService {
// 4. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size());
-
return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
}
@@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService {
}
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
- && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
+ && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
&& couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
}
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
index 175e33b197..bdd8b32825 100755
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
@@ -22,8 +22,7 @@ import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL;
+import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
/**
* 优惠劵模板 Service 实现类
@@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
- && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
+ && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
}
@@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Override
public void updateCouponTemplateTakeCount(Long id, int incrCount) {
- couponTemplateMapper.updateTakeCount(id, incrCount);
+ int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount);
+ if (updateCount == 0) {
+ throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
+ }
}
@Override
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java
index 582cadb0d4..0b945794ad 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java
@@ -222,6 +222,7 @@ public class PointActivityServiceImpl implements PointActivityService {
if (spu == null) {
throw exception(SPU_NOT_EXISTS);
}
+ products.forEach(product -> product.setSpuId(spuId));
// 2. 校验商品 sku 都存在
List skus = productSkuApi.getSkuListBySpuId(singletonList(spuId));
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
index dcc5596f25..3c84a172ab 100644
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
@@ -303,7 +303,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR);
}
SeckillConfigDO config = seckillConfigService.getCurrentSeckillConfig();
- if (config == null || !CollectionUtil.contains(activity.getConfigIds(), config.getId())) {
+ if (config == null
+ || !CollectionUtil.contains(activity.getConfigIds(), config.getId())
+ || !LocalDateTimeUtils.isBetween(config.getStartTime(), config.getEndTime())) {
throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR);
}
// 1.3 超过单次购买限制
diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java
index 9d93142e31..86126ca705 100644
--- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java
+++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java
@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -78,4 +78,4 @@ public class ProductStatisticsRespVO {
@Schema(description = "访客支付转化率(百分比)", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
private Integer browseConvertPercent;
-}
\ No newline at end of file
+}
diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java
index 5b14fa1d4e..9e27c6f3a1 100644
--- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java
+++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java
@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.statistics.controller.admin.trade.vo;
import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
-import com.alibaba.excel.annotation.ExcelProperty;
-import com.alibaba.excel.annotation.format.DateTimeFormat;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data;
import java.time.LocalDate;
diff --git a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
index 07d2f0d580..e47c85e9b4 100644
--- a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
+++ b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
@@ -76,8 +76,8 @@