Bootstrap

笔记报警管理

1. IOT创建新产品  睡眠检测带  
2. 养老后台  添加了一个设备  睡眠检测带_201_1
3. 新增了模拟器(3个模拟js运行)
4. 创建了消费者组(默认DEFAULT)
5. 创建订阅(3个产品的上报信息 传给DEFAULT)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

消息处理

前面我们已经完成了设备的管理,现在,我们就来处理设备联网之后上报数据的处理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从图中我们可以看到,当设备上报数据到IOT平台之后,业务系统可以从IOT平台中拉取数据到自己的数据库,方便在业务应用中去展示。

在IOT中数据流转是这样的,如下图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在IOT官方文档中,已经提供了对应的接收数据的解决方案,如下链接:

https://help.aliyun.com/zh/iot/developer-reference/connect-a-client-to-iot-platform-by-using-the-sdk-for-java?spm=a2c4g.11186623.0.0.7d7234bdQCi7MQ

官网SDK接入

导入依赖

<!-- amqp 1.0 qpid client -->
 <dependency>
   <groupId>org.apache.qpid</groupId>
   <artifactId>qpid-jms-client</artifactId>
   <version>0.57.0</version>
 </dependency>
 <!-- util for base64-->
 <dependency>
   <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.10</version>
</dependency>

下载Demo

下载地址:https://linkkit-export.oss-cn-shanghai.aliyuncs.com/amqp/amqp-demo.zip

接收数据

我们可以修改里面的参数,包含以下几个重要参数:

  • accessKey 秘钥key

  • accessSecret 秘钥

  • iotInstanceId 公共实例ID

  • clientId:InetAddress.getLocalHost().getHostAddress(); 获取本机ip作为clientId

  • consumerGroupId 消费者组

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

修改之后的代码:

package com.aliyun.iotx.demo;

import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;

import org.apache.commons.codec.binary.Base64;
import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionListener;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AmqpClient {
    private final static Logger logger = LoggerFactory.getLogger(AmqpClient.class);
    /**
     * 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考
     */
    private static String accessKey = "LTAI5tDQKg9F61aJhbmhqVRK";
    private static String accessSecret = "LYUKZH7HQGBoD025pmSq0fQsREaOYD";;
    private static String consumerGroupId = "eraicKJm98cQR0hHgsxb000100";

    //iotInstanceId:实例ID。若是2021年07月30日之前(不含当日)开通的公共实例,请填空字符串。
    private static String iotInstanceId = "iot-06z00frq8umvkx2";

    //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
    //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
    private static String clientId;

    static {
        try {
            clientId = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    //${YourHost}为接入域名,请参见AMQP客户端接入说明文档。
    private static String host = "iot-06z00frq8umvkx2.amqp.iothub.aliyuncs.com";

    // 指定单个进程启动的连接数
    // 单个连接消费速率有限,请参考使用限制,最大64个连接
    // 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接
    private static int connectionCount = 4;

    //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
    private final static ExecutorService executorService = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue(50000));

    public static void main(String[] args) throws Exception {
        List<Connection> connections = new ArrayList<>();

        //参数说明,请参见AMQP客户端接入说明文档。
        for (int i = 0; i < connectionCount; i++) {
            long timeStamp = System.currentTimeMillis();
            //签名方法:支持hmacmd5、hmacsha1和hmacsha256。
            String signMethod = "hmacsha1";

            //userName组装方法,请参见AMQP客户端接入说明文档。
            String userName = clientId + "-" + i + "|authMode=aksign"
                + ",signMethod=" + signMethod
                + ",timestamp=" + timeStamp
                + ",authId=" + accessKey
                + ",iotInstanceId=" + iotInstanceId
                + ",consumerGroupId=" + consumerGroupId
                + "|";
            //计算签名,password组装方法,请参见AMQP客户端接入说明文档。
            String signContent = "authId=" + accessKey + "&timestamp=" + timeStamp;
            String password = doSign(signContent, accessSecret, signMethod);
            String connectionUrl = "failover:(amqps://" + host + ":5671?amqp.idleTimeout=80000)"
                + "?failover.reconnectDelay=30";

            Hashtable<String, String> hashtable = new Hashtable<>();
            hashtable.put("connectionfactory.SBCF", connectionUrl);
            hashtable.put("queue.QUEUE", "default");
            hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
            Context context = new InitialContext(hashtable);
            ConnectionFactory cf = (ConnectionFactory)context.lookup("SBCF");
            Destination queue = (Destination)context.lookup("QUEUE");
            // 创建连接。
            Connection connection = cf.createConnection(userName, password);
            connections.add(connection);

            ((JmsConnection)connection).addConnectionListener(myJmsConnectionListener);
            // 创建会话。
            // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
            // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            connection.start();
            // 创建Receiver连接。
            MessageConsumer consumer = session.createConsumer(queue);
            consumer.setMessageListener(messageListener);
        }

        logger.info("amqp demo is started successfully, and will exit after 60s ");

        // 结束程序运行 
        Thread.sleep(6000 * 1000);
        logger.info("run shutdown");

        connections.forEach(c-> {
            try {
                c.close();
            } catch (JMSException e) {
                logger.error("failed to close connection", e);
            }
        });

        executorService.shutdown();
        if (executorService.awaitTermination(10, TimeUnit.SECONDS)) {
            logger.info("shutdown success");
        } else {
            logger.info("failed to handle messages");
        }
    }

    private static MessageListener messageListener = new MessageListener() {
        @Override
        public void onMessage(final Message message) {
            try {
                //1.收到消息之后一定要ACK。
                // 推荐做法:创建Session选择Session.AUTO_ACKNOWLEDGE,这里会自动ACK。
                // 其他做法:创建Session选择Session.CLIENT_ACKNOWLEDGE,这里一定要调message.acknowledge()来ACK。
                // message.acknowledge();
                //2.建议异步处理收到的消息,确保onMessage函数里没有耗时逻辑。
                // 如果业务处理耗时过程过长阻塞住线程,可能会影响SDK收到消息后的正常回调。
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        processMessage(message);
                    }
                });
            } catch (Exception e) {
                logger.error("submit task occurs exception ", e);
            }
        }
    };

    /**
     * 在这里处理您收到消息后的具体业务逻辑。
     */
    private static void processMessage(Message message) {
        try {
            byte[] body = message.getBody(byte[].class);
            String content = new String(body);
            String topic = message.getStringProperty("topic");
            String messageId = message.getStringProperty("messageId");
            logger.info("receive message"
                + ",\n topic = " + topic
                + ",\n messageId = " + messageId
                + ",\n content = " + content);
        } catch (Exception e) {
            logger.error("processMessage occurs error ", e);
        }
    }

    private static JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
        /**
         * 连接成功建立。
         */
        @Override
        public void onConnectionEstablished(URI remoteURI) {
            logger.info("onConnectionEstablished, remoteUri:{}", remoteURI);
        }

        /**
         * 尝试过最大重试次数之后,最终连接失败。
         */
        @Override
        public void onConnectionFailure(Throwable error) {
            logger.error("onConnectionFailure, {}", error.getMessage());
        }

        /**
         * 连接中断。
         */
        @Override
        public void onConnectionInterrupted(URI remoteURI) {
            logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
        }

        /**
         * 连接中断后又自动重连上。
         */
        @Override
        public void onConnectionRestored(URI remoteURI) {
            logger.info("onConnectionRestored, remoteUri:{}", remoteURI);
        }

        @Override
        public void onInboundMessage(JmsInboundMessageDispatch envelope) {}

        @Override
        public void onSessionClosed(Session session, Throwable cause) {}

        @Override
        public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {}

        @Override
        public void onProducerClosed(MessageProducer producer, Throwable cause) {}
    };

    /**
     * 计算签名,password组装方法,请参见AMQP客户端接入说明文档。
     */
    private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
        SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
        Mac mac = Mac.getInstance(signMethod);
        mac.init(signingKey);
        byte[] rawHmac = mac.doFinal(toSignString.getBytes());
        return Base64.encodeBase64String(rawHmac);
    }
}

以上代码启动之后,并不能接收到数据,因为设备并没有绑定topic,所以需要在物联网IOT平台设置topic,也就是消费者的消费者组

第一:找到 消息转发->服务端订阅->消费者组列表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建一个自己的消费者组

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建好之后可以查看到已经创建好的消费者组,并且自动生成了消费者组id

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

进入刚刚创建的消费者组,然后点击订阅产品,然后创建订阅

需要选择消费者组与推送消息类型(设备上报数据),如下图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

修改demo代码中的消费者组,改为自己创建的消费者组ID

private static String consumerGroupId = "eraicKJm98cQR0hHgsxb000100";

测试

找一个设备进行数据上报

demo代码中绑定对应的消费者组

启动后台代码,可以在日志中查看消费者到的数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接收设备端数据

SDK提供好的这个工具类,我们需要改造这个类,改造内容如下:

  • 让spring进行管理和监听,一旦有数据变化之后,就可以马上消费,可以让这个类实现ApplicationRunner接口,重新run方法
  • 可以在项目中自己配置线程池的使用
  • 所有的可变参数,如实例id、accessKey、accessSecret、consumerGroupId这些统一在配置文件中维护

改造AmqpClient

package com.zzyl.listener;

import com.zzyl.properties.AliIoTConfigProperties;
import org.apache.commons.codec.binary.Base64;
import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionListener;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;

@Component
public class AmqpClient implements ApplicationRunner {
    private final static Logger logger = LoggerFactory.getLogger(AmqpClient.class);

    @Autowired
    private AliIoTConfigProperties aliIoTConfigProperties;

    //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
    //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
    private static String clientId;

    static {
        try {
            clientId = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    // 指定单个进程启动的连接数
    // 单个连接消费速率有限,请参考使用限制,最大64个连接
    // 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接
    private static int connectionCount = 64;

    //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
    @Autowired
    private ExecutorService executorService;

    public void start() throws Exception {
        List<Connection> connections = new ArrayList<>();

        //参数说明,请参见AMQP客户端接入说明文档。
        for (int i = 0; i < connectionCount; i++) {
            long timeStamp = System.currentTimeMillis();
            //签名方法:支持hmacmd5、hmacsha1和hmacsha256。
            String signMethod = "hmacsha1";

            //userName组装方法,请参见AMQP客户端接入说明文档。
            String userName = clientId + "-" + i + "|authMode=aksign"
                    + ",signMethod=" + signMethod
                    + ",timestamp=" + timeStamp
                    + ",authId=" + aliIoTConfigProperties.getAccessKeyId()
                    + ",iotInstanceId=" + aliIoTConfigProperties.getIotInstanceId()
                    + ",consumerGroupId=" + aliIoTConfigProperties.getConsumerGroupId()
                    + "|";
            //计算签名,password组装方法,请参见AMQP客户端接入说明文档。
            String signContent = "authId=" + aliIoTConfigProperties.getAccessKeyId() + "&timestamp=" + timeStamp;
            String password = doSign(signContent, aliIoTConfigProperties.getAccessKeySecret(), signMethod);
            String connectionUrl = "failover:(amqps://" + aliIoTConfigProperties.getHost() + ":5671?amqp.idleTimeout=80000)"
                    + "?failover.reconnectDelay=30";

            Hashtable<String, String> hashtable = new Hashtable<>();
            hashtable.put("connectionfactory.SBCF", connectionUrl);
            hashtable.put("queue.QUEUE", "default");
            hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
            Context context = new InitialContext(hashtable);
            ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF");
            Destination queue = (Destination) context.lookup("QUEUE");
            // 创建连接。
            Connection connection = cf.createConnection(userName, password);
            connections.add(connection);

            ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
            // 创建会话。
            // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
            // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            connection.start();
            // 创建Receiver连接。
            MessageConsumer consumer = session.createConsumer(queue);
            consumer.setMessageListener(messageListener);
        }

        logger.info("amqp  is started successfully, and will exit after server shutdown ");
    }

    private MessageListener messageListener = message -> {
        try {
            //异步处理收到的消息,确保onMessage函数里没有耗时逻辑
            executorService.submit(() -> processMessage(message));
        } catch (Exception e) {
            logger.error("submit task occurs exception ", e);
        }
    };

    /**
     * 在这里处理您收到消息后的具体业务逻辑。
     */
    private void processMessage(Message message) {
        try {
            byte[] body = message.getBody(byte[].class);
            String contentStr = new String(body);
            String topic = message.getStringProperty("topic");
            String messageId = message.getStringProperty("messageId");
            logger.info("receive message"
                    + ",\n topic = " + topic
                    + ",\n messageId = " + messageId
                    + ",\n content = " + contentStr);

        } catch (Exception e) {
            logger.error("processMessage occurs error ", e);
        }
    }

    private JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
        /**
         * 连接成功建立。
         */
        @Override
        public void onConnectionEstablished(URI remoteURI) {
            logger.info("onConnectionEstablished, remoteUri:{}", remoteURI);
        }

        /**
         * 尝试过最大重试次数之后,最终连接失败。
         */
        @Override
        public void onConnectionFailure(Throwable error) {
            logger.error("onConnectionFailure, {}", error.getMessage());
        }

        /**
         * 连接中断。
         */
        @Override
        public void onConnectionInterrupted(URI remoteURI) {
            logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
        }

        /**
         * 连接中断后又自动重连上。
         */
        @Override
        public void onConnectionRestored(URI remoteURI) {
            logger.info("onConnectionRestored, remoteUri:{}", remoteURI);
        }

        @Override
        public void onInboundMessage(JmsInboundMessageDispatch envelope) {
        }

        @Override
        public void onSessionClosed(Session session, Throwable cause) {
        }

        @Override
        public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {
        }

        @Override
        public void onProducerClosed(MessageProducer producer, Throwable cause) {
        }
    };

    /**
     * 计算签名,password组装方法,请参见AMQP客户端接入说明文档。
     */
    private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
        SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
        Mac mac = Mac.getInstance(signMethod);
        mac.init(signingKey);
        byte[] rawHmac = mac.doFinal(toSignString.getBytes());
        return Base64.encodeBase64String(rawHmac);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        start();
    }
}

设备消息订阅

在接收消息之前,我们需要让设备绑定消费组列表,这样才能通过消费组去接收消息

第一:找到 消息转发->服务端订阅->消费者组列表

目前有一个默认的消费组

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第二:创建订阅,让产品与消费组进行关联

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

服务端订阅页面的订阅列表页签下,单击创建订阅

创建订阅对话框,设置参数后单击确认

参数说明
产品选择自己的产品
订阅类型选择AMQP
消费组选择默认消费组
推送消息类型选择设备上报消息

保存设备端数据

思路分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

功能实现

接收到的数据格式

{
    "deviceType":"ActiveInfraredIntrusionDetectors",
    "iotId":"DoXPJsxUkV0Kcw9zrLRaj0rk00",
    "requestId":"1699948275310",
    "checkFailedData":{

    },
    "productKey":"j0rkHpZoAQ3",
    "gmtCreate":1699948275451,
    "deviceName":"yangan_09",
    "items":{
        "CurrentHumidity":{
            "value":75,
            "time":1699948275447
        },
        "BatteryLevel":{
            "value":75,
            "time":1699948275447
        },
        "SmokeSensorState":{
            "value":0,
            "time":1699948275447
        },
        "IndoorTemperature":{
            "value":27,
            "time":1699948275447
        }
    }
}

修改AmqpClient类中的processMessage,方法

    @Autowired
    private DeviceMapper deviceMapper;

    @Autowired
    private DeviceDataService deviceDataService;

    /**
     * 在这里处理您收到消息后的具体业务逻辑。
     */
    private void processMessage(Message message) {
        try {
            byte[] body = message.getBody(byte[].class);
            String contentStr = new String(body);
            String topic = message.getStringProperty("topic");
            String messageId = message.getStringProperty("messageId");
            logger.info("receive message"
                    + ",\n topic = " + topic
                    + ",\n messageId = " + messageId
                    + ",\n content = " + contentStr);

            //解析数据
            Content content = JSONUtil.toBean(contentStr, Content.class);

            //查询设备是否存在
            LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(Device::getIotId,content.getIotId());
            Device device = deviceMapper.selectOne(wrapper);
            if (device == null){
                logger.error(content.getIotId()+"设备不存在");
                return;//结束本次调用
            }

            //从content中获取设备数据,保存到monitor_device_data表
            List<DeviceData> list = new ArrayList<>();
            content.getItems().forEach((k,v)->{
                DeviceData deviceData = new DeviceData();
                deviceData.setDeviceName(device.getDeviceName());//设备名称
                deviceData.setIotId(content.getIotId());//iotId
                deviceData.setNickname(device.getNickname());//备注名称
                deviceData.setProductKey(device.getProductKey());//设备key
                deviceData.setProductName(device.getProductName());//设备名称
                deviceData.setFunctionId(k);//功能名称
                deviceData.setAccessLocation(device.getRemark());//接入位置
                deviceData.setLocationType(device.getLocationType());//位置类型
                deviceData.setPhysicalLocationType(device.getPhysicalLocationType());//物理位置类型
                deviceData.setDeviceDescription(device.getDeviceDescription());//设备描述
                deviceData.setAlarmTime(LocalDateTimeUtil.of(v.getTime()));//数据上报时间
                deviceData.setDataValue(v.getValue()+"");//数据值
                list.add(deviceData);
            });
            //批量保存
            deviceDataService.saveBatch(list);
        } catch (Exception e) {
            logger.error("processMessage occurs error ", e);
        }
    }

测试

(1)启动模拟设备进行上报

(2)启动后端项目,查看数据库是否保存数据成功

查看设备的物模型数据

需求分析

我们先打开原型图,如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中的数据值,是从IOT平台实时获取到的数据值

当点击了某个功能(物模型)的查看数据按钮,则会显示这个功能的历史数据,可以按照时间范围进行检索,如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

时间范围=1小时、24小时、7天、自定义;当选择自定义时,出时间选择器文本框

目前需要两个接口,都是查询

  • 查询物模型列表及其数据:实时查询IOT平台,昨天已经实现,我们现在只需要实现第二个即可
  • 查询单个物模型的历史数据

DeviceDataController

package com.zzyl.controller.web;

import com.zzyl.base.PageResponse;
import com.zzyl.base.ResponseResult;
import com.zzyl.controller.BaseController;
import com.zzyl.dto.DeviceDataPageReqDto;
import com.zzyl.entity.DeviceData;
import com.zzyl.service.DeviceDataService;
import com.zzyl.vo.DeviceDataVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//设备数据
@RestController
public class DeviceDataController extends BaseController {

    @Autowired
    private DeviceDataService deviceDataService;

    //获取设备数据分页结果
    @GetMapping("/device-data/get-page")
    public ResponseResult getDeviceDataPage(DeviceDataPageReqDto deviceDataPageReqDto){
        PageResponse<DeviceData> pageResponse = deviceDataService.findByPage(deviceDataPageReqDto);
        return success(pageResponse);
    }
}

DeviceDataService

    //条件分页查询设备数据
    PageResponse<DeviceData> findByPage(DeviceDataPageReqDto deviceDataPageReqDto);

DeviceDataServiceImpl

    @Override
    public PageResponse<DeviceData> findByPage(DeviceDataPageReqDto dto) {
        //1. 分页参数
        Page<DeviceData> page = new Page<>(dto.getPageNum(), dto.getPageSize());

        //2. 业务参数
        LambdaQueryWrapper<DeviceData> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StrUtil.isNotEmpty(dto.getDeviceName()), DeviceData::getDeviceName, dto.getDeviceName())
                .eq(StrUtil.isNotEmpty(dto.getFunctionId()), DeviceData::getFunctionId, dto.getFunctionId())
                .between(dto.getStartDate() != null && dto.getEndDate() != null, DeviceData::getAlarmTime, dto.getStartDate(), dto.getEndDate());

        //3. 执行查询
        page = getBaseMapper().selectPage(page, wrapper);
        return new PageResponse(page);
    }

测试

打开设备详情,找到物模型数据,点击查看数据按钮,查询数据,是否正常

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

智能床位

需求分析

在床位管理里面有个智能床位,可以展示绑定智能设备的房间或床位以及设备对应的数据

其中数据有每分钟动态展示一次,如果某个设备的数据异常,则会在tab选项卡中进行提示

获取所有带智能设备的楼层

查询包含智能设备的楼层(房间和床位只有有一项有智能设备就要查)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

-- 查询出所有带有智能设备的楼层(楼层中至少一间房有智能设备 或者 楼层中至少某个床绑定了智能设备)
select bf.code,br.code,md.device_name,bb.bed_number,md2.device_name from base_floor bf
    left join base_room br on br.floor_id = bf.id
    left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
    left join base_bed bb on bb.room_id = br.id
    left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
where (md.id is not null or md2.id is not null)
group by bf.code
order by bf.code


-- 如果楼层的智能设备参与
select bf.code,br.code,md.device_name,bb.bed_number,md2.device_name from base_floor bf
    left join base_room br on br.floor_id = bf.id
    left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
    left join base_bed bb on bb.room_id = br.id
    left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
    left join monitor_device md3 on md3.physical_location_type = 0 and md2.binding_location = bf.id
where (md.id is not null or md2.id is not null or md3.id is not null)
group by bf.code
order by bf.code

FloorController

    //获取所有楼层 (智能楼层)
    @GetMapping("/floor/getAllFloorsWithDevice")
    public ResponseResult getAllFloorsWithDevice(){
        List<Floor> list = floorService.getAllFloorsWithDevice();
        return success(list);
    }

FloorService

    //查询包含智能设备的楼层
    List<Floor> getAllFloorsWithDevice();

FloorServiceImpl

    //查询包含智能设备的楼层
    @Override
    public List<Floor> getAllFloorsWithDevice() {
        return getBaseMapper().getAllFloorsWithDevice();
    }

FloorMapper

    //查询包含智能设备的楼层
    List<Floor> getAllFloorsWithDevice();

FloorMapper.xml

    <select id="getAllFloorsWithDevice" resultType="com.zzyl.entity.Floor">
        select bf.*
        from base_floor bf
                 left join base_room br on br.floor_id = bf.id
                 left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
                 left join base_bed bb on bb.room_id = br.id
                 left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
        where md.id is not null
           or md2.id is not null
        group by bf.code
        order by bf.code
    </select>
  • device d关联的是房间的设备,限定条件 d.location_type = 1 and d.physical_location_type = 1,位置类型为固定,物理位置类为房间
  • device dd关联的是床位的设备,限定条件 d.location_type = 1 and d.physical_location_type = 2,位置类型为固定,物理位置类为床位

获取房间中的智能设备及数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

-- 查询指定楼层下  房间中的设备信息   和 床位中设备信息
select *
    from base_room br
    left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
    left join base_bed bb on bb.room_id = br.id
    left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
    left join elder e on e.bed_id = bb.id
where (md.id is not null or md2.id is not null) and br.floor_id = 1

思路分析

根据楼层查询房间或者是床位的设备数据(最新的一条数据),实现方案有两种:

  • 通过楼层关联房间、床位、设备表2次、设备数据表2次共6张表查询对应的数据,此方案设备数据表数据较多,效率不高,不推荐

  • 使用缓存,因为只需要查询最近一次的设备数据,可以把最近一次采集的数据存储到redis中,然后让房间或者床位进行匹配

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    修改AmqpClient类中的processMessage方法,把设备的数据保存到缓存中

        @Autowired
        private DeviceMapper deviceMapper;
    
        @Autowired
        private DeviceDataService deviceDataService;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 在这里处理您收到消息后的具体业务逻辑。
         */
        private void processMessage(Message message) {
            try {
                byte[] body = message.getBody(byte[].class);
                String contentStr = new String(body);
                String topic = message.getStringProperty("topic");
                String messageId = message.getStringProperty("messageId");
                logger.info("receive message"
                        + ",\n topic = " + topic
                        + ",\n messageId = " + messageId
                        + ",\n content = " + contentStr);
    
                //解析数据
                Content content = JSONUtil.toBean(contentStr, Content.class);
    
                //查询设备是否存在
                LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
                wrapper.eq(Device::getIotId, content.getIotId());
                Device device = deviceMapper.selectOne(wrapper);
                if (device == null) {
                    logger.error(content.getIotId() + "设备不存在");
                    return;//结束本次调用
                }
    
                //从content中获取设备数据,保存到monitor_device_data表
                List<DeviceData> list = new ArrayList<>();
                content.getItems().forEach((k, v) -> {
                    DeviceData deviceData = new DeviceData();
                    deviceData.setDeviceName(device.getDeviceName());//设备名称
                    deviceData.setIotId(content.getIotId());//iotId
                    deviceData.setNickname(device.getNickname());//备注名称
                    deviceData.setProductKey(device.getProductKey());//设备key
                    deviceData.setProductName(device.getProductName());//设备名称
                    deviceData.setFunctionId(k);//功能名称
                    deviceData.setAccessLocation(device.getRemark());//接入位置
                    deviceData.setLocationType(device.getLocationType());//位置类型
                    deviceData.setPhysicalLocationType(device.getPhysicalLocationType());//物理位置类型
                    deviceData.setDeviceDescription(device.getDeviceDescription());//设备描述
                    deviceData.setAlarmTime(LocalDateTimeUtil.of(v.getTime()));//数据上报时间
                    deviceData.setDataValue(v.getValue() + "");//数据值
                    list.add(deviceData);
                });
    
                //批量保存
                deviceDataService.saveBatch(list);
    
                //将最新数据覆盖到Redis中
                redisTemplate.opsForHash().put("DEVICE_LAST_DATA", content.getIotId(), JSONUtil.toJsonStr(list));
                
            } catch (Exception e) {
                logger.error("processMessage occurs error ", e);
            }
        }
    

RoomController

    //获取所有房间(智能床位)
    @GetMapping("/room/getRoomsWithDeviceByFloorId/{floorId}")
    public ResponseResult getRoomsWithDeviceByFloorId(@PathVariable(name = "floorId") Long floorId){
        List<RoomVo> list  = roomService.getRoomsWithDeviceByFloorId(floorId);
        return success(list);
    }

RoomService

    //查询房间及关联的床位和设备
    List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId);

RoomServiceImpl

 @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId) {

        List<RoomVo> roomVoList = getBaseMapper().getRoomsWithDeviceByFloorId(floorId);

        roomVoList.forEach(roomVo -> {
            //寻找房间中的设备的数据
            List<DeviceVo> deviceVos = roomVo.getDeviceVos();
            deviceVos.forEach(deviceVo -> {
                //查redis中是否包含数据
                String deviceDataStr = (String) redisTemplate.opsForHash().get("DEVICE_LAST_DATA", deviceVo.getIotId());
                if(StrUtil.isEmpty(deviceDataStr)){
                    return;//在foreach中return是跳过当前循环
                }
                List<DeviceDataVo> deviceDataVos = JSONUtil.toList(deviceDataStr, DeviceDataVo.class);
                deviceVo.setDeviceDataVos(deviceDataVos);
                //设置一个状态,告诉前端设备中有数据
                roomVo.setStatus(2);
            });


            //房间中的床位中的设备的数据
            List<BedVo> bedVoList = roomVo.getBedVoList();
            bedVoList.forEach(bedVo -> {
                bedVo.getDeviceVos().forEach(deviceVo -> {
                    //查redis中是否包含数据
                    String deviceDataStr = (String) redisTemplate.opsForHash().get("DEVICE_LAST_DATA", deviceVo.getIotId());
                    if(StrUtil.isEmpty(deviceDataStr)){
                        return;//在foreach中return是跳过当前循环
                    }
                    List<DeviceDataVo> deviceDataVos = JSONUtil.toList(deviceDataStr, DeviceDataVo.class);
                    deviceVo.setDeviceDataVos(deviceDataVos);
                    //设置一个状态,告诉前端设备中有数据
                    roomVo.setStatus(2);
                    bedVo.setStatus(2);
                });
            });
        });
        return roomVoList;
    }

RoomMapper

    //查询楼层中的房间和床位设备及数据
    List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId);

RoomMapper.xml

    <resultMap id="RoomsWithDeviceResult" type="com.zzyl.vo.RoomVo">
        <id column="id" property="id"></id>
        <result property="code" column="code"/>
        <result property="sort" column="sort"/>
        <result property="sort" column="sort"/>
        <result property="floorId" column="floor_id"/>
        <result property="floorName" column="fname"/>
        <result column="create_by" property="createBy"/>
        <result column="update_by" property="updateBy"/>
        <result column="remark" property="remark"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
        <result column="price" property="price"/>
        <collection property="bedVoList" ofType="com.zzyl.vo.BedVo">
            <id column="bid" property="id"/>
            <result column="bed_number" property="bedNumber"/>
            <result column="bed_status" property="bedStatus"/>
            <result column="room_id" property="roomId"/>
            <result column="ename" property="name"/>
            <result column="eid" property="elderId"/>
            <collection property="deviceVos" ofType="com.zzyl.vo.DeviceVo">
                <id column="b_did" property="id"></id>
                <result column="b_iot_id" property="iotId"/>
                <result column="b_device_name" property="deviceName"/>
                <result column="b_product_key" property="productKey"/>
                <result column="b_product_name" property="productName"/>
            </collection>
        </collection>
        <collection property="deviceVos" ofType="com.zzyl.vo.DeviceVo">
            <id column="r_did" jdbcType="BIGINT" property="id"/>
            <result column="iot_id" jdbcType="VARCHAR" property="iotId"/>
            <result column="device_name" jdbcType="VARCHAR" property="deviceName"/>
            <result column="product_key" jdbcType="VARCHAR" property="productKey"/>
            <result column="product_name" jdbcType="BIT" property="productName"/>
        </collection>
    </resultMap>

    <select id="getRoomsWithDeviceByFloorId" resultMap="RoomsWithDeviceResult">
        select br.*,
               bb.id            as bed_id,
               bb.bed_number,
               bb.bed_status,
               bb.room_id,
               e.name           as ename,
               e.id             as eid,
               md.id            as r_did,
               md.iot_id,
               md.product_key   as product_key,
               md.device_name,
               md.product_name,
               md2.id           as b_did,
               md2.iot_id          b_iot_id,
               md2.product_key  as b_product_key,
               md2.device_name  as b_device_name,
               md2.product_name as b_product_name
        from base_room br
                 left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
                 left join base_bed bb on bb.room_id = br.id
                 left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
                 left join elder e on e.bed_id = bb.id
        where (md.id is not null or md2.id is not null) and br.floor_id = #{floorId}
    </select>

测试

在绑定设备的床位或房间中,启动设备模拟上报数据

在页面中查询上报之后的数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

小程序数据展示

需求分析

我们先来看原型图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在小程序端,绑定了老人之后,就可以查看老人的健康数据。

  • 在家人列表中,点击【健康数据】,跳转到【健康数据】,这里展示的是老人健康数据概览和当下最新指标数据
  • 当点击了某一个指标之后,可以查看某一个指标的历史数据,可以按天或周进行报表展示
  • 异常数据,后边讲完告警模块之后,再来完善异常数据展示

思路分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当查看家人列表的时候,需要查询当前登录人的老人列表,同时老人的其他信息也要查询出来(房间、床位、设备)

当点击健康数据的时候,需要根据当前老人绑定的产品和设备名称到IOT平台查询物模型数据

当点击某项指标数据,比如心率,可以查看这个指标数据的报表信息,报表包含了两种

  • 按照天查询,其中心率、血压、血氧、体温的传输频次:每3小时测量传输一次,所以一天展示8次
  • 按周查询,周数据值显示近七天,天数据为当天数据的平均值

根据上面的思路分析,我们共需要开发4个接口,分别是:家人列表、查询健康数据、按天统计、按周统计

其中的家人列表,在代码中已经提供,我们重点实现剩下的3个

查询健康数据

根据产品ID和设备名称查询物模型数据,我们在前一天已经实现过一次

小程序的查询需要定义在客户管理的控制层,CustomerUserController中实现,如下代码:

    @Autowired
    private Client client;
    @Autowired
    private AliIoTConfigProperties aliIoTConfigProperties;
    
    //查询指定设备属性状态
    @PostMapping("/customer/user/QueryDevicePropertyStatus")
    public ResponseResult QueryDevicePropertyStatus(@RequestBody QueryDevicePropertyStatusRequest request) throws Exception {
        request.setIotInstanceId(aliIoTConfigProperties.getIotInstanceId());
        QueryDevicePropertyStatusResponse deviceStatus = client.queryDevicePropertyStatus(request);
        return ResponseResult.success(deviceStatus.getBody().getData());
    }

测试条件有两个:1)家属端登录人需要绑定带有智能手表设备的老人 2)启动模拟数据上报

目前前端只开发了心率的动态展示和报表展示,大家看效果主要看心率这个物模型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按天统计查询指标数据

sql–mapper-controller-service

sql

select DATE_FORMAT(alarm_time,'%H') as date_time, round(avg(data_value),0) as data_value  from monitor_device_data
where iot_id = 'ip3hTvIkH8Lmejlad8qkk0sjl0'
and function_id='HeartRate'
and alarm_time between '2024-08-25 00:00:00' and  '2024-08-25 23:59:59'
group by date_time

DeviceDataMapper

	//按日查询设备数据
    List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);

DeviceDataMapper.xml

    <select id="queryDeviceDataListByDay" resultType="com.zzyl.vo.DeviceDataGraphVo">
        SELECT
            DATE_FORMAT(alarm_time, '%H:00') AS date_time,
            ROUND(AVG(data_value),0) AS data_value
        FROM
            monitor_device_data
        WHERE
            iot_id = #{iotId}
          AND function_id = #{functionId}
          AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
        GROUP BY
            date_time
        order by date_time
    </select>

AppMemberController

    @Autowired
    private DeviceDataService deviceDataService;

	//按日查询设备数据
    @GetMapping("/customer/user/queryDeviceDataListByDay")
    public ResponseResult queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime) {
        List<DeviceDataGraphVo> list = deviceDataService.queryDeviceDataListByDay(iotId, functionId, startTime, endTime);
        return ResponseResult.success(list);
    }

DeviceDataService

	//按日查询设备数据
    List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime);

DeviceDataServiceImpl

    @Override
    public List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime) {
        //按日聚合数据
        List<DeviceDataGraphVo> deviceDataGraphVoList = getBaseMapper().queryDeviceDataListByDay(iotId, functionId, LocalDateTimeUtil.of(startTime), LocalDateTimeUtil.of(endTime));
        //转换为map
        Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
        
        //构建一个24小时的集合
        List<DeviceDataGraphVo> list = DeviceDataGraphVo.dayInstance(LocalDateTimeUtil.of(startTime));
        list.forEach(d -> {
            //从map获取值,如果为空,则给默认为0
            Double val = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
            d.setDataValue(val);
        });
        return list;
    }

测试:在小程序端点击心率,可以查看报表数据,也可以选择时间来进行检索,如下图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按周统计查询指标数据

AppMemberController

    //按周查询设备数据
    @GetMapping("/customer/user/queryDeviceDataListByWeek")
    public ResponseResult queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime) {
        List<DeviceDataGraphVo> list = deviceDataService.queryDeviceDataListByWeek(iotId, functionId, startTime, endTime);
        return ResponseResult.success(list);
    }

DeviceDataService

    //按周查询设备数据
    List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime);

DeviceDataServiceImpl

    @Override
    public List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime) {
        //按周聚合数据
        List<DeviceDataGraphVo> deviceDataGraphVoList = getBaseMapper().queryDeviceDataListByWeek(iotId, functionId, LocalDateTimeUtil.of(startTime), LocalDateTimeUtil.of(endTime));
        //转换为map
        Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
        
        //构建一个7天的集合
        List<DeviceDataGraphVo> list = DeviceDataGraphVo.weekInstance(LocalDateTimeUtil.of(startTime));
        list.forEach(d -> {
            //获取数据,如果为空,则补为0
            Double dataValue = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
            d.setDataValue(dataValue);
        });
        return list;

DeviceDataMapper

    //按周查询设备数据
    List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);

DeviceDataMapper.xml

    <select id="queryDeviceDataListByWeek" resultType="com.zzyl.vo.DeviceDataGraphVo">
        SELECT
            DATE_FORMAT(alarm_time, '%m.%d') AS date_time,
            ROUND(AVG(data_value),0) AS data_value
        FROM
            monitor_device_data
        WHERE
            iot_id = #{iotId}
          AND function_id = #{functionId}
          AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
        GROUP BY
            date_time
        order by date_time
    </select>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作业

Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
    
    //构建一个7天的集合
    List<DeviceDataGraphVo> list = DeviceDataGraphVo.weekInstance(LocalDateTimeUtil.of(startTime));
    list.forEach(d -> {
        //获取数据,如果为空,则补为0
        Double dataValue = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
        d.setDataValue(dataValue);
    });
    return list;
### DeviceDataMapper

```java
    //按周查询设备数据
    List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);

DeviceDataMapper.xml

    <select id="queryDeviceDataListByWeek" resultType="com.zzyl.vo.DeviceDataGraphVo">
        SELECT
            DATE_FORMAT(alarm_time, '%m.%d') AS date_time,
            ROUND(AVG(data_value),0) AS data_value
        FROM
            monitor_device_data
        WHERE
            iot_id = #{iotId}
          AND function_id = #{functionId}
          AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
        GROUP BY
            date_time
        order by date_time
    </select>

[外链图片转存中…(img-nLTHAHPo-1724850575368)]

作业

定时任务 每天凌晨一点 删除上报时间超过7天

;