springboot集成mqtt服务,自主下发

maven的xml引入,前提是项目加入了

bash 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

单独加入

bash 复制代码
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-codec-mqtt</artifactId>
    <version>4.2.7.Final</version>
</dependency>

yam配置文件

bash 复制代码
# netty 服务
netty:
  server:
    enabled: true
    port: 1883
    boss-threads: 2
    worker-threads: 8
bash 复制代码
package com.cqcloud.platform.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.cqcloud.platform.handler.NettyServerFactory;
import com.cqcloud.platform.service.NettyService;

import io.netty.bootstrap.ServerBootstrap;
/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2023年9月1日 🐬🐇 💓💕
 */
@Configuration
@ConditionalOnClass(ServerBootstrap.class)
@EnableConfigurationProperties(NettyServerProperties.class)
@ConditionalOnProperty(prefix = "netty.server", name = "enabled", havingValue = "true", matchIfMissing = true)
public class NettyServerAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public NettyServerFactory nettyServerFactory(NettyServerProperties properties, NettyService nettyService) {
        return new NettyServerFactory(properties, nettyService);
    }
}
bash 复制代码
package com.cqcloud.platform.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2023年9月1日 🐬🐇 💓💕
 */
@ConfigurationProperties(prefix = "netty.server")
public class NettyServerProperties {
	private int port = 1883;
	private boolean enabled = true;
	private int bossThreads = 1;
	private int workerThreads = 0; // 0表示使用默认

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public int getBossThreads() {
		return bossThreads;
	}

	public void setBossThreads(int bossThreads) {
		this.bossThreads = bossThreads;
	}

	public int getWorkerThreads() {
		return workerThreads;
	}

	public void setWorkerThreads(int workerThreads) {
		this.workerThreads = workerThreads;
	}
}
bash 复制代码
package com.cqcloud.platform.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Random;
/**
 * MQTT 命令发送者
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2023年8月27日 🐬🐇 💓💕
 */
public class MqttCommandSender {

    public static void sendIoOutput(String camId, String ioNum, String action) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            ObjectNode gpio = mapper.createObjectNode();
            gpio.put("ioNum", ioNum).put("action", action);

            ObjectNode msg = mapper.createObjectNode();
            msg.put("cmd", "ioOutput")
                    .put("msgId", generateMessageId())
                    .put("utcTs", System.currentTimeMillis() / 1000)
                    .set("gpioData", gpio);

            TcpMqttServerHandler.sendToDevice(camId, msg.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void sendRs485Display(String camId, List<String> dataList) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            ArrayNode chn1Data = mapper.createArrayNode();

            for (String data : dataList) {
                ObjectNode dataNode = mapper.createObjectNode();
                dataNode.put("data", data);
                chn1Data.add(dataNode);
            }

            ObjectNode msg = mapper.createObjectNode();
            msg.put("cmd", "rs485Transmit")
                    .put("msgId", generateMessageId())
                    .put("utcTs", System.currentTimeMillis() / 1000)
                    .put("encodeType", "hex2string")
                    .set("chn1Data", chn1Data);

            TcpMqttServerHandler.sendToDevice(camId, msg.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 生成消息ID
     *
     * @return 生成的消息ID
     */
    private static String generateMessageId() {
        long millis = System.currentTimeMillis();
        String random = String.format("%07d", new Random().nextInt(10000000));
        return millis + random;
    }


    /**
     * 将中文字符串转换为 GBK 编码的十六进制字符串
     *
     * @param input 要转换的中文字符串
     * @return GBK 编码对应的十六进制字符串,如果编码失败则返回空字符串
     */
    public static String stringToHex(String input) {
        try {
            // 1. 将字符串转换为 GBK 编码的字节数组
            byte[] bytes = input.getBytes("GBK");

            // 2. 将字节数组转换为十六进制字符串
            StringBuilder hexString = new StringBuilder();
            for (byte b : bytes) {
                // 将每个字节转换为两位十六进制,并确保是大写
                String hex = String.format("%02X", b);
                hexString.append(hex);
            }

            return hexString.toString();
        } catch (UnsupportedEncodingException e) {
            System.err.println("不支持 GBK 编码: " + e.getMessage());
            return ""; // 或者抛出异常
        }
    }


}
bash 复制代码
package com.cqcloud.platform.handler;

import io.netty.channel.*;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import com.cqcloud.platform.config.NettyServerProperties;
import com.cqcloud.platform.service.NettyService;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.StandardCharsets;

/**
 * Netty服务工厂类
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2023年9月1日 🐬🐇 💓💕
 */
public class NettyServerFactory implements InitializingBean, DisposableBean {

    private final NettyServerProperties properties;
    private final NettyService nettyService;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private ChannelFuture channelFuture;

    public NettyServerFactory(NettyServerProperties properties, NettyService nettyService) {
        this.properties = properties;
        this.nettyService = nettyService;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (!properties.isEnabled()) {
            return;
        }
        // 配置线程组
        bossGroup = properties.getBossThreads() > 0 ?
                new NioEventLoopGroup(properties.getBossThreads()) :
                new NioEventLoopGroup();

        workerGroup = properties.getWorkerThreads() > 0 ?
                new NioEventLoopGroup(properties.getWorkerThreads()) :
                new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_RCVBUF, 30 * 1024 * 1024) // 系统接收缓冲区 2MB
                    .childOption(ChannelOption.SO_RCVBUF, 30 * 1024 * 1024) // 子通道接收缓冲区 2MB
                    .childOption(ChannelOption.RCVBUF_ALLOCATOR,
                            new AdaptiveRecvByteBufAllocator(64, 512 * 1024, 30 * 1024 * 1024)) // 自适应缓冲区
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 添加字符串编解码器
                            //pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
                            //pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));


                            // 添加MQTT解码器(核心:自动解析MQTT帧结构,处理分片)
                            // 参数:最大消息长度(根据图片大小调整,如20MB)
                            pipeline.addLast("mqttDecoder", new MqttDecoder(30 * 1024 * 1024));
                            // 添加MQTT编码器(发送响应时自动编码为MQTT协议格式)
                            pipeline.addLast("mqttEncoder", MqttEncoder.INSTANCE);
                            // 添加自定义处理器
                            // 再添加您原有的业务处理器
                            pipeline.addLast(new TcpMqttServerHandler(nettyService));
                        }


                    });

            channelFuture = bootstrap.bind(properties.getPort()).sync();
            System.out.println("Netty TCP Server started on port: " + properties.getPort());

            // 异步关闭监听
            channelFuture.channel().closeFuture().addListener(future -> {
                System.out.println("Netty TCP Server channel closed");
            });

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Netty server start interrupted", e);
        } catch (Exception e) {
            destroy();
            throw new RuntimeException("Failed to start Netty server", e);
        }
    }

    @Override
    public void destroy() {
        if (channelFuture != null) {
            channelFuture.channel().close();
        }
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        System.out.println("Netty TCP Server resources released");
    }

    public boolean isRunning() {
        return channelFuture != null && channelFuture.channel().isActive();
    }
}
bash 复制代码
package com.cqcloud.platform.handler;

import com.cqcloud.platform.service.NettyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Netty TCP服务器入口点
 * 实际启动由NettyServerFactory处理
 */
@Component
public class NettyTcpHandler {

    private final NettyService nettyService;

    @Autowired
    public NettyTcpHandler(NettyService nettyService) {
        this.nettyService = nettyService;
    }

    /**
     * 保持原有的静态方法兼容性
     */
    public static void start() {
        System.out.println("Netty server is now managed by Spring Boot auto-configuration");
        System.out.println("Please configure netty.server properties in application.yml");
    }

    public NettyService getNettyService() {
        return nettyService;
    }
}
bash 复制代码
package com.cqcloud.platform.handler;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.cqcloud.platform.common.matter.utils.SpringContextHolder;
import com.cqcloud.platform.entity.TcHeartbeatResponse;
import com.cqcloud.platform.service.NettyService;
import com.cqcloud.platform.tcsf.service.BarrierMqttService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.mqtt.*;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 支持 MQTT 遗嘱(LWT)的高并发安全服务端处理器(仅支持 QoS=0)
 * 设备上行主题:/device/{camId}/update
 * 服务端下行主题:/device/{camId}/get
 * 遗嘱主题:/device/{camId}/will
 */
@Slf4j
public class TcpMqttServerHandler extends SimpleChannelInboundHandler<MqttMessage> {

    private final NettyService iotPushService;
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    private static final ConcurrentMap<String, Channel> CAM_CHANNEL_MAP = new ConcurrentHashMap<>();
    private static final AttributeKey<String> CAM_ID_KEY = AttributeKey.valueOf("camId");
    private static final AttributeKey<MqttConnectMessage> CONNECT_MESSAGE_KEY = AttributeKey.valueOf("connectMessage");
    private static final Pattern DEVICE_TOPIC_PATTERN = Pattern.compile("^/device/([^/]+)/.*$");

    public TcpMqttServerHandler(NettyService iotPushService) {
        this.iotPushService = iotPushService;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception {
        // 记录收到的原始消息类型
        System.out.println("时间:"+LocalDateTime.now()+"  [MQTT] 收到消息类型: " + msg.fixedHeader().messageType());

        switch (msg.fixedHeader().messageType()) {
            case CONNECT:
                handleConnect(ctx, (MqttConnectMessage) msg);
                break;
            case PINGREQ:
                ctx.writeAndFlush(new MqttMessage(
                        new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0)
                ));
                break;
                // 主要业务入口 处理 PUBLISH
            case PUBLISH:
                handlePublish(ctx, (MqttPublishMessage) msg);
                break;
            case DISCONNECT:
                ctx.close();
                break;
            case SUBSCRIBE:
                handleSubscribe(ctx, (MqttSubscribeMessage) msg);
                break;
            default:
                System.out.println("时间:"+LocalDateTime.now()+"  [MQTT] 忽略不支持的消息类型: " + msg.fixedHeader().messageType());
                break;
        }
    }

    /**
     * 处理设备 CONNECT 连接请求
     */
    private void handleConnect(ChannelHandlerContext ctx, MqttConnectMessage connectMsg) {
        String clientId = connectMsg.payload().clientIdentifier();
        boolean hasWill = connectMsg.variableHeader().isWillFlag();
        String willTopic = hasWill ? connectMsg.payload().willTopic() : "N/A";

        System.out.println("时间:"+LocalDateTime.now()+"  [CONN] 设备尝试连接 - clientId: " + clientId + ", 含遗嘱: " + hasWill + ", 遗嘱主题: " + willTopic);

        // 保存 CONNECT 消息供后续 LWT 使用
        ctx.channel().attr(CONNECT_MESSAGE_KEY).set(connectMsg);

        // 回复 CONNACK
        ctx.writeAndFlush(new MqttConnAckMessage(
                new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
                new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, false)
        ));

        System.out.println("时间:"+LocalDateTime.now()+"  [CONN] 已接受设备连接 - clientId: " + clientId);
    }

    /**
     * 处理设备 PUBLISH 消息
     */
    private void handlePublish(ChannelHandlerContext ctx, MqttPublishMessage publishMsg) {
        // 解析主题
        String topic = publishMsg.variableHeader().topicName();
        // 解析负载
        ByteBuf payload = publishMsg.payload();
        // 解析 JSON
        byte[] bytes = new byte[payload.readableBytes()];
        // 读取字节
        payload.readBytes(bytes);
        // 解析 JSON
        String rawPayload = new String(bytes, StandardCharsets.UTF_8);
        // 绑定 camId
        bindCamIdIfNecessary(ctx, topic);
        // 提取 JSON
        String json = extractJson(rawPayload);
        if (json == null) {
            System.err.println("[ERROR] 无效 JSON 负载,丢弃消息 - 内容: " + rawPayload);
            return;
        }
        try {
            // 解析 JSON
            JsonNode root = JSON_MAPPER.readTree(json);
            // 命令
            String cmd = root.path("cmd").asText("");
            // camId
            String camId = ctx.channel().attr(CAM_ID_KEY).get();
            // 心跳
            if ("heartbeat".equals(cmd)) {
                System.out.println("时间:"+LocalDateTime.now()+"  [HEARTBEAT] 收到心跳请求 - camId: " + camId + ", msgId: " + root.path("msgId").asText(""));
                BarrierMqttService barrierMqttService = SpringContextHolder.getBean(BarrierMqttService.class);
                Object object = barrierMqttService.isPcAllowPass(camId);
                String response = String.valueOf(object);
                if(StrUtil.isNotEmpty(response)){
                    System.out.println("时间:"+LocalDateTime.now()+"  心跳应答内容: " + object);
                    sendToDevice(camId, object.toString());
                    return;
                }
                // 回复心跳
                replyHeartbeat(ctx, root);
                return;
            }
            System.out.println("时间:"+LocalDateTime.now()+"  [BUSINESS] 处理业务消息 - camId: " + camId + ", cmd: " + cmd);
            List<String> responses = iotPushService.handleMessage(camId, json);
            // 响应数据接口
            if (responses != null && !responses.isEmpty()) {
                System.out.println("时间:"+LocalDateTime.now()+"  [RESPONSE] 业务层返回 " + responses.size() + " 条响应,准备下发");
                for (String resp : responses) {
                    // 业务发送响应消息
                    sendToDevice(camId, resp);
                }
            }
        } catch (Exception e) {
            System.err.println("[EXCEPTION] 处理 PUBLISH 消息异常 - 主题: " + topic + ", 错误: " + e.getMessage());
        }
    }

    /**
     * 处理 SUBSCRIBE 请求(仅回复 SUBACK,不实际维护订阅关系)
     */
    private void handleSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage subscribeMsg) {
        MqttMessageIdVariableHeader messageIdVarHeader = subscribeMsg.variableHeader();
        int messageId = messageIdVarHeader.messageId();

        // 获取所有订阅的主题
        List<MqttTopicSubscription> topicSubscriptions = subscribeMsg.payload().topicSubscriptions();
        System.out.println("时间:"+LocalDateTime.now()+"  [SUBSCRIBE] 收到订阅请求 - messageId: " + messageId);

        for (MqttTopicSubscription subscription : topicSubscriptions) {
            String topic = subscription.topicName();
            MqttQoS qos = subscription.qualityOfService();
            System.out.println("时间:"+LocalDateTime.now()+"  [SUBSCRIBE] 客户端请求订阅主题: " + topic + ", QoS: " + qos.value());

            // 可选:校验主题是否合法(例如必须是 /device/xxx/get)
            // if (!isValidDownstreamTopic(topic)) { /* 拒绝 */ }
        }

        // 构造 SUBACK 响应(全部授予 QoS=0,因本服务仅支持 QoS=0)
        int[] grantedQos = new int[topicSubscriptions.size()];
        for (int i = 0; i < grantedQos.length; i++) {
            // 强制降级为 QoS=0(与你的 publish 一致)
            grantedQos[i] = 0;
        }

        MqttSubAckPayload subAckPayload = new MqttSubAckPayload(grantedQos);
        MqttSubAckMessage subAckMessage = new MqttSubAckMessage(
                new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
                messageIdVarHeader,
                subAckPayload
        );

        ctx.writeAndFlush(subAckMessage);
        System.out.println("时间:"+LocalDateTime.now()+"  [SUBSCRIBE] 已回复 SUBACK - messageId: " + messageId);
    }

    /**
     * 绑定 camId 到 Channel
     */
    private void bindCamIdIfNecessary(ChannelHandlerContext ctx, String topic) {
        String existing = ctx.channel().attr(CAM_ID_KEY).get();
        if (existing != null) {
            // 已绑定,无需重复操作
            return;
        }
        Matcher m = DEVICE_TOPIC_PATTERN.matcher(topic);
        if (m.matches()) {
            String camId = m.group(1);
            ctx.channel().attr(CAM_ID_KEY).set(camId);
            CAM_CHANNEL_MAP.put(camId, ctx.channel());
            System.out.println("时间:"+LocalDateTime.now()+"  [BIND] 成功绑定 camId 到通道 - camId: " + camId);
        } else {
            System.out.println("时间:"+LocalDateTime.now()+"  [WARN] 无法从主题提取 camId - 主题: " + topic);
        }
    }

    /**
     * 回复心跳
     */
    private void replyHeartbeat(ChannelHandlerContext ctx, JsonNode req) {
        ObjectNode rsp = JSON_MAPPER.createObjectNode();
        rsp.put("cmd", "heartbeatRsp");
        rsp.put("msgId", req.path("msgId").asText(""));
        rsp.put("status", "ok");

        String camId = ctx.channel().attr(CAM_ID_KEY).get();
        if (camId != null) {
            // 发送心跳响应
            sendToDevice(camId, rsp.toString());
            //System.out.println("时间:"+LocalDateTime.now()+"  [HEARTBEAT] 已回复心跳 - camId: " + camId);
        } else {
            System.err.println("[ERROR] 心跳回复失败:camId 未绑定");
        }
    }

    // ==================== 遗嘱处理核心逻辑 ====================

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        String camId = ctx.channel().attr(CAM_ID_KEY).get();
        if (camId != null) {
            CAM_CHANNEL_MAP.remove(camId);
            System.out.println("时间:"+LocalDateTime.now()+"  [DISCONNECT] 设备通道非活跃,已移除映射 - camId: " + camId);
        } else {
            System.out.println("时间:"+LocalDateTime.now()+"  [DISCONNECT] 通道关闭,但未绑定 camId");
        }

        // 检查是否需要触发遗嘱
        MqttConnectMessage connectMsg = ctx.channel().attr(CONNECT_MESSAGE_KEY).get();
        if (connectMsg != null && connectMsg.variableHeader().isWillFlag()) {
            String willTopic = connectMsg.payload().willTopic();
            byte[] willMessage = connectMsg.payload().willMessageInBytes();
            String willPayloadStr = new String(willMessage, StandardCharsets.UTF_8);

            System.out.println("时间:"+LocalDateTime.now()+"  [LWT] 检测到遗嘱标志,准备发布遗嘱 - 主题: " + willTopic);

            Matcher m = DEVICE_TOPIC_PATTERN.matcher(willTopic);
            if (m.matches()) {
                String extractedCamId = m.group(1);
                System.out.println("时间:"+LocalDateTime.now()+"  [LWT] 遗嘱 camId 提取成功: " + extractedCamId + ",内容: " + willPayloadStr);

                // 构造遗嘱消息(QoS=0)
                MqttPublishMessage willPublish = new MqttPublishMessage(
                        new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_MOST_ONCE,
                                connectMsg.variableHeader().isWillRetain(), willMessage.length),
                        new MqttPublishVariableHeader(willTopic, 0),
                        Unpooled.copiedBuffer(willMessage)
                );

                // ⚠️ 注意:此处通道已 inactive,不能通过 ctx 发送!
                // 实际应通过内部消息总线或另一个 MQTT 客户端发布
                System.out.println("时间:"+LocalDateTime.now()+"  [LWT] 遗嘱消息构造完成,需通过其他方式广播(当前仅打印)");
                System.out.println("时间:"+LocalDateTime.now()+"  [LWT][CONTENT] " + willPayloadStr);

                // TODO: 在真实系统中,此处应调用消息中间件或数据库记录
                // 例如:kafkaProducer.send(willTopic, willPayloadStr);
            } else {
                System.err.println("[LWT][ERROR] 遗嘱主题格式非法,拒绝发布 - 主题: " + willTopic);
            }
        } else {
            System.out.println("时间:"+LocalDateTime.now()+"  [LWT] 无遗嘱设置或连接信息缺失,跳过遗嘱发布");
        }

        super.channelInactive(ctx);
    }

    // ==================== 主动下发方法 ====================

    public static void sendToDevice(String camId, String payload) {
        Channel channel = CAM_CHANNEL_MAP.get(camId);
        if (channel == null || !channel.isActive()) {
            System.err.println("[SEND][ERROR] 目标设备离线或通道无效 - camId: " + camId);
            return;
        }

        String topic = "/device/" + camId + "/get";
        byte[] bytes = payload.getBytes(StandardCharsets.UTF_8);
        MqttPublishMessage msg = new MqttPublishMessage(
                new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_MOST_ONCE, false, bytes.length),
                new MqttPublishVariableHeader(topic, 0),
                Unpooled.copiedBuffer(bytes)
        );
        channel.writeAndFlush(msg);
        // 设置心跳响应记录实体(从JSON对象)
        setTcHeartbeatResponse(camId,String.valueOf(msg),payload);
        System.out.println("时间:"+LocalDateTime.now()+"  [SEND] 已向设备下发消息 - camId: " + camId + ", 主题: " + topic + ", 内容: " + payload);
    }

    // ==================== 异常处理 ====================

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        String camId = ctx.channel().attr(CAM_ID_KEY).get();
        System.err.println("[EXCEPTION] 通道异常 - camId: " + camId + ", 原因: " + cause.getMessage());
        cause.printStackTrace();
        ctx.close();
    }

    // ==================== 工具方法 ====================

    private static String extractJson(String s) {
        int i = s.indexOf('{');
        int j = s.lastIndexOf('}');
        if (i >= 0 && j > i) {
            return s.substring(i, j + 1);
        }
        return null;
    }

    /**
     * 设置心跳响应记录实体(从JSON对象)
     * @param responseContent 系统回复内容
     * @return 配置好的实体对象
     */
    private static void setTcHeartbeatResponse(String camId,String msg,String responseContent) {
        TcHeartbeatResponse result = new TcHeartbeatResponse();
        // 设置响应字段
        result.setCmd(camId);
        result.setOriginalJson(msg);
        result.setResponseContent(responseContent);
        // 设置系统字段
        result.setGmtCreate(LocalDateTime.now());
        result.setGmtModified(LocalDateTime.now());
        result.setDelFlag(0); // 正常状态
        CompletableFuture.runAsync(() -> {
            try {
                result.setId(String.valueOf(IdWorker.getId()));
                result.insert();
            } catch (Exception e) {
                // 处理异常
                log.error("收到识别结果固定记录插入失败", e);
            }
        });
    }
}
bash 复制代码
package com.cqcloud.platform.service;

import com.cqcloud.platform.dto.PlateResultData;
import com.google.gson.JsonObject;

import java.util.List;

/**
 * @author weimeilayer@gmail.com
 * @date 💓💕2023年9月8日🐬🐇💓💕
 */
public interface NettyService {
	/**
	 * 处理消息
	 * @param camId
	 * @param jsonMessage
	 * @return
	 */
	List<String> handleMessage(String camId, String jsonMessage);

	/**
	 * 设置车牌识别结果
	 * @param data 识别结果(为 MqttParkingControl.convertToPlateResultData(requestBody)  对象)
	 * @param originalMessage  保存原始JSON消息
	 * @param responseContent  发送下发内容
	 */
	void setTcPlateRecognitionResult(PlateResultData data, JsonObject originalMessage, String responseContent);

	/**
	 * 设置心跳响应
	 * @param responseContent  发送下发内容
	 */
	void setTcPlateRecognitionResultHeart(String devno,String carLicense,String responseContent);
}
bash 复制代码
package com.cqcloud.platform.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.cqcloud.platform.common.RandomUtil;
import com.cqcloud.platform.common.matter.utils.SpringContextHolder;
import com.cqcloud.platform.dto.PlateResultData;
import com.cqcloud.platform.entity.SysSocialDetails;
import com.cqcloud.platform.entity.TcPlateRecognitionResult;
import com.cqcloud.platform.req.Req_BaGateRequestBody;
import com.cqcloud.platform.service.NettyService;
import com.cqcloud.platform.service.SysFileService;
import com.cqcloud.platform.service.SysSocialDetailsService;
import com.cqcloud.platform.tcsf.service.BarrierMqttService;
import com.cqcloud.platform.tcsf.service.BaseWebService;
import com.cqcloud.platform.tcsf.service.task.ParkingHandleTask;
import com.cqcloud.platform.tcsf.utils.DateTimeUtil;
import com.cqcloud.platform.vo.CpCarLog;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;

/**
 * @author weimeilayer@gmail.com
 * @date 💓💕2023年9月8日🐬🐇💓💕
 */
@Slf4j
@Service
@AllArgsConstructor
public class NettyServiceImpl implements NettyService {

    private final BarrierMqttService barrierMqttService;

    @Override
    public List<String> handleMessage(String camId, String message) {
        //System.out.println("Service层处理消息: " + message);
        JsonObject jsonMessage = JsonParser.parseString(message).getAsJsonObject();
        try {
            String cmd = jsonMessage.get("cmd").getAsString();
            String deviceId = jsonMessage.get("devId").getAsString();
            System.out.println("解析到命令: " + cmd + ", 设备ID: " + deviceId);
            if ("plateResult".equals(cmd)) {
                return handlePlateResult(jsonMessage, deviceId);
            }
        } catch (Exception e) {
            System.out.println("消息不是JSON格式"+message);
        }
        // 如果移除 返回默认心跳消息
        return handleHeartbeat(jsonMessage,camId);
    }

    /**
     * 处理车牌识别结果消息
     * @param message 包含车牌识别结果的JSON对象,应包含content字段,content中包含plateNum和isWhitelist
     * @param deviceId 设备ID
     * @return 响应命令列表
     */
    private List<String> handlePlateResult(JsonObject message, String deviceId) {
        try {
            // 提取并封装所有字段
            PlateResultData data = extractPlateResultData(message);

            String id = data.getPlateNum() + deviceId;
            Long t1 = BaseWebService.reqMsgList.get(id);
          
            BaseWebService.reqMsgList.put(id, System.currentTimeMillis());
            // 使用转换方法创建请求体
            Req_BaGateRequestBody requestBody = convertToRequestBody(data);
            // 调用识别 MQTT服务
            if (!StringUtils.isEmpty(data.getPlateNum()) && !StringUtils.isEmpty(deviceId)) {
                try {
                    barrierMqttService.checkInMqtt(requestBody);
                    //BaseWebService.reqMsgList.remove(id);
                } catch (Exception e) {
                    System.out.println("处理结果异常: " + data.getPlateNum());
                }
            }
         
        } catch (Exception e) {
            System.out.println("处理结果时出错: " + e.getMessage());
        }
        // 如果报错 返回默认心跳消息
        return handleHeartbeat(message,deviceId);
    }
 
    /**
     * 检查手动测试配置
     * 如果启用则返回重定向URL,否则返回null
     */
    private List<String> checkManualTestConfig(String camId, String configType) {
        List<String> resultUrls = new ArrayList<>();
        try {
            SysSocialDetails condition = new SysSocialDetails();
            condition.setAppId(camId);
            condition.setType(configType);
            SysSocialDetailsService sysSocialDetailsService = SpringContextHolder.getBean(SysSocialDetailsService.class);
            List<SysSocialDetails> social = sysSocialDetailsService.list(new QueryWrapper<>(condition));
            if (social != null && !social.isEmpty()) {
                for (SysSocialDetails item : social) {
                    if ("7".equals(item.getHearderKey())) {
                        String url = item.getUrl();
                        System.out.println("收到识别结果内容: " + url);
                        resultUrls.add(url);
                    }
                }
            }
        } catch (Exception e) {
            log.error("检查手动测试配置失败", e);
        }
        return resultUrls;
    }

    // 提取并封装数据的方法
    private PlateResultData extractPlateResultData(JsonObject message) {
        PlateResultData data = new PlateResultData();

        // 提取顶层字段
        data.setCmd(message.get("cmd").getAsString());
        data.setMsgId(message.get("msgId").getAsString());
        data.setDevId(message.get("devId").getAsString());
        data.setParkId(message.get("parkId").getAsString());
        data.setDevIp(message.get("devIp").getAsString());
        data.setUtcTs(message.get("utcTs").getAsLong());

        // 提取content对象中的字段
        JsonObject content = message.getAsJsonObject("content");
        data.setType(content.get("type").getAsString());
        data.setPlateNum(content.get("plateNum").getAsString());
        return data;
    }


    /**
     * 处理设备心跳消息
     *
     * @param message 心跳消息内容,类型为JsonObject
     * @param deviceId 设备唯一标识符
     * @return 包含心跳响应消息的字符串列表
     */
    private List<String> handleHeartbeat(JsonObject message, String deviceId) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理心跳消息,设备: " + deviceId);
        // 调用生成心跳响应的方法
        String heartbeatResponse = generateHeartbeatResponse(message);
        // 添加响应
        responses.add(heartbeatResponse);
        return responses;
    }

    /**
     * 处理IO状态变化消息
     *
     * @param message 包含IO状态信息的JSON对象
     * @param deviceId 设备标识符
     * @return 响应消息列表
     */
    private List<String> handleIoStatus(JsonObject message, String deviceId) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理IO状态变化,设备: " + deviceId);
        // 处理IO状态逻辑
        return responses;
    }

    /**
     * 处理RS485数据
     *
     * @param message 包含RS485数据的JSON对象
     * @param deviceId 设备ID
     * @return 响应消息列表
     */
    private List<String> handleRs485Data(JsonObject message, String deviceId) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理RS485数据,设备: " + deviceId);
        // 处理RS485数据逻辑
        return responses;
    }

    /**
     * 处理设备信息
     *
     * @param message 包含设备信息的JSON对象
     * @param deviceId 设备标识符
     * @return 处理结果的响应列表
     */
    private List<String> handleDeviceInfo(JsonObject message, String deviceId) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理设备信息,设备: " + deviceId);
        // 处理设备信息逻辑
        return responses;
    }

    /**
     * 处理未知命令的消息
     *
     * @param message 包含命令信息的JSON对象
     * @param deviceId 发送命令的设备ID
     * @return 包含错误响应的字符串列表
     */
    private List<String> handleUnknownCommand(JsonObject message, String deviceId) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理未知命令,设备: " + deviceId);
        // 返回错误响应
        String errorResponse = generateErrorResponse(message, "unknown_command");
        responses.add(errorResponse);
        return responses;
    }

    /**
     * 处理原始消息并生成响应列表
     *
     * @param message 待处理的原始消息字符串
     * @return 包含处理结果的字符串列表
     */
    private List<String> handleRawMessage(String message) {
        List<String> responses = new ArrayList<>();
        System.out.println("处理原始消息: " + message);
        // 处理非JSON格式的原始消息
        return responses;
    }

    
    /**
     * 生成单个数据的RS485透传指令(兼容旧版本)
     */
    private String generateRs485TransmitCommand(String originalMsgId, String data, int channel) {
        List<String> dataList = new ArrayList<>();
        dataList.add(data);
        return generateRs485TransmitCommand(originalMsgId, dataList, channel);
    }
    /**
     * 工具方法:文本转16进制字符串
     */
    private static String textToHexString(String text) {
        try {
            byte[] bytes = text.getBytes("GBK"); // 显示屏通常使用GBK编码
            StringBuilder hexBuilder = new StringBuilder();
            for (byte b : bytes) {
                hexBuilder.append(String.format("%02X", b));
            }
            return hexBuilder.toString();
        } catch (Exception e) {
            return "";
        }
    }

    /**
     * 生成错误响应
     */
    private String generateErrorResponse(JsonObject message, String errorType) {
        String msgId = message.has("msgId") ? message.get("msgId").getAsString() : generateMessageId();

        JsonObject response = new JsonObject();
        response.addProperty("cmd", "errorRsp");
        response.addProperty("msgId", msgId);
        response.addProperty("status", errorType);

        return response.toString();
    }

    /**
     * 生成符合文档格式的消息ID(20位:13位毫秒时间+7位随机数)
     */
    private static String generateMessageId() {
        long millis = System.currentTimeMillis();
        String random = String.format("%07d", new Random().nextInt(10000000));
        return millis + random;
    }
}

控制器 自动下发测试

bash 复制代码
@SasIgnore(value = false)
@PostMapping("/testMqtt")
public String testMqtt(@RequestBody MqttPushDto dto) {
    String camId = Optional.ofNullable(dto.getCamId())
            .filter(s -> !s.trim().isEmpty()) // 等价于 isNotBlank
            .filter(s -> !s.isEmpty()) // 等价于 isNotEmpty
            .orElse("18xxxxx");
    String iotNum = Optional.ofNullable(dto.getIoNum())
            .filter(s -> !s.trim().isEmpty()) // 等价于 isNotBlank
            .filter(s -> !s.isEmpty()) // 等价于 isNotEmpty
            .orElse("io1");
    MqttCommandSender.sendIoOutput(camId, iotNum, "on");
    return "ok";
}
相关推荐
火车叼位1 小时前
兼容命令行与 Android Studio 的 JDK 策略:从踩坑到方案
后端
likuolei1 小时前
Eclipse 内置浏览器
java·ide·eclipse
IMPYLH1 小时前
Lua 的 pairs 函数
开发语言·笔记·后端·junit·单元测试·lua
俺叫啥好嘞1 小时前
日志输出配置
java·服务器·前端
用户345848285051 小时前
什么是 Java 内存模型(JMM)?
后端
南雨北斗1 小时前
kotlin中的继承和委托
后端
一 乐1 小时前
运动会|基于SpingBoot+vue的高校体育运动会管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·学习·springboot
白露与泡影1 小时前
Spring Boot 4.0 发布总结:新特性、依赖变更与升级指南
java·spring boot·后端
狂奔小菜鸡1 小时前
Day15 | Java内部类详解
java·后端·java ee