Prechádzať zdrojové kódy

feate: 温度区间查询

jackzhou 6 mesiacov pred
rodič
commit
edf0486812

+ 171 - 162
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/bean/influxdb/SensorData.java

@@ -1,9 +1,10 @@
 package vip.xiaonuo.coldchain.core.bean.influxdb;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.github.jfcloud.influxdb.model.JfcloudInFluxEntity;
 import com.influxdb.annotations.Column;
 import com.influxdb.annotations.Measurement;
-import com.influxdb.query.FluxRecord;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -11,11 +12,8 @@ import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import vip.xiaonuo.coldchain.core.config.JfcloudColdChainConstants;
 import vip.xiaonuo.coldchain.core.service.dataprocess.model.PowerEnum;
-
-import java.lang.reflect.Field;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.util.Date;
+import vip.xiaonuo.coldchain.modular.app.param.DoubleNullToDashSerializer;
+import vip.xiaonuo.coldchain.modular.app.param.FloatNullToDashSerializer;
 
 /**
  * SensorData类,用于表示传感器数据,继承自JfcloudInFluxEntity。
@@ -28,17 +26,24 @@ import java.util.Date;
 @Measurement(name = JfcloudColdChainConstants.INFLUXDB_DEFAULT_MEASUREMENT_NAME)
 @Slf4j
 public class SensorData extends JfcloudInFluxEntity {
-
     @Column(name = "temperature")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Float temperature;
 
     @Column(name = "humidity")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Float humidity;
 
     @Column(name = "co2")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Float co2;
 
     @Column(name = "battery", tag = true)
+    @JsonSerialize(using = FloatNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Float battery;
 
     @Column(name = "plugInStatus", tag = true)
@@ -48,9 +53,13 @@ public class SensorData extends JfcloudInFluxEntity {
     private String location;
 
     @Column(name = "longitude", tag = true)
+    @JsonSerialize(using = DoubleNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Double lng;
 
     @Column(name = "latitude", tag = true)
+    @JsonSerialize(using = DoubleNullToDashSerializer.class)  // 使用自定义序列化器
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
     private Double lat;
 
     @Column(name = "device_id", tag = true)
@@ -65,159 +74,159 @@ public class SensorData extends JfcloudInFluxEntity {
     @Column(name = "create_time", tag = true)
     private String createTime;
 
-    /**
-     * 使用反射和FluxRecord动态映射字段值到SensorData对象
-     *
-     * @param record FluxRecord数据记录
-     * @return 映射后的SensorData对象
-     */
-    public static SensorData mapToSensorData(FluxRecord record) {
-        SensorData sensorData = new SensorData();
-        try {
-            // 获取SensorData类的所有字段
-            Field[] fields = SensorData.class.getDeclaredFields();
-            for (Field field : fields) {
-                field.setAccessible(true);  // 设置字段可访问
-                // 如果字段没有@Column注解,则跳过
-                if (field.getAnnotation(Column.class) == null) {
-                    continue;
-                }
-
-                String fieldName = field.getName(); // 获取字段名
-                Object value = record.getValueByKey(fieldName); // 从FluxRecord中根据字段名获取值
-                if (value == null) {
-                    continue; // 如果FluxRecord中没有该字段的值,则跳过
-                }
-
-                // 根据字段类型进行相应的值转换
-                if (field.getType().equals(String.class)) {
-                    field.set(sensorData, value.toString());
-                } else if (field.getType().equals(Integer.class) || field.getType().equals(int.class)) {
-                    field.set(sensorData, convertToInteger(value));
-                } else if (field.getType().equals(Float.class) || field.getType().equals(float.class)) {
-                    field.set(sensorData, convertToFloat(value));
-                } else if (field.getType().equals(Double.class) || field.getType().equals(double.class)) {
-                    field.set(sensorData, convertToDouble(value));
-                } else if (field.getType().equals(Instant.class)) {
-                    field.set(sensorData, convertToInstant(value));
-                } else if (field.getType().equals(Date.class)) {
-                    field.set(sensorData, convertToDate(value));
-                } else if (field.getType().equals(LocalDate.class)) {
-                    field.set(sensorData, convertToLocalDate(value));
-                } else {
-                    log.warn("无法处理字段类型: " + field.getName() + ",字段值: " + value);
-                }
-            }
-        } catch (Exception e) {
-            log.error("从FluxRecord映射到SensorData时出错", e);
-        }
-        return sensorData;
-    }
-
-    // 类型转换方法
-
-    /**
-     * 将值转换为Integer类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的Integer值
-     */
-    private static Integer convertToInteger(Object value) {
-        try {
-            return (value instanceof Integer) ? (Integer) value : Integer.valueOf(value.toString());
-        } catch (NumberFormatException e) {
-            log.error("将值转换为Integer时出错: " + value, e);
-            return null;
-        }
-    }
-
-    /**
-     * 将值转换为Float类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的Float值
-     */
-    private static Float convertToFloat(Object value) {
-        try {
-            if (value instanceof Double) {
-                return ((Double) value).floatValue();  // 将Double 转换为Float
-            }
-            return (value instanceof Float) ? (Float) value : Float.valueOf(value.toString());
-        } catch (NumberFormatException e) {
-            log.error("将值转换为Float时出错: " + value, e);
-            return null;
-        }
-    }
-
-    /**
-     * 将值转换为Double类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的Double值
-     */
-    private static Double convertToDouble(Object value) {
-        try {
-            return (value instanceof Double) ? (Double) value : Double.valueOf(value.toString());
-        } catch (NumberFormatException e) {
-            log.error("将值转换为Double时出错: " + value, e);
-            return null;
-        }
-    }
-
-    /**
-     * 将值转换为Instant类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的Instant值
-     */
-    private static Instant convertToInstant(Object value) {
-        try {
-            return (value instanceof Instant) ? (Instant) value : Instant.parse(value.toString());
-        } catch (Exception e) {
-            log.error("将值转换为Instant时出错: " + value, e);
-            return null;
-        }
-    }
-
-    /**
-     * 将值转换为Date类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的Date值
-     */
-    private static Date convertToDate(Object value) {
-        try {
-            if (value instanceof Instant) {
-                return Date.from((Instant) value);
-            } else if (value instanceof String) {
-                return Date.from(Instant.parse((String) value));
-            } else {
-                return (Date) value;
-            }
-        } catch (Exception e) {
-            log.error("将值转换为Date时出错: " + value, e);
-            return null;
-        }
-    }
-
-    /**
-     * 将值转换为LocalDate类型
-     *
-     * @param value 要转换的值
-     * @return 转换后的LocalDate值
-     */
-    private static LocalDate convertToLocalDate(Object value) {
-        try {
-            if (value instanceof Instant) {
-                return ((Instant) value).atZone(java.time.ZoneId.systemDefault()).toLocalDate();
-            } else if (value instanceof String) {
-                return LocalDate.parse((String) value);
-            } else {
-                return (LocalDate) value;
-            }
-        } catch (Exception e) {
-            log.error("将值转换为LocalDate时出错: " + value, e);
-            return null;
-        }
-    }
+//    /**
+//     * 使用反射和FluxRecord动态映射字段值到SensorData对象
+//     *
+//     * @param record FluxRecord数据记录
+//     * @return 映射后的SensorData对象
+//     */
+//    public static SensorData mapToSensorData(FluxRecord record) {
+//        SensorData sensorData = new SensorData();
+//        try {
+//            // 获取SensorData类的所有字段
+//            Field[] fields = SensorData.class.getDeclaredFields();
+//            for (Field field : fields) {
+//                field.setAccessible(true);  // 设置字段可访问
+//                // 如果字段没有@Column注解,则跳过
+//                if (field.getAnnotation(Column.class) == null) {
+//                    continue;
+//                }
+//
+//                String fieldName = field.getName(); // 获取字段名
+//                Object value = record.getValueByKey(fieldName); // 从FluxRecord中根据字段名获取值
+//                if (value == null) {
+//                    continue; // 如果FluxRecord中没有该字段的值,则跳过
+//                }
+//
+//                // 根据字段类型进行相应的值转换
+//                if (field.getType().equals(String.class)) {
+//                    field.set(sensorData, value.toString());
+//                } else if (field.getType().equals(Integer.class) || field.getType().equals(int.class)) {
+//                    field.set(sensorData, convertToInteger(value));
+//                } else if (field.getType().equals(Float.class) || field.getType().equals(float.class)) {
+//                    field.set(sensorData, convertToFloat(value));
+//                } else if (field.getType().equals(Double.class) || field.getType().equals(double.class)) {
+//                    field.set(sensorData, convertToDouble(value));
+//                } else if (field.getType().equals(Instant.class)) {
+//                    field.set(sensorData, convertToInstant(value));
+//                } else if (field.getType().equals(Date.class)) {
+//                    field.set(sensorData, convertToDate(value));
+//                } else if (field.getType().equals(LocalDate.class)) {
+//                    field.set(sensorData, convertToLocalDate(value));
+//                } else {
+//                    log.warn("无法处理字段类型: " + field.getName() + ",字段值: " + value);
+//                }
+//            }
+//        } catch (Exception e) {
+//            log.error("从FluxRecord映射到SensorData时出错", e);
+//        }
+//        return sensorData;
+//    }
+//
+//    // 类型转换方法
+//
+//    /**
+//     * 将值转换为Integer类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的Integer值
+//     */
+//    private static Integer convertToInteger(Object value) {
+//        try {
+//            return (value instanceof Integer) ? (Integer) value : Integer.valueOf(value.toString());
+//        } catch (NumberFormatException e) {
+//            log.error("将值转换为Integer时出错: " + value, e);
+//            return null;
+//        }
+//    }
+//
+//    /**
+//     * 将值转换为Float类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的Float值
+//     */
+//    private static Float convertToFloat(Object value) {
+//        try {
+//            if (value instanceof Double) {
+//                return ((Double) value).floatValue();  // 将Double 转换为Float
+//            }
+//            return (value instanceof Float) ? (Float) value : Float.valueOf(value.toString());
+//        } catch (NumberFormatException e) {
+//            log.error("将值转换为Float时出错: " + value, e);
+//            return null;
+//        }
+//    }
+//
+//    /**
+//     * 将值转换为Double类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的Double值
+//     */
+//    private static Double convertToDouble(Object value) {
+//        try {
+//            return (value instanceof Double) ? (Double) value : Double.valueOf(value.toString());
+//        } catch (NumberFormatException e) {
+//            log.error("将值转换为Double时出错: " + value, e);
+//            return null;
+//        }
+//    }
+//
+//    /**
+//     * 将值转换为Instant类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的Instant值
+//     */
+//    private static Instant convertToInstant(Object value) {
+//        try {
+//            return (value instanceof Instant) ? (Instant) value : Instant.parse(value.toString());
+//        } catch (Exception e) {
+//            log.error("将值转换为Instant时出错: " + value, e);
+//            return null;
+//        }
+//    }
+//
+//    /**
+//     * 将值转换为Date类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的Date值
+//     */
+//    private static Date convertToDate(Object value) {
+//        try {
+//            if (value instanceof Instant) {
+//                return Date.from((Instant) value);
+//            } else if (value instanceof String) {
+//                return Date.from(Instant.parse((String) value));
+//            } else {
+//                return (Date) value;
+//            }
+//        } catch (Exception e) {
+//            log.error("将值转换为Date时出错: " + value, e);
+//            return null;
+//        }
+//    }
+//
+//    /**
+//     * 将值转换为LocalDate类型
+//     *
+//     * @param value 要转换的值
+//     * @return 转换后的LocalDate值
+//     */
+//    private static LocalDate convertToLocalDate(Object value) {
+//        try {
+//            if (value instanceof Instant) {
+//                return ((Instant) value).atZone(java.time.ZoneId.systemDefault()).toLocalDate();
+//            } else if (value instanceof String) {
+//                return LocalDate.parse((String) value);
+//            } else {
+//                return (LocalDate) value;
+//            }
+//        } catch (Exception e) {
+//            log.error("将值转换为LocalDate时出错: " + value, e);
+//            return null;
+//        }
+//    }
 }

+ 96 - 24
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/service/JfcloudSensorDataService.java

@@ -1,5 +1,6 @@
 package vip.xiaonuo.coldchain.core.service;
 
+import cn.hutool.core.lang.Assert;
 import com.github.jfcloud.influxdb.config.JfcloudInfluxDB2Properties;
 import com.github.jfcloud.influxdb.flux.JfcloudFluxDataService;
 import com.github.jfcloud.influxdb.flux.QueryCondition;
@@ -10,6 +11,8 @@ import org.springframework.stereotype.Service;
 import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
 
 import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -27,7 +30,6 @@ public class JfcloudSensorDataService extends JfcloudFluxDataService<SensorData>
     }
 
 
-
     /**
      * 根据 deviceId 和 roads 查询最新的数据
      *
@@ -36,26 +38,14 @@ public class JfcloudSensorDataService extends JfcloudFluxDataService<SensorData>
      * @return 最新的传感器数据
      */
     public SensorData queryLatestDataByDeviceIdAndRoads(String deviceId, Integer roads) {
-        // 构建 Flux 查询,获取最新一条记录
-//        String fluxQuery = String.format(
-//                "from(bucket: \"%s\") " +
-//                        "|> range(start: -30d) " +  // 查询过去 30 天的数据
-//                        "|> filter(fn: (r) => r._measurement == \"%s\" and r[\"device_id\"] == \"%s\" and r[\"roads\"] == \"%s\") " +
-//                        "|> sort(columns: [\"_time\"], desc: true) " +  // 按时间降序排序
-//                        "|> limit(n: 1)",  // 限制查询结果为 1 条
-//                getBucketName(),  // 动态获取桶名
-//                getMeasurement(SensorData.class),  // 动态获取 measurement 名称
-//                deviceId,  // 设备 ID
-//                roads  // 路数
-//        );
         String fluxQuery = String.format(
                 "from(bucket: \"%s\") " +
-                        "|> range(start: -30d) " +
+                        "|> range(start: -1d) " +
                         "|> filter(fn: (r) => r._measurement == \"%s\" and r[\"device_id\"] == \"%s\" and r[\"roads\"] == \"%s\") " +
                         "|> sort(columns: [\"_time\"], desc: true) " +
                         "|> limit(n: 1) " +
                         "|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") " +
-                        "|> keep(columns: [\"_time\", \"temperature\", \"humidity\", \"co2\", \"battery\", \"plugInStatus\", \"location\", \"longitude\", \"latitude\", \"device_id\", \"roads\", \"modelName\"])",
+                        "|> keep(columns: [\"_time\", \"temperature\", \"humidity\", \"co2\", \"battery\", \"plugInStatus\", \"location\", \"longitude\", \"latitude\", \"device_id\", \"roads\", \"modelName\", \"create_time\"])",
                 getBucketName(),
                 getMeasurement(SensorData.class),
                 deviceId,
@@ -67,7 +57,7 @@ public class JfcloudSensorDataService extends JfcloudFluxDataService<SensorData>
         // 将查询结果映射为 SensorData 实体
         return tables.stream()
                 .flatMap(table -> table.getRecords().stream())  // 拉平所有记录
-                .map(SensorData::mapToSensorData)  // 将每个记录映射为 SensorData 实体
+                .map(fluxRecord -> mapRecordToEntity(fluxRecord,getEntityClass()))  // 将每个记录映射为 SensorData 实体
                 .findFirst()  // 只取第一条记录(即最新一条记录)
                 .orElse(null);  // 如果没有数据,返回 null
     }
@@ -75,21 +65,103 @@ public class JfcloudSensorDataService extends JfcloudFluxDataService<SensorData>
     /**
      * 根据 deviceId、roads 和时间范围查询数据
      *
-     * @param deviceId  设备 ID
+     * @param deviceId     设备 ID
+     * @param roads        路数
+     * @param startTimeStr 查询开始时间
+     * @param endTimeStr   查询结束时间
+     * @return 查询到的传感器数据列表
+     */
+
+    public List<SensorData> queryDataByDeviceIdAndRoads(String deviceId, Integer roads, String startTimeStr, String endTimeStr) {
+        Assert.notNull(deviceId, "deviceId cannot be null");
+        Assert.notNull(roads, "roads cannot be null");
+        Assert.notNull(startTimeStr, "startTime cannot be null");
+        Assert.notNull(endTimeStr, "endTime cannot be null");
+        // 如果只有日期部分,则手动补充时间部分(00:00:00)
+        if (startTimeStr.length() == 10) {
+            startTimeStr += " 00:00:00";
+        }
+        if (endTimeStr.length() == 10) {
+            endTimeStr += " 23:59:59";
+        }
+        // 日期时间字符串解析格式
+        DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+                .withZone(ZoneId.of("UTC"));
+
+        // 将传入的日期时间字符串解析为 Instant 对象
+        Instant startTime = Instant.from(inputFormatter.parse(startTimeStr));
+        Instant endTime = Instant.from(inputFormatter.parse(endTimeStr));
+
+        // 格式化 Instant 为 InfluxDB 可以接受的字符串格式
+        DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("UTC"));
+        String startTimeFormatted = formatter.format(startTime);
+        String endTimeFormatted = formatter.format(endTime);
+
+        // 构建Flux查询
+//        String query = String.format(
+//                "from(bucket: \"%s\") |> range(start: %s, stop: %s) " +
+//                        "|> filter(fn: (r) => r._measurement == \"sensor_data\" and r[\"device_id\"] == \"%s\" and r[\"roads\"] == \"%d\") " +
+//                        "|> sort(columns: [\"_time\"], desc: true) " +
+//                        "|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") " +
+//                        "|> keep(columns: [\"_time\", \"temperature\", \"humidity\", \"co2\", \"battery\", \"plugInStatus\", \"location\", \"longitude\", \"latitude\", \"device_id\", \"roads\", \"modelName\", \"create_time\"])",
+//                getBucketName(), startTimeFormatted, endTimeFormatted, deviceId, roads
+//        );
+        String query = String.format(
+                "from(bucket: \"%s\") " +
+                        "|> range(start: %s, stop: %s) " +
+                        "|> filter(fn: (r) => r._measurement == \"sensor_data\" and r[\"device_id\"] == \"%s\" and r[\"roads\"] == \"%d\") " +
+                        "|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) " +  // 按小时聚合,使用平均值
+                        "|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") " +
+                        "|> keep(columns: [\"_time\", \"temperature\", \"humidity\", \"co2\", \"battery\", \"plugInStatus\", \"location\", \"longitude\", \"latitude\", \"device_id\", \"roads\", \"modelName\", \"create_time\"])",
+                getBucketName(), startTimeFormatted, endTimeFormatted, deviceId, roads
+        );
+
+        // 执行查询
+        QueryApi queryApi = jfcloudInfluxDBService.getInfluxDBClient().getQueryApi();
+        List<FluxTable> results = queryApi.query(query);
+
+        // 将查询结果转换为 SensorData 实体并返回
+        return results.stream()
+                .flatMap(table -> table.getRecords().stream())
+                .map(fluxRecord -> mapRecordToEntity(fluxRecord,getEntityClass()))  // 转换为 SensorData 实体
+                .collect(Collectors.toList());  // 返回多条数据
+    }
+
+
+    /**
+     * 根据时间范围、设备ID、路数和 bucket 查询传感器数据,并筛选需要的字段
+     *
+     * @param startTime 起始时间
+     * @param endTime   结束时间
+     * @param deviceId  设备ID
      * @param roads     路数
-     * @param startTime 查询开始时间
-     * @param endTime   查询结束时间
+     * @param fields    查询的字段列表,可以包含 "temperature", "humidity", "co2", "battery", "plugInStatus", etc.
      * @return 查询到的传感器数据列表
      */
-    public List<SensorData> queryDataByDeviceIdAndRoads(String deviceId, Integer roads, String startTime, String endTime) {
-        String query = String.format("SELECT * FROM \"%s\" WHERE \"device_id\" = '%s' AND \"roads\" = %d AND time >= '%s' AND time <= '%s' ORDER BY time DESC", getMeasurement(SensorData.class), deviceId, roads, startTime, endTime);
+    public List<SensorData> queryDataByTimeAndDevice(String startTime, String endTime, String deviceId, Integer roads, List<String> fields) {
+        // 构建查询字段的条件
+        String fieldsToSelect = fields.isEmpty() ? "*" : String.join(",", fields);
+        // 构建Flux查询语句
+        String query = String.format(
+                "from(bucket: \"%s\") |> range(start: %s, stop: %s) " +
+                        "|> filter(fn: (r) => r._measurement == \"sensor_data\" and r[\"device_id\"] == \"%s\" and r[\"roads\"] == \"%d\") " +
+                        "|> sort(columns: [\"_time\"], desc: true) " +
+                        "|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") " +
+                        "|> keep(columns: [\"_time\", %s])",
+                getBucketName(), startTime, endTime, deviceId, roads, fieldsToSelect
+        );
+
+        // 查询InfluxDB并返回结果
         QueryApi queryApi = jfcloudInfluxDBService.getInfluxDBClient().getQueryApi();
         List<FluxTable> results = queryApi.query(query);
+
         // 将查询结果转换为 SensorData 实体
-        return results.stream().flatMap(table -> table.getRecords().stream()).map(SensorData::mapToSensorData).collect(Collectors.toList());
+        return results.stream()
+                .flatMap(table -> table.getRecords().stream())
+                .map(fluxRecord -> mapRecordToEntity(fluxRecord,getEntityClass()))  // 转换为 SensorData 实体
+                .collect(Collectors.toList());  // 返回多条数据
     }
 
-
     /**
      * 根据 deviceId、roads、时间范围和查询条件查询数据
      *
@@ -105,6 +177,6 @@ public class JfcloudSensorDataService extends JfcloudFluxDataService<SensorData>
         QueryApi queryApi = jfcloudInfluxDBService.getInfluxDBClient().getQueryApi();
         List<FluxTable> tables = queryApi.query(fluxQuery);
         // 将查询结果映射为 SensorData 实体
-        return tables.stream().flatMap(table -> table.getRecords().stream()).map(SensorData::mapToSensorData).collect(Collectors.toList());
+        return tables.stream().flatMap(table -> table.getRecords().stream()).map(fluxRecord -> mapRecordToEntity(fluxRecord,getEntityClass())).collect(Collectors.toList());
     }
 }

+ 49 - 3
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/util/DateFormatter.java

@@ -8,11 +8,16 @@ package vip.xiaonuo.coldchain.core.util;
  * @date 2024/11/22 14:50:28
  */
 
+import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 
 public class DateFormatter {
 
@@ -57,13 +62,33 @@ public class DateFormatter {
         }
         // 将 Date 转换为 LocalDateTime
         LocalDateTime lastUpdatedLocal = lastUpdated.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-
         // 获取当前时间
         LocalDateTime now = LocalDateTime.now();
-
         // 计算两个日期的差值(以天为单位)
         long daysDiff = ChronoUnit.DAYS.between(lastUpdatedLocal, now);
+        // 根据日期差异来格式化输出
+        if (daysDiff <= 7) {
+            // 使用 Locale.CHINESE 进行格式化,显示中文星期几
+            DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.SIMPLIFIED_CHINESE);
+            return lastUpdatedLocal.format(dayFormatter);  // 返回中文星期几
+        } else {
+            // 超过 7 天,显示完整的日期时间格式
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            return lastUpdatedLocal.format(formatter);  // 返回完整的日期时间
+        }
+    }
 
+    public static String getFormattedSingleDate(Instant lastUpdated) {
+        // 如果 lastUpdated 为 null,返回默认值
+        if (lastUpdated == null) {
+            return "";
+        }
+        // 将 Instant 转换为 LocalDateTime
+        LocalDateTime lastUpdatedLocal = LocalDateTime.ofInstant(lastUpdated, ZoneId.systemDefault());
+        // 获取当前时间
+        LocalDateTime now = LocalDateTime.now();
+        // 计算两个日期的差值(以天为单位)
+        long daysDiff = ChronoUnit.DAYS.between(lastUpdatedLocal, now);
         // 根据日期差异来格式化输出
         if (daysDiff <= 7) {
             // 使用 Locale.CHINESE 进行格式化,显示中文星期几
@@ -75,5 +100,26 @@ public class DateFormatter {
             return lastUpdatedLocal.format(formatter);  // 返回完整的日期时间
         }
     }
+
+
+    public static String convertToUTCPlus8(String time) {
+        // 1. 解析时间为 Instant (UTC)
+        Instant instant = Instant.parse(time); // time 需要是 ISO 8601 格式,例如 "2024-11-25T07:42:48.861094300Z"
+        // 2. 将 Instant 转换为 UTC+8 (Asia/Shanghai)
+        ZonedDateTime utcPlus8Time = instant.atZone(ZoneId.of("Asia/Shanghai"));
+        // 3. 格式化为所需的字符串输出
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+        return formatter.format(utcPlus8Time);
+    }
+
+
+    public static String instantToUTC8String(Instant instant) {
+        // 1. 将 Instant 转换为指定时区的 ZonedDateTime
+        ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("Asia/Shanghai"));
+        // 2. 格式化为目标格式
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        // 3. 返回格式化后的时间字符串
+        return formatter.format(zonedDateTime);
+    }
 }
 

+ 8 - 76
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/app/controller/AppController.java

@@ -7,13 +7,14 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
 import vip.xiaonuo.coldchain.modular.app.param.*;
 import vip.xiaonuo.coldchain.modular.app.service.AppDeviceService;
 import vip.xiaonuo.coldchain.modular.app.service.MessageService;
 import vip.xiaonuo.common.pojo.CommonResult;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -21,6 +22,7 @@ import java.util.Map;
 @Tag(name = "APP移动端控制器")
 @RestController
 @RequestMapping("/coldchain/api/app")
+@Validated
 public class AppController {
     @Autowired
     private AppDeviceService appDeviceService;
@@ -66,92 +68,22 @@ public class AppController {
         }
     }
 
-
     // 查询设备数据接口(POST)
-    @PostMapping("/device/view/{deviceCode}")
+    @PostMapping("/device/view/{deviceCode}/{roads}")
     @Operation(summary = "查询采集设备数据")
-    public List<AppDeviceData> queryDeviceData(@RequestBody AppDeviceQueryParams appDeviceQueryParams, @PathVariable(value = "deviceCode", required = true) String deviceCode) {
-//        // 校验查询条件
-//        appDeviceQueryParams.validate();
-//        // 获取查询结果
-////        List<AppDeviceData> appDeviceDataList = appDeviceService.getDeviceData(appDeviceQueryParams, deviceCode);
-//        // 根据metric筛选数据
-//        Metric metric = appDeviceQueryParams.getMetric();
-//        if (metric == Metric.TEMPERATURE) {
-//            return filterTemperatureData(appDeviceDataList);
-//        } else if (metric == Metric.HUMIDITY) {
-//            return filterHumidityData(appDeviceDataList);
-//        } else if (metric == Metric.CO2_LEVEL) {
-//            return filterCo2LevelData(appDeviceDataList);
-//        } else {
-//            // 默认返回所有数据
-//            return appDeviceDataList;
-//        }
-        return null;
-    }
-
-    // 根据温度过滤数据
-    private List<AppDeviceData> filterTemperatureData(List<AppDeviceData> appDeviceDataList) {
-        // 根据需要筛选温度数据
-        return appDeviceDataList.stream().filter(data -> data.getTemperature() != null).toList();
-    }
-
-    // 根据湿度过滤数据
-    private List<AppDeviceData> filterHumidityData(List<AppDeviceData> appDeviceDataList) {
-        // 根据需要筛选湿度数据
-        return appDeviceDataList.stream().filter(data -> data.getHumidity() != null).toList();
-    }
-
-    // 根据CO2浓度过滤数据
-    private List<AppDeviceData> filterCo2LevelData(List<AppDeviceData> appDeviceDataList) {
-        // 根据需要筛选CO2浓度数据
-        return appDeviceDataList.stream().filter(data -> data.getCo2Level() != null).toList();
+    public CommonResult<Map<String, Object>> queryDeviceData(@RequestBody @Valid AppDeviceQueryParams appDeviceQueryParams, @PathVariable(value = "deviceCode", required = true) String deviceCode, @PathVariable(value = "roads", required = true) Integer roads) {
+        List<SensorData> sensorDataList = appDeviceService.queryDataByDeviceIdAndRoads(appDeviceQueryParams, deviceCode, roads);
+        Map<String, Object> stringObjectMap = SensorDataTransformer.transformToResponseFormat2(sensorDataList);
+        return CommonResult.data((stringObjectMap));
     }
 
     // 下载设备数据接口(POST)
     @PostMapping("/device/download/{deviceCode}")
     @Operation(summary = "下载采集器设备数据接口")
     public ResponseEntity<InputStreamResource> downloadDeviceData(@RequestBody AppDeviceQueryParams appDeviceQueryParams, @PathVariable(value = "deviceCode", required = true) String deviceCode) throws IOException {
-//
-//        // 校验查询条件
-//        appDeviceQueryParams.validate();
-//
-//        // 获取查询结果
-//        List<AppDeviceData> appDeviceDataList = appDeviceService.getDeviceData(appDeviceQueryParams, deviceCode);
-//
-//        // 根据metric筛选数据
-//        Metric metric = appDeviceQueryParams.getMetric();
-//        if (metric == Metric.TEMPERATURE) {
-//            appDeviceDataList = filterTemperatureData(appDeviceDataList);
-//        } else if (metric == Metric.HUMIDITY) {
-//            appDeviceDataList = filterHumidityData(appDeviceDataList);
-//        } else if (metric == Metric.CO2_LEVEL) {
-//            appDeviceDataList = filterCo2LevelData(appDeviceDataList);
-//        }
-//
-//        // 生成 CSV 或 Excel 文件数据流
-//        ByteArrayInputStream byteArrayInputStream = generateCSV(appDeviceDataList);
-//
-//        // 设置响应头,指示文件下载
-//        HttpHeaders headers = new HttpHeaders();
-//        headers.add("Content-Disposition", "attachment; filename=devices_data.csv");
-//
-//        return ResponseEntity.ok().headers(headers).contentType(MediaType.parseMediaType("text/csv")).body(new InputStreamResource(byteArrayInputStream));
         return null;
     }
 
-    // 生成 CSV 文件的 ByteArrayInputStream
-    private ByteArrayInputStream generateCSV(List<AppDeviceData> appDeviceDataList) {
-//        StringBuilder csvContent = new StringBuilder();
-//        csvContent.append("DeviceCode,Timestamp,Temperature,Humidity,CO2 Level,Battery Percentage,Device Name\n");
-//
-//        for (AppDeviceData data : appDeviceDataList) {
-//            csvContent.append(data.getDeviceCode()).append(",").append(data.getTimestamp()).append(",").append(data.getTemperature()).append(",").append(data.getHumidity()).append(",").append(data.getCo2Level()).append(",").append(data.getBatteryPercentage()).append(",").append(data.getDeviceName()).append("\n");
-//        }
-//
-//        return new ByteArrayInputStream(csvContent.toString().getBytes());
-        return null;
-    }
 
     @GetMapping("/message/unread/count")
     @Operation(summary = "获取未读消息统计")

+ 8 - 9
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/app/param/AppDeviceQueryParams.java

@@ -8,14 +8,18 @@ package vip.xiaonuo.coldchain.modular.app.param;
  * @date 2024/11/17 22:38:25
  */
 
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
-import java.time.LocalDateTime;
-
 @Data
 public class AppDeviceQueryParams {
-    private LocalDateTime startTime;     // 查询开始时间
-    private LocalDateTime endTime;       // 查询结束时间
+    @Schema(description = "起始时间")
+    @NotBlank(message = "起始时间不能为空")
+    private String startTime;     // 查询开始时间
+    @Schema(description = "截止时间")
+    @NotBlank(message = "截止不能为空")
+    private String endTime;       // 查询结束时间
     private Double minTemperature;       // 最低温度
     private Double maxTemperature;       // 最高温度
     private Double minHumidity;          // 最低湿度
@@ -24,9 +28,4 @@ public class AppDeviceQueryParams {
     private Double maxCo2Level;          // 最高二氧化碳浓度
     private Metric metric;  // 使用 Metric 枚举
 
-    public void validate() {
-        if (startTime != null && endTime != null && startTime.isAfter(endTime)) {
-            throw new IllegalArgumentException("Start time cannot be after end time.");
-        }
-    }
 }

+ 117 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/app/param/SensorDataTransformer.java

@@ -0,0 +1,117 @@
+package vip.xiaonuo.coldchain.modular.app.param;
+
+import lombok.experimental.UtilityClass;
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+import vip.xiaonuo.coldchain.core.util.DateFormatter;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@UtilityClass
+public class SensorDataTransformer {
+
+//    public static Map<String, Object> transformToResponseFormat(List<SensorData> sensorDataList) {
+//        // 使用 Java Streams API 按照字段进行分组,分别处理温度、湿度和二氧化碳
+//        Map<String, Object> response = new HashMap<>();
+//        // 按时间升序排序
+//        List<SensorData> sortedData = sensorDataList.stream().sorted((data1, data2) -> data1.getTime().compareTo(data2.getTime())).toList();
+//        // 温度 (时间 -> 温度)
+//        List<Map<String, Object>> temperatureData = sortedData.stream().filter(data -> data.getTemperature() != null) // 过滤掉没有温度值的数据
+//                .map(data -> {
+//                    Map<String, Object> timeTemperatureMap = new HashMap<>();
+//                    timeTemperatureMap.put("time", DateFormatter.instantToUTC8String(data.getTime()));  // 时间
+//                    timeTemperatureMap.put("temperature", data.getTemperature());  // 温度
+//                    return timeTemperatureMap;
+//                }).collect(Collectors.toList());
+//
+//        // 湿度 (时间 -> 湿度)
+//        List<Map<String, Object>> humidityData = sortedData.stream().filter(data -> data.getHumidity() != null) // 过滤掉没有湿度值的数据
+//                .map(data -> {
+//                    Map<String, Object> timeHumidityMap = new HashMap<>();
+//                    timeHumidityMap.put("time", DateFormatter.instantToUTC8String(data.getTime()));  // 时间
+//                    timeHumidityMap.put("humidity", data.getHumidity());  // 湿度
+//                    return timeHumidityMap;
+//                }).collect(Collectors.toList());
+//
+//        // 二氧化碳 (时间 -> 二氧化碳)
+//        List<Map<String, Object>> co2Data = sortedData.stream().filter(data -> data.getCo2() != null) // 过滤掉没有二氧化碳值的数据
+//                .map(data -> {
+//                    Map<String, Object> timeCo2Map = new HashMap<>();
+//                    timeCo2Map.put("time", DateFormatter.instantToUTC8String(data.getTime()));  // 时间
+//                    timeCo2Map.put("co2", data.getCo2());  // 二氧化碳
+//                    return timeCo2Map;
+//                }).collect(Collectors.toList());
+//
+//        // 将结果按结构化的方式放入 response
+//        response.put("temperature", temperatureData);
+//        response.put("humidity", humidityData);
+//        response.put("co2", co2Data);
+//
+//        return response;
+//    }
+
+
+    // 通用方法,提取时间和数值
+    private static Map<String, Object> extractData(List<SensorData> sortedData, String dataType) {
+        // 过滤并映射到时间和数值
+        List<String> times = sortedData.stream()
+                .filter(data -> {
+                    if ("temperature".equals(dataType)) return data.getTemperature() != null;
+                    if ("humidity".equals(dataType)) return data.getHumidity() != null;
+                    if ("co2".equals(dataType)) return data.getCo2() != null;
+                    return false;
+                })
+                .map(data -> DateFormatter.getFormattedSingleDate(data.getTime())) // 获取时间
+                .collect(Collectors.toList());
+
+        List<Float> values = sortedData.stream()
+                .filter(data -> {
+                    if ("temperature".equals(dataType)) return data.getTemperature() != null;
+                    if ("humidity".equals(dataType)) return data.getHumidity() != null;
+                    if ("co2".equals(dataType)) return data.getCo2() != null;
+                    return false;
+                })
+                .map(data -> {
+                    if ("temperature".equals(dataType)) return formatFloat(data.getTemperature());
+                    if ("humidity".equals(dataType)) return formatFloat(data.getHumidity());
+                    if ("co2".equals(dataType)) return formatFloat(data.getCo2());
+                    return null;
+                })
+                .collect(Collectors.toList());
+
+        Map<String, Object> dataMap = new HashMap<>();
+        dataMap.put("x", times);  // 时间数组
+        dataMap.put("y", values); // 数值数组
+        return dataMap;
+    }
+
+    // 格式化浮动值为保留两位小数
+    private static Float formatFloat(Float value) {
+        if (value == null) return null;
+        return Float.parseFloat(String.format("%.2f", value));
+    }
+
+    // 主方法,处理传感器数据
+    public static Map<String, Object> transformToResponseFormat2(List<SensorData> sensorDataList) {
+        // 按时间升序排序
+        List<SensorData> sortedData = sensorDataList.stream()
+                .sorted(Comparator.comparing(SensorData::getTime))  // 按时间升序排序
+                .collect(Collectors.toList());
+
+        // 提取温度、湿度和二氧化碳数据
+        Map<String, Object> temperatureData = extractData(sortedData, "temperature");
+        Map<String, Object> humidityData = extractData(sortedData, "humidity");
+        Map<String, Object> co2Data = extractData(sortedData, "co2");
+
+        // 创建最终的返回数据结构
+        Map<String, Object> response = new HashMap<>();
+        response.put("temperature", temperatureData);
+        response.put("humidity", humidityData);
+        response.put("co2", co2Data);
+
+        return response;
+    }
+}

+ 7 - 8
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/app/service/AppDeviceService.java

@@ -9,6 +9,7 @@ package vip.xiaonuo.coldchain.modular.app.service;
  */
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.github.jfcloud.influxdb.service.JfcloudInfluxDBService;
@@ -26,10 +27,7 @@ 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 java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 @Service
 @RequiredArgsConstructor
@@ -71,10 +69,8 @@ public class AppDeviceService {
         // 设置分页参数:当前页和每页记录数
         monitorTargetPageParam.setCurrent(appDevicePageParam.getCurrent());
         monitorTargetPageParam.setSize(appDevicePageParam.getSize());
-
         // 调用 monitorTargetService 查询当前用户的监控目标数据,分页结果
         Page<MonitorTarget> pageByUser = monitorTargetService.getPageByUser(monitorTargetPageParam);
-
         // 遍历监控目标分页记录
         pageByUser.getRecords().forEach(monitorTarget -> {
             // 创建新的 AppDevice 对象来存储转换后的设备信息
@@ -84,10 +80,8 @@ public class AppDeviceService {
             appDevice.setMonitorTargetRegionList(null);
             // 获取当前监控目标的 ID,用于查询子设备数据
             String monitorTargetId = monitorTarget.getId();
-
             // 获取当前监控目标下的所有监控区域(子设备数据)
             List<MonitorTargetRegion> monitorTargetRegions = monitorTargetRegionService.getRegionListByTargetId(monitorTargetId);
-
             // 如果监控目标下存在区域(即有子设备)
             if (!monitorTargetRegions.isEmpty()) {
                 // 遍历每个区域,将其转换为子设备数据,并添加到 AppDevice 的子设备列表中
@@ -160,4 +154,9 @@ public class AppDeviceService {
         BeanUtil.copyProperties(targetCount, deviceStatus);
         return deviceStatus;
     }
+
+    public List<SensorData> queryDataByDeviceIdAndRoads(AppDeviceQueryParams appDeviceQueryParams, String deviceCode, Integer roads) {
+        Assert.notNull(appDeviceQueryParams, "appDeviceQueryParams cannot be null");
+        return monitorDeviceService.queryDataByDeviceIdAndRoads(deviceCode, roads, appDeviceQueryParams.getStartTime(), appDeviceQueryParams.getEndTime());
+    }
 }