Browse Source

完成airNow设备的对接
新增大屏相关接口
新增基于接口的定时任务,实现MQTT的定时上报
优化接口

黄渊昊 1 month ago
parent
commit
4361e58fc5
32 changed files with 1196 additions and 52 deletions
  1. 6 1
      pom.xml
  2. 4 25
      snowy-plugin/snowy-plugin-coldchain/pom.xml
  3. 23 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/mapper/SensorAlarmMapper.java
  4. 0 3
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/mapper/mapping/SensorAlarmMapper.xml
  5. 1 2
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmService.java
  6. 54 8
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmServiceImpl.java
  7. 150 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/config/MqttConfig.java
  8. 39 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/controller/AirNowController.java
  9. 8 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/entity/AirNow.java
  10. 25 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/handler/AirNowScheduled.java
  11. 14 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/service/AirNowService.java
  12. 174 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/service/impl/AirNowServiceImpl.java
  13. 5 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/entity/MonitorDeviceType.java
  14. 6 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/mapper/MonitorDeviceTypeMapper.java
  15. 5 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/param/MonitorDeviceTypeAddParam.java
  16. 5 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/param/MonitorDeviceTypeEditParam.java
  17. 125 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/service/DeviceMonitorScheduleService.java
  18. 19 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/service/impl/MonitorDeviceTypeServiceImpl.java
  19. 69 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/controller/MonitorController.java
  20. 25 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/RegionDto.java
  21. 38 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/SensorAlarmDto.java
  22. 18 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/TrendDto.java
  23. 18 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/param/AlarmTimeParam.java
  24. 25 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/service/MonitorService.java
  25. 307 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/service/impl/MonitorServiceImpl.java
  26. 3 3
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorsearchhistory/controller/SearchHistoryController.java
  27. 2 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/MonitorTargetRegionService.java
  28. 8 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/impl/MonitorTargetRegionServiceImpl.java
  29. 4 0
      snowy-web-app/src/main/java/vip/xiaonuo/Application.java
  30. 3 1
      snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java
  31. 2 0
      snowy-web-app/src/main/resources/_sql/20250422.sql
  32. 11 6
      snowy-web-app/src/main/resources/application.properties

+ 6 - 1
pom.xml

@@ -386,7 +386,12 @@
             <dependency>
                 <groupId>org.springframework.integration</groupId>
                 <artifactId>spring-integration-mqtt</artifactId>
-                <version>5.3.2.RELEASE</version>
+                <version>6.4.3</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.paho</groupId>
+                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+                <version>1.2.5</version>
             </dependency>
 
 <!--

+ 4 - 25
snowy-plugin/snowy-plugin-coldchain/pom.xml

@@ -83,37 +83,16 @@
             <artifactId>snowy-plugin-dev</artifactId>
         </dependency>
 
-        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-integration -->
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-integration</artifactId>
-            <version>2.5.1</version>
-        </dependency>
-
-        <!-- https://mvnrepository.com/artifact/org.springframework.integration/spring-integration-stream -->
-        <dependency>
-            <groupId>org.springframework.integration</groupId>
-            <artifactId>spring-integration-stream</artifactId>
-            <version>5.5.5</version>
-        </dependency>
         <!-- https://mvnrepository.com/artifact/org.springframework.integration/spring-integration-mqtt -->
         <dependency>
             <groupId>org.springframework.integration</groupId>
             <artifactId>spring-integration-mqtt</artifactId>
-            <version>5.5.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
         </dependency>
 
-        <!--<dependency>
-            <groupId>cn.idev.excel</groupId>
-            <artifactId>fastexcel</artifactId>
-            <version>1.0.0</version>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.poi</groupId>
-                    <artifactId>poi</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>-->
         <dependency>
             <groupId>com.itextpdf</groupId>
             <artifactId>itext7-core</artifactId>

+ 23 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/mapper/SensorAlarmMapper.java

@@ -1,8 +1,14 @@
 package vip.xiaonuo.coldchain.core.alarm.mapper;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.ResultType;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.cursor.Cursor;
 import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
+import vip.xiaonuo.coldchain.core.alarm.enums.SensorAlarmType;
 import vip.xiaonuo.coldchain.modular.monitorsearchhistory.dto.TopWarningDto;
 
 import java.util.Date;
@@ -20,5 +26,21 @@ public interface SensorAlarmMapper extends BaseMapper<SensorAlarm> {
     /**
      * 获取报警次数最多的十条记录
      */
-    List<TopWarningDto> getTop10Warning(@Param("orgId") String orgId, @Param("month") String month, @Param("types") List<String> types, @Param("time") Date time);
+    List<TopWarningDto> getTop10Warning(@Param("orgId") String orgId, @Param("types") List<String> types, @Param("time") Date time);
+
+    @Select("SELECT COUNT(*) AS total " +
+            "FROM sensor_alarm " +
+            "WHERE " +
+            "  create_org = #{orgId} " +
+            "  AND type = #{type} " +
+            "  AND sensor_code = #{code} " +
+            "  AND sensor_route = #{route} " +
+            "  AND create_time >= #{startDate} " +
+            "  AND create_time <= #{endDate}")
+    Long countAlarms(@Param("orgId") String orgId,
+                     @Param("type") String type,
+                     @Param("code") String code,
+                     @Param("route") String route,
+                     @Param("startDate") String startDate,
+                     @Param("endDate") String endDate);
 }

+ 0 - 3
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/mapper/mapping/SensorAlarmMapper.xml

@@ -21,9 +21,6 @@
       <foreach collection="types" item="type" open="(" separator="," close=")">
         #{type}
       </foreach>
-      <if test="month != null">
-        AND s.alarm_time LIKE concat(#{month}, '%')
-      </if>
       AND s.CREATE_ORG = #{orgId}
       GROUP BY s.device_id , s.sensor_route) tmp
     ORDER BY tmp.totalWarnings DESC

+ 1 - 2
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmService.java

@@ -30,9 +30,8 @@ public interface SensorAlarmService extends IService<SensorAlarm> {
     /**
      * 获取报警次数最多的十条记录
      *
-     * @param month 查询的月份
      * @param types 报警类型
      * @return
      */
-    List<TopWarningDto> topWarning(String month, List<String> types);
+    List<TopWarningDto> topWarning(List<String> types);
 }

+ 54 - 8
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmServiceImpl.java

@@ -7,8 +7,10 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import jakarta.annotation.Resource;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -45,6 +47,9 @@ import static jakarta.xml.bind.DatatypeConverter.parseTime;
 @Slf4j
 public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, SensorAlarm> implements SensorAlarmService {
 
+    @Resource
+    private SensorAlarmMapper sensorAlarmMapper;
+
     /**
      * app接口
      *
@@ -76,12 +81,12 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
 
     @Override
     public long countAlarms(String startDate, String endDate, String sensorCode, String sensorRoute,SensorAlarmType type) {
-        if(Objects.isNull(type)){
+        // 获取当前登录用户ID
+        String orgId = StpLoginUserUtil.getLoginUser().getOrgId();
+        /*if(Objects.isNull(type)){
             type=SensorAlarmType.DATA_ALARM;
         }
         QueryWrapper<SensorAlarm> queryWrapper = new QueryWrapper<>();
-        // 获取当前登录用户ID
-        String orgId = StpLoginUserUtil.getLoginUser().getOrgId();
         // 创建组织ID条件
         queryWrapper.lambda().eq(SensorAlarm::getCreateOrg, orgId);
         queryWrapper.lambda().eq(SensorAlarm::getType, type.getDeviceCode());
@@ -95,9 +100,46 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
         // 时间范围查询,确保传入的日期格式正确
         if (startDate != null && endDate != null) {
             queryWrapper.lambda().between(SensorAlarm::getCreateTime, startDate, endDate);
-        }
+        }*/
+        Long l = sensorAlarmMapper.countAlarms(orgId, type.getDeviceCode(), sensorCode, sensorRoute, startDate, endDate);
         // 返回符合条件的记录数量
-        return this.count(queryWrapper);
+//        Long l1 = getBaseMapper().selectCount(queryWrapper);
+        return l;
+
+
+
+        /*// 1. 参数预处理(使用final确保不变性)
+        final SensorAlarmType finalType = Objects.requireNonNullElse(type, SensorAlarmType.DATA_ALARM);
+        final String orgId = StpLoginUserUtil.getLoginUser().getOrgId();
+
+        // 2. 使用更高效的查询构建方式
+        LambdaQueryWrapper<SensorAlarm> queryWrapper = Wrappers.lambdaQuery(SensorAlarm.class)
+                .eq(SensorAlarm::getCreateOrg, orgId)
+                .eq(SensorAlarm::getType, finalType.getDeviceCode());
+
+        // 3. 优化条件判断(减少lambda调用开销)
+        if (sensorCode != null) {
+            queryWrapper.eq(SensorAlarm::getSensorCode, sensorCode);
+        }
+        if (sensorRoute != null) {
+            queryWrapper.eq(SensorAlarm::getSensorRoute, sensorRoute);
+        }
+
+        // 4. 时间范围查询优化
+        if (startDate != null && endDate != null) {
+            queryWrapper.between(SensorAlarm::getCreateTime, startDate, endDate);
+        } else {
+            // 如果只提供单个时间参数,可以优化为单边查询
+            if (startDate != null) {
+                queryWrapper.ge(SensorAlarm::getCreateTime, startDate);
+            }
+            if (endDate != null) {
+                queryWrapper.le(SensorAlarm::getCreateTime, endDate);
+            }
+        }
+
+        // 5. 使用更高效的计数方法
+        return getBaseMapper().selectCount(queryWrapper);*/
     }
 
     @Override
@@ -125,12 +167,12 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
     }
 
     @Override
-    public List<TopWarningDto> topWarning(String month, List<String> types) {
+    public List<TopWarningDto> topWarning(List<String> types) {
         SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(new Date());
-        calendar.add(Calendar.MONTH, -1);
-        return getBaseMapper().getTop10Warning(loginUser.getOrgId(), month, types, calendar.getTime());
+        calendar.add(Calendar.DAY_OF_MONTH,-7);
+        return getBaseMapper().getTop10Warning(loginUser.getOrgId(), types, calendar.getTime());
     }
 
     public Page<SensorAlarm> getSensorAlarmPage(MessagePageParam messagePageParam) {
@@ -190,6 +232,10 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
         // 按创建时间降序排序
         queryWrapper.lambda().orderByDesc(SensorAlarm::getCreateTime);
         // 执行查询,传入分页对象
+        Page<Object> page = CommonPageRequest.defaultPage();
+        page.setOptimizeCountSql(false);
+        page.setSearchCount(false);
+        page.setTotal(count(queryWrapper));
         Page<SensorAlarm> sensorAlarmPage = this.page(CommonPageRequest.defaultPage(), queryWrapper).setOptimizeCountSql(false);
         stopWatch.stop();
         log.info(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));

+ 150 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/config/MqttConfig.java

@@ -0,0 +1,150 @@
+package vip.xiaonuo.coldchain.modular.airnow.config;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.core.MessageProducer;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import vip.xiaonuo.coldchain.modular.airnow.entity.AirNow;
+import vip.xiaonuo.coldchain.modular.airnow.service.AirNowService;
+
+@Configuration
+@Slf4j
+public class MqttConfig {
+
+    // MQTT服务器配置
+    private static final String BROKER_URL = "tcp://coldchain.nzkcloud.com:51883";
+    private static final String USERNAME = "coldchain";
+    private static final String PASSWORD = "C123456";
+    private static final String CLIENT_ID = "springboot-client-" + System.currentTimeMillis();
+
+    // 订阅主题 - 所有设备
+    @Value("${SUBSCRIBE_TOPIC}")
+    private String SUBSCRIBE_TOPIC;
+
+    // 发布主题 - 群发所有设备
+    @Value("${PUBLISH_TOPIC}")
+    private String PUBLISH_TOPIC;
+
+    @Resource
+    private AirNowService airNowService;
+
+    @Bean
+    public MqttPahoClientFactory mqttClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setServerURIs(new String[]{BROKER_URL});
+        options.setUserName(USERNAME);
+        options.setPassword(PASSWORD.toCharArray());
+        // 将连接选项应用到工厂
+        factory.setConnectionOptions(options);
+        return factory;
+    }
+
+    @Bean
+    public MessageChannel mqttInputChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean
+    public MessageProducer inbound() {
+        MqttPahoMessageDrivenChannelAdapter adapter =
+                new MqttPahoMessageDrivenChannelAdapter(CLIENT_ID + "-inbound", mqttClientFactory(), SUBSCRIBE_TOPIC);
+        adapter.setCompletionTimeout(5000);
+        adapter.setConverter(new DefaultPahoMessageConverter());
+        adapter.setQos(0); // QoS 0
+        adapter.setOutputChannel(mqttInputChannel());
+        return adapter;
+    }
+
+    @Bean
+    @ServiceActivator(inputChannel = "mqttInputChannel")
+    public MessageHandler handler() {
+        return message -> {
+            String payload = message.getPayload().toString();
+            log.info("Received MQTT message: {}", payload);
+            String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC, String.class);
+            log.info("Received MQTT message from topic: {}", topic);
+            String[] split = topic.split("/");
+            log.info("Received MQTT message from device: {}", split[2]);
+
+            AirNow airNow = new AirNow();
+            airNow.setMac(split[2]);
+
+            // 解析消息
+            JSONObject json = JSONUtil.parseObj(payload);
+            String type = json.getStr("type");
+
+            if ("sensor".equals(type)) {
+                // 处理传感器数据
+                handleSensorData(json, airNow);
+                airNowService.save(airNow);
+            } else if ("device".equals(type)) {
+                // 处理设备状态数据
+                handleDeviceStatus(json, airNow);
+                airNowService.updateDeviceStatus(airNow);
+            } else if ("online".equals(type)) {
+                return;
+            }
+        };
+    }
+
+
+    @Bean
+    public MessageChannel mqttOutboundChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean
+    @ServiceActivator(inputChannel = "mqttOutboundChannel")
+    public MessageHandler mqttOutbound() {
+        MqttPahoMessageHandler messageHandler =
+                new MqttPahoMessageHandler(CLIENT_ID + "-outbound", mqttClientFactory());
+        messageHandler.setAsync(true);
+        messageHandler.setDefaultTopic(PUBLISH_TOPIC);
+        messageHandler.setDefaultQos(0); // QoS 0
+        return messageHandler;
+    }
+
+    private void handleSensorData(JSONObject json, AirNow airNow) {
+        // 解析传感器数据
+        long timestamp = json.getLong("t");
+        int[] data = (int[]) json.getJSONArray("data").toArray(int[].class);
+
+        // 根据文档,索引12是温度(1位小数)
+        float co2 = data[11] / 10.0f;
+
+        airNow.setTimestamp(timestamp);
+        airNow.setCo2(co2);
+
+        log.info("[{}] Co2: {}ppm", timestamp, co2);
+    }
+
+    private void handleDeviceStatus(JSONObject json, AirNow airNow) {
+        // 解析设备状态
+        long timestamp = json.getLong("t");
+        String mac = json.getStr("mac");
+        int battery = json.getInt("battery");
+        String devType = json.getStr("devType");
+
+        airNow.setTimestamp(timestamp);
+        airNow.setDevType(devType);
+        airNow.setBattery(String.valueOf(battery));
+
+        log.info("[{}] Device {} battery level: {}", timestamp, mac, battery);
+    }
+}

+ 39 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/controller/AirNowController.java

@@ -0,0 +1,39 @@
+package vip.xiaonuo.coldchain.modular.airnow.controller;
+
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import vip.xiaonuo.coldchain.modular.airnow.service.AirNowService;
+import vip.xiaonuo.common.pojo.CommonResult;
+
+@RestController
+@RequestMapping("/airnow")
+@Slf4j
+public class AirNowController {
+
+    @Resource
+    private AirNowService mqttService;
+
+    @GetMapping("/request")
+    public CommonResult<String> requestSensorData() {
+        mqttService.querySensorData();
+        return CommonResult.ok();
+    }
+
+    @GetMapping("/status")
+    public CommonResult<String> requestDeviceStatus() {
+        mqttService.queryDeviceStatus();
+        return CommonResult.ok();
+    }
+
+    @GetMapping("/test")
+    public void test() {
+        mqttService.querySensorData();
+        mqttService.queryDeviceStatus();
+    }
+}
+

+ 8 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/entity/airnow.java → snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/entity/AirNow.java

@@ -6,7 +6,11 @@ import lombok.Setter;
 
 @Getter
 @Setter
-public class airnow {
+public class AirNow {
+    /** 时间戳(秒) */
+    @Schema(description = "时间戳(秒)")
+    private Long timestamp;
+
     /** mac地址 */
     @Schema(description = "mac地址")
     private String mac;
@@ -46,4 +50,7 @@ public class airnow {
     /** 历史记录条数 */
     @Schema(description = "历史记录条数")
     private Integer historyCount;
+
+    @Schema(description = "co2")
+    private Float co2;
 }

+ 25 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/handler/AirNowScheduled.java

@@ -0,0 +1,25 @@
+package vip.xiaonuo.coldchain.modular.airnow.handler;
+
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import vip.xiaonuo.coldchain.modular.airnow.service.AirNowService;
+
+@Component
+@Slf4j
+@ConditionalOnProperty(name = "airnow.save.enable", havingValue = "true")
+public class AirNowScheduled {
+
+    @Resource
+    private AirNowService airNowService;
+
+//    @Scheduled(cron = "${airnow.save.crno}")
+    @Transactional
+    public void save() {
+        airNowService.querySensorData();
+        airNowService.queryDeviceStatus();
+    }
+}

+ 14 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/service/AirNowService.java

@@ -0,0 +1,14 @@
+package vip.xiaonuo.coldchain.modular.airnow.service;
+
+import vip.xiaonuo.coldchain.modular.airnow.entity.AirNow;
+
+public interface AirNowService {
+
+    void querySensorData();
+
+    void queryDeviceStatus();
+
+    void save(AirNow airNow);
+
+    void updateDeviceStatus(AirNow airNow);
+}

+ 174 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/airnow/service/impl/AirNowServiceImpl.java

@@ -0,0 +1,174 @@
+package vip.xiaonuo.coldchain.modular.airnow.service.impl;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.github.jfcloud.influxdb.flux.JfcloudFluxDataService;
+import com.github.jfcloud.influxdb.model.JfcloudInFluxEntity;
+import com.github.jfcloud.influxdb.service.JfcloudInfluxDBService;
+import com.influxdb.client.QueryApi;
+import com.influxdb.query.FluxRecord;
+import com.influxdb.query.FluxTable;
+import jakarta.annotation.Resource;
+import lombok.Value;
+import org.apache.poi.ss.formula.functions.T;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.integration.support.MessageBuilder;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import vip.xiaonuo.coldchain.core.alarm.service.check.DefaultSensorAlarmChecker;
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+import vip.xiaonuo.coldchain.core.service.FluxQueryBuilder;
+import vip.xiaonuo.coldchain.modular.airnow.entity.AirNow;
+import vip.xiaonuo.coldchain.modular.airnow.service.AirNowService;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.ZoneOffset;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class AirNowServiceImpl implements AirNowService {
+
+    private static final Logger log = LoggerFactory.getLogger(AirNowServiceImpl.class);
+    @Resource
+    private MessageChannel mqttOutboundChannel;
+    @Resource
+    JfcloudInfluxDBService jfcloudInfluxDBService;
+    @Resource
+    JfcloudFluxDataService jfcloudFluxDataService;
+    @Resource
+    DefaultSensorAlarmChecker defaultSensorAlarmChecker;
+
+    // 查询传感器数据
+    public void querySensorData() {
+        JSONObject request = new JSONObject();
+        request.set("type", "sensor");
+
+        Message<String> message = MessageBuilder.withPayload(request.toString()).build();
+        mqttOutboundChannel.send(message);
+    }
+
+    // 查询设备状态
+    public void queryDeviceStatus() {
+        JSONObject request = new JSONObject();
+        request.set("type", "device");
+
+        Message<String> message = MessageBuilder.withPayload(request.toString()).build();
+        mqttOutboundChannel.send(message);
+    }
+
+    @Async
+    @Override
+    public void save(AirNow airNow) {
+        SensorData sensorData = new SensorData();
+        /*switch (airNow.getBattery()) {
+            case "0":
+                sensorData.setBattery(Float.valueOf(0));
+                break;
+            case "1":
+                sensorData.setBattery(Float.valueOf(25));
+                break;
+            case "2":
+                sensorData.setBattery(Float.valueOf(50));
+                break;
+            case "3":
+                sensorData.setBattery(Float.valueOf(75));
+                break;
+            case "4":
+                sensorData.setBattery(Float.valueOf(100));
+                break;
+        }*/
+        sensorData.setCo2(airNow.getCo2() * 100);
+        sensorData.setDeviceId(airNow.getMac());
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        sensorData.setCreateTime(sdf.format(new Date(airNow.getTimestamp() * 1000L)));
+//        sensorData.setModelName(airNow.getDevType());
+        jfcloudInfluxDBService.writePojo(sensorData);
+    }
+
+    @Async
+    public void updateDeviceStatus(AirNow airNow) {
+        String bucketName = jfcloudFluxDataService.getBucketName();
+        StringBuilder query = new StringBuilder();
+        query.append("from(bucket: \"").append(bucketName).append("\")")
+                .append("  |> range(start: -1h)")
+                .append("  |> filter(fn: (r) => r[\"_measurement\"] == \"sensor_data\")")
+                .append("  |> filter(fn: (r) => r[\"device_id\"] == \"").append(airNow.getMac()).append("\")")
+                .append("  |> last()");
+        log.info(query.toString());
+        // 执行查询并处理结果
+        QueryApi queryApi = jfcloudInfluxDBService.getInfluxDBClient().getQueryApi();
+        List<FluxTable> results = queryApi.query(String.valueOf(query));
+        SensorData sensorData = new SensorData();
+        for (FluxTable result : results) {
+            for (FluxRecord record : result.getRecords()) {
+                Map<String, Object> values = record.getValues();
+                String field = String.valueOf(values.get("_field"));
+                switch (field) {
+                    case "co2":
+                        Float co2 = Float.valueOf(String.valueOf(values.get("_value")));
+                        sensorData.setCo2(co2);
+                        break;
+                    case "create_time":
+                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+                        // 2019-05-21T08:44:00Z 对应的时间格式 yyyy-MM-dd'T'HH:mm:ss'Z'
+                        Date date = null;
+
+                        try {
+                            // 使用 DateTimeFormatter 解析 ISO 8601 格式的日期时间字符串
+                            DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+                            OffsetDateTime offsetDateTime = OffsetDateTime.parse(String.valueOf(values.get("_time")), formatter);
+
+                            // 转换为本地时间并加 8 小时(假设需要转换为东八区时间)
+                            offsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).plusHours(8);
+
+                            // 格式化为 "yyyy-MM-dd HH:mm:ss" 的字符串
+                            String createTime = offsetDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+                            sensorData.setCreateTime(createTime);
+                        } catch (Exception e) {
+                            throw new RuntimeException("日期解析失败: " + e.getMessage(), e);
+                        }
+                        break;
+                    case "plugInStatus":
+                        sensorData.setPlugInStatus(String.valueOf(values.get("_value")));
+                        break;
+                    case "type":
+                        sensorData.setType(Integer.parseInt(String.valueOf(values.get("_value"))));
+                        break;
+                }
+                String deviceId = (String)values.get("device_id");
+                sensorData.setDeviceId(deviceId);
+            }
+        }
+        switch (airNow.getBattery()) {
+            case "0":
+                sensorData.setBattery(Float.valueOf(0));
+                break;
+            case "1":
+                sensorData.setBattery(Float.valueOf(25));
+                break;
+            case "2":
+                sensorData.setBattery(Float.valueOf(50));
+                break;
+            case "3":
+                sensorData.setBattery(Float.valueOf(75));
+                break;
+            case "4":
+                sensorData.setBattery(Float.valueOf(100));
+                break;
+        }
+        sensorData.setModelName(airNow.getDevType());
+        jfcloudInfluxDBService.writePojo(sensorData);
+        defaultSensorAlarmChecker.checkAlarm(sensorData);
+    }
+}

+ 5 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/entity/MonitorDeviceType.java

@@ -74,6 +74,11 @@ public class MonitorDeviceType extends CommonEntity {
     @Schema(description = "设备参数集合")
     private String parameters;
 
+    /**
+     * 数据上报间隔
+     */
+    private String uploadInterval;
+
     /** 报警上限 */
     @Schema(description = "报警上限")
     private Float temperatureUp;

+ 6 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/mapper/MonitorDeviceTypeMapper.java

@@ -13,8 +13,11 @@
 package vip.xiaonuo.coldchain.modular.monitordevicetype.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Select;
 import vip.xiaonuo.coldchain.modular.monitordevicetype.entity.MonitorDeviceType;
 
+import java.util.List;
+
 /**
  * 监控设备型号Mapper接口
  *
@@ -22,4 +25,7 @@ import vip.xiaonuo.coldchain.modular.monitordevicetype.entity.MonitorDeviceType;
  * @date  2024/11/25 10:15
  **/
 public interface MonitorDeviceTypeMapper extends BaseMapper<MonitorDeviceType> {
+    @Select("SELECT code, upload_interval FROM monitor_device_type " +
+            "WHERE code = \"AN-CW\"")
+    List<MonitorDeviceType> selectScheduledTypes();
 }

+ 5 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/param/MonitorDeviceTypeAddParam.java

@@ -59,6 +59,11 @@ public class MonitorDeviceTypeAddParam {
     @Schema(description = "设备参数集合")
     private String parameters;
 
+    /**
+     * 数据上报间隔
+     */
+    private Integer uploadInterval;
+
     /** 删除标志 */
     @Schema(description = "删除标志")
     private String isDeleted;

+ 5 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/param/MonitorDeviceTypeEditParam.java

@@ -60,6 +60,11 @@ public class MonitorDeviceTypeEditParam {
     @Schema(description = "设备参数集合")
     private String parameters;
 
+    /**
+     * 数据上报间隔
+     */
+    private Integer uploadInterval;
+
     /** 删除标志 */
     @Schema(description = "删除标志")
     private String isDeleted;

+ 125 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/service/DeviceMonitorScheduleService.java

@@ -0,0 +1,125 @@
+package vip.xiaonuo.coldchain.modular.monitordevicetype.service;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.scheduling.support.CronTrigger;
+import org.springframework.stereotype.Service;
+import vip.xiaonuo.coldchain.modular.airnow.service.AirNowService;
+import vip.xiaonuo.coldchain.modular.monitordevicetype.entity.MonitorDeviceType;
+import vip.xiaonuo.coldchain.modular.monitordevicetype.mapper.MonitorDeviceTypeMapper;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+
+@Service
+@Slf4j
+public class DeviceMonitorScheduleService {
+
+    @Autowired
+    private MonitorDeviceTypeMapper deviceTypeMapper;
+
+    @Autowired
+    private ThreadPoolTaskScheduler taskScheduler;
+
+    @Resource
+    private AirNowService airNowService;
+
+    private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
+
+    private final Map<String, String> cronMap = new ConcurrentHashMap<>();
+
+    /**
+     * 初始化定时任务
+     */
+    @PostConstruct
+    public void initScheduledTasks() {
+        // 从monitor_device_type表获取需要定时监控的设备类型
+        List<MonitorDeviceType> deviceTypes = deviceTypeMapper.selectScheduledTypes();
+
+        // 为每种设备类型创建定时任务
+        deviceTypes.forEach(deviceType -> {
+            if (StrUtil.isNotBlank(deviceType.getUploadInterval())) {
+                scheduleDeviceMonitoring(deviceType);
+            }
+        });
+    }
+
+    /**
+     * 调度设备监控任务
+     */
+    private void scheduleDeviceMonitoring(MonitorDeviceType deviceType) {
+        String taskKey = "monitor_" + deviceType.getCode();
+
+        // 取消已存在的任务
+        cancelTask(taskKey);
+
+        // 创建定时任务
+        Runnable task = () -> monitorDevicesByType(deviceType.getCode());
+        ScheduledFuture<?> future = taskScheduler.schedule(task, new CronTrigger(deviceType.getUploadInterval())
+        );
+
+        scheduledTasks.put(taskKey, future);
+        cronMap.put(taskKey, deviceType.getUploadInterval());
+    }
+
+    /**
+     * 执行设备监控
+     */
+    private void monitorDevicesByType(String deviceTypeCode) {
+        // 实现具体的逻辑
+        log.info("执行设备类型[{}]的定时监控任务,时间{}", deviceTypeCode, LocalDateTime.now());
+
+        if (deviceTypeCode.equals("AN-CW")) {
+            // 先获取设备状态,后获取influxdb中的数据更新传感器数据
+            airNowService.querySensorData();
+            airNowService.queryDeviceStatus();
+        } else {
+            log.info("该设备暂无适配");
+        }
+    }
+
+    /**
+     * 取消任务
+     */
+    private void cancelTask(String taskKey) {
+        ScheduledFuture<?> future = scheduledTasks.get(taskKey);
+        if (future != null) {
+            future.cancel(true);
+            scheduledTasks.remove(taskKey);
+        }
+    }
+
+    /**
+     * 刷新定时任务(当monitor_device_type表数据变更时调用)
+     */
+//    @Scheduled(cron = "0 0/59 * * * ?")
+    @Scheduled(cron = "0/10 * * * * ?")
+    public void refreshScheduledTasks() {
+        Map<String, String> newCronMap = new ConcurrentHashMap<>();
+        // 从monitor_device_type表获取需要定时监控的设备类型
+        List<MonitorDeviceType> deviceTypes = deviceTypeMapper.selectScheduledTypes();
+        deviceTypes.forEach(deviceType -> {
+            if (StrUtil.isNotBlank(deviceType.getUploadInterval())) {
+                String taskKey = "monitor_" + deviceType.getCode();
+                newCronMap.put(taskKey, deviceType.getUploadInterval());
+            }
+        });
+
+        if (!newCronMap.equals(cronMap)) {
+            // 取消所有现有任务
+            scheduledTasks.values().forEach(future -> future.cancel(true));
+            scheduledTasks.clear();
+
+            // 重新初始化任务
+            initScheduledTasks();
+        }
+    }
+}

+ 19 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/service/impl/MonitorDeviceTypeServiceImpl.java

@@ -62,7 +62,15 @@ public class MonitorDeviceTypeServiceImpl extends ServiceImpl<MonitorDeviceTypeM
         } else {
             queryWrapper.lambda().orderByAsc(MonitorDeviceType::getSortCode);
         }
-        return this.page(CommonPageRequest.defaultPage(), queryWrapper);
+        Page<MonitorDeviceType> page = this.page(CommonPageRequest.defaultPage(), queryWrapper);
+        for (MonitorDeviceType record : page.getRecords()) {
+            if (StrUtil.isNotBlank(record.getUploadInterval())) {
+                String[] split = record.getUploadInterval().split(" ");
+                String[] split1 = split[1].split("/");
+                record.setUploadInterval(split1[1]);
+            }
+        }
+        return page;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -78,6 +86,11 @@ public class MonitorDeviceTypeServiceImpl extends ServiceImpl<MonitorDeviceTypeM
         if (this.getByName(monitorDeviceType.getName())) {
             throw new CommonException("设备名已注册使用:{}", monitorDeviceType.getName());
         }
+        if (monitorDeviceTypeAddParam.getUploadInterval() < 60 && monitorDeviceTypeAddParam.getUploadInterval() >= 1) {
+            monitorDeviceType.setUploadInterval("0 0/" + monitorDeviceTypeAddParam.getUploadInterval() + " * * * ?");
+        } else {
+            throw new CommonException("上传间隔必须在0-60min之间");
+        }
         this.save(monitorDeviceType);
     }
 
@@ -139,6 +152,11 @@ public class MonitorDeviceTypeServiceImpl extends ServiceImpl<MonitorDeviceTypeM
                 throw new CommonException("设备编码已注册使用:{}", deviceType.getCode());
             }
         }
+        if (monitorDeviceTypeEditParam.getUploadInterval() < 60 && monitorDeviceTypeEditParam.getUploadInterval() >= 1) {
+            monitorDeviceType.setUploadInterval("0 0/" + monitorDeviceTypeEditParam.getUploadInterval() + " * * * ?");
+        } else {
+            throw new CommonException("上传间隔必须在0-60min之间");
+        }
         this.updateById(monitorDeviceType);
     }
 

+ 69 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/controller/MonitorController.java

@@ -0,0 +1,69 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
+import vip.xiaonuo.coldchain.modular.monitordevice.entity.MonitorDevice;
+import vip.xiaonuo.coldchain.modular.monitornotice.entity.Trend;
+import vip.xiaonuo.coldchain.modular.monitornotice.param.TrendParam;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.RegionDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.SensorAlarmDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.TrendDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.service.MonitorService;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
+import vip.xiaonuo.common.pojo.CommonResult;
+
+import java.util.List;
+
+@Tag(name = "监控大屏控制器")
+@RestController
+@RequestMapping(("/coldchain/monitorscreen"))
+public class MonitorController {
+    @Resource
+    private MonitorService monitorService;
+    @Resource
+    private MonitorTargetRegionService monitorTargetRegionService;
+
+    @Operation(summary = "获取设备列表")
+    @GetMapping("/region/device")
+    public CommonResult<List<RegionDto>> getRegionList() {
+        return CommonResult.data(monitorService.getRegionList());
+    }
+
+    @Operation(summary = "获取预警动态(1d,3d,1w,1m)")
+    @GetMapping("/warning-info")
+    public CommonResult<List<SensorAlarm>> getWarningInfo(String time) {
+        return CommonResult.data(monitorService.getWarningInfo(time));
+    }
+
+    @Operation(summary = "获取预警情况柱状图(1d,3d,1w,1m)")
+    @GetMapping("/warning-bar")
+    public CommonResult<List<SensorAlarmDto>> getWarningBar(String time) {
+        return CommonResult.data(monitorService.getWarningInfoBar(time));
+    }
+
+    @Operation(summary = "获取设备清单")
+    @GetMapping("/monitor/device")
+    public CommonResult<List<MonitorDevice>> getDeviceList() {
+        return CommonResult.data(monitorService.getDeviceList());
+    }
+
+    @Operation(summary = "获取点位趋势图")
+    @GetMapping("/trend")
+    public CommonResult<TrendDto> getTrend(TrendParam trendParam) {
+        return CommonResult.data(monitorService.getTrend(trendParam));
+    }
+
+    @Operation(summary = "获取设备点位列表")
+    @GetMapping("/region/list")
+    public CommonResult<List<MonitorTargetRegion>> getRegions() {
+        return CommonResult.data(monitorTargetRegionService.myList());
+    }
+}
+

+ 25 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/RegionDto.java

@@ -0,0 +1,25 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.dto;
+
+import lombok.Data;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
+
+@Data
+public class RegionDto {
+    private MonitorTargetRegion monitorTargetRegion;
+
+    private Float temperature;
+
+    private Float humidity;
+
+    private Float co2;
+
+    private String orgName;
+
+    private String location;
+
+    private String time;
+
+    private String status;
+
+    private String deviceType;
+}

+ 38 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/SensorAlarmDto.java

@@ -0,0 +1,38 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class SensorAlarmDto {
+
+    /**
+     * 设备ID,指示告警是由哪个设备触发的
+     */
+    @Schema(description = "设备ID,指示告警是由哪个设备触发的")
+    private String deviceId;
+
+    /**
+     * 设备名称,给出触发告警的设备的名称(可选)
+     */
+    @Schema(description = "设备名称,给出触发告警的设备的名称(可选)")
+    private String deviceName;
+
+    /**
+     * 传感器路数
+     */
+    @Schema(description = "传感器路数")
+    private Integer sensorRoute;
+
+    /**
+     * 温度异常次数
+     */
+    @Schema(description = "数据异常次数")
+    private Integer dataExceptionCount;
+
+    /**
+     * 设备异常次数
+     */
+    @Schema(description = "设备异常次数")
+    private Integer deviceExceptionCount;
+}

+ 18 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/dto/TrendDto.java

@@ -0,0 +1,18 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+@Data
+public class TrendDto {
+    private LinkedHashSet<String> xData = new LinkedHashSet<>();
+
+    private List<Float> temperature = new ArrayList<>();
+
+    private List<Float> humidity = new ArrayList<>();
+
+    private List<Float> co2 = new ArrayList<>();
+}

+ 18 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/param/AlarmTimeParam.java

@@ -0,0 +1,18 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.param;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AlarmTimeParam {
+    private Date createTime;
+
+    private String alarmTime;
+
+    private String deviceName;
+
+    private String priority;
+
+    private String alarmType;
+}

+ 25 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/service/MonitorService.java

@@ -0,0 +1,25 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.service;
+
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
+import vip.xiaonuo.coldchain.modular.monitordevice.entity.MonitorDevice;
+import vip.xiaonuo.coldchain.modular.monitornotice.entity.Trend;
+import vip.xiaonuo.coldchain.modular.monitornotice.param.TrendParam;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.RegionDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.SensorAlarmDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.TrendDto;
+import vip.xiaonuo.common.pojo.CommonResult;
+
+import java.util.List;
+
+public interface MonitorService {
+
+    List<RegionDto> getRegionList();
+
+    List<MonitorDevice> getDeviceList();
+
+    List<SensorAlarm> getWarningInfo(String s);
+
+    List<SensorAlarmDto> getWarningInfoBar(String s);
+
+    TrendDto getTrend(TrendParam trendParam);
+}

+ 307 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorscreen/service/impl/MonitorServiceImpl.java

@@ -0,0 +1,307 @@
+package vip.xiaonuo.coldchain.modular.monitorscreen.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.annotation.Resource;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
+import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
+import vip.xiaonuo.coldchain.core.alarm.service.SensorAlarmService;
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+import vip.xiaonuo.coldchain.modular.app.param.AppDeviceQueryParams;
+import vip.xiaonuo.coldchain.modular.app.param.SensorEchartDataResult;
+import vip.xiaonuo.coldchain.modular.app.service.AppDeviceService;
+import vip.xiaonuo.coldchain.modular.monitordevice.entity.MonitorDevice;
+import vip.xiaonuo.coldchain.modular.monitordevice.service.MonitorDeviceService;
+import vip.xiaonuo.coldchain.modular.monitordevicetype.entity.MonitorDeviceType;
+import vip.xiaonuo.coldchain.modular.monitordevicetype.service.MonitorDeviceTypeService;
+import vip.xiaonuo.coldchain.modular.monitornotice.entity.Trend;
+import vip.xiaonuo.coldchain.modular.monitornotice.param.TrendParam;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.RegionDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.SensorAlarmDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.dto.TrendDto;
+import vip.xiaonuo.coldchain.modular.monitorscreen.param.AlarmTimeParam;
+import vip.xiaonuo.coldchain.modular.monitorscreen.service.MonitorService;
+import vip.xiaonuo.coldchain.modular.monitortarget.entity.MonitorTarget;
+import vip.xiaonuo.coldchain.modular.monitortarget.service.MonitorTargetService;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
+import vip.xiaonuo.common.enums.CommonDeleteFlagEnum;
+import vip.xiaonuo.common.pojo.CommonResult;
+
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Service
+public class MonitorServiceImpl implements MonitorService {
+
+    @Resource
+    private MonitorTargetRegionService monitorTargetRegionService;
+    @Resource
+    private MonitorDeviceService monitorDeviceService;
+    @Resource
+    private SensorAlarmService sensorAlarmService;
+    @Resource
+    private AppDeviceService appDeviceService;
+    @Resource
+    private MonitorDeviceTypeService monitorDeviceTypeService;
+    @Resource
+    private MonitorTargetService monitorTargetService;
+
+    /*@Override
+    public List<RegionDto> getRegionList() {
+        List<RegionDto> regionDtoList = new ArrayList<>();
+        SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
+        LambdaQueryWrapper<MonitorTargetRegion> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(MonitorTargetRegion::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE)
+                .eq(MonitorTargetRegion::getCreateOrg, loginUser.getOrgId());
+        List<MonitorTargetRegion> regionList = monitorTargetRegionService.list();
+        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
+        if (!regionList.isEmpty()) {
+            for (MonitorTargetRegion region : regionList) {
+                RegionDto regionDto = new RegionDto();
+                regionDto.setMonitorTargetRegion(region);
+                SensorData sensorData = monitorDeviceService.queryLatestDataByDeviceIdAndRoads(region.getDeviceCode(), region.getSensorRoute());
+                if (ObjectUtil.isNull(sensorData)) {
+                    continue;
+                }
+                regionDto.setTemperature(sensorData.getTemperature());
+                regionDto.setHumidity(sensorData.getHumidity());
+                regionDto.setCo2(sensorData.getCo2());
+                regionDto.setTime(time);
+                regionDto.setOrgName(loginUser.getOrgName());
+                regionDtoList.add(regionDto);
+            }
+        }
+        return regionDtoList;
+    }*/
+
+    @Override
+    public List<RegionDto> getRegionList() {
+        SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
+        LambdaQueryWrapper<MonitorTargetRegion> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(MonitorTargetRegion::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE)
+                .eq(MonitorTargetRegion::getCreateOrg, loginUser.getOrgId());
+        List<MonitorTargetRegion> regionList = monitorTargetRegionService.list(queryWrapper); // 修复查询条件
+        List<RegionDto> regionDtoList = new ArrayList<>(regionList.size()); // 预分配容量
+
+        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); // 时间格式化移至循环外
+        String orgName = loginUser.getOrgName(); // 避免重复调用
+
+        List<MonitorTarget> monitorTargetList = monitorTargetService.myList(true);
+        List<MonitorDeviceType> monitorDeviceTypeList = monitorDeviceTypeService.myList(null);
+
+        regionList.parallelStream().forEach(region -> { // 并行处理
+            RegionDto regionDto = new RegionDto();
+            for (MonitorDeviceType deviceType : monitorDeviceTypeList) {
+                if (region.getModelName().equals(deviceType.getCode())) {
+                    regionDto.setDeviceType(deviceType.getName());
+                }
+            }
+            regionDto.setMonitorTargetRegion(region);
+
+            for (MonitorTarget monitorTarget : monitorTargetList) {
+                if (monitorTarget.getId().equals(region.getMonitorTargetId())) {
+                    switch (monitorTarget.getStatus()) {
+                        case "0":
+                        default:
+                            regionDto.setStatus("正常");
+                            break;
+                        case "1":
+                            regionDto.setStatus("停用");
+                            break;
+                    }
+                }
+            }
+
+            SensorData sensorData = monitorDeviceService.queryLatestDataByDeviceIdAndRoads(
+                    region.getDeviceCode(), region.getSensorRoute());
+            if (ObjectUtil.isNotNull(sensorData)) {
+                regionDto.setTemperature(sensorData.getTemperature());
+                regionDto.setHumidity(sensorData.getHumidity());
+                regionDto.setCo2(sensorData.getCo2());
+                regionDto.setTime(time);
+                regionDto.setOrgName(orgName);
+            }
+            synchronized (regionDtoList) { // 确保线程安全
+                regionDtoList.add(regionDto);
+            }
+        });
+        return regionDtoList;
+    }
+
+    public List<MonitorDevice> getDeviceList() {
+        SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
+        LambdaQueryWrapper<MonitorDevice> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(MonitorDevice::getCreateOrg, loginUser.getOrgId()).eq(MonitorDevice::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE);
+        List<MonitorDevice> monitorDeviceList = monitorDeviceService.list(queryWrapper);
+        List<MonitorDeviceType> monitorDeviceTypeList = monitorDeviceTypeService.myList(null);
+        for (MonitorDevice monitorDevice : monitorDeviceList) {
+            for (MonitorDeviceType deviceType : monitorDeviceTypeList) {
+                if (deviceType.getCode().equals(monitorDevice.getModelName())) {
+                    monitorDevice.setTypeName(deviceType.getName());
+                }
+            }
+        }
+        return monitorDeviceList;
+    }
+
+    @Override
+    public List<SensorAlarm> getWarningInfo(String s) {
+        List<SensorAlarm> sensorAlarmList = sensorAlarmService.list(getSensorAlarmQueryWrapper(s));
+        // 1. 按deviceId分组
+        Map<String, List<SensorAlarm>> groupedByDevice = sensorAlarmList.stream()
+                .filter(a -> a.getDeviceId() != null && a.getAlarmTime() != null)
+                .collect(Collectors.groupingBy(SensorAlarm::getDeviceId));
+
+        List<SensorAlarm> result = new ArrayList<>();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+        groupedByDevice.forEach((deviceId, group) -> {
+            // 2. 按时间降序排序(最新数据在前)
+            group.sort((a1, a2) -> {
+                LocalDateTime t1 = LocalDateTime.parse(a1.getAlarmTime(), formatter);
+                LocalDateTime t2 = LocalDateTime.parse(a2.getAlarmTime(), formatter);
+                return t2.compareTo(t1); // 降序
+            });
+
+            // 3. 遍历去重逻辑
+            List<SensorAlarm> filteredGroup = new ArrayList<>();
+            LocalDateTime lastTime = null;
+            for (SensorAlarm alarm : group) {
+                LocalDateTime currentTime = LocalDateTime.parse(alarm.getAlarmTime(), formatter);
+                if (lastTime == null) {
+                    filteredGroup.add(alarm);
+                    lastTime = currentTime;
+                } else {
+                    // 计算时间间隔
+                    Duration duration = Duration.between(currentTime, lastTime);
+                    if (duration.toMinutes() >= 5) { // 间隔≥5分钟则保留
+                        filteredGroup.add(alarm);
+                        lastTime = currentTime;
+                    }
+                }
+            }
+            result.addAll(filteredGroup);
+        });
+
+        return result;
+    }
+
+    @Override
+    public List<SensorAlarmDto> getWarningInfoBar(String s) {
+        LambdaQueryWrapper<SensorAlarm> queryWrapper = getSensorAlarmQueryWrapper(s);
+        List<SensorAlarm> sensorAlarmList = sensorAlarmService.list(queryWrapper);
+
+        //按 sensorCode 分组,并统计不同 type 的告警次数
+        Map<String, Map<String, Long>> stats = sensorAlarmList.stream()
+                .collect(Collectors.groupingBy(
+                        SensorAlarm::getDeviceId,
+                        Collectors.groupingBy(
+                                SensorAlarm::getType,
+                                Collectors.counting()
+                        )
+                ));
+
+        //转换为 SensorAlarmDto 列表
+        return stats.entrySet().stream()
+                .map(entry -> {
+                    String deviceId = entry.getKey();
+                    Map<String, Long> typeCounts = entry.getValue();
+
+                    // 获取第一条记录(用于填充 deviceName 等字段)
+                    SensorAlarm sample = sensorAlarmList.stream()
+                            .filter(alarm -> alarm.getDeviceId().equals(deviceId))
+                            .findFirst()
+                            .orElse(null);
+
+                    SensorAlarmDto dto = new SensorAlarmDto();
+                    dto.setDeviceId(deviceId);
+                    if (sample != null) {
+                        dto.setDeviceName(sample.getDeviceName());  // 填充 deviceName
+                        dto.setSensorRoute(sample.getSensorRoute());  // 填充 sensorRoute
+                    }
+
+                    // 统计 type 数量
+                    typeCounts.forEach((type, count) -> {
+                        if (type.equals("0")) {
+                            dto.setDataExceptionCount(count.intValue());
+                            dto.setDeviceExceptionCount(0);
+                        } else if (type.equals("1")) {
+                            dto.setDataExceptionCount(0);
+                            dto.setDeviceExceptionCount(count.intValue());
+                        }
+                    });
+                    return dto;
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public TrendDto getTrend(TrendParam trendParam) {
+        MonitorDevice monitorDevice = monitorDeviceService.getById(trendParam.getDeviceId());
+        MonitorTargetRegion oneByDeviceCodeAndSensorNo = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(monitorDevice.getDeviceCode(), trendParam.getRoads());
+        AppDeviceQueryParams appDeviceQueryParams = new AppDeviceQueryParams();
+        appDeviceQueryParams.setSensorCode(oneByDeviceCodeAndSensorNo.getSensorCode());
+        appDeviceQueryParams.setStartTime(trendParam.getStartTime());
+        appDeviceQueryParams.setEndTime(trendParam.getEndTime());
+        appDeviceQueryParams.setSensorRoute(trendParam.getRoads());
+        appDeviceQueryParams.setAggregationWindow(trendParam.getAggregationWindow());
+        SensorEchartDataResult sensorEchartDataResult = appDeviceService.queryDataByDeviceIdAndRoads(appDeviceQueryParams);
+        List<Float> tList = sensorEchartDataResult.getTemperature().getY();
+        List<Float> hList = sensorEchartDataResult.getHumidity().getY();
+        List<Float> cList = sensorEchartDataResult.getCo2().getY();
+        TrendDto trend = new TrendDto();
+        DecimalFormat df = new DecimalFormat("#.00");
+        trend.setTemperature(tList);
+        trend.setHumidity(hList);
+        trend.setCo2(cList);
+        if (!tList.isEmpty()) {
+            trend.setXData(sensorEchartDataResult.getTemperature().getX());
+        } else if (!hList.isEmpty()) {
+            trend.setXData(sensorEchartDataResult.getHumidity().getX());
+        } else if (!cList.isEmpty()) {
+            trend.setXData(sensorEchartDataResult.getCo2().getX());
+        }
+        return trend;
+    }
+
+    private LambdaQueryWrapper<SensorAlarm> getSensorAlarmQueryWrapper(String s) {
+        SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
+        LambdaQueryWrapper<SensorAlarm> queryWrapper = new LambdaQueryWrapper<>();
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime threshold;
+
+        switch (s) {
+            case "1d":
+                threshold = now.minusDays(1);
+                break;
+            case "3d":
+                threshold = now.minusDays(3);
+                break;
+            case "1w":
+            default:
+                threshold = now.minusWeeks(1);
+                break;
+            case "1m":
+                threshold = now.minusMonths(1);
+                break;
+        }
+
+        // 直接传递LocalDateTime或格式化字符串(根据数据库支持选择)
+        queryWrapper.ge(SensorAlarm::getCreateTime, threshold) // 或 DATE_TIME_FORMATTER.format(threshold)
+                .eq(SensorAlarm::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE)
+                .eq(SensorAlarm::getCreateOrg, loginUser.getOrgId())
+                .orderByDesc(SensorAlarm::getCreateTime);
+        return queryWrapper;
+    }
+}

+ 3 - 3
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitorsearchhistory/controller/SearchHistoryController.java

@@ -46,10 +46,10 @@ public class SearchHistoryController {
      */
     @Operation(summary = "获取报警次数最多的十条记录")
     @GetMapping("/coldchain/searchhistory/topWarning")
-    public CommonResult<JSONObject> topWarning(@RequestParam(value = "month", required = false) String month) {
+    public CommonResult<JSONObject> topWarning() {
         JSONObject obj = new JSONObject()
-                .fluentPut("warning", sensorAlarmService.topWarning(month, List.of(SensorAlarmType.DATA_ALARM.getDeviceCode(), SensorAlarmType.DATA_RESTORE_ALARM.getDeviceCode())))
-                .fluentPut("abnormal", sensorAlarmService.topWarning(month, List.of(SensorAlarmType.SENSOR_OFF_LINE.getDeviceCode(), SensorAlarmType.SENSOR_ON_LINE.getDeviceCode())));
+                .fluentPut("warning", sensorAlarmService.topWarning(List.of(SensorAlarmType.DATA_ALARM.getDeviceCode(), SensorAlarmType.DATA_RESTORE_ALARM.getDeviceCode())))
+                .fluentPut("abnormal", sensorAlarmService.topWarning(List.of(SensorAlarmType.SENSOR_OFF_LINE.getDeviceCode(), SensorAlarmType.SENSOR_ON_LINE.getDeviceCode())));
         return CommonResult.data(obj);
     }
 

+ 2 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/MonitorTargetRegionService.java

@@ -116,4 +116,6 @@ public interface MonitorTargetRegionService extends IService<MonitorTargetRegion
      * 根据监控对象id获取监测设备编号列表
      */
     List<String> listByDeviceCodeByTargetId(String targetId);
+
+    List<MonitorTargetRegion> myList();
 }

+ 8 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/impl/MonitorTargetRegionServiceImpl.java

@@ -286,6 +286,14 @@ public class MonitorTargetRegionServiceImpl extends ServiceImpl<MonitorTargetReg
         return codeList;
     }
 
+    @Override
+    public List<MonitorTargetRegion> myList() {
+        SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
+        LambdaQueryWrapper<MonitorTargetRegion> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(MonitorTargetRegion::getCreateOrg, loginUser.getOrgId()).eq(MonitorTargetRegion::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE);
+        return list(queryWrapper);
+    }
+
     private void fillParentLocationInfo(List<MonitorTargetRegion> resourceList) {
         if (CollUtil.isNotEmpty(resourceList)) {
             List<MonitorTargetRegion> locationTypes = resourceList.stream().filter(distinctByKey(MonitorTargetRegion::getParentId)).collect(Collectors.toList());

+ 4 - 0
snowy-web-app/src/main/java/vip/xiaonuo/Application.java

@@ -18,6 +18,8 @@ import org.springframework.boot.Banner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.web.bind.annotation.GetMapping;
 import vip.xiaonuo.core.config.JfcloudApplication;
 
@@ -29,6 +31,8 @@ import vip.xiaonuo.core.config.JfcloudApplication;
  */
 @Slf4j
 @JfcloudApplication
+@EnableScheduling
+@EnableAsync
 public class Application {
     /* 解决druid 日志报错:discard long time none received connection:xxx */
     static {

+ 3 - 1
snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java

@@ -472,7 +472,9 @@ public class GlobalConfigure implements WebMvcConfigurer {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
-        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
+        paginationInterceptor.setOptimizeJoin(true);
+        mybatisPlusInterceptor.addInnerInterceptor(paginationInterceptor);
         return mybatisPlusInterceptor;
     }
 

+ 2 - 0
snowy-web-app/src/main/resources/_sql/20250422.sql

@@ -0,0 +1,2 @@
+ALTER TABLE `coldchain`.`monitor_device_type`
+    ADD COLUMN `upload_interval` varchar(255) NULL DEFAULT NULL COMMENT '数据上报间隔' AFTER `parameters`;

+ 11 - 6
snowy-web-app/src/main/resources/application.properties

@@ -33,11 +33,12 @@ spring.datasource.dynamic.datasource.master.password=Root123...
 spring.datasource.dynamic.strict=true
 
 # MQTT
-spring.mqtt.url=mqtt://coldchain.nzkcloud.com:51183
-spring.mqtt.username=coldchain
-spring.mqtt.password=C123456
-spring.mqtt.client-id=provider-id
-spring.mqtt.default.topic=topic
+mqtt.serverUri=tcp://coldchain.nzkcloud.com:51183
+mqtt.username=coldchain
+mqtt.password=C123456
+mqtt.clientId=springboot_client_${random.uuid}
+SUBSCRIBE_TOPIC=coldchain/airnow/+/post
+PUBLISH_TOPIC=coldchain/airnow/set
 
 # influxdb
 spring.data.influxdb.url=${INFLUXDB_URL:http://jfcloud-k6-influxdb:8086}
@@ -265,4 +266,8 @@ file.path=/software/coldchain/ui/dist
 #file.path=C:/Users/xiaozun/Desktop
 
 qp.appKey=CdiC6zSNR
-qp.appSecret=a098d282b91211efbca452540055385a
+qp.appSecret=a098d282b91211efbca452540055385a
+
+airnow.save.enable=true
+#airnow.save.crno=0/10 * * * * ?
+airnow.save.crno=0 0/1 * * * ?