Browse Source

perf:优化传感器数据导出接口,加快导出速度

黄渊昊 1 month ago
parent
commit
eaab996313

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

@@ -206,12 +206,12 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
             queryWrapper.lambda().and(q -> q.like(SensorAlarm::getMessage, messagePageParam.getKeyword()).or().like(SensorAlarm::getDeviceName, messagePageParam.getKeyword()));
         }
         // 时间查询
-        if (StrUtil.isNotBlank(messagePageParam.getStartTime())) {
-            queryWrapper.lambda().ge(SensorAlarm::getCreateTime, DateUtil.parse(messagePageParam.getStartTime(), "yyyy-MM-dd HH:mm:ss"));  // greater than or equal to start time
+        if (StrUtil.isNotBlank(messagePageParam.getAlarmType())) {
+            queryWrapper.lambda().eq(SensorAlarm::getAlarmType, messagePageParam.getAlarmType());
         }
-        if (StrUtil.isNotBlank(messagePageParam.getEndTime()) && StrUtil.isNotBlank(messagePageParam.getAlarmType())) {
+        if (StrUtil.isNotBlank(messagePageParam.getEndTime()) && StrUtil.isNotBlank(messagePageParam.getStartTime())) {
+            queryWrapper.lambda().ge(SensorAlarm::getCreateTime, DateUtil.parse(messagePageParam.getStartTime(), "yyyy-MM-dd HH:mm:ss"));  // greater than or equal to start time
             queryWrapper.lambda().le(SensorAlarm::getCreateTime, DateUtil.parse(messagePageParam.getEndTime(), "yyyy-MM-dd HH:mm:ss"));  // greater than or equal to start time
-            queryWrapper.lambda().eq(SensorAlarm::getAlarmType, messagePageParam.getAlarmType());
         } else {
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
             Calendar calendar = Calendar.getInstance();

+ 2 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/app/param/export/AppTrendParam.java

@@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotBlank;
 import lombok.Getter;
 import lombok.Setter;
 
+import java.io.File;
+
 @Getter
 @Setter
 public class AppTrendParam {

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

@@ -12,6 +12,7 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.StopWatch;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -19,6 +20,8 @@ import com.github.jfcloud.influxdb.flux.AggregationWindow;
 import com.google.common.collect.Lists;
 import com.itextpdf.io.image.ImageData;
 import com.itextpdf.io.image.ImageDataFactory;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.kernel.pdf.WriterProperties;
 import com.itextpdf.layout.element.Image;
 import com.itextpdf.kernel.colors.ColorConstants;
 import com.itextpdf.kernel.font.PdfFont;
@@ -53,15 +56,24 @@ import vip.xiaonuo.common.enums.CommonDeleteFlagEnum;
 import vip.xiaonuo.common.exception.CommonException;
 
 import java.io.*;
+import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 @Slf4j
 @Service
@@ -516,281 +528,161 @@ public class AppDeviceService {
         return monitorTargetRegionService.updateById(monitorTargetRegion);
     }
 
-    /*public void export(HttpServletResponse response, AppTrendParam trendParam) {
-        StopWatch stopWatch = new StopWatch("传感器数据导出");
+    private static final DecimalFormat TEMP_FORMAT = new DecimalFormat("0.0");
 
-        stopWatch.start("查询设备信息");
-        MonitorTargetRegion monitorTargetRegion = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(trendParam.getSensorCode(), trendParam.getRoads());
-        stopWatch.stop();
-        if (Objects.isNull(monitorTargetRegion)) {
-            throw new CommonException("未找到设备编号 [{}] 和传感器编号 [{}] 对应的监控目标区域。", trendParam.getSensorCode(), trendParam.getRoads());
-        }
-
-        stopWatch.start("查询传感器数据");
-        AppDeviceQueryParams appDeviceQueryParams = BeanUtil.copyProperties(trendParam, AppDeviceQueryParams.class);
-        appDeviceQueryParams.setSensorRoute(trendParam.getRoads());
-        SensorEchartDataResult sensorEchartDataResult = queryDataByDeviceIdAndRoads(appDeviceQueryParams);
-        stopWatch.stop();
-
-        // 计算统计信息
-        stopWatch.start("计算统计信息");
-        List<Float> temperatureData = sensorEchartDataResult.getTemperature().getY();
-        float maxTemp = Collections.max(temperatureData);
-        float minTemp = Collections.min(temperatureData);
-        float avgTemp = (float) temperatureData.stream().mapToDouble(f -> f).average().orElse(0.0);
-        stopWatch.stop();
-
-        stopWatch.start("导出数据");
-        List<ExportParam> exportParamList = new ArrayList<>();
-        ExportParam exportParam = new ExportParam();
-        exportParam.setRegionName(monitorTargetRegion.getName());
-        exportParam.setDeviceCode(trendParam.getSensorCode());
-        exportParam.setSensorRoad(trendParam.getRoads());
-        String sensorType = monitorTargetRegion.getSensorType().toUpperCase();
-
-        // 仅处理温度数据('W'类型)
-        if (sensorType.contains("W")) {
-            exportParam.setDataType("温度");
-            LinkedHashSet<String> x = sensorEchartDataResult.getTemperature().getX();
-            List<Float> y = sensorEchartDataResult.getTemperature().getY();
-            int i = 0;
-            for (String time : x) {
-                ExportParam exportParamOut = BeanUtil.copyProperties(exportParam, ExportParam.class);
-                exportParamOut.setTime(time);
-                exportParamOut.setData(y.get(i));
-                exportParamList.add(exportParamOut);
-                i++;
+    public void export(HttpServletResponse response, AppTrendParam trendParam, MultipartFile file) {
+        // 初始化配置和计时器
+        TEMP_FORMAT.setRoundingMode(RoundingMode.HALF_UP);
+        try {
+            // 并行执行设备查询和数据查询
+            CompletableFuture<MonitorTargetRegion> regionFuture = CompletableFuture.supplyAsync(() ->
+                    monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(trendParam.getSensorCode(), trendParam.getRoads()));
+
+            AppDeviceQueryParams appDeviceQueryParams = BeanUtil.copyProperties(trendParam, AppDeviceQueryParams.class);
+            appDeviceQueryParams.setSensorRoute(trendParam.getRoads());
+            CompletableFuture<SensorEchartDataResult> dataFuture = CompletableFuture.supplyAsync(() ->
+                    queryDataByDeviceIdAndRoads(appDeviceQueryParams));
+
+            // 等待查询完成并获取结果
+            MonitorTargetRegion monitorTargetRegion = regionFuture.get();
+            if (Objects.isNull(monitorTargetRegion)) {
+                throw new CommonException("未找到设备编号 [{}] 和传感器编号 [{}] 对应的监控目标区域。",
+                        trendParam.getSensorCode(), trendParam.getRoads());
             }
-        }
-
-        String fileName = "传感器数据报告.pdf";
-        response.setContentType("application/pdf");
-        response.setCharacterEncoding("utf-8");
-        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" +
-                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
-
-        try (OutputStream outputStream = response.getOutputStream()) {
-            PdfFont chineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
-            PdfWriter writer = new PdfWriter(outputStream);
-            PdfDocument pdfDoc = new PdfDocument(writer);
-            Document document = new Document(pdfDoc, PageSize.A4);
-            document.setMargins(15, 20, 10, 20);
 
-            // 报告标题
-            Paragraph title = new Paragraph("传感器数据报告")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(20)
-                    .setBold()
-                    .setMarginBottom(10);
-            document.add(title);
+            SensorEchartDataResult sensorEchartDataResult = dataFuture.get();
+
+            // 并行计算统计信息
+            Map<String, Float[]> statsMap = calculateAllStatsParallel(sensorEchartDataResult);
+            Float maxTemp = statsMap.get("temperature")[0];
+            Float minTemp = statsMap.get("temperature")[1];
+            Float avgTemp = statsMap.get("temperature")[2];
+            Float maxHum = statsMap.get("humidity")[0];
+            Float minHum = statsMap.get("humidity")[1];
+            Float avgHum = statsMap.get("humidity")[2];
+            Float maxCo2 = statsMap.get("co2")[0];
+            Float minCo2 = statsMap.get("co2")[1];
+            Float avgCo2 = statsMap.get("co2")[2];
+
+            // 并行构建导出数据列表
+            Map<String, List<ExportParam>> exportDataMap = buildExportDataParallel(
+                    monitorTargetRegion, trendParam, sensorEchartDataResult);
+            List<ExportParam> tempExportParamList = exportDataMap.get("temperature");
+            List<ExportParam> humExportParamList = exportDataMap.get("humidity");
+            List<ExportParam> co2ExportParamList = exportDataMap.get("co2");
+
+            // 生成PDF报告
+            generatePdfReport(response, file, monitorTargetRegion, trendParam,
+                    sensorEchartDataResult, statsMap, exportDataMap);
+        } catch (Exception e) {
+            throw new RuntimeException("PDF生成失败", e);
+        }
+    }
 
-            // 报告下载时间
-            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
-            Paragraph downloadTime = new Paragraph("报告下载时间: " + sdf.format(new Date()))
-                    .setFont(chineseFont)
-                    .setFontSize(10)
-                    .setTextAlignment(TextAlignment.LEFT);
-            document.add(downloadTime);
+    // 并行计算所有统计信息
+    private Map<String, Float[]> calculateAllStatsParallel(SensorEchartDataResult sensorEchartDataResult) {
+        CompletableFuture<Float[]> tempStatsFuture = CompletableFuture.supplyAsync(() ->
+                calculateStats(sensorEchartDataResult.getTemperature()));
+        CompletableFuture<Float[]> humStatsFuture = CompletableFuture.supplyAsync(() ->
+                calculateStats(sensorEchartDataResult.getHumidity()));
+        CompletableFuture<Float[]> co2StatsFuture = CompletableFuture.supplyAsync(() ->
+                calculateStats(sensorEchartDataResult.getCo2()));
 
-            Paragraph deviceInfo = new Paragraph("设备信息")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(15)
-                    .setBold()
-                    .setMarginBottom(2);
-            document.add(deviceInfo);
-
-            // 设备信息表格
-            Table deviceTable = new Table(new float[]{2, 3, 2, 3}).useAllAvailableWidth();
-            addDeviceTableRow(deviceTable, "设备名称:", monitorTargetRegion.getName().split("-")[0], "温度上限:", monitorTargetRegion.getTemperatureUp() + "°C");
-            addDeviceTableRow(deviceTable, "点位名称:", monitorTargetRegion.getName(), "温度下限:", monitorTargetRegion.getTemperatureDown() + "");
-            addDeviceTableRow(deviceTable, "冷链设备编号:", trendParam.getSensorCode(), "传感器路数:", trendParam.getRoads() + "路");
-            deviceTable.setMarginBottom(10);
-            document.add(deviceTable);
+        CompletableFuture.allOf(tempStatsFuture, humStatsFuture, co2StatsFuture).join();
 
-            Paragraph data = new Paragraph("记录信息")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(15)
-                    .setBold()
-                    .setMarginBottom(2);
-            document.add(data);
-
-            // 记录信息表格
-            Table recordTable = new Table(new float[]{2, 3, 2, 3}).useAllAvailableWidth();
-            addRecordTableRow(recordTable, "开始时间:", trendParam.getStartTime(), "最高温度:", String.format("%.1f°C", maxTemp));
-            addRecordTableRow(recordTable, "结束时间:", trendParam.getEndTime(), "最低温度:", String.format("%.1f°C", minTemp));
-            addRecordTableRow(recordTable, "", "", "平均温度:", String.format("%.1f°C", avgTemp));
-            recordTable.setMarginBottom(10);
-            document.add(recordTable);
+        Map<String, Float[]> statsMap = new ConcurrentHashMap<>();
+        try {
+            statsMap.put("temperature", tempStatsFuture.get());
+            statsMap.put("humidity", humStatsFuture.get());
+            statsMap.put("co2", co2StatsFuture.get());
+        } catch (Exception e) {
+            throw new RuntimeException("计算统计数据失败", e);
+        }
 
-            Paragraph curve = new Paragraph("温度曲线")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(15)
-                    .setBold();
-            document.add(curve);
+        return statsMap;
+    }
 
-            try {
-                // 1. 创建适用于String类型X轴的数据集)
-                DefaultCategoryDataset dataset = new DefaultCategoryDataset();
-                List<Float> yValues = sensorEchartDataResult.getTemperature().getY();
-                List<String> xLabels = new ArrayList<>(sensorEchartDataResult.getTemperature().getX());
-
-                // 填充分类数据集(行键,数值,列键)
-                for (int i = 0; i < xLabels.size(); i++) {
-                    dataset.addValue(yValues.get(i), "温度", xLabels.get(i)); // 列键使用时间字符串
-                }
+    // 计算单个数据集的统计信息
+    private Float[] calculateStats(SensorEchartData data) {
+        if (Objects.isNull(data) || data.getY() == null || data.getY().isEmpty()) {
+            return new Float[]{null, null, null};
+        }
 
-                // 2. 创建分类折线图(替换核心方法)[1,3](@ref)
-                JFreeChart chart = ChartFactory.createLineChart(
-                        null, // 隐藏标题
-                        "时间",  // X轴标签(原"时间序列"改为更直观的"时间")
-                        "温度(℃)",
-                        dataset,
-                        PlotOrientation.VERTICAL,
-                        false, // 不显示图例
-                        false,
-                        false
-                );
-
-                // 3. 样式优化(需调整为CategoryPlot)[5,7](@ref)
-                CategoryPlot plot = chart.getCategoryPlot();
-                plot.setBackgroundPaint(Color.WHITE);
-
-                // 设置X/Y轴字体(解决中文显示问题)[3,8](@ref)
-                Font songFont = new Font("宋体", Font.PLAIN, 11);
-                CategoryAxis domainAxis = plot.getDomainAxis();
-                domainAxis.setLabelFont(songFont);
-                domainAxis.setTickLabelFont(songFont);
-                domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45); // 标签倾斜45度防重叠
-
-                ValueAxis rangeAxis = plot.getRangeAxis();
-                rangeAxis.setLabelFont(songFont);
-                double minValue;
-                double maxValue;
-                if (Collections.min(yValues) > 0) {
-                    minValue = Collections.min(yValues) * 0.95; // 下边界留5%余量
-                } else {
-                    minValue = Collections.min(yValues) * 1.05;
-                }
-                if (Collections.max(yValues) < 0) {
-                    maxValue = Collections.max(yValues) * 0.95; // 下边界留5%余量
-                } else {
-                    maxValue = Collections.max(yValues) * 1.05; // 上边界留5%余量
-                }
-                rangeAxis.setRange(minValue, maxValue);
+        DoubleSummaryStatistics stats = data.getY().parallelStream()
+                .mapToDouble(f -> f)
+                .summaryStatistics();
+        return new Float[]{(float) stats.getMax(), (float) stats.getMin(), (float) stats.getAverage()};
+    }
 
-                // 设置刻度密度(根据数据范围动态调整)
-                if ((maxValue - minValue) < 50) { // 温差较小时缩小刻度间隔
-                    rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
-                }
+    // 并行构建所有导出数据列表
+    private Map<String, List<ExportParam>> buildExportDataParallel(
+            MonitorTargetRegion monitorTargetRegion, AppTrendParam trendParam,
+            SensorEchartDataResult sensorEchartDataResult) {
 
-                // 4. 自定义折线样式(使用CategoryItemRenderer)[5](@ref)
-                LineAndShapeRenderer renderer = new LineAndShapeRenderer();
-                renderer.setSeriesPaint(0, new Color(79, 129, 189));
-                renderer.setSeriesStroke(0, new BasicStroke(1.8f));
-                renderer.setSeriesShapesVisible(0, true); // 显示数据点
-                plot.setRenderer(renderer);
+        ExportParam template = new ExportParam();
+        template.setRegionName(monitorTargetRegion.getName());
+        template.setDeviceCode(trendParam.getSensorCode());
+        template.setSensorRoad(trendParam.getRoads());
+        String sensorType = monitorTargetRegion.getSensorType().toUpperCase();
 
+        List<CompletableFuture<Map.Entry<String, List<ExportParam>>>> futures = new ArrayList<>();
 
-                // 5. 处理时间标签过长问题(动态调整间隔)[6,7](@ref)
-                if(xLabels.size() > 20) { // 当数据点超过20个时启用标签间隔
-                    domainAxis.setTickLabelsVisible(true);
-                    domainAxis.setTickMarksVisible(true);
-//                    domainAxis.setMaximumCategoryLabelLines(3); // 允许换行显示
-                    domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); // 垂直显示
-                }
+        if (sensorType.contains("W")) {
+            futures.add(CompletableFuture.supplyAsync(() ->
+                    Map.entry("temperature", buildExportParamList(template, sensorEchartDataResult.getTemperature(), "温度"))));
+        }
+        if (sensorType.contains("S")) {
+            futures.add(CompletableFuture.supplyAsync(() ->
+                    Map.entry("humidity", buildExportParamList(template, sensorEchartDataResult.getHumidity(), "湿度"))));
+        }
+        if (sensorType.contains("C")) {
+            futures.add(CompletableFuture.supplyAsync(() ->
+                    Map.entry("co2", buildExportParamList(template, sensorEchartDataResult.getCo2(), "二氧化碳"))));
+        }
 
-                // 6. 生成图像并插入PDF(保持原有逻辑)
-                BufferedImage chartImage = chart.createBufferedImage(600, 450); // 加宽画布
-                ImageData imageData = ImageDataFactory.create(chartImage, null);
-                com.itextpdf.layout.element.Image pdfImage = new com.itextpdf.layout.element.Image(imageData)
-                        .setHorizontalAlignment(HorizontalAlignment.CENTER);
-                document.add(pdfImage);
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
 
+        Map<String, List<ExportParam>> exportDataMap = new ConcurrentHashMap<>();
+        futures.forEach(future -> {
+            try {
+                Map.Entry<String, List<ExportParam>> entry = future.get();
+                exportDataMap.put(entry.getKey(), entry.getValue());
             } catch (Exception e) {
-                log.error("折线图生成失败: {}", e.getMessage());
-                throw new RuntimeException("图表生成异常", e);
+                throw new RuntimeException("构建导出数据失败", e);
             }
+        });
 
-            document.add(new AreaBreak());
-
-            // 温度数据表格
-            Paragraph dataTitle = new Paragraph("温度数据")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(15)
-                    .setBold()
-                    .setMarginBottom(2);
-            document.add(dataTitle);
-
-            Table dataTable = new Table(2).useAllAvailableWidth();
-            dataTable.addHeaderCell(createCell("时间", true));
-            dataTable.addHeaderCell(createCell("°C", true));
-
-            exportParamList.forEach(param -> {
-                dataTable.addCell(createCell(param.getTime(), false));
-                dataTable.addCell(createCell(String.format("%.1f", param.getData()), false));
-            });
-            document.add(dataTable);
+        return exportDataMap;
+    }
 
-            document.close();
-        } catch (Exception e) {
-            throw new RuntimeException("PDF生成失败", e);
-        } finally {
-            stopWatch.stop();
-            log.info("PDF导出耗时:{}", stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
+    // 构建单个数据类型的导出参数列表
+    private List<ExportParam> buildExportParamList(ExportParam template, SensorEchartData data, String dataType) {
+        if (data == null || data.getX() == null || data.getY() == null) {
+            return new ArrayList<>();
         }
-    }*/
 
-    public void export(HttpServletResponse response, AppTrendParam trendParam, MultipartFile file) {
-        StopWatch stopWatch = new StopWatch("传感器数据导出");
+        List<ExportParam> list = new ArrayList<>(data.getX().size());
+        ExportParam param = BeanUtil.copyProperties(template, ExportParam.class);
+        param.setDataType(dataType);
 
-        stopWatch.start("查询设备信息");
-        MonitorTargetRegion monitorTargetRegion = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(trendParam.getSensorCode(), trendParam.getRoads());
-        stopWatch.stop();
-        if (Objects.isNull(monitorTargetRegion)) {
-            throw new CommonException("未找到设备编号 [{}] 和传感器编号 [{}] 对应的监控目标区域。", trendParam.getSensorCode(), trendParam.getRoads());
+        Iterator<String> xIter = data.getX().iterator();
+        Iterator<Float> yIter = data.getY().iterator();
+
+        while (xIter.hasNext() && yIter.hasNext()) {
+            ExportParam copy = BeanUtil.copyProperties(param, ExportParam.class);
+            copy.setTime(xIter.next());
+            copy.setData(yIter.next());
+            list.add(copy);
         }
 
-        stopWatch.start("查询传感器数据");
-        AppDeviceQueryParams appDeviceQueryParams = BeanUtil.copyProperties(trendParam, AppDeviceQueryParams.class);
-        appDeviceQueryParams.setSensorRoute(trendParam.getRoads());
-        SensorEchartDataResult sensorEchartDataResult = queryDataByDeviceIdAndRoads(appDeviceQueryParams);
-        stopWatch.stop();
-
-        // 计算统计信息
-        stopWatch.start("计算统计信息");
-        List<Float> temperatureData = sensorEchartDataResult.getTemperature().getY();
-        float maxTemp = Collections.max(temperatureData);
-        float minTemp = Collections.min(temperatureData);
-        float avgTemp = (float) temperatureData.stream().mapToDouble(f -> f).average().orElse(0.0);
-        stopWatch.stop();
-
-        stopWatch.start("导出数据");
-        List<ExportParam> exportParamList = new ArrayList<>();
-        ExportParam exportParam = new ExportParam();
-        exportParam.setRegionName(monitorTargetRegion.getName());
-        exportParam.setDeviceCode(trendParam.getSensorCode());
-        exportParam.setSensorRoad(trendParam.getRoads());
-        String sensorType = monitorTargetRegion.getSensorType().toUpperCase();
+        return list;
+    }
 
-        // 仅处理温度数据('W'类型)
-        if (sensorType.contains("W")) {
-            exportParam.setDataType("温度");
-            LinkedHashSet<String> x = sensorEchartDataResult.getTemperature().getX();
-            List<Float> y = sensorEchartDataResult.getTemperature().getY();
-            int i = 0;
-            for (String time : x) {
-                ExportParam exportParamOut = BeanUtil.copyProperties(exportParam, ExportParam.class);
-                exportParamOut.setTime(time);
-                exportParamOut.setData(y.get(i));
-                exportParamList.add(exportParamOut);
-                i++;
-            }
-        }
+    // 生成PDF报告
+    private void generatePdfReport(HttpServletResponse response, MultipartFile file,
+                                   MonitorTargetRegion monitorTargetRegion, AppTrendParam trendParam,
+                                   SensorEchartDataResult sensorEchartDataResult, Map<String, Float[]> statsMap,
+                                   Map<String, List<ExportParam>> exportDataMap) throws IOException {
 
         String fileName = "传感器数据报告.pdf";
         response.setContentType("application/pdf");
@@ -800,180 +692,297 @@ public class AppDeviceService {
 
         try (OutputStream outputStream = response.getOutputStream()) {
             PdfFont chineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
-            PdfWriter writer = new PdfWriter(outputStream);
+            WriterProperties writerProperties = new WriterProperties()
+                    .setFullCompressionMode(true)
+                    .useSmartMode();
+            PdfWriter writer = new PdfWriter(outputStream, writerProperties);
             PdfDocument pdfDoc = new PdfDocument(writer);
             Document document = new Document(pdfDoc, PageSize.A4);
             document.setMargins(15, 20, 10, 20);
 
-            // 报告标题
-            Paragraph title = new Paragraph("传感器数据报告")
+            // 预构建所有PDF元素
+            List<IElement> elements = new ArrayList<>();
+
+            // 添加报告标题
+            elements.add(new Paragraph("传感器数据报告")
                     .setFont(chineseFont)
                     .setTextAlignment(TextAlignment.CENTER)
                     .setFontSize(20)
                     .setBold()
-                    .setMarginBottom(10);
-            document.add(title);
+                    .setMarginBottom(10));
 
-            // 报告下载时间
-            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
-            Paragraph downloadTime = new Paragraph("报告下载时间: " + sdf.format(new Date()))
+            // 添加报告下载时间
+            elements.add(new Paragraph("报告下载时间: " + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()))
                     .setFont(chineseFont)
                     .setFontSize(10)
-                    .setTextAlignment(TextAlignment.LEFT);
-            document.add(downloadTime);
+                    .setTextAlignment(TextAlignment.LEFT));
 
-            Paragraph deviceInfo = new Paragraph("设备信息")
+            // 添加设备信息标题
+            elements.add(new Paragraph("设备信息")
                     .setFont(chineseFont)
                     .setTextAlignment(TextAlignment.CENTER)
                     .setFontSize(15)
                     .setBold()
-                    .setMarginBottom(2);
-            document.add(deviceInfo);
+                    .setMarginBottom(2));
 
+            // 构建设备信息表格
             float[] columnWidths = {2, 4, 2, 3};
-
-            // 设备信息表格
             Table deviceTable = new Table(UnitValue.createPercentArray(columnWidths))
                     .useAllAvailableWidth()
-                    .setHorizontalAlignment(HorizontalAlignment.CENTER);;
-            addDeviceTableRow(deviceTable, "设备名称:", monitorTargetRegion.getName().split("-")[0], "温度上限:", monitorTargetRegion.getTemperatureUp() + "°C");
-            addDeviceTableRow(deviceTable, "点位名称:", monitorTargetRegion.getName(), "温度下限:", monitorTargetRegion.getTemperatureDown() + "");
-            addDeviceTableRow(deviceTable, "冷链设备编号:", trendParam.getSensorCode(), "传感器路数:", trendParam.getRoads() + "路");
+                    .setHorizontalAlignment(HorizontalAlignment.CENTER);
+
+            String sensorType = monitorTargetRegion.getSensorType().toUpperCase();
+            addTableRow(deviceTable, "设备名称:", monitorTargetRegion.getName().split("-")[0],
+                    "点位名称:", monitorTargetRegion.getName());
+
+            if (sensorType.contains("W")) {
+                addTableRow(deviceTable, "温度上限:", monitorTargetRegion.getTemperatureUp() + "°C",
+                        "温度下限:", monitorTargetRegion.getTemperatureDown() + "°C");
+            }
+            if (sensorType.contains("S")) {
+                addTableRow(deviceTable, "湿度上限:", monitorTargetRegion.getHumidityUp() + "%",
+                        "湿度下限:", monitorTargetRegion.getHumidityDown() + "%");
+            }
+            if (sensorType.contains("C")) {
+                addTableRow(deviceTable, "二氧化碳上限:", monitorTargetRegion.getCo2Up() + "ppm",
+                        "二氧化碳下限:", monitorTargetRegion.getCo2Down() + "ppm");
+            }
+
+            addTableRow(deviceTable, "冷链设备编号:", trendParam.getSensorCode(),
+                    "传感器路数:", trendParam.getRoads() + "路");
             deviceTable.setMarginBottom(10);
-            document.add(deviceTable);
+            elements.add(deviceTable);
 
-            Paragraph data = new Paragraph("记录信息")
+            // 添加记录信息标题
+            elements.add(new Paragraph("记录信息")
                     .setFont(chineseFont)
                     .setTextAlignment(TextAlignment.CENTER)
                     .setFontSize(15)
                     .setBold()
-                    .setMarginBottom(2);
-            document.add(data);
+                    .setMarginBottom(2));
 
-            // 记录信息表格
+            // 构建记录信息表格
             Table recordTable = new Table(UnitValue.createPercentArray(columnWidths))
                     .useAllAvailableWidth()
                     .setHorizontalAlignment(HorizontalAlignment.CENTER);
-            addRecordTableRow(recordTable, "开始时间:", trendParam.getStartTime(), "最高温度:", String.format("%.1f°C", maxTemp));
-            addRecordTableRow(recordTable, "结束时间:", trendParam.getEndTime(), "最低温度:", String.format("%.1f°C", minTemp));
-            addRecordTableRow(recordTable, "", "", "平均温度:", String.format("%.1f°C", avgTemp));
-            recordTable.setMarginBottom(10);
-            document.add(recordTable);
 
-            if (!file.isEmpty()) {
-                Paragraph curve = new Paragraph("温度曲线")
-                        .setFont(chineseFont)
-                        .setTextAlignment(TextAlignment.CENTER)
-                        .setFontSize(15)
-                        .setBold();
-                document.add(curve);
+            Float[] tempStats = statsMap.get("temperature");
+            Float[] humStats = statsMap.get("humidity");
+            Float[] co2Stats = statsMap.get("co2");
+
+            if (sensorType.contains("W")) {
+                addTableRow(recordTable, "开始时间:", trendParam.getStartTime(),
+                        "最高温度:", TEMP_FORMAT.format(tempStats[0]) + "°C");
+                addTableRow(recordTable, "结束时间:", trendParam.getEndTime(),
+                        "最低温度:", TEMP_FORMAT.format(tempStats[1]) + "°C");
+                addTableRow(recordTable, "", "",
+                        "平均温度:", TEMP_FORMAT.format(tempStats[2]) + "°C");
+            }
+            if (sensorType.contains("S")) {
+                addTableRow(recordTable, "开始时间:", trendParam.getStartTime(),
+                        "最高湿度:", TEMP_FORMAT.format(humStats[0]) + "%");
+                addTableRow(recordTable, "结束时间:", trendParam.getEndTime(),
+                        "最低湿度:", TEMP_FORMAT.format(humStats[1]) + "%");
+                addTableRow(recordTable, "", "",
+                        "平均湿度:", TEMP_FORMAT.format(humStats[2]) + "%");
+            }
+            if (sensorType.contains("C")) {
+                addTableRow(recordTable, "开始时间:", trendParam.getStartTime(),
+                        "最高Co2浓度:", TEMP_FORMAT.format(co2Stats[0]) + "ppm");
+                addTableRow(recordTable, "结束时间:", trendParam.getEndTime(),
+                        "最低Co2浓度:", TEMP_FORMAT.format(co2Stats[1]) + "ppm");
+                addTableRow(recordTable, "", "",
+                        "平均Co2浓度:", TEMP_FORMAT.format(co2Stats[2]) + "ppm");
+            }
+            recordTable.setMarginBottom(10);
+            elements.add(recordTable);
+
+            // 添加图表图片
+            if (ObjectUtil.isNotNull(file)) {
+                if (sensorType.contains("W")) {
+                    elements.add(new Paragraph("温度曲线")
+                            .setFont(chineseFont)
+                            .setTextAlignment(TextAlignment.CENTER)
+                            .setFontSize(15)
+                            .setBold());
+                }
+                if (sensorType.contains("S")) {
+                    elements.add(new Paragraph("湿度曲线")
+                            .setFont(chineseFont)
+                            .setTextAlignment(TextAlignment.CENTER)
+                            .setFontSize(15)
+                            .setBold());
+                }
+                if (sensorType.contains("C")) {
+                    elements.add(new Paragraph("二氧化碳曲线")
+                            .setFont(chineseFont)
+                            .setTextAlignment(TextAlignment.CENTER)
+                            .setFontSize(15)
+                            .setBold());
+                }
 
                 try {
-                    // 获取图片字节数据
-                    byte[] imageBytes = file.getBytes();
-
+                    byte[] imageBytes = file.getBytes(); // 一次性读取
                     ImageData imageData = ImageDataFactory.create(imageBytes);
                     Image image = new Image(imageData);
 
-                    float pageWidth = PageSize.A4.getWidth();
-                    float scaledWidth = pageWidth * 0.8f;
+                    // 获取页面实际可用宽度(考虑页边距)
+                    float pageWidth = PageSize.A4.getWidth() - document.getLeftMargin() - document.getRightMargin();
 
-                    image.scaleToFit(scaledWidth, image.getImageHeight());
-                    image.setHorizontalAlignment(HorizontalAlignment.CENTER);
-                    document.add(image);
+                    // 计算缩放比例,使图片宽度等于页面可用宽度
+                    float scaleRatio = pageWidth / image.getImageWidth();
+                    image.scale(scaleRatio, scaleRatio); // 等比例缩放
 
+                    // 设置对齐方式
+                    image.setHorizontalAlignment(HorizontalAlignment.CENTER);
+                    elements.add(image);
                 } catch (IOException e) {
                     throw new RuntimeException("图片处理失败: " + e.getMessage());
                 }
             }
 
-            // 温度数据表格
-            Paragraph dataTitle = new Paragraph("温度数据")
-                    .setFont(chineseFont)
-                    .setTextAlignment(TextAlignment.CENTER)
-                    .setFontSize(15)
-                    .setBold()
-                    .setMarginBottom(2);
-            document.add(dataTitle);
-
-            Table dataTable = new Table(new float[]{2,1,2,1,2,1,2,1}).useAllAvailableWidth();
-            for (int i = 0; i < 4; i++) {
-                dataTable.addHeaderCell(createCell("时间", true));
-                dataTable.addHeaderCell(createCell("°C", true));
+            // 批量添加所有元素到文档
+            for (IElement element : elements) {
+                if (element instanceof IBlockElement) {
+                    document.add((IBlockElement) element);
+                } else if (element instanceof Image) {
+                    document.add((Image) element);
+                }
             }
 
-            // 数据分组逻辑(每组4条记录)
-            List<List<ExportParam>> groups = new ArrayList<>();
-            List<ExportParam> currentGroup = new ArrayList<>();
-            for (ExportParam param : exportParamList) {
-                currentGroup.add(param);
-                if (currentGroup.size() == 4) { // 每组4条记录(每行显示4个时间+温度)
-                    groups.add(currentGroup);
-                    currentGroup = new ArrayList<>();
-                }
+            // 添加数据表格 - 调整顺序使CO2数据最后显示
+            if (sensorType.contains("W")) {
+                createChart(document, chineseFont, exportDataMap.get("temperature"), "W");
             }
-            // 处理剩余数据
-            if (!currentGroup.isEmpty()) {
-                groups.add(currentGroup);
+            if (sensorType.contains("S")) {
+                createChart(document, chineseFont, exportDataMap.get("humidity"), "S");
             }
-
-            // 填充表格数据
-            for (List<ExportParam> group : groups) {
-                // 添加每组数据
-                for (ExportParam param : group) {
-                    dataTable.addCell(createCell(param.getTime(), false));
-                    dataTable.addCell(createCell(String.format("%.1f", param.getData()), false));
-                }
-                // 补全空单元格(每组不足4条时)
-                int missing = 4 - group.size();
-                for (int i = 0; i < missing; i++) {
-                    dataTable.addCell(createCell("", false));
-                    dataTable.addCell(createCell("", false));
-                }
+            if (sensorType.contains("C")) {
+                createChart(document, chineseFont, exportDataMap.get("co2"), "C");
             }
-            document.add(dataTable);
 
             document.close();
-        } catch (Exception e) {
-            throw new RuntimeException("PDF生成失败", e);
-        } finally {
-            stopWatch.stop();
-            log.info("PDF导出耗时:{}", stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
         }
     }
 
-    // 辅助方法:添加设备信息行
-    private void addDeviceTableRow(Table table, String label1, String value1, String label2, String value2) {
-        table.addCell(createCell(label1, true));
-        table.addCell(createCell(value1, false));
-        table.addCell(createCell(label2, true));
-        table.addCell(createCell(value2, false));
+    // 添加设备表格行
+    private void addTableRow(Table table, String label1, String value1, String label2, String value2) throws IOException {
+        PdfFont CHINESE_FONT = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
+        table.addCell(new Cell().add(new Paragraph(label1).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(value1).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(label2).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(value2).setFont(CHINESE_FONT)));
     }
 
-    // 辅助方法:添加记录信息
-    private void addRecordTableRow(Table table, String label1, String value1, String label2, String value2) {
-        table.addCell(createCell(label1, true));
-        table.addCell(createCell(value1, false));
-        table.addCell(createCell(label2, true));
-        table.addCell(createCell(value2, false));
-    }
+// 添加记录表格
+/*    private void addTableRow(Table table, String label1, String value1, String label2, String value2) {
+        table.addCell(new Cell().add(new Paragraph(label1).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(value1).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(label2).setFont(CHINESE_FONT)));
+        table.addCell(new Cell().add(new Paragraph(value2).setFont(CHINESE_FONT)));
+    }*/
 
-    // 辅助方法:创建带样式的单元格
-    private Cell createCell(String content, boolean isHeader) {
-        Cell cell = null;
-        try {
-            cell = new Cell().add(new Paragraph(content))
-                    .setFont(PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H"))
-                    .setPadding(5);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
+    // 创建数据图表
+    private void createChart(Document document, PdfFont font, List<ExportParam> dataList, String type) {
+        if (dataList == null || dataList.isEmpty()) return;
+
+        Paragraph chartTitle = new Paragraph();
+        switch (type) {
+            case "W":
+                chartTitle = new Paragraph("温度数据").setFont(font).setBold();
+                break;
+            case "S":
+                chartTitle = new Paragraph("湿度数据").setFont(font).setBold();
+                break;
+            case "C":
+                chartTitle = new Paragraph("二氧化碳数据").setFont(font).setBold();
+                break;
         }
-        if (isHeader) {
-            cell.setBackgroundColor(ColorConstants.LIGHT_GRAY)
-                    .setTextAlignment(TextAlignment.CENTER);
-        } else {
-            cell.setTextAlignment(TextAlignment.LEFT);
+
+        document.add(chartTitle.setTextAlignment(TextAlignment.CENTER).setFontSize(15));
+
+        // 创建8列表格,一宽一窄交替(宽列3倍于窄列)
+        float[] columnWidths = {3, 1, 3, 1, 3, 1}; // 宽:窄 = 3:1
+        Table dataTable = new Table(UnitValue.createPercentArray(columnWidths))
+                .useAllAvailableWidth()
+                .setHorizontalAlignment(HorizontalAlignment.CENTER);
+
+        // 添加表头 - 8列,交替显示"时间"和单位
+        for (int i = 0; i < 3; i++) {
+            dataTable.addHeaderCell(new Cell().add(new Paragraph("时间").setFont(font)));
+            dataTable.addHeaderCell(new Cell().add(new Paragraph(type.equals("C") ? "浓度" :
+                    (type.equals("W") ? "温度" : "湿度")).setFont(font)));
+        }
+
+        // 添加数据行,每行4组数据(8列)
+        for (int i = 0; i < dataList.size(); i += 4) {
+            // 处理每4个数据点为一组(对应8列)
+            for (int j = 0; j < 4 && (i + j) < dataList.size(); j++) {
+                ExportParam param = dataList.get(i + j);
+
+                // 宽列:时间
+                dataTable.addCell(new Cell().add(new Paragraph(formatTime(param.getTime())).setFont(font)));
+
+                // 窄列:数值
+                String value = TEMP_FORMAT.format(param.getData());
+                if (type.equals("W")) {
+                    value += "°C";
+                } else if (type.equals("S")) {
+                    value += "%";
+                } else {
+                    value += "ppm";
+                }
+                dataTable.addCell(new Cell().add(new Paragraph(value).setFont(font)));
+            }
+
+            // 如果最后一组不足4个,用空单元格填充
+            int remaining = 4 - (dataList.size() - i) % 4;
+            if (remaining > 0 && remaining < 4 && i + 4 >= dataList.size()) {
+                for (int k = 0; k < remaining * 2; k++) {
+                    dataTable.addCell(new Cell().add(new Paragraph("").setFont(font)));
+                }
+            }
+        }
+
+        document.add(dataTable.setMarginBottom(20));
+    }
+
+    private String formatTime(String time) {
+        String[] split = time.split(" ");
+        String regex = "[\u4e00-\u9fa5]";
+        Pattern pattern = Pattern.compile(regex);
+        Matcher matcher = pattern.matcher(split[0]);
+        String s = String.join("-", matcher.replaceAll("-").split("-"));
+        if (split.length != 2) {
+            return time;
         }
-        return cell;
+        return switch (split[1]) {
+            case "零点" -> s + " 00:00";
+            case "一点" -> s + " 01:00";
+            case "两点" -> s + " 02:00";
+            case "三点" -> s + " 03:00";
+            case "四点" -> s + " 04:00";
+            case "五点" -> s + " 05:00";
+            case "六点" -> s + " 06:00";
+            case "七点" -> s + " 07:00";
+            case "八点" -> s + " 08:00";
+            case "九点" -> s + " 09:00";
+            case "十点" -> s + " 10:00";
+            case "十一点" -> s + " 11:00";
+            case "十二点" -> s + " 12:00";
+            case "十三点" -> s + " 13:00";
+            case "十四点" -> s + " 14:00";
+            case "十五点" -> s + " 15:00";
+            case "十六点" -> s + " 16:00";
+            case "十七点" -> s + " 17:00";
+            case "十八点" -> s + " 18:00";
+            case "十九点" -> s + " 19:00";
+            case "二十点" -> s + " 20:00";
+            case "二十一点" -> s + " 21:00";
+            case "二十二点" -> s + " 22:00";
+            case "二十三点" -> s + " 23:00";
+            case "二十四点" -> s + " 24:00";
+            default -> time;
+        };
     }
 }

+ 62 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitornotice/service/impl/MonitorNoticeServiceImpl.java

@@ -19,6 +19,7 @@ import org.springframework.stereotype.Service;
 import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
 import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
 import vip.xiaonuo.coldchain.modular.app.param.AppDeviceQueryParams;
+import vip.xiaonuo.coldchain.modular.app.param.SensorEchartData;
 import vip.xiaonuo.coldchain.modular.app.param.SensorEchartDataResult;
 import vip.xiaonuo.coldchain.modular.app.service.AppDeviceService;
 import vip.xiaonuo.coldchain.modular.monitordevice.entity.MonitorDevice;
@@ -36,6 +37,10 @@ import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
 import java.time.LocalTime;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static java.time.LocalTime.now;
 
@@ -180,6 +185,12 @@ public class MonitorNoticeServiceImpl extends ServiceImpl<MonitorNoticeMapper, M
             trend.setTMax((float) tMax);
             trend.setTMin((float) tMin);
             trend.setTAvg(Float.parseFloat(df.format(tAvg)));
+            LinkedHashSet<String> x = sensorEchartDataResult.getTemperature().getX();
+            LinkedHashSet<String> newX = new LinkedHashSet<>();
+            x.forEach(xData -> {
+                newX.add(formatTime(xData));
+            });
+            sensorEchartDataResult.setTemperature(new SensorEchartData(newX, tList));
         }
         if (!hList.isEmpty()) {
             double hMax = hList.stream().mapToDouble(Number::floatValue).min().getAsDouble();
@@ -188,6 +199,12 @@ public class MonitorNoticeServiceImpl extends ServiceImpl<MonitorNoticeMapper, M
             trend.setHMax((float) hMax);
             trend.setHMin((float) hMin);
             trend.setHAvg((float) hAvg);
+            LinkedHashSet<String> x = sensorEchartDataResult.getHumidity().getX();
+            LinkedHashSet<String> newX = new LinkedHashSet<>();
+            x.forEach(xData -> {
+                newX.add(formatTime(xData));
+            });
+            sensorEchartDataResult.setHumidity(new SensorEchartData(newX, tList));
         }
         if (!cList.isEmpty()) {
             double cMax = cList.stream().mapToDouble(Number::floatValue).min().getAsDouble();
@@ -196,11 +213,56 @@ public class MonitorNoticeServiceImpl extends ServiceImpl<MonitorNoticeMapper, M
             trend.setCMax((float) cMax);
             trend.setCMin((float) cMin);
             trend.setCAvg((float) cAvg);
+            LinkedHashSet<String> x = sensorEchartDataResult.getCo2().getX();
+            LinkedHashSet<String> newX = new LinkedHashSet<>();
+            x.forEach(xData -> {
+                newX.add(formatTime(xData));
+            });
+            sensorEchartDataResult.setCo2(new SensorEchartData(newX, tList));
         }
         trend.setSensorEchartDataResult(sensorEchartDataResult);
         return trend;
     }
 
+    private String formatTime(String time) {
+        String[] split = time.split(" ");
+        String regex = "[\u4e00-\u9fa5]";
+        Pattern pattern = Pattern.compile(regex);
+        Matcher matcher = pattern.matcher(split[0]);
+        String s = String.join("-", matcher.replaceAll("-").split("-"));
+        if (split.length != 2) {
+            return time;
+        }
+        return switch (split[1]) {
+            case "零点" -> s + " 00:00";
+            case "一点" -> s + " 01:00";
+            case "两点" -> s + " 02:00";
+            case "三点" -> s + " 03:00";
+            case "四点" -> s + " 04:00";
+            case "五点" -> s + " 05:00";
+            case "六点" -> s + " 06:00";
+            case "七点" -> s + " 07:00";
+            case "八点" -> s + " 08:00";
+            case "九点" -> s + " 09:00";
+            case "十点" -> s + " 10:00";
+            case "十一点" -> s + " 11:00";
+            case "十二点" -> s + " 12:00";
+            case "十三点" -> s + " 13:00";
+            case "十四点" -> s + " 14:00";
+            case "十五点" -> s + " 15:00";
+            case "十六点" -> s + " 16:00";
+            case "十七点" -> s + " 17:00";
+            case "十八点" -> s + " 18:00";
+            case "十九点" -> s + " 19:00";
+            case "二十点" -> s + " 20:00";
+            case "二十一点" -> s + " 21:00";
+            case "二十二点" -> s + " 22:00";
+            case "二十三点" -> s + " 23:00";
+            case "二十四点" -> s + " 24:00";
+            default -> time;
+        };
+    }
+
     @Override
     public Realtime getRealtime(RealtimeParam realtimeParam) {
         MonitorDevice monitorDevice = monitorDeviceService.getById(realtimeParam.getDeviceId());