From 017c66134d5ffa97a019005196badc9661c8af99 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 10 Dec 2025 21:32:30 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E6=9C=80=E8=BF=91=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastbee-admin/src/main/resources/logback.xml | 2 +- .../common/enums/DeviceLogTypeEnum.java | 4 +- .../utils/gateway/protocol/ByteUtils.java | 50 +++ .../iot/data/aspect/DeviceDebugLogAspect.java | 293 ++++++++++++++++++ .../consumer/DevicePropFetchConsumer.java | 8 +- .../iot/data/emq/subscribeCallback.java | 1 + .../impl/DeviceMessageServiceImpl.java | 16 +- .../impl/ProtocolManagerServiceImpl.java | 10 +- .../fastbee/modbus/codec/MessageAdapter.java | 2 +- 9 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java diff --git a/fastbee-admin/src/main/resources/logback.xml b/fastbee-admin/src/main/resources/logback.xml index 41c5a3ec..a493ed09 100644 --- a/fastbee-admin/src/main/resources/logback.xml +++ b/fastbee-admin/src/main/resources/logback.xml @@ -3,7 +3,7 @@ - + diff --git a/fastbee-common/src/main/java/com/fastbee/common/enums/DeviceLogTypeEnum.java b/fastbee-common/src/main/java/com/fastbee/common/enums/DeviceLogTypeEnum.java index f5dba491..78d640a1 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/enums/DeviceLogTypeEnum.java +++ b/fastbee-common/src/main/java/com/fastbee/common/enums/DeviceLogTypeEnum.java @@ -24,7 +24,9 @@ public enum DeviceLogTypeEnum { DEVICE_UPDATE(4, "设备升级"), DEVICE_ONLINE(5, "设备上线"), DEVICE_OFFLINE(6, "设备离线"), - SCENE_VARIABLE_REPORT(7, "场景录入、运算变量上报下发"); + SCENE_VARIABLE_REPORT(7, "场景录入、运算变量上报下发"), + DATA_PARSE_ERROR(8, "数据解析错误"), + COMMAND_SEND_ERROR(9, "指令发送错误");; private final Integer type; diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java index 796e4534..f5ee36db 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/gateway/protocol/ByteUtils.java @@ -321,6 +321,56 @@ public class ByteUtils { } + /** + * 将字符串转换为字节数组,强制按十六进制处理 + * 清理字符串后直接转换为十六进制字节数组 + * + * @param command 命令字符串 + * @return 字节数组,转换失败返回空数组 + */ + public static byte[] convertToHexByteArray(String command) { + if (command == null || command.isEmpty()) { + return new byte[0]; + } + try { + // 清理字符串:去除空格、去除0x前缀、补齐偶数长度 + String cleanedCommand = command.trim() + .replaceAll("\\s+", "") // 去除所有空格 + .replaceAll("(?i)^0x", ""); // 去除0x或0X前缀 + + // 如果长度为奇数,在前面补0 + if (cleanedCommand.length() % 2 != 0) { + cleanedCommand = "0" + cleanedCommand; + } + + // 使用现有的hexToByte方法 + byte[] result = hexToByte(cleanedCommand); + return result != null ? result : new byte[0]; + } catch (Exception e) { + // 如果标准方法失败,手动转换为十六进制字节数组 + try { + String cleanedCommand = command.trim() + .replaceAll("\\s+", "") + .replaceAll("(?i)^0x", ""); + + if (cleanedCommand.length() % 2 != 0) { + cleanedCommand = "0" + cleanedCommand; + } + + // 手动将每两个字符转换为一个字节 + byte[] result = new byte[cleanedCommand.length() / 2]; + for (int i = 0; i < cleanedCommand.length(); i += 2) { + String hexByte = cleanedCommand.substring(i, i + 2); + result[i / 2] = (byte) Integer.parseInt(hexByte, 16); + } + return result; + } catch (Exception e2) { + // 如果手动转换也失败,返回空数组 + return new byte[0]; + } + } + } + public static byte getByte(byte[] src, int offset) { return src[offset]; } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java new file mode 100644 index 00000000..7a4b7132 --- /dev/null +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java @@ -0,0 +1,293 @@ +package com.fastbee.iot.data.aspect; + +import com.fastbee.common.core.device.DeviceAndProtocol; +import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.core.mq.message.DeviceData; +import com.fastbee.common.core.mq.message.DeviceMessage; +import com.fastbee.common.enums.DeviceLogTypeEnum; +import com.fastbee.common.utils.DateUtils; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.domain.DeviceLog; +import com.fastbee.iot.service.IDeviceService; +import com.fastbee.iot.service.IProductService; +import com.fastbee.iot.tdengine.service.ILogService; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 设备调试错误日志AOP切面 + * 用于拦截数据解析和指令发送的异常,自动记录错误日志 + * + * @author fastbee + * @date 2025-01-08 + */ +@Aspect +@Component +@Order(100) +@Slf4j +public class DeviceDebugLogAspect { + + @Resource + private ILogService logService; + + @Resource + private IDeviceService deviceService; + + @Resource + private IProductService productService; + + /** + * 拦截数据解析方法 + */ + @Pointcut("execution(* com.fastbee.iot.data.service.impl.DeviceReportMessageServiceImpl.parseReportMsg(..))") + public void parseReportMsgPointcut() { + } + + /** + * 拦截指令发送方法 + */ + @Pointcut("execution(* com.fastbee.iot.data.service.impl.DeviceMessageServiceImpl.messagePost(..))") + public void messagePostPointcut() { + } + + /** + * 拦截协议解析方法 + */ + @Pointcut("execution(* com.fastbee.protocol.base.protocol.IProtocol.decode(..))") + public void protocolDecodePointcut() { + } + + /** + * 拦截数据解析异常 + */ + @AfterThrowing(pointcut = "parseReportMsgPointcut()", throwing = "e") + public void afterThrowingParseReportMsg(JoinPoint joinPoint, Throwable e) { + try { + Object[] args = joinPoint.getArgs(); + if (args != null && args.length > 0 && args[0] instanceof DeviceReportBo) { + DeviceReportBo bo = (DeviceReportBo) args[0]; + String serialNumber = bo.getSerialNumber(); + if (StringUtils.isEmpty(serialNumber)) { + log.warn("设备编号为空,跳过错误日志记录"); + return; + } + + // 处理原始数据,可能是byte[]或String + String sourceData = ""; + try { + if (bo.getData() != null && bo.getData().length > 0) { + sourceData = ByteBufUtil.hexDump(bo.getData()); + } else if (StringUtils.isNotEmpty(bo.getSources())) { + sourceData = bo.getSources(); + } + } catch (Exception ex) { + log.warn("处理原始数据失败: {}", ex.getMessage()); + sourceData = bo.getSources() != null ? bo.getSources() : ""; + } + + String errorMsg = e.getMessage(); + if (StringUtils.isEmpty(errorMsg)) { + errorMsg = e.getClass().getName(); + } + + // 直接从bo中获取协议编码,避免额外的查询 + String protocolCode = bo.getProtocolCode(); + + saveErrorLog(serialNumber, sourceData, errorMsg, protocolCode, bo.getTopicName(), + DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "PARSE_ERROR"); + } + } catch (Exception ex) { + log.error("记录数据解析错误日志失败: {}", ex.getMessage(), ex); + } + } + + /** + * 拦截指令发送异常 + */ + @AfterThrowing(pointcut = "messagePostPointcut()", throwing = "e") + public void afterThrowingMessagePost(JoinPoint joinPoint, Throwable e) { + try { + Object[] args = joinPoint.getArgs(); + if (args != null && args.length > 0 && args[0] instanceof DeviceMessage) { + DeviceMessage deviceMessage = (DeviceMessage) args[0]; + String serialNumber = deviceMessage.getSerialNumber(); + if (StringUtils.isEmpty(serialNumber)) { + log.warn("设备编号为空,跳过错误日志记录"); + return; + } + + // 处理原始数据 + String sourceData = ""; + try { + if (deviceMessage.getMessage() != null) { + Object msg = deviceMessage.getMessage(); + if (msg instanceof byte[]) { + sourceData = ByteBufUtil.hexDump((byte[]) msg); + } else { + sourceData = msg.toString(); + } + } + } catch (Exception ex) { + log.warn("处理原始数据失败: {}", ex.getMessage()); + } + + String errorMsg = e.getMessage(); + if (StringUtils.isEmpty(errorMsg)) { + errorMsg = e.getClass().getName(); + } + + // 获取协议编码(通过设备编号查询,有缓存) + String protocolCode = getProtocolCodeBySerialNumber(serialNumber); + + saveErrorLog(serialNumber, sourceData, errorMsg, protocolCode, deviceMessage.getTopicName(), + DeviceLogTypeEnum.COMMAND_SEND_ERROR.getType(), "SEND_ERROR"); + } + } catch (Exception ex) { + log.error("记录指令发送错误日志失败: {}", ex.getMessage(), ex); + } + } + + /** + * 拦截协议解析异常 + */ + @AfterThrowing(pointcut = "protocolDecodePointcut()", throwing = "e") + public void afterThrowingProtocolDecode(JoinPoint joinPoint, Throwable e) { + try { + Object[] args = joinPoint.getArgs(); + if (args != null && args.length > 0 && args[0] instanceof DeviceData) { + DeviceData deviceData = + (DeviceData) args[0]; + String serialNumber = deviceData.getSerialNumber(); + if (StringUtils.isEmpty(serialNumber)) { + log.warn("设备编号为空,跳过错误日志记录"); + return; + } + + // 处理原始数据 + String sourceData = ""; + try { + ByteBuf buf = deviceData.getBuf(); + if (buf != null) { + sourceData = ByteBufUtil.hexDump(buf); + } else if (deviceData.getData() != null && deviceData.getData().length > 0) { + sourceData = ByteBufUtil.hexDump(deviceData.getData()); + } + } catch (Exception ex) { + log.warn("处理原始数据失败: {}", ex.getMessage()); + } + + String errorMsg = e.getMessage(); + if (StringUtils.isEmpty(errorMsg)) { + errorMsg = e.getClass().getName(); + } + + // 获取协议编码:优先从productId获取(有缓存),否则通过设备编号查询 + String protocolCode = null; + if (deviceData.getProductId() != null) { + protocolCode = getProtocolCodeByProductId(deviceData.getProductId()); + } + if (StringUtils.isEmpty(protocolCode) && StringUtils.isNotEmpty(serialNumber)) { + protocolCode = getProtocolCodeBySerialNumber(serialNumber); + } + + // 协议解析错误也归类为数据解析错误 + saveErrorLog(serialNumber, sourceData, errorMsg, protocolCode, deviceData.getTopicName(), + DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "PARSE_ERROR"); + } + } catch (Exception ex) { + log.error("记录协议解析错误日志失败: {}", ex.getMessage(), ex); + } + } + + /** + * 根据设备编号获取协议编码(有缓存) + */ + private String getProtocolCodeBySerialNumber(String serialNumber) { + try { + DeviceAndProtocol deviceAndProtocol = + deviceService.selectProtocolBySerialNumber(serialNumber); + if (deviceAndProtocol != null) { + return deviceAndProtocol.getProtocolCode(); + } + } catch (Exception ex) { + log.warn("获取协议编码失败: {}, 设备编号: {}", ex.getMessage(), serialNumber); + } + return null; + } + + /** + * 根据产品ID获取协议编码(有缓存) + */ + private String getProtocolCodeByProductId(Long productId) { + try { + if (productId != null) { + return productService.getProtocolByProductId(productId); + } + } catch (Exception ex) { + log.warn("根据产品ID获取协议编码失败: {}, 产品ID: {}", ex.getMessage(), productId); + } + return null; + } + + /** + * 保存错误日志到iot_device_log表 + */ + private void saveErrorLog(String serialNumber, String sourceData, String errorMsg, + String protocolCode, String topicName, Integer logType, String identity) { + try { + if (StringUtils.isEmpty(serialNumber)) { + log.warn("设备编号为空,无法保存错误日志"); + return; + } + + // 获取设备信息 + Device device = null; + try { + device = deviceService.selectDeviceBySerialNumber(serialNumber); + } catch (Exception ex) { + log.warn("获取设备信息失败: {}, 设备编号: {}", ex.getMessage(), serialNumber); + } + + if (device == null) { + log.warn("设备不存在,无法保存错误日志: {}", serialNumber); + return; + } + + DeviceLog deviceLog = new DeviceLog(); + deviceLog.setSerialNumber(serialNumber); + deviceLog.setDeviceId(device.getDeviceId()); + deviceLog.setDeviceName(device.getDeviceName()); + deviceLog.setLogType(logType); + // 存储原始数据包(限制64字符) + deviceLog.setLogValue(StringUtils.substring(sourceData, 0, 64)); + // 存储错误信息(限制200字符) + deviceLog.setRemark(StringUtils.substring(errorMsg, 0, 500)); + deviceLog.setIdentity(identity); + deviceLog.setModelName(logType == DeviceLogTypeEnum.DATA_PARSE_ERROR.getType() ? + "数据解析错误" : "指令发送错误"); + deviceLog.setCreateTime(DateUtils.getNowDate()); + deviceLog.setUserId(device.getTenantId()); + deviceLog.setUserName(device.getTenantName()); + deviceLog.setTenantId(device.getTenantId()); + deviceLog.setTenantName(device.getTenantName()); + deviceLog.setIsMonitor(0); + deviceLog.setMode(3); // 3=其他 + + logService.saveDeviceLog(deviceLog); + log.info("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}", + serialNumber, identity, StringUtils.substring(errorMsg, 0, 50)); + } catch (Exception ex) { + log.error("保存错误日志失败: {}", ex.getMessage(), ex); + } + } +} \ No newline at end of file diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java index 9637cf75..d6b2c74f 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java @@ -15,6 +15,7 @@ import com.fastbee.common.enums.TopicType; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.gateway.mq.TopicsUtils; +import com.fastbee.common.utils.gateway.protocol.ByteUtils; import com.fastbee.iot.data.service.impl.MessageManager; import com.fastbee.iot.domain.DeviceJob; import com.fastbee.iot.domain.ModbusJob; @@ -81,6 +82,7 @@ public class DevicePropFetchConsumer { public void consume(ModbusPollJob job) { Integer type = job.getType(); DeviceJob deviceJob = job.getDeviceJob(); + // type 1:手动下发 2:定时任务执行 ModbusPollMsg task = type == 1 ? job.getPollMsg() : getCommandList(deviceJob); if (CollectionUtils.isEmpty(task.getCommandList()))return; String serialNumber = task.getSerialNumber(); @@ -96,17 +98,19 @@ public class DevicePropFetchConsumer { for (String command : commandList) { String cacheKey = RedisKeyBuilder.buildModbusRuntimeCacheKey(serialNumber); redisCache.zSetAdd(cacheKey, command, DateUtils.getTimestampSeconds()); + // 不进行格式校验,直接转换数据 + byte[] commandData = ByteUtils.convertToHexByteArray(command); switch (serverType) { //通过mqtt内部客户端 下发指令 case MQTT: - publish(topic, ByteBufUtil.decodeHexDump(command)); + publish(topic, commandData); log.info("=>MQTT-线程=[{}],轮询指令:[{}],主题:[{}]", Thread.currentThread().getName(), command, topic); break; // 下发TCP客户端 case TCP: Message msg = new Message(); msg.setClientId(serialNumber); - msg.setPayload(Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(command))); + msg.setPayload(Unpooled.wrappedBuffer(commandData)); messageManager.requestR(serialNumber, msg, Message.class); log.info("=>TCP-线程=[{}],轮询指令:[{}]", Thread.currentThread().getName(), command); break; diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/emq/subscribeCallback.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/emq/subscribeCallback.java index 662248f0..b870a0ba 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/emq/subscribeCallback.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/emq/subscribeCallback.java @@ -6,6 +6,7 @@ import com.fastbee.common.enums.TopicType; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.gateway.mq.TopicsUtils; +import com.fastbee.common.utils.gateway.protocol.ByteUtils; import com.fastbee.ruleEngine.context.MsgContext; import com.fastbee.iot.ruleEngine.RuleProcess; import com.fastbee.mqttclient.IEmqxMessageProducer; diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java index 784eaada..486f5661 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java @@ -19,6 +19,7 @@ import com.fastbee.common.utils.BitUtils; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.gateway.CRC16Utils; +import com.fastbee.common.utils.gateway.protocol.ByteUtils; import com.fastbee.common.utils.gateway.mq.TopicsUtils; import com.fastbee.common.utils.modbus.ModbusUtils; import com.fastbee.iot.cache.ITSLCache; @@ -113,9 +114,22 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { break; case PROPERTY_POST: //下发的不经过mqtt或TCP直接转发到数据处理模块 + // 强制按十六进制处理,直接转发原始数据 + byte[] messageData; + Object message = deviceMessage.getMessage(); + if (message == null) { + messageData = new byte[0]; + } else if (message instanceof byte[]) { + messageData = (byte[]) message; + } else if (message instanceof String) { + messageData = ByteUtils.convertToHexByteArray((String) message); + } else { + // 其他类型直接转换为字符串后按十六进制处理 + messageData = ByteUtils.convertToHexByteArray(message.toString()); + } DeviceReportBo reportBo = DeviceReportBo.builder() .serverType(ServerType.explain(transport)) - .data(BitUtils.hexStringToByteArray(deviceMessage.getMessage().toString())) + .data(messageData) .platformDate(DateUtils.getNowDate()) .serialNumber(serialNumber) .topicName(topicName).build(); diff --git a/fastbee-protocol/fastbee-protocol-base/src/main/java/com/fastbee/protocol/service/impl/ProtocolManagerServiceImpl.java b/fastbee-protocol/fastbee-protocol-base/src/main/java/com/fastbee/protocol/service/impl/ProtocolManagerServiceImpl.java index 8fac0d41..122d0abe 100644 --- a/fastbee-protocol/fastbee-protocol-base/src/main/java/com/fastbee/protocol/service/impl/ProtocolManagerServiceImpl.java +++ b/fastbee-protocol/fastbee-protocol-base/src/main/java/com/fastbee/protocol/service/impl/ProtocolManagerServiceImpl.java @@ -15,7 +15,9 @@ import com.fastbee.protocol.domain.DeviceProtocol; import com.fastbee.protocol.service.IProtocolManagerService; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.formula.functions.T; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -52,7 +54,9 @@ public class ProtocolManagerServiceImpl implements IProtocolManagerService { protocol.setDelFlag(0); List protocolList = protocolService.selectByCondition(protocol); annotations.forEach((key,value)->{ - SysProtocol annotation = value.getClass().getAnnotation(SysProtocol.class); + // 获取目标类而非代理类,避免AOP代理导致注解丢失 + Class targetClass = AopUtils.getTargetClass(value); + SysProtocol annotation = AnnotationUtils.findAnnotation(targetClass, SysProtocol.class); ProtocolDto protocolDto = new ProtocolDto(); protocolDto.setCode(annotation.protocolCode()); protocolDto.setName(annotation.name()); @@ -97,7 +101,9 @@ public class ProtocolManagerServiceImpl implements IProtocolManagerService { Map annotations = SpringUtils.getBeanWithAnnotation(SysProtocol.class); IProtocol protocol = null; for (IProtocol item : annotations.values()) { - SysProtocol annotation = item.getClass().getAnnotation(SysProtocol.class); + // 获取目标类而非代理类,避免AOP代理导致注解丢失 + Class targetClass = AopUtils.getTargetClass(item); + SysProtocol annotation = AnnotationUtils.findAnnotation(targetClass, SysProtocol.class); protocolMap.put(annotation.protocolCode(), item); if (annotation.protocolCode().equals(protocolCode)){ protocol = item; diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/MessageAdapter.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/MessageAdapter.java index 1b53ef72..68c642aa 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/MessageAdapter.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/MessageAdapter.java @@ -51,7 +51,7 @@ public class MessageAdapter implements MessageDecoder, MessageEncoder{ Long productId = null; String devNum = new String(ByteBufUtil.getBytes(buf)); String dump = ByteBufUtil.hexDump(buf); - log.info("=>上报hex数据:{}", dump); + log.info("{}=>上报hex数据:{}",clientId, dump); //这里兼容一下TCP整包发送的数据(整包==设备编号,数据一起发送) if (clientId == null){