!53 修复websocket客户端消息解析问题。

Merge pull request !53 from xiaobin/master
This commit is contained in:
奈科斯(NexIoT)物联网平台
2026-01-18 10:08:42 +00:00
committed by Gitee
24 changed files with 2406 additions and 415 deletions
@@ -12,6 +12,11 @@
package cn.universal.dm.device.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -39,10 +44,7 @@ import cn.universal.persistence.entity.IoTDeviceSubscribe;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.query.IoTDeviceQuery;
import jakarta.annotation.Resource;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* IoT设备服务抽象基类
@@ -266,13 +268,20 @@ public abstract class AbstratIoTService extends IoTDownAdapter {
}
// 编解码覆盖原始消息
messageProAndData(jsonObject, codec);
// 【关键修复】确保messageType总是被设置,无论是PROPERTIES还是EVENT
if (codec.getMessageType() != null) {
builder.messageType(codec.getMessageType());
} else {
builder.messageType(MessageType.PROPERTIES); // 默认为属性消息
}
if (MessageType.EVENT.equals(codec.getMessageType())) {
String event = codec.getEvent();
if (StrUtil.isBlank(event)) {
event = jsonObject.getStr("event");
}
builder.event(event);
builder.messageType(codec.getMessageType());
if ("offline".equals(event)) {
ioTDeviceActionAfterService.offline(
ioTDeviceDTO.getProductKey(), ioTDeviceDTO.getDeviceId());
@@ -12,13 +12,15 @@
package cn.universal.dm.device.service.push;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.persistence.dto.IoTDeviceDTO;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.persistence.dto.IoTDeviceDTO;
import lombok.extern.slf4j.Slf4j;
/**
* 规则引擎处理器 - 消息过滤和规则检查
*
@@ -73,22 +75,26 @@ public class RuleEngineProcessor implements UPProcessor<BaseUPRequest> {
// 规则1:设备必须存在且有效
if (deviceDTO == null) {
log.debug("[规则引擎] 设备不存在过滤消息: {}", request.getIotId());
log.warn("[规则引擎] 【规则1-设备不存在过滤消息IotId: {}", request.getIotId());
return false;
}
// 规则2:应用必须启用
if (deviceDTO.isAppDisable()) {
log.debug("[规则引擎] 应用已禁用过滤消息: {}", request.getIotId());
log.warn("[规则引擎] 【规则2-应用已禁用过滤消息IotId: {}, AppId: {}",
request.getIotId(), deviceDTO.getApplicationId());
return false;
}
// 规则3:设备必须在线
if (deviceDTO.getState() == null || !deviceDTO.getState()) {
log.debug("[规则引擎] 设备离线过滤消息: {}", request.getIotId());
log.warn("[规则引擎] 【规则3-设备离线过滤消息IotId: {}, DeviceState: {}",
request.getIotId(), deviceDTO.getState());
return false;
}
log.debug("[规则引擎] ✅ 消息通过所有规则检查,IotId: {}, AppId: {}, DeviceState: {}",
request.getIotId(), deviceDTO.getApplicationId(), deviceDTO.getState());
return true;
}
}
@@ -31,6 +31,7 @@ import cn.universal.core.message.UPRequest;
import cn.universal.dm.device.service.AbstratIoTService;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.persistence.dto.IoTDeviceDTO;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.query.IoTDeviceQuery;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import lombok.extern.slf4j.Slf4j;
@@ -166,8 +167,13 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
// 改进:首先检查协议定义是否存在
// 如果不存在,尝试通过设备标识查询真实的productKey(对标MQTT的处理方式)
// 注意:即使协议定义不存在,也应该继续处理消息,支持无物模型定义场景的消息入库
String resolvedProductKey = productKey;
if (getProtocolDefinitionNoScript(productKey) == null && StrUtil.isNotBlank(deviceId)) {
Object protocolDef = getProtocolDefinitionNoScript(productKey);
// 对标MQTT改进:当协议定义为null时,也允许消息继续处理
// 这样可以支持物模型未定义但仍需要入库的场景
if (protocolDef == null && StrUtil.isNotBlank(deviceId)) {
log.debug("[{}] 协议定义未找到 productKey: {},尝试通过设备标识查询真实productKey - deviceId: {}",
getName(), productKey, deviceId);
resolvedProductKey = resolveProductKeyByDevice(deviceId, productKey);
@@ -176,6 +182,9 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
request.setProductKey(resolvedProductKey);
productKey = resolvedProductKey;
}
} else if (protocolDef == null) {
log.info("[{}] 产品物模型定义为空 productKey: {},但将继续处理消息以支持无物模型定义场景",
getName(), productKey);
}
// 调用产品编解码器(使用UPRequest基类)
@@ -251,12 +260,19 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
/**
* 转换编解码器结果 - 对标MQTT实现,处理设备存在和不存在两种情况
*
* 修复:即使设备不存在(deviceDTO=null),也应生成BaseUPRequest,让后续处理器有机会处理
* 这样可以支持自动注册流程中的消息处理
* 关键修复:正确区分iotId和deviceId
* - deviceId: 设备标识符(如 "M902L25A00001"
* - iotId: 设备唯一ID = productKey + deviceId(如 "aaguUu9Sp4poM902L25A00001"
*
* 修复逻辑:
* 1. 多源提取deviceIdcodecResult → request → 从iotId中提取
* 2. 构建正确的iotId = productKey + deviceId
* 3. 即使设备不存在,也应生成BaseUPRequest供后续处理器使用
*/
private BaseUPRequest convertCodecResult(WebSocketUPRequest request, UPRequest codecResult) {
try {
IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO();
String productKey = request.getProductKey();
// 使用编解码器返回的payload(包含转换后的properties/data
String payloadForParsing = codecResult.getPayload() != null
@@ -264,11 +280,12 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
: request.getPayload();
JSONObject messageJson = parseJsonPayload(payloadForParsing);
// 诊断日志
log.debug("[{}] 转换编解码结果 - messageType: {}, codecResult.properties: {}, messageJson中是否有properties: {}, deviceDTO: {}",
getName(), codecResult.getMessageType(),
codecResult.getProperties() != null ? codecResult.getProperties().size() + "字段" : "null",
messageJson != null && messageJson.containsKey("properties") ? "" : "",
// 【关键修复】正确解析和构建deviceId与iotId
String resolvedDeviceId = resolveDeviceId(request, codecResult);
String resolvedIotId = resolveIotId(productKey, resolvedDeviceId, request.getIotId());
log.debug("[{}] 字段解析结果 - productKey: {}, resolvedDeviceId: {}, resolvedIotId: {}, deviceDTO: {}",
getName(), productKey, resolvedDeviceId, resolvedIotId,
deviceDTO != null ? "已填充" : "null");
BaseUPRequest upRequest;
@@ -276,19 +293,40 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
if (deviceDTO != null) {
// 设备存在:使用标准的buildCodecNotNullBean方法
BaseUPRequest.BaseUPRequestBuilder<?, ?> builder = BaseUPRequest.builder()
.iotId(StrUtil.isNotBlank(codecResult.getDeviceId()) ? codecResult.getDeviceId() : request.getIotId())
.productKey(request.getProductKey())
.deviceName(request.getDeviceName());
.iotId(resolvedIotId) // 使用正确解析的iotId
.deviceId(resolvedDeviceId) // 使用正确解析的deviceId
.productKey(productKey)
.deviceName(request.getDeviceName())
.ioTDeviceDTO(deviceDTO); // 关键:设置设备信息
// 也设置产品信息,以便消息能被正确入库
IoTProduct ioTProduct = request.getIoTProduct();
if (ioTProduct != null) {
builder.ioTProduct(ioTProduct);
}
// 【关键修复】确保messageType被设置,即使codecResult中没有定义
if (codecResult.getMessageType() != null) {
builder.messageType(codecResult.getMessageType());
} else {
builder.messageType(IoTConstant.MessageType.PROPERTIES); // 默认为属性消息
}
buildCodecNotNullBean(messageJson, deviceDTO, codecResult, builder);
upRequest = builder.build();
log.debug("[{}] 编解码结果转换成功(设备存在)- iotId: {}, deviceId: {}, messageType: {}",
getName(), resolvedIotId, resolvedDeviceId, upRequest.getMessageType());
} else {
// 设备不存在:构建简化的BaseUPRequest,不调用会访问deviceDTO的方法
log.warn("[{}] 设备信息未填充,将生成不含设备详情的BaseUPRequest。productKey={}, iotId={}",
getName(), request.getProductKey(), request.getIotId());
log.warn("[{}] 设备信息未填充,将生成不含设备详情的BaseUPRequest。productKey: {}, deviceId: {}, iotId: {}",
getName(), productKey, resolvedDeviceId, resolvedIotId);
BaseUPRequest.BaseUPRequestBuilder<?, ?> builder = BaseUPRequest.builder()
.iotId(StrUtil.isNotBlank(codecResult.getDeviceId()) ? codecResult.getDeviceId() : request.getIotId())
.productKey(request.getProductKey())
.iotId(resolvedIotId) // 使用正确解析的iotId
.deviceId(resolvedDeviceId) // 使用正确解析的deviceId
.productKey(productKey)
.deviceName(request.getDeviceName())
.messageType(codecResult.getMessageType() != null ? codecResult.getMessageType() : IoTConstant.MessageType.PROPERTIES);
@@ -313,12 +351,11 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
}
upRequest = builder.build();
log.debug("[{}] 编解码结果转换成功(设备不存在)- iotId: {}, deviceId: {}, messageType: {}",
getName(), resolvedIotId, resolvedDeviceId, upRequest.getMessageType());
}
log.debug("[{}] 编解码器结果转换成功 - messageType: {}, data: {}, properties: {}",
getName(), upRequest.getMessageType(),
upRequest.getData() != null ? upRequest.getData().size() + "字段" : "null",
upRequest.getProperties() != null ? upRequest.getProperties().size() + "字段" : "null");
return upRequest;
} catch (Exception e) {
@@ -327,6 +364,96 @@ public class WebSocketCodecProcessor extends AbstratIoTService implements WebSoc
}
}
/**
* 智能解析deviceId - 多源提取,优先级顺序
*
* 优先级:
* 1. codecResult.getDeviceId() - 编解码器返回的deviceId(最可信)
* 2. request.getDeviceId() - 请求中的deviceId
* 3. 从request.getIotId()中提取 - 如果iotId中包含productKey前缀则提取后缀
* 4. 从messageJson中解析 - 如果payload包含deviceId字段
*/
private String resolveDeviceId(WebSocketUPRequest request, UPRequest codecResult) {
String productKey = request.getProductKey();
// 优先级1:使用编解码器返回的deviceId
if (StrUtil.isNotBlank(codecResult.getDeviceId())) {
log.debug("[{}] deviceId来源:编解码器返回值 = {}", getName(), codecResult.getDeviceId());
return codecResult.getDeviceId();
}
// 优先级2:使用请求中的deviceId
if (StrUtil.isNotBlank(request.getDeviceId())) {
log.debug("[{}] deviceId来源:WebSocket请求字段 = {}", getName(), request.getDeviceId());
return request.getDeviceId();
}
// 优先级3:从iotId中提取(处理WebSocket错误将deviceId存入iotId的情况)
String iotId = request.getIotId();
if (StrUtil.isNotBlank(iotId) && StrUtil.isNotBlank(productKey)) {
// 检查iotId是否包含productKey前缀
if (iotId.startsWith(productKey)) {
String extractedDeviceId = iotId.substring(productKey.length());
if (StrUtil.isNotBlank(extractedDeviceId)) {
log.debug("[{}] deviceId来源:从iotId中提取 (iotId中包含productKey) = {}",
getName(), extractedDeviceId);
return extractedDeviceId;
}
}
// 如果iotId不包含productKey前缀,可能iotId本身就是deviceId(错误情况)
// 在这种情况下直接返回iotId作为deviceId
if (iotId.length() < 50) { // deviceId通常较短
log.debug("[{}] deviceId来源:iotId不包含productKey前缀,直接使用iotId = {}",
getName(), iotId);
return iotId;
}
}
// 优先级4:返回null表示无法解析
log.warn("[{}] 无法解析deviceId - codecResult.deviceId: {}, request.deviceId: {}, request.iotId: {}",
getName(),
codecResult.getDeviceId() != null ? codecResult.getDeviceId() : "null",
request.getDeviceId() != null ? request.getDeviceId() : "null",
iotId != null ? iotId : "null");
return null;
}
/**
* 智能构建正确的iotId
*
* iotId应该是 productKey + deviceId 的组合
*
* 逻辑:
* 1. 如果已有正确的iotId且格式正确,直接返回
* 2. 否则从productKey和deviceId构建
* 3. 如果productKey不存在,返回deviceId作为fallback
*/
private String resolveIotId(String productKey, String deviceId, String existingIotId) {
// 如果productKey和deviceId都存在,构建完整的iotId
if (StrUtil.isNotBlank(productKey) && StrUtil.isNotBlank(deviceId)) {
String constructedIotId = productKey + deviceId;
log.debug("[{}] iotId构建:productKey({}) + deviceId({}) = {}",
getName(), productKey, deviceId, constructedIotId);
return constructedIotId;
}
// 如果deviceId为空但existingIotId存在,返回existingIotId
if (StrUtil.isNotBlank(existingIotId)) {
log.debug("[{}] iotId使用存在值:{}", getName(), existingIotId);
return existingIotId;
}
// 最后手段:返回deviceId(即使deviceId应该不同)
if (StrUtil.isNotBlank(deviceId)) {
log.warn("[{}] productKey缺失,使用deviceId作为iotId{}", getName(), deviceId);
return deviceId;
}
log.error("[{}] 无法构建有效的iotId - productKey: {}, deviceId: {}, existingIotId: {}",
getName(), productKey, deviceId, existingIotId);
return existingIotId; // 返回原值作为最后fallback
}
/**
* 尝试Base64解码
*/
@@ -1,125 +0,0 @@
/*
*
* Copyright (c) 2026, NexIoT. All Rights Reserved.
*
* @Description: 本文件由 gitee.com/NexIoT 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
* @Author: gitee.com/NexIoT
* @Email: wo8335224@gmail.com
* @Wechat: outlookFil
*
*
*/
package cn.universal.websocket.protocol.processor.up;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.universal.dm.device.service.action.IoTDeviceActionAfterService;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket 设备状态处理器 - 处理设备上线、离线等状态变化
*
* 对标 MQTT 中的 IoTDeviceActionAfterService 设备生命周期管理
* 职责:将接收到的消息转化为设备上线/离线事件,并执行相关业务逻辑
*
* @author gitee.com/NexIoT
* @version 1.0
* @since 2026/1/16
*/
@Slf4j(topic = "websocket")
@Component
public class WebSocketDeviceStatusProcessor implements WebSocketUPProcessor {
@Autowired
private IoTDeviceActionAfterService deviceActionAfterService;
@Override
public String getName() {
return "WebSocket设备状态处理器";
}
@Override
public String getDescription() {
return "处理WebSocket设备的上线、离线等状态变化";
}
@Override
public int getOrder() {
return 600; // 在编解码后执行
}
@Override
public int getPriority() {
return 10;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean preCheck(WebSocketUPRequest request) {
return request != null && request.getUpRequestList() != null && !request.getUpRequestList().isEmpty();
}
@Override
public boolean supports(WebSocketUPRequest request) {
// 只要有 BaseUPRequest,就支持处理
return request.getUpRequestList() != null && request.getUpRequestList().size() > 0;
}
@Override
public ProcessorResult process(WebSocketUPRequest request) {
try {
log.debug("[{}] 开始处理设备状态,SessionID: {}", getName(), request.getSessionId());
List<BaseUPRequest> upRequestList = request.getUpRequestList();
if (upRequestList == null || upRequestList.isEmpty()) {
return ProcessorResult.CONTINUE;
}
// 对标 MQTT 的设备生命周期管理
// 根据消息类型判断是否需要处理设备上线状态
for (BaseUPRequest upRequest : upRequestList) {
handleDeviceStatus(request, upRequest);
}
log.debug("[{}] 设备状态处理完成,SessionID: {}", getName(), request.getSessionId());
return ProcessorResult.CONTINUE;
} catch (Exception e) {
log.error("[{}] 设备状态处理异常,SessionID: {}", getName(), request.getSessionId(), e);
return ProcessorResult.CONTINUE; // 状态处理失败不影响消息流程
}
}
/**
* 处理设备状态变化
*/
private void handleDeviceStatus(WebSocketUPRequest request, BaseUPRequest upRequest) {
// 此处可扩展为处理更复杂的状态逻辑
// 当前实现简化版:主要由后续的规则引擎和数据处理器处理
log.debug("[{}] 处理设备状态 - iotId: {}, messageType: {}",
getName(), upRequest.getIotId(), upRequest.getMessageType());
}
@Override
public void postProcess(WebSocketUPRequest request, ProcessorResult result) {
if (result == ProcessorResult.CONTINUE) {
log.debug("[{}] 设备状态处理后置处理完成,SessionID: {}", getName(), request.getSessionId());
}
}
@Override
public void onError(WebSocketUPRequest request, Exception e) {
log.error("[{}] 设备状态处理异常,SessionID: {}", getName(), request.getSessionId(), e);
}
}
@@ -1,122 +0,0 @@
/*
*
* Copyright (c) 2026, NexIoT. All Rights Reserved.
*
* @Description: 本文件由 gitee.com/NexIoT 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
* @Author: gitee.com/NexIoT
* @Email: wo8335224@gmail.com
* @Wechat: outlookFil
*
*
*/
package cn.universal.websocket.protocol.processor.up;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.universal.dm.device.service.push.RuleEngineProcessor;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket 规则引擎处理器 - 对标 MQTT 的规则引擎处理
*
* 职责:对消息进行规则过滤,判断是否满足配置的规则条件
* 与 MQTT 复用同一个规则引擎实现(RuleEngineProcessor
*
* @author gitee.com/NexIoT
* @version 1.0
* @since 2026/1/16
*/
@Slf4j(topic = "websocket")
@Component
public class WebSocketRuleEngineProcessor implements WebSocketUPProcessor {
@Autowired(required = false)
private RuleEngineProcessor ruleEngineProcessor;
@Override
public String getName() {
return "WebSocket规则引擎处理器";
}
@Override
public String getDescription() {
return "根据规则引擎过滤和处理WebSocket消息";
}
@Override
public int getOrder() {
return 700; // 在设备状态处理后执行
}
@Override
public int getPriority() {
return 10;
}
@Override
public boolean isEnabled() {
return ruleEngineProcessor != null;
}
@Override
public boolean preCheck(WebSocketUPRequest request) {
return request != null && request.getUpRequestList() != null && !request.getUpRequestList().isEmpty();
}
@Override
public boolean supports(WebSocketUPRequest request) {
// 规则引擎处理器支持所有有 BaseUPRequest 的消息
return request.getUpRequestList() != null && !request.getUpRequestList().isEmpty();
}
@Override
public ProcessorResult process(WebSocketUPRequest request) {
try {
if (ruleEngineProcessor == null) {
log.warn("[{}] 规则引擎处理器未初始化,跳过处理", getName());
return ProcessorResult.CONTINUE;
}
log.debug("[{}] 开始规则引擎处理,SessionID: {}", getName(), request.getSessionId());
List<BaseUPRequest> upRequestList = request.getUpRequestList();
if (upRequestList == null || upRequestList.isEmpty()) {
return ProcessorResult.CONTINUE;
}
// 调用规则引擎处理器过滤消息
// 注:此处简化实现,实际可能需要根据规则引擎的返回结果过滤消息列表
for (BaseUPRequest upRequest : upRequestList) {
// 规则引擎会在内部判断消息是否符合规则
// 并输出相应的日志(过滤掉不符合规则的消息)
log.debug("[{}] 规则引擎检查消息 - iotId: {}, messageType: {}",
getName(), upRequest.getIotId(), upRequest.getMessageType());
}
log.debug("[{}] 规则引擎处理完成,SessionID: {}", getName(), request.getSessionId());
return ProcessorResult.CONTINUE;
} catch (Exception e) {
log.error("[{}] 规则引擎处理异常,SessionID: {}", getName(), request.getSessionId(), e);
return ProcessorResult.CONTINUE; // 规则引擎失败不影响消息流程
}
}
@Override
public void postProcess(WebSocketUPRequest request, ProcessorResult result) {
if (result == ProcessorResult.CONTINUE) {
log.debug("[{}] 规则引擎处理后置处理完成,SessionID: {}", getName(), request.getSessionId());
}
}
@Override
public void onError(WebSocketUPRequest request, Exception e) {
log.error("[{}] 规则引擎处理异常,SessionID: {}", getName(), request.getSessionId(), e);
}
}
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
@@ -20,6 +20,7 @@ import org.springframework.stereotype.Component;
import cn.universal.dm.device.service.processor.DeviceCachePostProcessor;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import java.time.LocalDateTime;
@@ -19,6 +19,7 @@ import org.springframework.stereotype.Component;
import cn.universal.websocket.protocol.entity.WebSocketSession;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import cn.universal.websocket.protocol.service.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
@@ -0,0 +1,155 @@
/*
*
* Copyright (c) 2026, NexIoT. All Rights Reserved.
*
* @Description: 本文件由 gitee.com/NexIoT 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
* @Author: gitee.com/NexIoT
* @Email: wo8335224@gmail.com
* @Wechat: outlookFil
*
*
*/
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.hutool.core.collection.CollUtil;
import cn.universal.dm.device.service.AbstratIoTService;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.persistence.dto.IoTDeviceDTO;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket 日志与影子处理器 - 对标 MQTT 的日志影子处理
*
* <p>职责:保存设备日志与更新设备影子,补齐 WebSocket 入库链路
*
* @author gitee.com/NexIoT
* @version 1.0
* @since 2026/1/17
*/
@Slf4j(topic = "websocket")
@Component
public class WebSocketLogShadowProcessor extends AbstratIoTService
implements WebSocketUPProcessor {
@Override
public String getName() {
return "WebSocket日志影子处理器";
}
@Override
public String getDescription() {
return "保存设备日志并更新设备影子";
}
@Override
public int getOrder() {
return 1100; // 在数据桥接(1000)之后、发布推送(2000)之前执行
}
@Override
public int getPriority() {
return 10;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean preCheck(WebSocketUPRequest request) {
return request != null
&& request.getUpRequestList() != null
&& !request.getUpRequestList().isEmpty();
}
@Override
public boolean supports(WebSocketUPRequest request) {
return request.getIoTDeviceDTO() != null
&& request.getIoTProduct() != null
&& CollUtil.isNotEmpty(request.getUpRequestList());
}
@Override
public ProcessorResult process(WebSocketUPRequest request) {
try {
IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO();
IoTProduct ioTProduct = request.getIoTProduct();
List<BaseUPRequest> upRequestList = request.getUpRequestList();
if (deviceDTO == null || ioTProduct == null || CollUtil.isEmpty(upRequestList)) {
return ProcessorResult.CONTINUE;
}
int logCount = 0;
int shadowCount = 0;
for (BaseUPRequest upRequest : upRequestList) {
if (upRequest == null || upRequest.isDebug()) {
continue;
}
try {
iIoTDeviceDataService.saveDeviceLog(upRequest, deviceDTO, ioTProduct);
logCount++;
} catch (Exception e) {
log.warn(
"[{}] 保存设备日志异常 - iotId: {}, messageType: {}, error: {}",
getName(),
upRequest.getIotId(),
upRequest.getMessageType(),
e.getMessage());
}
try {
iotDeviceShadowService.doShadow(upRequest, deviceDTO);
shadowCount++;
} catch (Exception e) {
log.warn(
"[{}] 更新设备影子异常 - iotId: {}, messageType: {}, error: {}",
getName(),
upRequest.getIotId(),
upRequest.getMessageType(),
e.getMessage());
}
}
request.setContextValue("logShadowProcessed", true);
request.setContextValue("logProcessedCount", logCount);
request.setContextValue("shadowUpdatedCount", shadowCount);
log.debug(
"[{}] 日志影子处理完成,日志: {}, 影子: {}, SessionID: {}",
getName(),
logCount,
shadowCount,
request.getSessionId());
return ProcessorResult.CONTINUE;
} catch (Exception e) {
log.error("[{}] 日志影子处理异常,SessionID: {}", getName(), request.getSessionId(), e);
return ProcessorResult.CONTINUE;
}
}
@Override
public void postProcess(WebSocketUPRequest request, ProcessorResult result) {
if (result == ProcessorResult.CONTINUE) {
log.debug("[{}] 日志影子处理后置完成,SessionID: {}", getName(), request.getSessionId());
}
}
@Override
public void onError(WebSocketUPRequest request, Exception e) {
log.error("[{}] 日志影子处理异常,SessionID: {}", getName(), request.getSessionId(), e);
}
}
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import org.springframework.stereotype.Component;
@@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -10,11 +10,12 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import org.springframework.stereotype.Component;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -10,7 +10,7 @@
*
*/
package cn.universal.websocket.protocol.processor.up;
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
@@ -20,6 +20,7 @@ import org.springframework.stereotype.Component;
import cn.universal.dm.device.service.push.PushStrategyManager;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -0,0 +1,172 @@
/*
*
* Copyright (c) 2026, NexIoT. All Rights Reserved.
*
* @Description: 本文件由 gitee.com/NexIoT 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
* @Author: gitee.com/NexIoT
* @Email: wo8335224@gmail.com
* @Wechat: outlookFil
*
*
*/
package cn.universal.websocket.protocol.processor.up.common;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.universal.dm.device.service.action.IoTDeviceActionAfterService;
import cn.universal.dm.device.service.push.RuleEngineProcessor;
import cn.universal.persistence.base.BaseUPRequest;
import cn.universal.websocket.protocol.entity.WebSocketUPRequest;
import cn.universal.websocket.protocol.processor.up.WebSocketUPProcessor;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket 规则引擎处理器 - 对标 MQTT 的规则引擎处理
*
* 职责:对消息进行规则过滤,判断是否满足配置的规则条件
* 与 MQTT 复用同一个规则引擎实现(RuleEngineProcessor
*
* @author gitee.com/NexIoT
* @version 1.0
* @since 2026/1/16
*/
@Slf4j(topic = "websocket")
@Component
public class WebSocketRuleEngineProcessor implements WebSocketUPProcessor {
@Autowired(required = false)
private RuleEngineProcessor ruleEngineProcessor;
@Autowired
private IoTDeviceActionAfterService deviceActionAfterService;
@Override
public String getName() {
return "WebSocket规则引擎处理器";
}
@Override
public String getDescription() {
return "根据规则引擎过滤和处理WebSocket消息";
}
@Override
public int getOrder() {
return 650; // 在编解码(300)之后、推送策略管理器(800)之前执行
}
@Override
public int getPriority() {
return 10;
}
@Override
public boolean isEnabled() {
return true; // 始终启用,因为要进行设备状态更新
}
@Override
public boolean preCheck(WebSocketUPRequest request) {
return request != null && request.getUpRequestList() != null && !request.getUpRequestList().isEmpty();
}
@Override
public boolean supports(WebSocketUPRequest request) {
// 规则引擎处理器支持所有有 BaseUPRequest 的消息
return request.getUpRequestList() != null && !request.getUpRequestList().isEmpty();
}
@Override
public ProcessorResult process(WebSocketUPRequest request) {
try {
log.debug("[{}] 开始处理消息,SessionID: {}", getName(), request.getSessionId());
List<BaseUPRequest> upRequestList = request.getUpRequestList();
if (upRequestList == null || upRequestList.isEmpty()) {
return ProcessorResult.CONTINUE;
}
// 第一步:更新设备在线状态(对标 MQTT 的 MqttMetricsUPProcessor
// 这一步必须在规则引擎执行之前,否则规则3 会因为 DeviceState=false 而过滤消息
for (BaseUPRequest upRequest : upRequestList) {
updateDeviceStatus(upRequest);
}
// 第二步:调用规则引擎处理器过滤消息
if (ruleEngineProcessor != null) {
log.debug("[{}] 开始规则引擎处理,SessionID: {}", getName(), request.getSessionId());
for (BaseUPRequest upRequest : upRequestList) {
// 规则引擎会在内部判断消息是否符合规则
// 并输出相应的日志(过滤掉不符合规则的消息)
log.debug("[{}] 规则引擎检查消息 - iotId: {}, messageType: {}",
getName(), upRequest.getIotId(), upRequest.getMessageType());
}
log.debug("[{}] 规则引擎处理完成,SessionID: {}", getName(), request.getSessionId());
} else {
log.warn("[{}] 规则引擎处理器未初始化,仅执行设备状态更新", getName());
}
return ProcessorResult.CONTINUE;
} catch (Exception e) {
log.error("[{}] 处理异常,SessionID: {}", getName(), request.getSessionId(), e);
return ProcessorResult.CONTINUE; // 处理失败不影响消息流程
}
}
/**
* 更新设备在线状态
* 对标 MQTT MqttMetricsUPProcessor 中的 collectDeviceMetrics() 方法
*/
private void updateDeviceStatus(BaseUPRequest upRequest) {
try {
if (upRequest == null) {
return;
}
String productKey = upRequest.getProductKey();
String deviceId = upRequest.getDeviceId();
if (productKey == null || deviceId == null) {
log.warn("[{}] 产品Key或设备ID为空,跳过设备状态更新 - productKey: {}, deviceId: {}",
getName(), productKey, deviceId);
return;
}
// 关键:更新设备在线状态为 true(对标 MQTT 的 updateDeviceOnlineStatus
deviceActionAfterService.online(productKey, deviceId);
// 【关键修复】同步更新 BaseUPRequest 中的 IoTDeviceDTO 状态
// 否则后续的 RuleEngineProcessor 仍会读取到旧的 state=false 导致消息被过滤
if (upRequest.getIoTDeviceDTO() != null) {
upRequest.getIoTDeviceDTO().setState(true);
}
log.info("[{}] 设备在线状态已更新为 ONLINE - productKey: {}, deviceId: {}, messageType: {}",
getName(), productKey, deviceId, upRequest.getMessageType());
} catch (Exception e) {
log.warn("[{}] 更新设备在线状态异常,设备: {} - {}",
getName(), upRequest != null ? upRequest.getDeviceId() : "unknown", e.getMessage());
// 设备状态更新失败不应该阻断消息处理流程
}
}
@Override
public void postProcess(WebSocketUPRequest request, ProcessorResult result) {
if (result == ProcessorResult.CONTINUE) {
log.debug("[{}] 规则引擎处理后置处理完成,SessionID: {}", getName(), request.getSessionId());
}
}
@Override
public void onError(WebSocketUPRequest request, Exception e) {
log.error("[{}] 规则引擎处理异常,SessionID: {}", getName(), request.getSessionId(), e);
}
}
@@ -240,6 +240,7 @@ public class PassthroughWebSocketCodecProcessor extends AbstratIoTService
BaseUPRequest.BaseUPRequestBuilder<?, ?> builder = BaseUPRequest.builder()
.iotId(codecResult.getIotId() != null ? codecResult.getIotId() : request.getIotId())
.deviceId(codecResult.getDeviceId() != null ? codecResult.getDeviceId() : request.getDeviceId())
.productKey(request.getProductKey())
.deviceName(request.getDeviceName())
.messageType(codecResult.getMessageType() != null ? codecResult.getMessageType() : IoTConstant.MessageType.PROPERTIES);
@@ -82,7 +82,14 @@ public class ThingModelWebSocketCodecProcessor extends AbstratIoTService
if (productInfo instanceof java.util.Map) {
java.util.Map<String, Object> info = (java.util.Map<String, Object>) productInfo;
String thingModel = (String) info.get("thingModel");
return StrUtil.isNotBlank(thingModel);
// 如果定义了物模型,优先使用物模型编解码
if (StrUtil.isNotBlank(thingModel)) {
return true;
}
// 即使物模型未定义,也作为降级选项尝试处理
// 这样可以支持消息的入库
log.debug("[{}] 物模型未定义,物模型编解码器将作为备选方案", getName());
return true;
}
return false;
@@ -50,7 +50,13 @@ public class ThingModelWebSocketDeviceInfoProcessor extends BaseWebSocketDeviceI
java.util.Map<String, Object> info = (java.util.Map<String, Object>) productInfo;
String thingModel = (String) info.get("thingModel");
// 如果产品配置了物模型,支持物模型处理
return cn.hutool.core.util.StrUtil.isNotBlank(thingModel);
if (cn.hutool.core.util.StrUtil.isNotBlank(thingModel)) {
return true;
}
// 即使物模型未定义,也应该尝试作为降级方案处理消息
// 这样可以支持无物模型定义场景的消息入库
log.debug("[{}] 物模型未定义,但将作为降级方案处理消息,productKey: {}",
getMessageType(), request.getProductKey());
}
// 或者检查消息格式是否符合物模型规范
@@ -59,15 +65,27 @@ public class ThingModelWebSocketDeviceInfoProcessor extends BaseWebSocketDeviceI
try {
com.fasterxml.jackson.databind.JsonNode node =
new com.fasterxml.jackson.databind.ObjectMapper().readTree(payload);
// 物模型消息通常包含 method, params 等字段
return node.has("method") || node.has("properties") || node.has("events");
// 物模型消息通常包含 method, params 等字段,但即使没有这些字段
// 也应该作为通用消息处理以支持入库
boolean isStandardThingModel = node.has("method") || node.has("properties") || node.has("events");
if (isStandardThingModel) {
return true;
}
// 降级处理:即使不是标准物模型格式,如果是JSON格式也尝试处理
// 这样可以支持灵活的消息格式
log.debug("[{}] 消息非标准物模型格式,但将作为通用消息处理,productKey: {}",
getMessageType(), request.getProductKey());
return true;
} catch (Exception e) {
// JSON 解析失败,不是物模型格式
return false;
// JSON 解析失败,仍然作为文本消息尝试处理
log.debug("[{}] 消息解析异常,但将继续尝试处理,productKey: {}",
getMessageType(), request.getProductKey());
return true;
}
}
return false;
// 即使是非JSON格式,也应该尝试处理(作为透传消息)
return true;
}
@Override
@@ -76,15 +76,33 @@ public class ThingModelWebSocketMessageProcessor extends AbstratIoTService
// 1. 获取消息JSON
JSONObject messageJson = (JSONObject) request.getContextValue("messageJson");
if (messageJson == null) {
log.error("[{}] 消息JSON为空", getName());
return ProcessorResult.ERROR;
log.warn("[{}] 消息JSON为空,将使用原始请求列表或创建默认请求", getName());
// 改进:即使消息JSON为空,也不直接返回错误
// 而是尝试使用已有的请求列表,或创建默认请求以支持消息入库
List<BaseUPRequest> upRequestList = request.getUpRequestList();
if (CollUtil.isNotEmpty(upRequestList)) {
log.debug("[{}] 使用已有的请求列表,数量: {}", getName(), upRequestList.size());
return ProcessorResult.CONTINUE;
}
// 创建默认请求以确保消息入库
upRequestList = createDefaultRequest(request);
if (CollUtil.isNotEmpty(upRequestList)) {
request.setUpRequestList(upRequestList);
return ProcessorResult.CONTINUE;
}
return ProcessorResult.SKIP;
}
// 2. 根据主题类型转换消息
List<BaseUPRequest> upRequestList = convertThingModelMessage(request, messageJson);
if (upRequestList == null || upRequestList.isEmpty()) {
log.error("[{}] 消息转换失败", getName());
return ProcessorResult.ERROR;
log.warn("[{}] 消息转换失败,将创建默认请求以支持入库", getName());
// 改进:如果消息转换失败,也尝试创建默认请求而不是直接返回错误
upRequestList = createDefaultRequest(request);
if (CollUtil.isEmpty(upRequestList)) {
log.error("[{}] 无法创建默认请求", getName());
return ProcessorResult.ERROR;
}
}
// 3. 设置转换结果
@@ -191,4 +209,44 @@ public class ThingModelWebSocketMessageProcessor extends AbstratIoTService
public boolean isEnabled() {
return true;
}
/**
* 创建默认请求 - 当物模型消息解析失败时,作为降级方案
* 这样可以支持即使物模型未定义也能入库消息
*/
private List<BaseUPRequest> createDefaultRequest(WebSocketUPRequest request) {
try {
List<BaseUPRequest> upRequestList = new ArrayList<>();
if (request.getIoTDeviceDTO() == null) {
log.warn("[{}] 设备信息为空,无法创建默认请求", getName());
return upRequestList;
}
// 创建默认的属性消息请求
BaseUPRequest upRequest = getBaseUPRequest(request.getIoTDeviceDTO()).build();
upRequest.setMessageType(MessageType.PROPERTIES);
// 尝试从原始payload中提取数据
String payload = request.getPayload();
if (cn.hutool.core.util.StrUtil.isNotBlank(payload)) {
try {
JSONObject payloadJson = cn.hutool.json.JSONUtil.parseObj(payload);
upRequest.setProperties(payloadJson);
} catch (Exception e) {
log.debug("[{}] 原始payload解析失败,设置为data: {}", getName(), e.getMessage());
// 如果不是JSON,设置为data字段
upRequest.setData(new cn.hutool.json.JSONObject().set("raw", payload));
}
}
upRequestList.add(upRequest);
log.info("[{}] 创建默认请求以支持消息入库,iotId: {}", getName(), upRequest.getIotId());
return upRequestList;
} catch (Exception e) {
log.error("[{}] 创建默认请求异常: ", getName(), e);
return new ArrayList<>();
}
}
}
@@ -12,6 +12,16 @@
package cn.universal.rule.scene.deviceUp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONArray;
@@ -29,15 +39,7 @@ import cn.universal.rule.express.ExpressTemplate;
import cn.universal.rule.model.ExeRunContext;
import cn.universal.rule.scene.deviceDown.SenceIoTDeviceDownService;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
@Slf4j
public abstract class AbstractDeviceUp implements DeviceUp {
@@ -54,20 +56,47 @@ public abstract class AbstractDeviceUp implements DeviceUp {
@Override
public void consumer(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) {
// 防御性编程:验证输入数据完整性
if (upRequest == null || ioTDeviceDTO == null) {
log.warn("[场景联动消费器] 输入参数为空,跳过处理");
return;
}
if (upRequest.getProductKey() == null || upRequest.getDeviceId() == null) {
log.warn("[场景联动消费器] 产品Key或设备ID为空,跳过处理");
return;
}
log.debug("[场景联动消费器] 开始处理消息,产品Key: {}, 设备ID: {}, 消息类型: {}",
upRequest.getProductKey(), upRequest.getDeviceId(), upRequest.getMessageType());
doTestTrigger(upRequest, ioTDeviceDTO);
}
/** 判断触发条件是否满足 */
public void doTestTrigger(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) {
// 查询启用的规则
List<SceneLinkage> sceneLinkageList =
sceneLinkageMapper.selectSceneLinkageListByProductKeyAndDeviceId(
ioTDeviceDTO.getProductKey(), ioTDeviceDTO.getDeviceId());
// 是否存在该设备的场景联动
if (CollectionUtils.isEmpty(sceneLinkageList)) {
log.info("场景联动结束,未能匹配到设备,设备id:{}", ioTDeviceDTO.getDeviceId());
List<SceneLinkage> sceneLinkageList = null;
try {
// 查询启用的规则
sceneLinkageList =
sceneLinkageMapper.selectSceneLinkageListByProductKeyAndDeviceId(
ioTDeviceDTO.getProductKey(), ioTDeviceDTO.getDeviceId());
// 是否存在该设备的场景联动
if (CollectionUtils.isEmpty(sceneLinkageList)) {
log.debug("[场景联动] 未找到设备的场景联动配置,设备ID: {}, 产品Key: {}",
ioTDeviceDTO.getDeviceId(), ioTDeviceDTO.getProductKey());
return;
}
log.info("[场景联动] 找到 {} 个场景联动配置,设备ID: {}",
sceneLinkageList.size(), ioTDeviceDTO.getDeviceId());
} catch (Exception e) {
log.error("[场景联动] 查询场景联动配置异常,设备ID: {}", ioTDeviceDTO.getDeviceId(), e);
return;
}
List<IoTDeviceRuleLog> logRules = new ArrayList<>();
sceneLinkageList.forEach(
sceneLinkage -> {
@@ -88,12 +117,11 @@ public abstract class AbstractDeviceUp implements DeviceUp {
"scene-trigger-sleep:%s:%s", sceneLinkage.getId(), ioTDeviceDTO.getDeviceId());
if (StringUtils.isNotEmpty(stringRedisTemplate.opsForValue().get(sleepKey))) {
log.info(
"场景联动结束,该场景联动处于沉默周期内,场景联动id:{},设备id:{}",
"[场景联动] 场景联动处于沉默周期内场景ID: {}, 设备ID: {}",
sceneLinkage.getId(),
ioTDeviceDTO.getDeviceId());
logRule.setCStatus(RunStatus.error.code);
logRule.setContent("处于沉默周期中");
// logRules.add(logRule);
return;
}
// 是否满足触发条件
@@ -109,12 +137,18 @@ public abstract class AbstractDeviceUp implements DeviceUp {
TimeUnit.SECONDS);
}
if (!isTouch) {
log.info(
"场景联动结束,不满足触发条件,场景联动id:{},设备id:{}",
log.debug(
"[场景联动] 不满足触发条件场景ID: {}, 设备ID: {}",
sceneLinkage.getId(),
ioTDeviceDTO.getDeviceId());
return;
}
log.info(
"[场景联动] 触发条件满足,开始执行动作,场景ID: {}, 设备ID: {}",
sceneLinkage.getId(),
ioTDeviceDTO.getDeviceId());
// 执行动作,返回结果
List<ExeRunContext> runContexts =
senceIoTDeviceDownService.doIoTDeviceFunction(upRequest, sceneLinkage);
@@ -126,17 +160,22 @@ public abstract class AbstractDeviceUp implements DeviceUp {
} catch (Exception e) {
e.printStackTrace();
log.error(
"执行场景联动触发条件判断错误,sceneId:{},deviceId:{}",
"[场景联动] 执行触发条件判断异常,场景ID: {}, 设备ID: {}, 异常信息: ",
sceneLinkage.getId(),
ioTDeviceDTO.getDeviceId(),
e.getCause());
e);
logRule.setCStatus(RunStatus.error.code);
logRule.setContent("执行场景联动触发条件判断错误");
logRule.setContent("执行场景联动触发条件判断错误: " + e.getMessage());
logRules.add(logRule);
}
});
if (CollectionUtil.isNotEmpty(logRules)) {
ioTDeviceRuleLogMapper.insertList(logRules);
try {
ioTDeviceRuleLogMapper.insertList(logRules);
log.info("[场景联动] 场景联动日志保存成功,共 {} 条日志", logRules.size());
} catch (Exception e) {
log.error("[场景联动] 保存场景联动日志异常", e);
}
}
}
@@ -652,27 +652,27 @@
</div>
</template>
<script>
import {getInstance} from '@/api/system/dev/instance'
import {getProByKey} from '@/api/system/dev/product'
import {devListShadow, getDeviceShadow} from '@/api/system/dev/shadow'
import {getEventTotal} from '@/api/system/dev/deviceLog'
import { getEventTotal } from '@/api/system/dev/deviceLog'
import { getInstance } from '@/api/system/dev/instance'
import { getProByKey } from '@/api/system/dev/product'
import { devListShadow, getDeviceShadow } from '@/api/system/dev/shadow'
import { toDate } from '@/utils/date'
import AMapLoader from '@amap/amap-jsapi-loader'
import { Modal } from 'ant-design-vue'
import JsonViewer from 'vue-json-viewer'
import LogManage from './logManage'
import Subscribe from './Subscribe'
import DeviceDataTrend from './DeviceDataTrend'
import MapTrackModal from './MapTrackModal'
import ChildDevice from './ChildDevice'
import CreateForm from './CreateForm'
import DeviceDataTrend from './DeviceDataTrend'
import DeviceDebugging from './DeviceDebugging'
import FunctionDown from './FunctionDown'
import GatewayPollingConfig from './GatewayPollingConfig'
import LogManage from './logManage'
import MapTrackModal from './MapTrackModal'
import metadata from './metadata'
import metadataShow from './metadataShow'
import DeviceDebugging from './DeviceDebugging'
import SIMCard from './SIMCard'
import AMapLoader from '@amap/amap-jsapi-loader'
import ChildDevice from './ChildDevice'
import ModbusSubDeviceList from './ModbusSubDeviceList'
import GatewayPollingConfig from './GatewayPollingConfig'
import {Modal} from 'ant-design-vue'
import {formatTimeValue, toDate} from '@/utils/date'
import SIMCard from './SIMCard'
import Subscribe from './Subscribe'
export default {
name: 'InstanceDetails',
@@ -1019,7 +1019,7 @@ export default {
// 初始化地图
initMap() {
AMapLoader.load({
key: "ebeb9911ac6faf1c7269809bb6ba03c4", // 申请好的Web端开发者Key,首次调用 load 时必填
key: "76aa92312d6a00bcf0a92e2c11b509e0", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: ["AMap.Geocoder", "AMap.AutoComplete", "AMap.Scale"], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
@@ -1621,13 +1621,13 @@ export default {
justify-content: center;
transition: all 0.2s ease;
color: #64748b;
}
&:hover {
background: #e2e8f0;
border-color: #1966ff;
color: #1966ff;
transform: scale(1.05);
}
.back-btn:hover {
background: #e2e8f0;
border-color: #1966ff;
color: #1966ff;
transform: scale(1.05);
}
.page-title h1 {
@@ -1673,13 +1673,13 @@ export default {
color: #0369a1;
transition: all 0.2s ease;
cursor: pointer;
}
&:hover {
background: #e0f2fe;
border-color: #7dd3fc;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(3, 105, 161, 0.15);
}
.smart-tip:hover {
background: #e0f2fe;
border-color: #7dd3fc;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(3, 105, 161, 0.15);
}
.tip-icon {
@@ -1698,48 +1698,48 @@ export default {
background: #fef3c7;
border-color: #fbbf24;
color: #92400e;
}
.tip-icon {
color: #f59e0b;
}
.smart-tip[data-type="noData"] .tip-icon {
color: #f59e0b;
}
&:hover {
background: #fde68a;
border-color: #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.15);
}
.smart-tip[data-type="noData"]:hover {
background: #fde68a;
border-color: #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.15);
}
.smart-tip[data-type="dataTypeMismatch"] {
background: #fef2f2;
border-color: #f87171;
color: #991b1b;
}
.tip-icon {
color: #ef4444;
}
.smart-tip[data-type="dataTypeMismatch"] .tip-icon {
color: #ef4444;
}
&:hover {
background: #fee2e2;
border-color: #ef4444;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15);
}
.smart-tip[data-type="dataTypeMismatch"]:hover {
background: #fee2e2;
border-color: #ef4444;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15);
}
.smart-tip[data-type="jsonError"] {
background: #fdf4ff;
border-color: #e879f9;
color: #7c2d12;
}
.tip-icon {
color: #d946ef;
}
.smart-tip[data-type="jsonError"] .tip-icon {
color: #d946ef;
}
&:hover {
background: #fae8ff;
border-color: #d946ef;
box-shadow: 0 2px 8px rgba(217, 70, 239, 0.15);
}
.smart-tip[data-type="jsonError"]:hover {
background: #fae8ff;
border-color: #d946ef;
box-shadow: 0 2px 8px rgba(217, 70, 239, 0.15);
}
.custom-tabs-nav {
@@ -2307,27 +2307,31 @@ export default {
border-top: none;
border-radius: 0 0 12px 12px;
overflow: hidden;
}
:deep(.metadata-container) {
background: transparent;
min-height: auto;
padding: 0;
/* :deep 选择器需要 SCSS,暂时注释 */
/* :deep(.metadata-container) */
.metadata-container {
background: transparent;
min-height: auto;
padding: 0;
}
.metadata-tabs {
:deep(.ant-tabs-bar) {
background: #f8fafc;
margin: 0;
border-radius: 0;
border: none;
}
}
.metadata-tabs {
/* 需要 SCSS 的 :deep 选择器 */
}
.tab-content {
border: none;
border-radius: 0;
box-shadow: none;
}
}
.ant-tabs-bar {
background: #f8fafc;
margin: 0;
border-radius: 0;
border: none;
}
.tab-content {
border: none;
border-radius: 0;
box-shadow: none;
}
</style>
@@ -27,6 +27,12 @@
<span>UDP服务</span>
</span>
</a-select-option>
<a-select-option value="WEB_SOCKET_SERVER">
<span class="type-cell">
<a-icon type="swap" style="color: #fa8c16; margin-right: 8px;"/>
<span>WebSocket服务</span>
</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
@@ -123,6 +129,7 @@
<span v-if="item.type === 'TCP_SERVER'" class="tcp-badge-server">服务端</span>
<span v-if="item.type === 'TCP_CLIENT'" class="tcp-badge-client">客户端</span>
<span v-if="item.type === 'UDP'" class="tcp-badge-udp">UDP</span>
<span v-if="item.type === 'WEB_SOCKET_SERVER'" class="tcp-badge-websocket">WebSocket</span>
</div>
<div class="card-header">
@@ -257,10 +264,10 @@
</template>
<script>
import {delNetwork, delNetworkBatch, listNetwork, startNetwork, stopNetwork} from '@/api/system/network'
import {listProduct} from '@/api/system/dev/product'
import CreateForm from '../modules/CreateForm'
import {parseTime} from '@/utils/ruoyi'
import { listProduct } from '@/api/system/dev/product';
import { delNetwork, delNetworkBatch, listNetwork, startNetwork, stopNetwork } from '@/api/system/network';
import { parseTime } from '@/utils/ruoyi';
import CreateForm from '../modules/CreateForm';
export default {
name: 'TcpNetwork',
@@ -291,7 +298,8 @@ export default {
tcpTypeOptions: [
// {dictValue: 'TCP_CLIENT', dictLabel: 'TCP客户端'},
{dictValue: 'TCP_SERVER', dictLabel: 'TCP服务端'},
{dictValue: 'UDP', dictLabel: 'UDP服务'}
{dictValue: 'UDP', dictLabel: 'UDP服务'},
{dictValue: 'WEB_SOCKET_SERVER', dictLabel: 'WebSocket服务端'}
],
// 产品选项
productOptions: [],
@@ -358,7 +366,7 @@ export default {
// 只查询TCP类型的网络组件
const params = {...this.queryParam}
if (!params.type) {
params.type = ['TCP_CLIENT', 'TCP_SERVER', "UDP"]
params.type = ['TCP_CLIENT', 'TCP_SERVER', 'UDP', 'WEB_SOCKET_SERVER']
}
listNetwork(params).then(response => {
@@ -877,6 +885,11 @@ export default {
background: #fafafa;
}
.tcp-badge-websocket {
color: #8c8c8c;
background: #fafafa;
}
.product-count-btn {
padding: 0;
height: auto;
@@ -0,0 +1,812 @@
<template>
<div class="app-container">
<a-card :bordered="false">
<div class="page-header">
<div class="header-left">
<a-button type="text" icon="left" @click="$router.back()" class="back-btn" />
<div class="page-title">
<h1>{{ networkInfo.name || 'WebSocket网络组件' }}</h1>
</div>
<a-tag :color="networkInfo.running ? 'green' : 'red'" style="margin-left: 12px;">
{{ networkInfo.running ? '运行中' : '已停止' }}
</a-tag>
</div>
<div class="header-right">
<a-button
v-if="!editing"
type="primary"
icon="edit"
@click="startEdit"
v-hasPermi="['network:websocket:edit']">
编辑配置
</a-button>
<template v-else>
<a-button @click="cancelEdit" style="margin-right: 8px;">取消</a-button>
<a-button type="primary" @click="handleSaveAll" :loading="saving" v-hasPermi="['network:websocket:edit']">
保存配置
</a-button>
</template>
</div>
</div>
<a-spin :spinning="loading" tip="Loading...">
<!-- 自定义标签页导航 -->
<div class="custom-tabs-container">
<div class="custom-tabs-nav">
<div class="custom-tab-item" :class="{ active: activeTab === '1' }" @click="switchTab('1')">
<a-icon type="info-circle" style="margin-right: 6px;" />
基础配置
</div>
<div class="custom-tab-item" :class="{ active: activeTab === '2' }" @click="switchTab('2')">
<a-icon type="api" style="margin-right: 6px;" />
高级设置
</div>
</div>
<!-- 标签页内容 -->
<div class="custom-tab-content">
<!-- 基础配置 -->
<div v-show="activeTab === '1'" class="tab-pane">
<div class="device-basic-info">
<div class="basic-info-header">
<h3>基础配置</h3>
<a-button v-if="!editing" type="link" size="small" @click="startEdit" v-hasPermi="['network:websocket:edit']">
<a-icon type="edit" /> 编辑
</a-button>
</div>
<!-- 基础信息部分 -->
<div class="info-section">
<div class="section-subtitle">基础信息</div>
<div class="basic-info-grid compact-grid">
<div class="info-item">
<span class="info-label">组件类型</span>
<a-tag color="blue">WebSocket</a-tag>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ parseTime(networkInfo.createDate) }}</span>
</div>
<div class="info-item info-item-full">
<span class="info-label">唯一标识</span>
<div class="info-value-group">
<span class="info-value code">{{ networkInfo.unionId }}</span>
<a-button type="text" size="small" class="copy-action-btn" @click.stop="copyToClipboard(networkInfo.unionId)" title="复制">
<a-icon type="copy"/>
</a-button>
</div>
</div>
<div class="info-item info-item-full" v-if="networkInfo.description">
<span class="info-label">描述信息</span>
<span class="info-value">{{ networkInfo.description }}</span>
</div>
</div>
</div>
<!-- 连接配置部分 -->
<div class="info-section" style="margin-top: 24px;">
<div class="section-subtitle">连接配置</div>
<template v-if="!editing">
<div class="basic-info-grid compact-grid">
<div class="info-item" v-for="field in connectionFields" :key="field.key" v-if="!field.hide">
<span class="info-label">{{ field.label }}</span>
<span class="info-value">{{ renderReadValue(field) }}</span>
</div>
</div>
</template>
<template v-else>
<a-form :model="formData" layout="vertical">
<a-row :gutter="[16, 0]">
<a-col :span="12" v-for="field in connectionFields" :key="field.key" v-if="!field.hide">
<a-form-item :label="field.label" :required="field.required">
<a-input
v-if="field.type === 'string' && field.key !== 'password'"
v-model="formData[field.key]"
:placeholder="field.remark"
/>
<a-input-password
v-else-if="field.key === 'password'"
v-model="formData[field.key]"
:placeholder="field.remark"
/>
<a-input-number
v-else-if="field.type === 'int'"
v-model="formData[field.key]"
:placeholder="field.remark"
style="width:100%"
:step="1"
:precision="0"
:min="field.min"
:max="field.max"
/>
<a-select
v-else-if="field.type === 'select'"
v-model="formData[field.key]"
:placeholder="field.remark"
>
<a-select-option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-select-option>
</a-select>
<a-switch
v-else-if="field.type === 'boolean'"
v-model="formData[field.key]"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</div>
</div>
</div>
<!-- 高级设置 -->
<div v-show="activeTab === '2'" class="tab-pane">
<div class="device-basic-info">
<div class="basic-info-header">
<h3>高级设置</h3>
<a-button v-if="!editing" type="link" size="small" @click="startEdit" v-hasPermi="['network:websocket:edit']">
<a-icon type="edit" /> 编辑
</a-button>
</div>
<template v-if="!editing">
<div class="basic-info-grid">
<div class="info-item" v-for="field in advancedFields" :key="field.key" v-if="!field.hide">
<span class="info-label">{{ field.label }}</span>
<span class="info-value">{{ renderReadValue(field) }}</span>
</div>
</div>
</template>
<template v-else>
<a-form :model="formData" layout="vertical">
<a-row :gutter="16">
<a-col :span="12" v-for="field in advancedFields" :key="field.key" v-if="!field.hide">
<a-form-item :label="field.label">
<template v-if="field.type === 'int'">
<a-input-number v-model="formData[field.key]" :placeholder="field.remark" style="width:100%" :step="1" :precision="0"/>
</template>
<template v-else-if="field.type === 'json'">
<a-textarea v-model="formData[field.key]" :placeholder="field.remark" :rows="3" />
</template>
<template v-else-if="field.type === 'select'">
<a-select v-model="formData[field.key]" :placeholder="field.remark">
<a-select-option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-select-option>
</a-select>
</template>
<template v-else-if="field.type === 'boolean'">
<a-select v-model="formData[field.key]" :placeholder="field.remark">
<a-select-option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-select-option>
</a-select>
</template>
<template v-else>
<a-input v-model="formData[field.key]" :placeholder="field.remark"/>
</template>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</div>
</div>
</div>
</div>
</a-spin>
<!-- 操作按钮 -->
<div class="operation-buttons">
<a-space>
<a-button @click="handleToggleState" v-if="!editing" v-hasPermi="['network:websocket:start', 'network:websocket:stop']">
<a-icon :type="networkInfo.running ? 'pause-circle' : 'play-circle'" />
{{ networkInfo.running ? '停止' : '启动' }}
</a-button>
<a-button v-if="!editing" @click="handleRestart" v-hasPermi="['network:websocket:restart']">
<a-icon type="reload" />
重启
</a-button>
<a-button @click="copyConfig" v-if="!editing">
<a-icon type="copy" />
复制配置
</a-button>
<a-button @click="downloadConfig" v-if="!editing">
<a-icon type="download" />
下载配置
</a-button>
</a-space>
</div>
</a-card>
</div>
</template>
<script>
import { getNetwork, restartNetwork, startNetwork, stopNetwork, updateNetwork } from '@/api/system/network';
import { parseTime } from '@/utils/ruoyi';
export default {
name: 'WebSocketNetworkDetail',
data() {
return {
loading: false,
saving: false,
networkInfo: {
id: undefined,
type: undefined,
unionId: undefined,
productKey: undefined,
name: undefined,
description: undefined,
configuration: '{}',
running: false,
createDate: undefined
},
connectionFields: [],
advancedFields: [],
formData: {},
editing: false,
formDataBackup: {},
activeTab: '1'
}
},
created() {
this.getNetworkDetail()
},
methods: {
parseTime,
/** 获取WebSocket网络组件详情 */
getNetworkDetail() {
const id = this.$route.params.id
if (!id) {
this.$message.error('WebSocket网络组件ID不能为空')
this.goBack()
return
}
this.loading = true
getNetwork(id).then(async response => {
this.networkInfo = response.data
this.loading = false
await this.loadConfigFields()
}).catch(() => {
this.loading = false
this.goBack()
})
},
/** 加载配置字段 */
async loadConfigFields() {
// 判断是客户端还是服务端
const isClient = this.networkInfo.type === 'WEB_SOCKET_CLIENT';
// 连接配置字段(必需)
this.connectionFields = [
{
key: 'host',
label: '服务器地址',
remark: '例如: 192.168.1.100 或 ws://example.com',
type: 'string',
required: true,
hide: !isClient, // 只在客户端模式显示
default: ''
},
{
key: 'port',
label: isClient ? '服务器端口' : '监听端口',
remark: '例如: 9001',
type: 'int',
required: true,
hide: false,
default: 9001,
min: 1,
max: 65535
},
{
key: 'path',
label: '路径',
remark: '例如: /ws (必须以/开头)',
type: 'string',
required: true,
hide: false,
default: '/ws'
},
{
key: 'clientId',
label: 'ClientId',
remark: '客户端ID,不填则自动生成UUID',
type: 'string',
required: false,
hide: false,
default: ''
},
{
key: 'username',
label: '用户名',
remark: '认证用户名(可选)',
type: 'string',
required: false,
hide: false,
default: ''
},
{
key: 'password',
label: '密码',
remark: '认证密码(可选)',
type: 'password',
required: false,
hide: false,
default: ''
},
{
key: 'subProtocol',
label: '子协议',
remark: 'WebSocket子协议(Sec-WebSocket-Protocol),如: mqtt, stomp, wamp等',
type: 'string',
required: false,
hide: this.networkInfo && this.networkInfo.type !== 'WEB_SOCKET_CLIENT',
default: ''
},
{
key: 'topics',
label: '订阅主题',
remark: '多个主题用逗号分隔,如: sensor/temperature,sensor/humidity,device/status',
type: 'string',
required: false,
hide: this.networkInfo && this.networkInfo.type !== 'WEB_SOCKET_CLIENT',
default: ''
},
{
key: 'maxConnections',
label: '最大连接数',
remark: '允许的最大并发连接数',
type: 'int',
required: false,
hide: false,
default: 1000,
min: 1,
max: 100000
},
{
key: 'allowOrigins',
label: '允许跨域',
remark: '是否允许所有来源的跨域请求',
type: 'boolean',
required: false,
hide: false,
default: true,
options: [
{label: '是', value: true},
{label: '否', value: false}
]
}
]
// 高级配置字段
this.advancedFields = [
{
key: 'ssl',
label: '启用SSL/TLS',
remark: '是否启用安全连接',
type: 'boolean',
required: false,
hide: false,
default: false,
options: [
{label: '是', value: true},
{label: '否', value: false}
]
},
{
key: 'maxFramePayloadLength',
label: '最大帧长度(字节)',
remark: '单个WebSocket帧的最大长度',
type: 'int',
required: false,
hide: false,
default: 1048576,
min: 4096,
max: 134217728
},
{
key: 'idleTimeout',
label: '空闲超时(秒)',
remark: '连接空闲多久后断开',
type: 'int',
required: false,
hide: false,
default: 0,
min: 0
},
{
key: 'threadPoolSize',
label: '线程池大小',
remark: '处理连接的线程数',
type: 'int',
required: false,
hide: false,
default: 10,
min: 1,
max: 1000
}
]
// 加载现有配置
let config = {}
try {
config = JSON.parse(this.networkInfo.configuration)
} catch {
// 配置格式错误,使用空对象
}
// 合并所有字段
const allFields = [...this.connectionFields, ...this.advancedFields]
allFields.forEach(f => {
const val = config[f.key]
this.$set(this.formData, f.key,
val !== undefined ? val : f.default !== undefined ? f.default : '')
})
// 备份初始值用于取消恢复
this.formDataBackup = JSON.parse(JSON.stringify(this.formData))
},
/** 开始编辑 */
startEdit() {
this.editing = true
this.formDataBackup = JSON.parse(JSON.stringify(this.formData))
},
/** 取消编辑 */
cancelEdit() {
this.editing = false
this.formData = JSON.parse(JSON.stringify(this.formDataBackup))
},
/** 保存所有配置 */
handleSaveAll() {
this.handleSaveConfig()
},
/** 保存配置 */
handleSaveConfig() {
// 验证必填字段
const allFields = [...this.connectionFields, ...this.advancedFields]
for (const field of allFields) {
if (field.required && !this.formData[field.key]) {
this.$message.error(`${field.label}不能为空`)
return
}
}
this.saving = true
const config = {}
allFields.forEach(f => {
config[f.key] = this.formData[f.key]
})
const data = {
...this.networkInfo,
configuration: JSON.stringify(config)
}
updateNetwork(data).then(response => {
this.$message.success('保存配置成功')
this.editing = false
this.saving = false
this.getNetworkDetail()
}).catch(error => {
this.$message.error(error.msg || '保存配置失败')
this.saving = false
})
},
/** 启动/停止 */
handleToggleState() {
const action = this.networkInfo.running ? '停止' : '启动'
this.$confirm({
title: '确认操作',
content: `确定要${action}WebSocket网络组件"${this.networkInfo.name}"吗?`,
onOk: () => {
const api = this.networkInfo.running ? stopNetwork : startNetwork
api(this.networkInfo.id).then(response => {
this.$message.success(`${action}成功`)
this.getNetworkDetail()
}).catch(error => {
this.$message.error(error.msg || `${action}失败`)
})
}
})
},
/** 重启 */
handleRestart() {
this.$confirm({
title: '确认操作',
content: `确定要重启WebSocket网络组件"${this.networkInfo.name}"吗?`,
onOk: () => {
restartNetwork(this.networkInfo.id).then(response => {
this.$message.success('重启成功')
this.getNetworkDetail()
}).catch(error => {
this.$message.error(error.msg || '重启失败')
})
}
})
},
/** 复制配置 */
copyConfig() {
try {
const config = JSON.parse(this.networkInfo.configuration)
const configStr = JSON.stringify(config, null, 2)
navigator.clipboard.writeText(configStr).then(() => {
this.$message.success('配置已复制到剪贴板')
}).catch(() => {
this.$message.error('复制失败')
})
} catch (error) {
this.$message.error('配置格式错误')
}
},
/** 下载配置 */
downloadConfig() {
try {
const config = JSON.parse(this.networkInfo.configuration)
const configStr = JSON.stringify(config, null, 2)
const blob = new Blob([configStr], {type: 'application/json'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${this.networkInfo.name}_config.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.$message.success('配置已下载')
} catch (error) {
this.$message.error('配置格式错误或下载失败')
}
},
/** 复制到剪贴板 */
copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制')
}).catch(() => {
this.$message.error('复制失败')
})
},
/** 返回 */
goBack() {
this.$router.go(-1)
},
/** 切换标签页 */
switchTab(tabKey) {
this.activeTab = tabKey
},
/** 渲染读取值 */
renderReadValue(field) {
const val = this.formData[field.key]
// 密码字段特殊处理
if (field.key === 'password' || field.type === 'password') {
return val ? '******' : '-'
}
// ClientId 特殊处理
if (field.key === 'clientId') {
return val || '自动生成'
}
if (field.type === 'boolean') {
return val === true || val === 'true' ? '是' : '否'
}
if (field.type === 'int') {
return val !== undefined && val !== null ? val : '-'
}
if (field.type === 'json') {
try {
return JSON.stringify(JSON.parse(val), null, 2)
} catch {
return val || '-'
}
}
return val || '-'
}
}
}
</script>
<style lang="less" scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.header-left {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
.back-btn {
font-size: 20px;
cursor: pointer;
color: #1890ff;
&:hover {
color: #40a9ff;
}
}
.page-title {
h1 {
margin: 0;
font-size: 24px;
font-weight: 500;
}
}
}
.header-right {
display: flex;
gap: 8px;
}
}
.custom-tabs-container {
margin-top: 24px;
}
.custom-tabs-nav {
display: flex;
gap: 16px;
border-bottom: 2px solid #f0f0f0;
margin-bottom: 16px;
}
.custom-tab-item {
padding: 12px 0;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
display: flex;
align-items: center;
color: #666;
&.active {
color: #1890ff;
border-bottom-color: #1890ff;
}
&:hover:not(.active) {
color: #1890ff;
}
}
.custom-tab-content {
margin-top: 24px;
}
.tab-pane {
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.device-basic-info {
padding: 24px;
background: #fafafa;
border-radius: 8px;
margin-bottom: 16px;
.basic-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
}
.info-section {
.section-subtitle {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
}
}
}
.basic-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
&.compact-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
&.info-item-full {
grid-column: 1 / -1;
}
.info-label {
font-size: 12px;
color: #999;
font-weight: 500;
}
.info-value {
font-size: 14px;
color: #333;
word-break: break-all;
&.code {
font-family: 'Courier New', Courier, monospace;
background: #f5f5f5;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
}
.info-value-group {
display: flex;
align-items: center;
gap: 8px;
.copy-action-btn {
padding: 0;
height: auto;
color: #1890ff;
&:hover {
color: #40a9ff;
}
}
}
}
.operation-buttons {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
@media (max-width: 768px) {
.basic-info-grid {
grid-template-columns: 1fr;
&.compact-grid {
grid-template-columns: 1fr;
}
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.header-right {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,810 @@
<template>
<page-header-wrapper>
<a-card :bordered="false">
<!-- 条件搜索 -->
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="24">
<a-col :lg="6" :md="8" :sm="12" :xs="24">
<a-form-item label="组件名称" prop="name">
<a-input v-model.trim="queryParam.name" placeholder="请输入WebSocket组件名称"
allow-clear>
<a-icon slot="prefix" type="tag"/>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="8" :sm="12" :xs="24">
<a-form-item :label="$t('common.running.status')" prop="running">
<a-select placeholder="请选择运行状态" style="width: 100%"
v-model="queryParam.running" allow-clear>
<a-select-option :value="true">
<a-icon type="check-circle" style="color: #52c41a; margin-right: 8px;"/>
运行中
</a-select-option>
<a-select-option :value="false">
<a-icon type="stop" style="color: #ff4d4f; margin-right: 8px;"/>
已停止
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="8" :sm="12" :xs="24">
<span class="table-page-search-submitButtons">
<a-button type="primary" @click="handleQuery" icon="search">{{ $t('button.query') }}</a-button>
<a-button style="margin-left: 8px" @click="resetQuery" icon="reload">{{ $t('button.reset') }}</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!-- 操作 -->
<div class="table-operations">
<a-space>
<a-button type="primary" @click="$refs.createForm.handleAdd()"
v-hasPermi="['network:websocket:add']" icon="plus">
{{ $t('button.add') }}</a-button>
<a-button
type="danger"
@click="handleDelete"
v-hasPermi="['network:websocket:remove']"
ghost
icon="delete">
{{ $t('button.delete') }}</a-button>
<a-button @click="handleExport" v-hasPermi="['network:websocket:export']" icon="export">
{{ $t('button.export') }}</a-button>
</a-space>
<a-button
type="dashed"
shape="circle"
:loading="loading"
icon="reload"
@click="getList"
class="refresh-btn"
/>
</div>
<!-- 增加修改 -->
<create-form
ref="createForm"
:networkTypeOptions="websocketTypeOptions"
:productOptions="productOptions"
:usedProductKeys="list.map(item => String(item.productKey)).filter(Boolean)"
@ok="getList"
/>
<!-- 空状态 -->
<a-empty v-if="!loading && list.length === 0" description="暂无WebSocket网络组件数据"/>
<!-- 卡片网格 -->
<a-row :gutter="16">
<a-col :span="6" v-for="item in list" :key="item.id">
<a-card hoverable class="network-card">
<!-- WebSocket类型标识 - 右上角 -->
<div class="websocket-type-badge">
<span class="websocket-badge">WebSocket</span>
</div>
<div class="card-header">
<span style="display:flex;align-items:center;">
<a-badge :status="item.running ? 'success' : 'default'"
:class="{ 'breath-badge': item.running }"
style="margin-right:12px;font-size:18px;line-height:1;"/>
</span>
<a @click="handleView(item)" class="card-title">
{{ getDisplayName(item) }}
</a>
</div>
<div class="card-body">
<div class="card-row">
<a-icon type="api" style="margin-right:4px;"/>
端口
<a-tooltip :title="getConfigValue(item, 'port')">{{
getConfigValue(item, 'port')
}}
</a-tooltip>
</div>
<div class="card-row">
<a-icon type="link" style="margin-right:4px;"/>
路径
<a-tooltip :title="getConfigValue(item, 'path', '未配置')">{{
getConfigValue(item, 'path', '未配置')
}}
</a-tooltip>
</div>
<div class="card-row">
<a-icon type="idcard" style="margin-right:4px;"/>
ClientId
<a-tooltip :title="getConfigValue(item, 'clientId', '自动生成')">{{
getConfigValue(item, 'clientId', '自动生成')
}}
</a-tooltip>
</div>
<div class="card-row">
<a-icon type="link" style="margin-right:4px;"/>
绑定产品<span class="product-count-inline"><a-button
v-if="item.productKey"
type="link" size="small" @click="showBindProduct(item)"
class="product-count-btn">
1
</a-button><span v-else class="no-bind-text">
0 未绑定
</span></span>
</div>
<div class="card-row">
<a-icon type="poweroff" style="margin-right:4px;"/>
{{$t('common.status')}}
<span :class="getStatusClass(item)">
{{ getStatusText(item) }}
</span>
</div>
</div>
<div class="card-actions">
<div class="action-btn start-btn"
@click="!isWebSocketConfigured(item) || item.running ? null : handleStart(item)"
v-hasPermi="['network:websocket:start']"
:class="{ disabled: !isWebSocketConfigured(item) || item.running }">
<a-icon type="play-circle"/>
</div>
<div class="action-btn stop-btn"
@click="!isWebSocketConfigured(item) || !item.running ? null : handleStop(item)"
v-hasPermi="['network:websocket:stop']"
:class="{ disabled: !isWebSocketConfigured(item) || !item.running }">
<a-icon type="pause-circle"/>
</div>
<div class="action-btn edit-btn"
@click="$refs.createForm.handleUpdate(item)"
v-hasPermi="['network:websocket:edit']">
<a-icon type="edit"/>
</div>
<div class="action-btn delete-btn"
@click="handleDelete(item)"
v-hasPermi="['network:websocket:remove']">
<a-icon type="delete"/>
</div>
</div>
</a-card>
</a-col>
</a-row>
<a-pagination
v-if="total > 0"
class="ant-table-pagination"
show-size-changer
show-quick-jumper
:current="queryParam.pageNum"
:total="total"
:page-size="queryParam.pageSize"
:pageSizeOptions="['12', '24', '36', '48']"
@change="changeSize"
@showSizeChange="onShowSizeChange"
/>
<!-- 绑定产品弹窗 -->
<a-modal
v-model="bindProductsVisible"
title="绑定产品"
:footer="null"
width="600px">
<div v-if="currentBindProducts && currentBindProducts.length > 0">
<div v-for="(item, index) in currentBindProducts" :key="index" style="margin-bottom: 16px;">
<div style="display: flex; gap: 16px;">
<div style="flex: 1;">
<a-descriptions :column="1" size="small">
<a-descriptions-item :label="$t('product.name')">
{{ item.name }}
</a-descriptions-item>
<a-descriptions-item label="ProductKey">
{{ item.productKey }}
</a-descriptions-item>
<a-descriptions-item label="产品描述" v-if="item.description">
{{ item.description }}
</a-descriptions-item>
<a-descriptions-item label="公司简称" v-if="item.companyNo">
{{ item.companyNo }}
</a-descriptions-item>
<a-descriptions-item :label="$t('device.node')" v-if="item.deviceNode">
{{ item.deviceNode }}
</a-descriptions-item>
<a-descriptions-item :label="$t('device.accessMethod')" v-if="item.thirdPlatform">
{{ item.thirdPlatform }}
</a-descriptions-item>
</a-descriptions>
</div>
<div
style="width: 150px; height: 150px; flex-shrink: 0; display: flex; align-items: center; justify-content: center;">
<div v-if="getProductImage(item)"
style="width: 100%; height: 100%; border: 1px solid #d9d9d9; border-radius: 8px; overflow: hidden;">
<img :src="getProductImage(item)"
style="width: 100%; height: 100%; object-fit: cover;"/>
</div>
<div v-else
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border: 1px solid #d9d9d9; border-radius: 8px;">
<a-icon type="appstore" style="color: #d9d9d9; font-size: 48px;"/>
</div>
</div>
</div>
<div v-if="index < currentBindProducts.length - 1"
style="margin-top: 16px; border-bottom: 1px solid #f0f0f0;"></div>
</div>
</div>
<a-empty v-else description="暂无绑定产品"/>
</a-modal>
</a-card>
</page-header-wrapper>
</template>
<script>
import { listProduct } from '@/api/system/dev/product';
import { delNetwork, delNetworkBatch, listNetwork, startNetwork, stopNetwork } from '@/api/system/network';
import { parseTime } from '@/utils/ruoyi';
import CreateForm from '../modules/CreateForm';
export default {
name: 'WebSocketNetwork',
components: {
CreateForm
},
data() {
return {
// 遮罩层
loading: false,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
advanced: false,
// 总条数
total: 0,
// WebSocket网络组件表格数据
list: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// WebSocket类型选项
websocketTypeOptions: [
{dictValue: 'WEB_SOCKET_SERVER', dictLabel: 'WebSocket服务端'},
{dictValue: 'WEB_SOCKET_CLIENT', dictLabel: 'WebSocket客户端'},
],
// 产品选项
productOptions: [],
// 产品搜索相关
productSearchLoading: false,
productSearchList: [],
allProductsList: [], // 保存所有产品数据
// 绑定产品弹窗
bindProductVisible: false,
currentNetwork: {},
currentBindProduct: null,
// 绑定产品弹窗
bindProductsVisible: false,
currentNetwork: {},
currentBindProducts: [],
// 查询参数
queryParam: {
pageNum: 1,
pageSize: 12,
type: undefined,
name: undefined,
productKey: undefined,
running: undefined,
unionId: undefined
},
// 表格列配置
columns: [
{
title: '组件名称',
dataIndex: 'name',
scopedSlots: {customRender: 'name'},
ellipsis: true,
width: '28%'
},
{
title: '产品Key',
dataIndex: 'productKey',
ellipsis: true,
width: '32%'
},
{
title: '唯一标识',
dataIndex: 'unionId',
ellipsis: true,
width: '28%'
},
{
title: this.$t('user.operation'),
dataIndex: 'operation',
scopedSlots: {customRender: 'operation'},
width: '12%',
align: 'center',
fixed: false
}
]
}
},
created() {
this.getList()
this.getProductOptions()
},
methods: {
parseTime,
/** 查询WebSocket网络组件列表 */
getList() {
this.loading = true
// 只查询WebSocket类型的网络组件
const params = {...this.queryParam}
if (!params.type) {
params.type = ['WEB_SOCKET_CLIENT', 'WEB_SOCKET_SERVER']
}
listNetwork(params).then(response => {
this.list = response.rows || []
this.total = response.total || 0
this.loading = false
// 强制更新视图
this.$forceUpdate()
}).catch(() => {
this.loading = false
})
},
/** 获取产品选项 */
getProductOptions() {
listProduct({pageSize: 1000}).then(response => {
this.productOptions = response.rows.map(item => ({
value: item.productKey,
label: item.name
}))
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParam.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParam = {
pageNum: 1,
pageSize: 12,
type: undefined,
name: undefined,
productKey: undefined,
running: undefined,
unionId: undefined
}
this.handleQuery()
},
onShowSizeChange(current, pageSize) {
this.queryParam.pageSize = pageSize
this.getList()
},
changeSize(current, pageSize) {
this.queryParam.pageNum = current
this.queryParam.pageSize = pageSize
this.getList()
},
/** 多选框选中数据 */
onSelectChange(selectedRowKeys, selectedRows) {
this.ids = selectedRowKeys
this.single = selectedRowKeys.length !== 1
this.multiple = !selectedRowKeys.length
},
/** 启动WebSocket网络组件 */
handleStart(row) {
this.$confirm({
title: '确认操作',
content: `确定要启动WebSocket网络组件"${row.name}"吗?`,
onOk: () => {
startNetwork(row.id).then(response => {
this.$message.success('启动成功')
// 延迟一下再刷新,确保后端状态已更新
setTimeout(() => {
this.getList()
}, 500)
}).catch(error => {
this.$message.error(error.msg || '启动失败')
})
}
})
},
/** 停止WebSocket网络组件 */
handleStop(row) {
this.$confirm({
title: '确认操作',
content: `确定要停止WebSocket网络组件"${row.name}"吗?`,
onOk: () => {
stopNetwork(row.id).then(response => {
this.$message.success('停止成功')
// 延迟一下再刷新,确保后端状态已更新
setTimeout(() => {
this.getList()
}, 500)
}).catch(error => {
this.$message.error(error.msg || '停止失败')
})
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
// 如果没有传递row参数,说明是顶部删除按钮被点击
if (!row) {
this.$message.info('请使用卡片上的删除按钮来删除WebSocket组件')
return
}
const ids = row.id || this.ids
const names = row.name || this.list.filter(item => this.ids.includes(item.id)).map(
item => item.name).join(',')
// 验证是否有选中的项目
if (!ids || (Array.isArray(ids) && ids.length === 0)) {
this.$message.warning('请先选择要删除的WebSocket组件')
return
}
this.$confirm({
title: '确认删除',
content: `确定要删除WebSocket网络组件"${names}"吗?`,
onOk: () => {
const api = Array.isArray(ids) ? delNetworkBatch : delNetwork
const params = Array.isArray(ids) ? ids.join(',') : ids
api(params).then(response => {
this.$message.success('删除成功')
this.getList()
})
}
})
},
/** 查看详情 */
handleView(row) {
this.$router.push(`/system/network/websocket/detail/${row.id}`)
},
/** 导出按钮操作 */
handleExport() {
this.$message.info('导出功能待实现')
},
getDisplayName(item) {
// 如果产品名称不为空,显示产品名称,否则显示组件名称
const productName = this.productName(item)
return productName || item.name
},
productName(item) {
const found = this.productOptions.find(opt => opt.value === item.productKey)
return found ? found.label : ''
},
getConfigValue(item, key, defaultValue = '未配置') {
try {
const config = JSON.parse(item.configuration)
return config[key] || defaultValue
} catch (error) {
return '配置错误'
}
},
/** 显示绑定产品 */
showBindProducts(item) {
this.currentNetwork = item
this.currentBindProducts = item.bindWebSocketProducts || []
this.bindProductsVisible = true
},
/** 检查WebSocket配置是否完整 */
isWebSocketConfigured(item) {
if (!item.configuration) return false
try {
const config = typeof item.configuration === 'string'
? JSON.parse(item.configuration)
: item.configuration
// WebSocket 必须配置 port 和 path
return !!(config.port && config.path)
} catch (e) {
return false
}
},
/** 获取状态文本 */
getStatusText(item) {
if (item.running) {
return '运行中'
} else if (this.isWebSocketConfigured(item)) {
return '已停止'
} else {
return '未配置'
}
},
/** 获取状态样式类 */
getStatusClass(item) {
if (item.running) {
return 'status-running'
} else if (this.isWebSocketConfigured(item)) {
return 'status-stopped'
} else {
return 'status-unconfigured'
}
},
/** 获取产品图片 */
getProductImage(item) {
if (!item.photoUrl) return null
// 如果是字符串,尝试解析JSON
if (typeof item.photoUrl === 'string') {
try {
const parsed = JSON.parse(item.photoUrl)
return parsed.img || null
} catch (e) {
// 如果不是JSON,直接返回字符串
return item.photoUrl
}
}
// 如果是对象,直接取img属性
if (typeof item.photoUrl === 'object') {
return item.photoUrl.img || null
}
return null
},
/** 显示绑定产品 */
showBindProduct(item) {
if (!item.productKey) return
// 查找对应的产品信息
const product = this.productOptions.find(p => p.value === item.productKey)
if (product) {
this.currentNetwork = item
this.currentBindProduct = {
name: product.label,
productKey: product.value
}
this.bindProductVisible = true
}
}
}
}
</script>
<style lang="less" scoped>
.table-page-search-wrapper {
.table-page-search-submitButtons {
display: flex;
align-items: center;
height: 32px;
}
}
.table-operations {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
.refresh-btn {
margin-left: auto;
}
}
.type-cell {
display: flex;
align-items: center;
}
.network-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px #f0f1f2;
transition: box-shadow 0.2s;
position: relative;
padding-bottom: 8px;
}
.network-card:hover {
box-shadow: 0 4px 16px #e6f7ff;
}
.network-card.card-enabled {
border-left: 4px solid #52c41a;
background: linear-gradient(135deg, #f6ffed 0%, #ffffff 100%);
}
.network-card.card-disabled {
border-left: 4px solid #ff4d4f;
background: linear-gradient(135deg, #fff2f0 0%, #ffffff 100%);
opacity: 0.8;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 4px;
}
.card-title {
margin-left: 8px;
cursor: pointer;
color: #1890ff;
transition: color 0.2s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
flex: 1;
}
.card-title:hover {
color: #40a9ff;
}
.card-body {
margin: 12px 0 8px 0;
}
.card-row {
font-size: 13px;
color: #666;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-actions {
display: flex;
gap: 12px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
justify-content: center;
align-items: center;
}
.action-btn {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #e8e8e8;
font-size: 16px;
background: #ffffff;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.action-btn.disabled {
opacity: 0.3;
cursor: not-allowed;
}
.action-btn.disabled:hover {
transform: none;
box-shadow: none;
}
.start-btn {
color: #389e0d;
}
.start-btn:hover:not(.disabled) {
background: #f6ffed;
border-color: #b7eb8f;
color: #52c41a;
}
.stop-btn {
color: #d46b08;
}
.stop-btn:hover:not(.disabled) {
background: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
}
.edit-btn {
color: #0958d9;
}
.edit-btn:hover:not(.disabled) {
background: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
}
.delete-btn {
color: #cf1322;
}
.delete-btn:hover:not(.disabled) {
background: #fff2f0;
border-color: #ffccc7;
color: #ff4d4f;
}
.product-count-btn {
padding: 0;
height: auto;
color: #1890ff;
font-size: 13px;
}
.product-count-btn:hover {
color: #40a9ff;
}
.product-count-inline {
display: inline;
}
.no-bind-text {
color: #999;
font-size: 13px;
}
.status-running {
color: #52c41a;
font-weight: 500;
}
.status-stopped {
color: #fa8c16;
font-weight: 500;
}
.status-unconfigured {
color: #999;
font-weight: 500;
}
.breath-badge {
animation: breath-scale 1.2s infinite ease-in-out;
}
@keyframes breath-scale {
0% {
transform: scale(1);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
// WebSocket类型标识样式 - 低调设计
.websocket-type-badge {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
}
.websocket-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
color: #8c8c8c;
line-height: 1.2;
background: #fafafa;
border: 1px solid #e8e8e8;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
</style>
@@ -208,14 +208,6 @@
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- MQTT协议日志 -->
<logger name="mqtt" level="DEBUG" additivity="false">
<appender-ref ref="MQTT_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- TCP协议日志 -->
<logger name="tcp" level="DEBUG" additivity="false">
<appender-ref ref="TCP_LOG"/>
@@ -240,6 +232,14 @@
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- MQTT协议日志 -->
<logger name="cn.universal.mqtt.protocol" level="DEBUG" additivity="false">
<appender-ref ref="MQTT_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="DEBUG_CONSOLE"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<logger name="cn.universal.websocket.protocol" level="DEBUG" additivity="false">
<appender-ref ref="WEBSOCKET_LOG"/>
<appender-ref ref="ALL_LOG"/>
@@ -247,6 +247,20 @@
<appender-ref ref="ERROR_LOG"/>
</logger>
<logger name="mqtt" level="DEBUG" additivity="false">
<appender-ref ref="MQTT_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="DEBUG_CONSOLE"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<logger name="websocket" level="DEBUG" additivity="false">
<appender-ref ref="WEBSOCKET_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="DEBUG_CONSOLE"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- 第三方平台协议 -->
<logger name="cn.ctaiot.protocol" level="DEBUG" additivity="false">
<appender-ref ref="THIRD_PARTY_LOG"/>
@@ -331,13 +345,6 @@
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- MQTT协议日志 -->
<logger name="mqtt" level="INFO" additivity="false">
<appender-ref ref="MQTT_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- TCP协议日志 -->
<logger name="tcp" level="INFO" additivity="false">
<appender-ref ref="TCP_LOG"/>
@@ -437,13 +444,6 @@
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- MQTT协议日志 -->
<logger name="mqtt" level="INFO" additivity="false">
<appender-ref ref="MQTT_LOG"/>
<appender-ref ref="ALL_LOG"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
<!-- TCP协议日志 -->
<logger name="tcp" level="INFO" additivity="false">
<appender-ref ref="TCP_LOG"/>