Bläddra i källkod

feat(water): deliver supply monitoring (#8)

Kazerin 3 dagar sedan
förälder
incheckning
d564a63feb

+ 124 - 0
pipe-network-service/sql/water_supply_monitoring_test_data.sql

@@ -0,0 +1,124 @@
+-- ============================================================
+-- 供水管网 T4 运行监测 测试数据脚本(KingbaseES)
+-- 说明:
+--   1. 所有测试数据均使用 WS-TEST- / ws_test_ 前缀,可重复执行。
+--   2. 本脚本只初始化测试行,不创建监测业务表;执行前请确认以下表已存在:
+--      app_user.equipment_type、app_user.equipment_base、app_user.radar_data、
+--      app_user.telemetry_data、app_user.firefighting_pressure、app_user.noise_info
+--   3. 覆盖场景:雷达流量、遥测流量、压力、漏失、无数据设备、
+--      数值范围过滤、时间范围过滤与近24小时之外的旧趋势点。
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 0. 清理历史测试数据(先清测量值,再清设备与类型)
+-- ------------------------------------------------------------
+DELETE FROM app_user.radar_data WHERE device_code LIKE 'WS-TEST-%';
+DELETE FROM app_user.telemetry_data WHERE device_code LIKE 'WS-TEST-%';
+DELETE FROM app_user.firefighting_pressure WHERE telemetering_station LIKE 'WS-TEST-%';
+DELETE FROM app_user.noise_info WHERE encode LIKE 'WS-TEST-%';
+DELETE FROM app_user.equipment_base WHERE equipment_id LIKE 'WS-TEST-EQ-%';
+DELETE FROM app_user.equipment_type WHERE id LIKE 'WS-TEST-TYPE-%';
+
+-- ------------------------------------------------------------
+-- 1. 设备类型与监测设备(equipment_type_id 显式关联类型主键)
+-- ------------------------------------------------------------
+INSERT INTO app_user.equipment_type
+    (id, type_id, type_name, parent_type_id, type_remark)
+VALUES
+    ('WS-TEST-TYPE-FLOW', 'ws_test_flow_monitoring', '流量监测(测试)', '0', '供水T4流量监测测试类型'),
+    ('WS-TEST-TYPE-PRESSURE', 'ws_test_pressure_monitoring', '压力监测(测试)', '0', '供水T4压力监测测试类型'),
+    ('WS-TEST-TYPE-LEAKAGE', 'ws_test_leakage_monitoring', '漏失监测(测试)', '0', '供水T4漏失监测测试类型');
+
+INSERT INTO app_user.equipment_base
+    (equipment_id, equipment_code, equipment_name, equipment_model, equipment_type_id,
+     longitude, latitude, equipment_location, maintainer, maintainer_phone,
+     ownership_unit, remark, create_time, update_time, del_flag, district)
+VALUES
+    ('WS-TEST-EQ-FLOW-RADAR', 'WS-TEST-FLOW-RADAR', '测试雷达流量监测点', 'RADAR-FLOW-TEST',
+     'WS-TEST-TYPE-FLOW', 113.28000000, 23.12000000, '供水测试大道1号',
+     '供水测试维护员', '13800000001', '供水测试公司', '供水T4运行监测测试数据',
+     now() - interval '7 days', now(), '0', '供水测试区'),
+    ('WS-TEST-EQ-FLOW-TELEMETRY', 'WS-TEST-FLOW-TELEMETRY', '测试遥测终端流量监测点', 'TELEMETRY-FLOW-TEST',
+     'WS-TEST-TYPE-FLOW', 113.29000000, 23.13000000, '供水测试大道2号',
+     '供水测试维护员', '13800000002', '供水测试公司', '供水T4运行监测测试数据',
+     now() - interval '7 days', now(), '0', '供水测试区'),
+    ('WS-TEST-EQ-PRESSURE', 'WS-TEST-PRESSURE', '测试压力监测点', 'PRESSURE-TEST',
+     'WS-TEST-TYPE-PRESSURE', 113.30000000, 23.14000000, '供水测试大道3号',
+     '供水测试维护员', '13800000003', '供水测试公司', '供水T4运行监测测试数据',
+     now() - interval '7 days', now(), '0', '供水测试区'),
+    ('WS-TEST-EQ-LEAKAGE', 'WS-TEST-LEAKAGE', '测试漏失监测点', 'LEAKAGE-TEST',
+     'WS-TEST-TYPE-LEAKAGE', 113.31000000, 23.15000000, '供水测试大道4号',
+     '供水测试维护员', '13800000004', '供水测试公司', '供水T4运行监测测试数据',
+     now() - interval '7 days', now(), '0', '供水测试区'),
+    ('WS-TEST-EQ-FLOW-EMPTY', 'WS-TEST-FLOW-EMPTY', '测试无测量流量监测点', 'FLOW-EMPTY-TEST',
+     'WS-TEST-TYPE-FLOW', 113.32000000, 23.16000000, '供水测试大道5号',
+     '供水测试维护员', '13800000005', '供水测试公司', '供水T4运行监测无数据测试设备',
+     now() - interval '7 days', now(), '0', '供水测试区');
+
+-- ------------------------------------------------------------
+-- 2. 雷达流量计数据
+--    数值范围 20-40 时:最新 120 不符合,应回退到 30。
+--    5 天前数据用于验证趋势时间范围过滤。
+-- ------------------------------------------------------------
+INSERT INTO app_user.radar_data
+    (id, system_identifier, device_code, meter1_instant_flow, flow_speed, create_time)
+VALUES
+    ('WS-TEST-RADAR-FLOW-001', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-RADAR', '30', '1.20',
+     now() - interval '3 hours'),
+    ('WS-TEST-RADAR-FLOW-002', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-RADAR', '120', '3.50',
+     now() - interval '1 hour'),
+    ('WS-TEST-RADAR-FLOW-003', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-RADAR', '18', '0.80',
+     now() - interval '5 days');
+
+-- ------------------------------------------------------------
+-- 3. 遥测终端流量数据(device_code 与 equipment_base.equipment_code 关联)
+-- ------------------------------------------------------------
+INSERT INTO app_user.telemetry_data
+    (id, system_identifier, device_code, meter1_instant_flow, meter1_positive_total,
+     flow_speed, temperature, create_time)
+VALUES
+    ('WS-TEST-TELEMETRY-001', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-TELEMETRY', '42', '12345',
+     '1.80', '24.50', now() - interval '2 hours'),
+    ('WS-TEST-TELEMETRY-002', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-TELEMETRY', '36', '12300',
+     '1.50', '24.00', now() - interval '4 hours'),
+    ('WS-TEST-TELEMETRY-003', 'WS-TEST-SYSTEM', 'WS-TEST-FLOW-TELEMETRY', '20', '12200',
+     '0.90', '23.50', now() - interval '5 days');
+
+-- ------------------------------------------------------------
+-- 4. 消防压力数据(telemetering_station 与 equipment_base.equipment_code 关联)
+-- ------------------------------------------------------------
+INSERT INTO app_user.firefighting_pressure
+    (id, central_station, telemetering_station, sending_time, observed_time,
+     pressure_value, create_time)
+VALUES
+    ('WS-TEST-PRESSURE-001', 'WS-TEST-CENTER', 'WS-TEST-PRESSURE',
+     now() - interval '3 hours', now() - interval '3 hours', 0.42, now() - interval '3 hours'),
+    ('WS-TEST-PRESSURE-002', 'WS-TEST-CENTER', 'WS-TEST-PRESSURE',
+     now() - interval '1 hour', now() - interval '1 hour', 0.95, now() - interval '1 hour'),
+    ('WS-TEST-PRESSURE-003', 'WS-TEST-CENTER', 'WS-TEST-PRESSURE',
+     now() - interval '5 days', now() - interval '5 days', 0.20, now() - interval '5 days');
+
+-- ------------------------------------------------------------
+-- 5. 漏失噪声数据(encode 与 equipment_base.equipment_code 关联)
+-- ------------------------------------------------------------
+INSERT INTO app_user.noise_info
+    (encode, power, gain, rms, center_frequency, ts, create_time, update_time)
+VALUES
+    ('WS-TEST-LEAKAGE', 12.00, 2.00, 120, 800.00,
+     to_char(now() - interval '2 hours', 'YYYY-MM-DD HH24:MI:SS'),
+     now() - interval '2 hours', now() - interval '2 hours'),
+    ('WS-TEST-LEAKAGE', 9.00, 2.00, 100, 780.00,
+     to_char(now() - interval '4 hours', 'YYYY-MM-DD HH24:MI:SS'),
+     now() - interval '4 hours', now() - interval '4 hours'),
+    ('WS-TEST-LEAKAGE', 5.00, 1.00, 80, 700.00,
+     to_char(now() - interval '5 days', 'YYYY-MM-DD HH24:MI:SS'),
+     now() - interval '5 days', now() - interval '5 days');
+
+-- ------------------------------------------------------------
+-- 快速核验:
+--   流量分页(默认最新值):雷达120、遥测42,共2条;
+--   流量分页 flowMin=20&flowMax=40:雷达30、遥测36,共2条;
+--   压力分页 pressureMin=0.30&pressureMax=0.60:0.42,共1条;
+--   漏失分页 powerMin=8&powerMax=15:12,共1条;
+--   WS-TEST-FLOW-EMPTY 无测量数据,不应出现在任何监测页。
+-- ============================================================

+ 23 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/FlowMonitoring/FlowMonitoringController.java

@@ -1,9 +1,12 @@
 package com.zksy.web.controller.WaterSupply.FlowMonitoring;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.Monitoring.service.MonitoringExportService;
 import com.zksy.base.domain.vo.FlowMonitoringVO;
 import com.zksy.base.service.MonitoringService;
+import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -13,6 +16,10 @@ import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +33,9 @@ public class FlowMonitoringController {
     @Autowired
     private MonitoringService monitoringService;
 
+    @Autowired
+    private MonitoringExportService monitoringExportService;
+
     @GetMapping("/page")
     @ApiOperation("流量监测分页查询")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:flow:list')")
@@ -48,25 +58,32 @@ public class FlowMonitoringController {
     @GetMapping("/trend/{equipmentId}")
     @ApiOperation("流量近24小时趋势数据")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:flow:list')")
-    public AjaxResult getFlowTrend24h(@PathVariable String equipmentId) {
-        List<Map<String, Object>> trend = monitoringService.getFlowTrend24h(equipmentId);
+    public AjaxResult getFlowTrend(@PathVariable String equipmentId,
+                                    @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+                                    @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+        List<Map<String, Object>> trend = monitoringService.getFlowTrend(equipmentId, startTime, endTime);
         return AjaxResult.success(trend);
     }
 
     @GetMapping("/export")
     @ApiOperation("流量监测数据导出")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:flow:export')")
-    public AjaxResult exportFlowMonitorData(
+    @Log(title = "供水流量监测", businessType = BusinessType.EXPORT)
+    public void exportFlowMonitorData(HttpServletResponse response,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("安装位置") @RequestParam(required = false) String equipmentLocation,
             @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
             @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
             @ApiParam("流量最小值") @RequestParam(required = false) Double flowMin,
-            @ApiParam("流量最大值") @RequestParam(required = false) Double flowMax) {
+            @ApiParam("流量最大值") @RequestParam(required = false) Double flowMax) throws IOException {
         Page<FlowMonitoringVO> page = monitoringService.getFlowMonitorPage(
-                1, 10000, equipmentName, equipmentCode, equipmentLocation,
+                1, Integer.MAX_VALUE, equipmentName, equipmentCode, equipmentLocation,
                 startTime, endTime, flowMin, flowMax);
-        return AjaxResult.success(page.getRecords());
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setHeader("Content-Disposition", "attachment;filename="
+                + URLEncoder.encode("流量监测数据.xlsx", StandardCharsets.UTF_8.name()));
+        monitoringExportService.writeFlow(response.getOutputStream(), page.getRecords());
     }
 }

+ 23 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/LeakageMonitoring/LeakageMonitoringController.java

@@ -1,9 +1,12 @@
 package com.zksy.web.controller.WaterSupply.LeakageMonitoring;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.Monitoring.service.MonitoringExportService;
 import com.zksy.base.domain.vo.LeakageMonitoringVO;
 import com.zksy.base.service.MonitoringService;
+import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -13,6 +16,10 @@ import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +33,9 @@ public class LeakageMonitoringController {
     @Autowired
     private MonitoringService monitoringService;
 
+    @Autowired
+    private MonitoringExportService monitoringExportService;
+
     @GetMapping("/page")
     @ApiOperation("漏失监测分页查询")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:leakage:list')")
@@ -48,25 +58,32 @@ public class LeakageMonitoringController {
     @GetMapping("/trend/{equipmentId}")
     @ApiOperation("漏失近24小时趋势数据")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:leakage:list')")
-    public AjaxResult getLeakageTrend24h(@PathVariable String equipmentId) {
-        List<Map<String, Object>> trend = monitoringService.getLeakageTrend24h(equipmentId);
+    public AjaxResult getLeakageTrend(@PathVariable String equipmentId,
+                                      @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+                                      @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+        List<Map<String, Object>> trend = monitoringService.getLeakageTrend(equipmentId, startTime, endTime);
         return AjaxResult.success(trend);
     }
 
     @GetMapping("/export")
     @ApiOperation("漏失监测数据导出")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:leakage:export')")
-    public AjaxResult exportLeakageMonitorData(
+    @Log(title = "供水漏失监测", businessType = BusinessType.EXPORT)
+    public void exportLeakageMonitorData(HttpServletResponse response,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("安装位置") @RequestParam(required = false) String equipmentLocation,
             @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
             @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
             @ApiParam("功率最小值") @RequestParam(required = false) Double powerMin,
-            @ApiParam("功率最大值") @RequestParam(required = false) Double powerMax) {
+            @ApiParam("功率最大值") @RequestParam(required = false) Double powerMax) throws IOException {
         Page<LeakageMonitoringVO> page = monitoringService.getLeakageMonitorPage(
-                1, 10000, equipmentName, equipmentCode, equipmentLocation,
+                1, Integer.MAX_VALUE, equipmentName, equipmentCode, equipmentLocation,
                 startTime, endTime, powerMin, powerMax);
-        return AjaxResult.success(page.getRecords());
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setHeader("Content-Disposition", "attachment;filename="
+                + URLEncoder.encode("漏失监测数据.xlsx", StandardCharsets.UTF_8.name()));
+        monitoringExportService.writeLeakage(response.getOutputStream(), page.getRecords());
     }
 }

+ 23 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PressureMonitoring/PressureMonitoringController.java

@@ -1,9 +1,12 @@
 package com.zksy.web.controller.WaterSupply.PressureMonitoring;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.Monitoring.service.MonitoringExportService;
 import com.zksy.base.domain.vo.PressureMonitoringVO;
 import com.zksy.base.service.MonitoringService;
+import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -13,6 +16,10 @@ import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +33,9 @@ public class PressureMonitoringController {
     @Autowired
     private MonitoringService monitoringService;
 
+    @Autowired
+    private MonitoringExportService monitoringExportService;
+
     @GetMapping("/page")
     @ApiOperation("压力监测分页查询")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:pressure:list')")
@@ -48,25 +58,32 @@ public class PressureMonitoringController {
     @GetMapping("/trend/{equipmentId}")
     @ApiOperation("压力近24小时趋势数据")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:pressure:list')")
-    public AjaxResult getPressureTrend24h(@PathVariable String equipmentId) {
-        List<Map<String, Object>> trend = monitoringService.getPressureTrend24h(equipmentId);
+    public AjaxResult getPressureTrend(@PathVariable String equipmentId,
+                                       @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+                                       @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) {
+        List<Map<String, Object>> trend = monitoringService.getPressureTrend(equipmentId, startTime, endTime);
         return AjaxResult.success(trend);
     }
 
     @GetMapping("/export")
     @ApiOperation("压力监测数据导出")
     @PreAuthorize("@ss.hasPermi('waterSupply:monitoring:pressure:export')")
-    public AjaxResult exportPressureMonitorData(
+    @Log(title = "供水压力监测", businessType = BusinessType.EXPORT)
+    public void exportPressureMonitorData(HttpServletResponse response,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("安装位置") @RequestParam(required = false) String equipmentLocation,
             @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
             @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
             @ApiParam("压力最小值(Mpa)") @RequestParam(required = false) Double pressureMin,
-            @ApiParam("压力最大值(Mpa)") @RequestParam(required = false) Double pressureMax) {
+            @ApiParam("压力最大值(Mpa)") @RequestParam(required = false) Double pressureMax) throws IOException {
         Page<PressureMonitoringVO> page = monitoringService.getPressureMonitorPage(
-                1, 10000, equipmentName, equipmentCode, equipmentLocation,
+                1, Integer.MAX_VALUE, equipmentName, equipmentCode, equipmentLocation,
                 startTime, endTime, pressureMin, pressureMax);
-        return AjaxResult.success(page.getRecords());
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setHeader("Content-Disposition", "attachment;filename="
+                + URLEncoder.encode("压力监测数据.xlsx", StandardCharsets.UTF_8.name()));
+        monitoringExportService.writePressure(response.getOutputStream(), page.getRecords());
     }
 }

+ 18 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/Monitoring/service/MonitoringExportService.java

@@ -0,0 +1,18 @@
+package com.zksy.WaterSupply.Monitoring.service;
+
+import com.zksy.base.domain.vo.FlowMonitoringVO;
+import com.zksy.base.domain.vo.LeakageMonitoringVO;
+import com.zksy.base.domain.vo.PressureMonitoringVO;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+public interface MonitoringExportService {
+
+    void writeFlow(OutputStream outputStream, List<FlowMonitoringVO> records) throws IOException;
+
+    void writePressure(OutputStream outputStream, List<PressureMonitoringVO> records) throws IOException;
+
+    void writeLeakage(OutputStream outputStream, List<LeakageMonitoringVO> records) throws IOException;
+}

+ 118 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/Monitoring/service/impl/MonitoringExportServiceImpl.java

@@ -0,0 +1,118 @@
+package com.zksy.WaterSupply.Monitoring.service.impl;
+
+import com.zksy.WaterSupply.Monitoring.service.MonitoringExportService;
+import com.zksy.base.domain.vo.FlowMonitoringVO;
+import com.zksy.base.domain.vo.LeakageMonitoringVO;
+import com.zksy.base.domain.vo.PressureMonitoringVO;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Service
+public class MonitoringExportServiceImpl implements MonitoringExportService {
+
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final String[] FLOW_HEADERS = {"设备ID", "设备编码", "设备名称", "安装位置", "设备子类型", "数据来源", "瞬时流量(m³/h)", "流速(m/s)", "正向累计流量", "温度", "采集时间", "经纬度"};
+    private static final String[] PRESSURE_HEADERS = {"设备ID", "设备编码", "设备名称", "安装位置", "设备子类型", "压力值(MPa)", "采集时间", "经纬度"};
+    private static final String[] LEAKAGE_HEADERS = {"设备ID", "设备编码", "设备名称", "安装位置", "设备子类型", "功率(kW)", "增益", "RMS", "中心频率", "采集时间", "经纬度"};
+
+    @Override
+    public void writeFlow(OutputStream outputStream, List<FlowMonitoringVO> records) throws IOException {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("流量监测");
+            writeHeader(sheet, FLOW_HEADERS);
+            int rowIndex = 1;
+            for (FlowMonitoringVO record : records) {
+                Row row = sheet.createRow(rowIndex++);
+                writeCell(row, 0, record.getEquipmentId());
+                writeCell(row, 1, record.getEquipmentCode());
+                writeCell(row, 2, record.getEquipmentName());
+                writeCell(row, 3, record.getEquipmentLocation());
+                writeCell(row, 4, record.getEquipmentTypeName());
+                writeCell(row, 5, record.getDataSource());
+                writeCell(row, 6, record.getInstantFlow());
+                writeCell(row, 7, record.getFlowSpeed());
+                writeCell(row, 8, record.getPositiveTotal());
+                writeCell(row, 9, record.getTemperature());
+                writeCell(row, 10, formatTime(record.getMonitorTime()));
+                writeCell(row, 11, record.getGps());
+            }
+            workbook.write(outputStream);
+        }
+    }
+
+    @Override
+    public void writePressure(OutputStream outputStream, List<PressureMonitoringVO> records) throws IOException {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("压力监测");
+            writeHeader(sheet, PRESSURE_HEADERS);
+            int rowIndex = 1;
+            for (PressureMonitoringVO record : records) {
+                Row row = sheet.createRow(rowIndex++);
+                writeCell(row, 0, record.getEquipmentId());
+                writeCell(row, 1, record.getEquipmentCode());
+                writeCell(row, 2, record.getEquipmentName());
+                writeCell(row, 3, record.getEquipmentLocation());
+                writeCell(row, 4, record.getEquipmentTypeName());
+                writeNumber(row, 5, record.getPressureValue());
+                writeCell(row, 6, formatTime(record.getMonitorTime()));
+                writeCell(row, 7, record.getGps());
+            }
+            workbook.write(outputStream);
+        }
+    }
+
+    @Override
+    public void writeLeakage(OutputStream outputStream, List<LeakageMonitoringVO> records) throws IOException {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("漏失监测");
+            writeHeader(sheet, LEAKAGE_HEADERS);
+            int rowIndex = 1;
+            for (LeakageMonitoringVO record : records) {
+                Row row = sheet.createRow(rowIndex++);
+                writeCell(row, 0, record.getEquipmentId());
+                writeCell(row, 1, record.getEquipmentCode());
+                writeCell(row, 2, record.getEquipmentName());
+                writeCell(row, 3, record.getEquipmentLocation());
+                writeCell(row, 4, record.getEquipmentTypeName());
+                writeNumber(row, 5, record.getPower());
+                writeNumber(row, 6, record.getGain());
+                writeNumber(row, 7, record.getRms());
+                writeNumber(row, 8, record.getCenterFrequency());
+                writeCell(row, 9, formatTime(record.getMonitorTime()));
+                writeCell(row, 10, record.getGps());
+            }
+            workbook.write(outputStream);
+        }
+    }
+
+    private void writeHeader(Sheet sheet, String[] headers) {
+        Row row = sheet.createRow(0);
+        for (int i = 0; i < headers.length; i++) {
+            row.createCell(i).setCellValue(headers[i]);
+        }
+    }
+
+    private void writeCell(Row row, int index, String value) {
+        row.createCell(index).setCellValue(value == null ? "" : value);
+    }
+
+    private void writeNumber(Row row, int index, Number value) {
+        if (value == null) {
+            row.createCell(index).setCellValue("");
+        } else {
+            row.createCell(index).setCellValue(value.doubleValue());
+        }
+    }
+
+    private String formatTime(java.time.LocalDateTime value) {
+        return value == null ? "" : TIME_FORMATTER.format(value);
+    }
+}

+ 3 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/MonitoringService.java

@@ -29,9 +29,9 @@ public interface MonitoringService {
                                                     LocalDateTime startTime, LocalDateTime endTime,
                                                     Double powerMin, Double powerMax);
 
-    List<Map<String, Object>> getFlowTrend24h(String equipmentId);
+    List<Map<String, Object>> getFlowTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime);
 
-    List<Map<String, Object>> getPressureTrend24h(String equipmentId);
+    List<Map<String, Object>> getPressureTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime);
 
-    List<Map<String, Object>> getLeakageTrend24h(String equipmentId);
+    List<Map<String, Object>> getLeakageTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime);
 }

+ 351 - 324
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MonitoringServiceImpl.java

@@ -20,18 +20,20 @@ import com.zksy.base.radar.mapper.RadarDataMapper;
 import com.zksy.base.service.MonitoringService;
 import com.zksy.base.telemetry.domain.TelemetryData;
 import com.zksy.base.telemetry.mapper.TelemetryDataMapper;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
-@Slf4j
 @Service
 public class MonitoringServiceImpl implements MonitoringService {
 
+    private static final String RADAR_SOURCE = "雷达流量计";
+    private static final String TELEMETRY_SOURCE = "遥测终端";
+
     @Autowired
     private EquipmentBaseMapper equipmentBaseMapper;
 
@@ -52,412 +54,437 @@ public class MonitoringServiceImpl implements MonitoringService {
 
     @Override
     public Page<FlowMonitoringVO> getFlowMonitorPage(long pageNum, long pageSize,
-                                                      String equipmentName, String equipmentCode,
-                                                      String equipmentLocation,
-                                                      LocalDateTime startTime, LocalDateTime endTime,
-                                                      Double flowMin, Double flowMax) {
-        LambdaQueryWrapper<EquipmentBase> baseWrapper = new LambdaQueryWrapper<>();
-        baseWrapper.like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
-                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
-                .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
-                .orderByDesc(EquipmentBase::getCreateTime);
-        constrainToMonitoringTypes(baseWrapper, "flow");
-
-        Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
-        Page<FlowMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
-
-        if (CollUtil.isEmpty(basePage.getRecords())) {
-            resultPage.setRecords(new ArrayList<>());
-            return resultPage;
+                                                     String equipmentName, String equipmentCode,
+                                                     String equipmentLocation,
+                                                     LocalDateTime startTime, LocalDateTime endTime,
+                                                     Double flowMin, Double flowMax) {
+        List<EquipmentBase> devices = selectMonitoringDevices(equipmentName, equipmentCode, equipmentLocation, "flow");
+        if (CollUtil.isEmpty(devices)) {
+            return emptyPage(pageNum, pageSize);
         }
 
-        List<EquipmentBase> devices = basePage.getRecords();
-        List<String> deviceCodes = devices.stream()
-                .map(EquipmentBase::getEquipmentCode).filter(StrUtil::isNotBlank).collect(Collectors.toList());
-        List<String> typeIds = devices.stream()
-                .map(EquipmentBase::getEquipmentTypeId).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
-
-        Map<String, EquipmentType> typeMap = new HashMap<>();
-        if (CollUtil.isNotEmpty(typeIds)) {
-            List<EquipmentType> types = equipmentTypeMapper.selectBatchIds(typeIds);
-            typeMap = types.stream().collect(Collectors.toMap(EquipmentType::getTypeId, t -> t, (a, b) -> a));
-        }
+        Map<String, EquipmentType> typeMap = selectTypeMap(devices);
+        Map<String, RadarData> radarMap = selectLatestRadar(devices, startTime, endTime, flowMin, flowMax);
+        Map<String, TelemetryData> telemetryMap = selectLatestTelemetry(devices, startTime, endTime, flowMin, flowMax);
 
-        Map<String, RadarData> radarLatestMap = new HashMap<>();
-        Map<String, TelemetryData> telemetryLatestMap = new HashMap<>();
-
-        if (CollUtil.isNotEmpty(deviceCodes)) {
-            List<RadarData> radarDataList = radarDataMapper.selectList(
-                    new LambdaQueryWrapper<RadarData>().in(RadarData::getDeviceCode, deviceCodes)
-                            .ge(startTime != null, RadarData::getCreateTime, startTime)
-                            .le(endTime != null, RadarData::getCreateTime, endTime)
-                            .orderByDesc(RadarData::getCreateTime)
-            );
-            if (CollUtil.isNotEmpty(radarDataList)) {
-                Set<String> seen = new HashSet<>();
-                for (RadarData d : radarDataList) {
-                    if (seen.add(d.getDeviceCode())) {
-                        radarLatestMap.put(d.getDeviceCode(), d);
-                    }
-                }
-            }
-
-            List<TelemetryData> telemetryDataList = telemetryDataMapper.selectList(
-                    new LambdaQueryWrapper<TelemetryData>().in(TelemetryData::getDeviceCode, deviceCodes)
-                            .ge(startTime != null, TelemetryData::getCreateTime, startTime)
-                            .le(endTime != null, TelemetryData::getCreateTime, endTime)
-                            .orderByDesc(TelemetryData::getCreateTime)
-            );
-            if (CollUtil.isNotEmpty(telemetryDataList)) {
-                Set<String> seen = new HashSet<>();
-                for (TelemetryData d : telemetryDataList) {
-                    if (seen.add(d.getDeviceCode())) {
-                        telemetryLatestMap.put(d.getDeviceCode(), d);
-                    }
-                }
-            }
-        }
-
-        List<FlowMonitoringVO> voList = new ArrayList<>();
+        List<FlowMonitoringVO> allRows = new ArrayList<>();
         for (EquipmentBase device : devices) {
-            FlowMonitoringVO vo = new FlowMonitoringVO();
-            vo.setEquipmentId(device.getEquipmentId());
-            vo.setEquipmentCode(device.getEquipmentCode());
-            vo.setEquipmentName(device.getEquipmentName());
-            vo.setEquipmentLocation(device.getEquipmentLocation());
-            vo.setEquipmentTypeId(device.getEquipmentTypeId());
-            EquipmentType type = typeMap.get(device.getEquipmentTypeId());
-            if (type != null) {
-                vo.setEquipmentTypeName(type.getTypeName());
-            }
-            vo.setLongitude(device.getLongitude());
-            vo.setLatitude(device.getLatitude());
-            if (device.getLongitude() != null && device.getLatitude() != null) {
-                vo.setGps(device.getLongitude() + "," + device.getLatitude());
-            }
-
             String code = device.getEquipmentCode();
-            boolean matched = false;
+            RadarData radar = radarMap.get(code);
+            TelemetryData telemetry = telemetryMap.get(code);
+            if (radar == null && telemetry == null) {
+                continue;
+            }
 
-            RadarData radar = radarLatestMap.get(code);
-            if (radar != null && isFlowInRange(radar.getMeter1InstantFlow(), flowMin, flowMax)) {
-                vo.setDataSource("雷达流量计");
+            FlowMonitoringVO vo = new FlowMonitoringVO();
+            fillBaseFields(vo, device, typeMap.get(device.getEquipmentTypeId()));
+            if (radar != null) {
+                vo.setDataSource(RADAR_SOURCE);
                 vo.setInstantFlow(radar.getMeter1InstantFlow());
                 vo.setFlowSpeed(radar.getFlowSpeed());
                 vo.setMonitorTime(radar.getCreateTime());
-                matched = true;
-            }
-
-            if (!matched) {
-                TelemetryData telemetry = telemetryLatestMap.get(code);
-                if (telemetry != null && isFlowInRange(telemetry.getMeter1PositiveTotal(), flowMin, flowMax)) {
-                    vo.setDataSource("遥测终端");
-                    vo.setPositiveTotal(telemetry.getMeter1PositiveTotal());
-                    vo.setTemperature(telemetry.getTemperature());
-                    vo.setFlowSpeed(telemetry.getFlowSpeed());
-                    vo.setMonitorTime(telemetry.getCreateTime());
-                    matched = true;
-                }
-            }
-
-            if (matched || (flowMin == null && flowMax == null)) {
-                voList.add(vo);
-            }
-        }
-
-        resultPage.setRecords(voList);
-        return resultPage;
-    }
-
-    private boolean isFlowInRange(String flowStr, Double min, Double max) {
-        if (min == null && max == null) {
-            return true;
-        }
-        if (StrUtil.isBlank(flowStr)) {
-            return false;
-        }
-        try {
-            double val = Double.parseDouble(flowStr);
-            if (min != null && val < min) {
-                return false;
-            }
-            if (max != null && val > max) {
-                return false;
+            } else {
+                vo.setDataSource(TELEMETRY_SOURCE);
+                vo.setInstantFlow(telemetry.getMeter1InstantFlow());
+                vo.setPositiveTotal(telemetry.getMeter1PositiveTotal());
+                vo.setTemperature(telemetry.getTemperature());
+                vo.setFlowSpeed(telemetry.getFlowSpeed());
+                vo.setMonitorTime(telemetry.getCreateTime());
             }
-            return true;
-        } catch (NumberFormatException e) {
-            return false;
+            allRows.add(vo);
         }
+        return pageOf(allRows, pageNum, pageSize);
     }
 
     @Override
     public Page<PressureMonitoringVO> getPressureMonitorPage(long pageNum, long pageSize,
-                                                              String equipmentName, String equipmentCode,
-                                                              String equipmentLocation,
-                                                              LocalDateTime startTime, LocalDateTime endTime,
-                                                              Double pressureMin, Double pressureMax) {
-        LambdaQueryWrapper<EquipmentBase> baseWrapper = new LambdaQueryWrapper<>();
-        baseWrapper.like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
-                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
-                .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
-                .orderByDesc(EquipmentBase::getCreateTime);
-        constrainToMonitoringTypes(baseWrapper, "pressure");
-
-        Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
-        Page<PressureMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
-
-        if (CollUtil.isEmpty(basePage.getRecords())) {
-            resultPage.setRecords(new ArrayList<>());
-            return resultPage;
+                                                             String equipmentName, String equipmentCode,
+                                                             String equipmentLocation,
+                                                             LocalDateTime startTime, LocalDateTime endTime,
+                                                             Double pressureMin, Double pressureMax) {
+        List<EquipmentBase> devices = selectMonitoringDevices(equipmentName, equipmentCode, equipmentLocation, "pressure");
+        if (CollUtil.isEmpty(devices)) {
+            return emptyPage(pageNum, pageSize);
         }
 
-        List<EquipmentBase> devices = basePage.getRecords();
-        List<String> deviceCodes = devices.stream()
-                .map(EquipmentBase::getEquipmentCode).filter(StrUtil::isNotBlank).collect(Collectors.toList());
-        List<String> typeIds = devices.stream()
-                .map(EquipmentBase::getEquipmentTypeId).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
-
-        Map<String, EquipmentType> typeMap = new HashMap<>();
-        if (CollUtil.isNotEmpty(typeIds)) {
-            List<EquipmentType> types = equipmentTypeMapper.selectBatchIds(typeIds);
-            typeMap = types.stream().collect(Collectors.toMap(EquipmentType::getTypeId, t -> t, (a, b) -> a));
-        }
-
-        Map<String, FirefightingPressure> pressureLatestMap = new HashMap<>();
-        if (CollUtil.isNotEmpty(deviceCodes)) {
-            List<FirefightingPressure> pressureList = firefightingPressureMapper.selectList(
-                    new LambdaQueryWrapper<FirefightingPressure>().in(FirefightingPressure::getTelemeteringStation, deviceCodes)
+        Map<String, EquipmentType> typeMap = selectTypeMap(devices);
+        Map<String, FirefightingPressure> pressureMap = new HashMap<>();
+        List<String> deviceCodes = deviceCodes(devices);
+        if (!deviceCodes.isEmpty()) {
+            List<FirefightingPressure> records = firefightingPressureMapper.selectList(
+                    new LambdaQueryWrapper<FirefightingPressure>()
+                            .in(FirefightingPressure::getTelemeteringStation, deviceCodes)
                             .ge(startTime != null, FirefightingPressure::getCreateTime, startTime)
                             .le(endTime != null, FirefightingPressure::getCreateTime, endTime)
-                            .ge(pressureMin != null, FirefightingPressure::getPressureValue, pressureMin)
-                            .le(pressureMax != null, FirefightingPressure::getPressureValue, pressureMax)
-                            .orderByDesc(FirefightingPressure::getCreateTime)
-            );
-            if (CollUtil.isNotEmpty(pressureList)) {
-                Set<String> seen = new HashSet<>();
-                for (FirefightingPressure p : pressureList) {
-                    if (seen.add(p.getTelemeteringStation())) {
-                        pressureLatestMap.put(p.getTelemeteringStation(), p);
-                    }
-                }
-            }
+                            .orderByDesc(FirefightingPressure::getCreateTime));
+            pressureMap = latestBy(records, FirefightingPressure::getTelemeteringStation,
+                    record -> inRange(record.getPressureValue(), pressureMin, pressureMax, startTime, endTime, record.getCreateTime()));
         }
 
-        List<PressureMonitoringVO> voList = new ArrayList<>();
+        List<PressureMonitoringVO> allRows = new ArrayList<>();
         for (EquipmentBase device : devices) {
-            PressureMonitoringVO vo = new PressureMonitoringVO();
-            vo.setEquipmentId(device.getEquipmentId());
-            vo.setEquipmentCode(device.getEquipmentCode());
-            vo.setEquipmentName(device.getEquipmentName());
-            vo.setEquipmentLocation(device.getEquipmentLocation());
-            vo.setEquipmentTypeId(device.getEquipmentTypeId());
-            EquipmentType type = typeMap.get(device.getEquipmentTypeId());
-            if (type != null) {
-                vo.setEquipmentTypeName(type.getTypeName());
-            }
-            vo.setLongitude(device.getLongitude());
-            vo.setLatitude(device.getLatitude());
-            if (device.getLongitude() != null && device.getLatitude() != null) {
-                vo.setGps(device.getLongitude() + "," + device.getLatitude());
-            }
-
-            FirefightingPressure pressure = pressureLatestMap.get(device.getEquipmentCode());
-            if (pressure != null) {
-                vo.setPressureValue(pressure.getPressureValue());
-                vo.setMonitorTime(pressure.getCreateTime());
+            FirefightingPressure pressure = pressureMap.get(device.getEquipmentCode());
+            if (pressure == null) {
+                continue;
             }
-            voList.add(vo);
+            PressureMonitoringVO vo = new PressureMonitoringVO();
+            fillBaseFields(vo, device, typeMap.get(device.getEquipmentTypeId()));
+            vo.setPressureValue(pressure.getPressureValue());
+            vo.setMonitorTime(pressure.getCreateTime());
+            allRows.add(vo);
         }
-
-        resultPage.setRecords(voList);
-        return resultPage;
+        return pageOf(allRows, pageNum, pageSize);
     }
 
     @Override
     public Page<LeakageMonitoringVO> getLeakageMonitorPage(long pageNum, long pageSize,
-                                                            String equipmentName, String equipmentCode,
-                                                            String equipmentLocation,
-                                                            LocalDateTime startTime, LocalDateTime endTime,
-                                                            Double powerMin, Double powerMax) {
-        LambdaQueryWrapper<EquipmentBase> baseWrapper = new LambdaQueryWrapper<>();
-        baseWrapper.like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
-                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
-                .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
-                .orderByDesc(EquipmentBase::getCreateTime);
-        constrainToMonitoringTypes(baseWrapper, "leakage");
-
-        Page<EquipmentBase> basePage = equipmentBaseMapper.selectPage(new Page<>(pageNum, pageSize), baseWrapper);
-        Page<LeakageMonitoringVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
-
-        if (CollUtil.isEmpty(basePage.getRecords())) {
-            resultPage.setRecords(new ArrayList<>());
-            return resultPage;
-        }
-
-        List<EquipmentBase> devices = basePage.getRecords();
-        List<String> deviceCodes = devices.stream()
-                .map(EquipmentBase::getEquipmentCode).filter(StrUtil::isNotBlank).collect(Collectors.toList());
-        List<String> typeIds = devices.stream()
-                .map(EquipmentBase::getEquipmentTypeId).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
-
-        Map<String, EquipmentType> typeMap = new HashMap<>();
-        if (CollUtil.isNotEmpty(typeIds)) {
-            List<EquipmentType> types = equipmentTypeMapper.selectBatchIds(typeIds);
-            typeMap = types.stream().collect(Collectors.toMap(EquipmentType::getTypeId, t -> t, (a, b) -> a));
+                                                           String equipmentName, String equipmentCode,
+                                                           String equipmentLocation,
+                                                           LocalDateTime startTime, LocalDateTime endTime,
+                                                           Double powerMin, Double powerMax) {
+        List<EquipmentBase> devices = selectMonitoringDevices(equipmentName, equipmentCode, equipmentLocation, "leakage");
+        if (CollUtil.isEmpty(devices)) {
+            return emptyPage(pageNum, pageSize);
         }
 
-        Map<String, NoiseInfo> noiseLatestMap = new HashMap<>();
-        if (CollUtil.isNotEmpty(deviceCodes)) {
-            List<NoiseInfo> noiseList = noiseInfoMapper.selectList(
-                    new LambdaQueryWrapper<NoiseInfo>().in(NoiseInfo::getEncode, deviceCodes)
+        Map<String, EquipmentType> typeMap = selectTypeMap(devices);
+        Map<String, NoiseInfo> noiseMap = new HashMap<>();
+        List<String> deviceCodes = deviceCodes(devices);
+        if (!deviceCodes.isEmpty()) {
+            List<NoiseInfo> records = noiseInfoMapper.selectList(
+                    new LambdaQueryWrapper<NoiseInfo>()
+                            .in(NoiseInfo::getEncode, deviceCodes)
                             .ge(startTime != null, NoiseInfo::getCreateTime, startTime)
                             .le(endTime != null, NoiseInfo::getCreateTime, endTime)
-                            .ge(powerMin != null, NoiseInfo::getPower, powerMin)
-                            .le(powerMax != null, NoiseInfo::getPower, powerMax)
-                            .orderByDesc(NoiseInfo::getCreateTime)
-            );
-            if (CollUtil.isNotEmpty(noiseList)) {
-                Set<String> seen = new HashSet<>();
-                for (NoiseInfo n : noiseList) {
-                    if (seen.add(n.getEncode())) {
-                        noiseLatestMap.put(n.getEncode(), n);
-                    }
-                }
-            }
+                            .orderByDesc(NoiseInfo::getCreateTime));
+            noiseMap = latestBy(records, NoiseInfo::getEncode,
+                    record -> inRange(record.getPower(), powerMin, powerMax, startTime, endTime, record.getCreateTime()));
         }
 
-        List<LeakageMonitoringVO> voList = new ArrayList<>();
+        List<LeakageMonitoringVO> allRows = new ArrayList<>();
         for (EquipmentBase device : devices) {
-            LeakageMonitoringVO vo = new LeakageMonitoringVO();
-            vo.setEquipmentId(device.getEquipmentId());
-            vo.setEquipmentCode(device.getEquipmentCode());
-            vo.setEquipmentName(device.getEquipmentName());
-            vo.setEquipmentLocation(device.getEquipmentLocation());
-            vo.setEquipmentTypeId(device.getEquipmentTypeId());
-            EquipmentType type = typeMap.get(device.getEquipmentTypeId());
-            if (type != null) {
-                vo.setEquipmentTypeName(type.getTypeName());
+            NoiseInfo noise = noiseMap.get(device.getEquipmentCode());
+            if (noise == null) {
+                continue;
             }
-            vo.setLongitude(device.getLongitude());
-            vo.setLatitude(device.getLatitude());
-            if (device.getLongitude() != null && device.getLatitude() != null) {
-                vo.setGps(device.getLongitude() + "," + device.getLatitude());
-            }
-
-            NoiseInfo noise = noiseLatestMap.get(device.getEquipmentCode());
-            if (noise != null) {
-                vo.setPower(noise.getPower());
-                vo.setGain(noise.getGain());
-                vo.setRms(noise.getRms());
-                vo.setCenterFrequency(noise.getCenterFrequency());
-                vo.setMonitorTime(noise.getCreateTime());
-            }
-            voList.add(vo);
+            LeakageMonitoringVO vo = new LeakageMonitoringVO();
+            fillBaseFields(vo, device, typeMap.get(device.getEquipmentTypeId()));
+            vo.setPower(noise.getPower());
+            vo.setGain(noise.getGain());
+            vo.setRms(noise.getRms());
+            vo.setCenterFrequency(noise.getCenterFrequency());
+            vo.setMonitorTime(noise.getCreateTime());
+            allRows.add(vo);
         }
-
-        resultPage.setRecords(voList);
-        return resultPage;
+        return pageOf(allRows, pageNum, pageSize);
     }
 
     @Override
-    public List<Map<String, Object>> getFlowTrend24h(String equipmentId) {
+    public List<Map<String, Object>> getFlowTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime) {
         EquipmentBase device = equipmentBaseMapper.selectById(equipmentId);
         if (device == null || StrUtil.isBlank(device.getEquipmentCode())) {
             return new ArrayList<>();
         }
 
-        LocalDateTime now = LocalDateTime.now();
-        LocalDateTime before24h = now.minusHours(24);
+        LocalDateTime start = normalizeTrendStart(startTime, endTime);
         String code = device.getEquipmentCode();
-
         List<Map<String, Object>> result = new ArrayList<>();
 
         List<RadarData> radarList = radarDataMapper.selectList(
-                new LambdaQueryWrapper<RadarData>().eq(RadarData::getDeviceCode, code)
-                        .ge(RadarData::getCreateTime, before24h)
-                        .orderByAsc(RadarData::getCreateTime)
-        );
+                new LambdaQueryWrapper<RadarData>()
+                        .eq(RadarData::getDeviceCode, code)
+                        .ge(RadarData::getCreateTime, start)
+                        .le(endTime != null, RadarData::getCreateTime, endTime)
+                        .orderByAsc(RadarData::getCreateTime));
         if (CollUtil.isNotEmpty(radarList)) {
-            for (RadarData d : radarList) {
-                Map<String, Object> point = new LinkedHashMap<>();
-                point.put("time", d.getCreateTime());
-                point.put("instantFlow", d.getMeter1InstantFlow());
-                point.put("flowSpeed", d.getFlowSpeed());
-                result.add(point);
+            for (RadarData data : radarList) {
+                if (!inTimeRange(data.getCreateTime(), start, endTime)) {
+                    continue;
+                }
+                result.add(flowTrendPoint(device, data.getCreateTime(), data.getMeter1InstantFlow(),
+                        data.getFlowSpeed(), null, null));
             }
             return result;
         }
 
         List<TelemetryData> telemetryList = telemetryDataMapper.selectList(
-                new LambdaQueryWrapper<TelemetryData>().eq(TelemetryData::getDeviceCode, code)
-                        .ge(TelemetryData::getCreateTime, before24h)
-                        .orderByAsc(TelemetryData::getCreateTime)
-        );
-        for (TelemetryData d : telemetryList) {
-            Map<String, Object> point = new LinkedHashMap<>();
-            point.put("time", d.getCreateTime());
-            point.put("positiveTotal", d.getMeter1PositiveTotal());
-            point.put("temperature", d.getTemperature());
-            point.put("flowSpeed", d.getFlowSpeed());
-            result.add(point);
+                new LambdaQueryWrapper<TelemetryData>()
+                        .eq(TelemetryData::getDeviceCode, code)
+                        .ge(TelemetryData::getCreateTime, start)
+                        .le(endTime != null, TelemetryData::getCreateTime, endTime)
+                        .orderByAsc(TelemetryData::getCreateTime));
+        for (TelemetryData data : telemetryList) {
+            if (!inTimeRange(data.getCreateTime(), start, endTime)) {
+                continue;
+            }
+            result.add(flowTrendPoint(device, data.getCreateTime(), data.getMeter1InstantFlow(),
+                    data.getFlowSpeed(), data.getMeter1PositiveTotal(), data.getTemperature()));
         }
         return result;
     }
 
     @Override
-    public List<Map<String, Object>> getPressureTrend24h(String equipmentId) {
+    public List<Map<String, Object>> getPressureTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime) {
         EquipmentBase device = equipmentBaseMapper.selectById(equipmentId);
         if (device == null || StrUtil.isBlank(device.getEquipmentCode())) {
             return new ArrayList<>();
         }
 
-        LocalDateTime now = LocalDateTime.now();
-        LocalDateTime before24h = now.minusHours(24);
-
+        LocalDateTime start = normalizeTrendStart(startTime, endTime);
         List<FirefightingPressure> list = firefightingPressureMapper.selectList(
-                new LambdaQueryWrapper<FirefightingPressure>().eq(FirefightingPressure::getTelemeteringStation, device.getEquipmentCode())
-                        .ge(FirefightingPressure::getCreateTime, before24h)
-                        .orderByAsc(FirefightingPressure::getCreateTime)
-        );
+                new LambdaQueryWrapper<FirefightingPressure>()
+                        .eq(FirefightingPressure::getTelemeteringStation, device.getEquipmentCode())
+                        .ge(FirefightingPressure::getCreateTime, start)
+                        .le(endTime != null, FirefightingPressure::getCreateTime, endTime)
+                        .orderByAsc(FirefightingPressure::getCreateTime));
 
         List<Map<String, Object>> result = new ArrayList<>();
-        for (FirefightingPressure p : list) {
-            Map<String, Object> point = new LinkedHashMap<>();
-            point.put("time", p.getCreateTime());
-            point.put("pressureValue", p.getPressureValue());
+        for (FirefightingPressure pressure : list) {
+            if (!inTimeRange(pressure.getCreateTime(), start, endTime)) {
+                continue;
+            }
+            Map<String, Object> point = trendPoint(device, pressure.getCreateTime());
+            point.put("pressureValue", pressure.getPressureValue());
             result.add(point);
         }
         return result;
     }
 
     @Override
-    public List<Map<String, Object>> getLeakageTrend24h(String equipmentId) {
+    public List<Map<String, Object>> getLeakageTrend(String equipmentId, LocalDateTime startTime, LocalDateTime endTime) {
         EquipmentBase device = equipmentBaseMapper.selectById(equipmentId);
         if (device == null || StrUtil.isBlank(device.getEquipmentCode())) {
             return new ArrayList<>();
         }
 
-        LocalDateTime now = LocalDateTime.now();
-        LocalDateTime before24h = now.minusHours(24);
-
+        LocalDateTime start = normalizeTrendStart(startTime, endTime);
         List<NoiseInfo> list = noiseInfoMapper.selectList(
-                new LambdaQueryWrapper<NoiseInfo>().eq(NoiseInfo::getEncode, device.getEquipmentCode())
-                        .ge(NoiseInfo::getCreateTime, before24h)
-                        .orderByAsc(NoiseInfo::getCreateTime)
-        );
+                new LambdaQueryWrapper<NoiseInfo>()
+                        .eq(NoiseInfo::getEncode, device.getEquipmentCode())
+                        .ge(NoiseInfo::getCreateTime, start)
+                        .le(endTime != null, NoiseInfo::getCreateTime, endTime)
+                        .orderByAsc(NoiseInfo::getCreateTime));
 
         List<Map<String, Object>> result = new ArrayList<>();
-        for (NoiseInfo n : list) {
-            Map<String, Object> point = new LinkedHashMap<>();
-            point.put("time", n.getCreateTime());
-            point.put("power", n.getPower());
-            point.put("gain", n.getGain());
-            point.put("rms", n.getRms());
+        for (NoiseInfo noise : list) {
+            if (!inTimeRange(noise.getCreateTime(), start, endTime)) {
+                continue;
+            }
+            Map<String, Object> point = trendPoint(device, noise.getCreateTime());
+            point.put("power", noise.getPower());
+            point.put("gain", noise.getGain());
+            point.put("rms", noise.getRms());
+            point.put("centerFrequency", noise.getCenterFrequency());
             result.add(point);
         }
         return result;
     }
 
+    private List<EquipmentBase> selectMonitoringDevices(String equipmentName, String equipmentCode,
+                                                        String equipmentLocation, String monitorKind) {
+        LambdaQueryWrapper<EquipmentBase> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
+                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
+                .like(StrUtil.isNotBlank(equipmentLocation), EquipmentBase::getEquipmentLocation, equipmentLocation)
+                .orderByDesc(EquipmentBase::getCreateTime);
+        constrainToMonitoringTypes(wrapper, monitorKind);
+        return equipmentBaseMapper.selectList(wrapper);
+    }
+
+    private Map<String, EquipmentType> selectTypeMap(List<EquipmentBase> devices) {
+        boolean anyTypePresent = devices.stream()
+                .map(EquipmentBase::getEquipmentTypeId)
+                .anyMatch(StrUtil::isNotBlank);
+        if (!anyTypePresent) {
+            return Collections.emptyMap();
+        }
+        return equipmentTypeMapper.selectList(new LambdaQueryWrapper<>()).stream()
+                .collect(Collectors.toMap(EquipmentType::getId, Function.identity(), (left, right) -> left));
+    }
+
+    private Map<String, RadarData> selectLatestRadar(List<EquipmentBase> devices,
+                                                      LocalDateTime startTime, LocalDateTime endTime,
+                                                      Double flowMin, Double flowMax) {
+        List<String> deviceCodes = deviceCodes(devices);
+        if (deviceCodes.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<RadarData> records = radarDataMapper.selectList(
+                new LambdaQueryWrapper<RadarData>()
+                        .in(RadarData::getDeviceCode, deviceCodes)
+                        .ge(startTime != null, RadarData::getCreateTime, startTime)
+                        .le(endTime != null, RadarData::getCreateTime, endTime)
+                        .orderByDesc(RadarData::getCreateTime));
+        return latestBy(records, RadarData::getDeviceCode,
+                record -> flowInRange(record.getMeter1InstantFlow(), flowMin, flowMax)
+                        && inTimeRange(record.getCreateTime(), startTime, endTime));
+    }
+
+    private Map<String, TelemetryData> selectLatestTelemetry(List<EquipmentBase> devices,
+                                                             LocalDateTime startTime, LocalDateTime endTime,
+                                                             Double flowMin, Double flowMax) {
+        List<String> deviceCodes = deviceCodes(devices);
+        if (deviceCodes.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<TelemetryData> records = telemetryDataMapper.selectList(
+                new LambdaQueryWrapper<TelemetryData>()
+                        .in(TelemetryData::getDeviceCode, deviceCodes)
+                        .ge(startTime != null, TelemetryData::getCreateTime, startTime)
+                        .le(endTime != null, TelemetryData::getCreateTime, endTime)
+                        .orderByDesc(TelemetryData::getCreateTime));
+        return latestBy(records, TelemetryData::getDeviceCode,
+                record -> flowInRange(record.getMeter1InstantFlow(), flowMin, flowMax)
+                        && inTimeRange(record.getCreateTime(), startTime, endTime));
+    }
+
+    private <T> Map<String, T> latestBy(List<T> records, Function<T, String> codeGetter, Function<T, Boolean> qualifier) {
+        Map<String, T> result = new HashMap<>();
+        for (T record : records) {
+            String code = codeGetter.apply(record);
+            if (StrUtil.isBlank(code) || !Boolean.TRUE.equals(qualifier.apply(record))) {
+                continue;
+            }
+            T current = result.get(code);
+            if (current == null || getTime(record).compareTo(getTime(current)) > 0) {
+                result.put(code, record);
+            }
+        }
+        return result;
+    }
+
+    private LocalDateTime getTime(Object value) {
+        if (value instanceof RadarData) return ((RadarData) value).getCreateTime();
+        if (value instanceof TelemetryData) return ((TelemetryData) value).getCreateTime();
+        if (value instanceof FirefightingPressure) return ((FirefightingPressure) value).getCreateTime();
+        if (value instanceof NoiseInfo) return ((NoiseInfo) value).getCreateTime();
+        return null;
+    }
+
+    private boolean flowInRange(String value, Double min, Double max) {
+        if (min == null && max == null) {
+            return true;
+        }
+        if (StrUtil.isBlank(value)) {
+            return false;
+        }
+        try {
+            return inRange(Double.parseDouble(value), min, max, null, null, null);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    private boolean inRange(Double value, Double min, Double max, LocalDateTime startTime,
+                            LocalDateTime endTime, LocalDateTime actualTime) {
+        boolean timeFilterRequired = startTime != null || endTime != null;
+        if (timeFilterRequired && !inTimeRange(actualTime, startTime, endTime)) {
+            return false;
+        }
+        if (min == null && max == null) {
+            return true;
+        }
+        if (value == null) {
+            return false;
+        }
+        return (min == null || value >= min) && (max == null || value <= max);
+    }
+
+    private boolean inTimeRange(LocalDateTime value, LocalDateTime startTime, LocalDateTime endTime) {
+        return value != null
+                && (startTime == null || !value.isBefore(startTime))
+                && (endTime == null || !value.isAfter(endTime));
+    }
+
+    private <T> Page<T> emptyPage(long pageNum, long pageSize) {
+        return pageOf(Collections.emptyList(), pageNum, pageSize);
+    }
+
+    private <T> Page<T> pageOf(List<T> records, long pageNum, long pageSize) {
+        long safePageNum = Math.max(pageNum, 1);
+        long safePageSize = Math.max(pageSize, 1);
+        Page<T> page = new Page<>(safePageNum, safePageSize, records.size());
+        int fromIndex = (int) Math.min((safePageNum - 1) * safePageSize, records.size());
+        int toIndex = (int) Math.min(fromIndex + safePageSize, records.size());
+        page.setRecords(new ArrayList<>(records.subList(fromIndex, toIndex)));
+        return page;
+    }
+
+    private List<String> deviceCodes(List<EquipmentBase> devices) {
+        return devices.stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toList());
+    }
+
+    private void fillBaseFields(FlowMonitoringVO vo, EquipmentBase device, EquipmentType type) {
+        vo.setEquipmentId(device.getEquipmentId());
+        vo.setEquipmentCode(device.getEquipmentCode());
+        vo.setEquipmentName(device.getEquipmentName());
+        vo.setEquipmentLocation(device.getEquipmentLocation());
+        vo.setEquipmentTypeId(device.getEquipmentTypeId());
+        if (type != null) vo.setEquipmentTypeName(type.getTypeName());
+        vo.setLongitude(device.getLongitude());
+        vo.setLatitude(device.getLatitude());
+        if (device.getLongitude() != null && device.getLatitude() != null) {
+            vo.setGps(device.getLongitude() + "," + device.getLatitude());
+        }
+    }
+
+    private void fillBaseFields(PressureMonitoringVO vo, EquipmentBase device, EquipmentType type) {
+        vo.setEquipmentId(device.getEquipmentId());
+        vo.setEquipmentCode(device.getEquipmentCode());
+        vo.setEquipmentName(device.getEquipmentName());
+        vo.setEquipmentLocation(device.getEquipmentLocation());
+        vo.setEquipmentTypeId(device.getEquipmentTypeId());
+        if (type != null) vo.setEquipmentTypeName(type.getTypeName());
+        vo.setLongitude(device.getLongitude());
+        vo.setLatitude(device.getLatitude());
+        if (device.getLongitude() != null && device.getLatitude() != null) {
+            vo.setGps(device.getLongitude() + "," + device.getLatitude());
+        }
+    }
+
+    private void fillBaseFields(LeakageMonitoringVO vo, EquipmentBase device, EquipmentType type) {
+        vo.setEquipmentId(device.getEquipmentId());
+        vo.setEquipmentCode(device.getEquipmentCode());
+        vo.setEquipmentName(device.getEquipmentName());
+        vo.setEquipmentLocation(device.getEquipmentLocation());
+        vo.setEquipmentTypeId(device.getEquipmentTypeId());
+        if (type != null) vo.setEquipmentTypeName(type.getTypeName());
+        vo.setLongitude(device.getLongitude());
+        vo.setLatitude(device.getLatitude());
+        if (device.getLongitude() != null && device.getLatitude() != null) {
+            vo.setGps(device.getLongitude() + "," + device.getLatitude());
+        }
+    }
+
+    private Map<String, Object> trendPoint(EquipmentBase device, LocalDateTime time) {
+        Map<String, Object> point = new LinkedHashMap<>();
+        point.put("equipmentId", device.getEquipmentId());
+        point.put("equipmentCode", device.getEquipmentCode());
+        point.put("equipmentName", device.getEquipmentName());
+        point.put("time", time);
+        return point;
+    }
+
+    private Map<String, Object> flowTrendPoint(EquipmentBase device, LocalDateTime time, String instantFlow,
+                                               String flowSpeed, String positiveTotal, String temperature) {
+        Map<String, Object> point = trendPoint(device, time);
+        point.put("instantFlow", instantFlow);
+        point.put("flowSpeed", flowSpeed);
+        point.put("positiveTotal", positiveTotal);
+        point.put("temperature", temperature);
+        return point;
+    }
+
+    private LocalDateTime normalizeTrendStart(LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime != null) {
+            return startTime;
+        }
+        return endTime != null ? null : LocalDateTime.now().minusHours(24);
+    }
+
     /** Restrict the equipment query before pagination so each monitoring page only contains its sub-devices. */
     private void constrainToMonitoringTypes(LambdaQueryWrapper<EquipmentBase> wrapper, String monitorKind) {
         String[] keywords;
@@ -472,17 +499,17 @@ public class MonitoringServiceImpl implements MonitoringService {
                 keywords = new String[]{"流量", "flow", "雷达", "遥测", "telemetry"};
                 break;
         }
-        List<EquipmentType> types = equipmentTypeMapper.selectList(new LambdaQueryWrapper<EquipmentType>());
+        List<EquipmentType> types = equipmentTypeMapper.selectList(new LambdaQueryWrapper<>());
         Set<String> ids = types.stream()
                 .filter(type -> {
-                    String value = ((type.getTypeId() == null ? "" : type.getTypeId()) + " "
+                    String value = ((type.getId() == null ? "" : type.getId()) + " "
+                            + (type.getTypeId() == null ? "" : type.getTypeId()) + " "
                             + (type.getTypeName() == null ? "" : type.getTypeName())).toLowerCase(Locale.ROOT);
                     return Arrays.stream(keywords).anyMatch(keyword -> value.contains(keyword.toLowerCase(Locale.ROOT)));
                 })
                 .map(EquipmentType::getId)
                 .filter(StrUtil::isNotBlank)
                 .collect(Collectors.toSet());
-        // Keep an empty result portable across database drivers that reject IN ().
         wrapper.in(EquipmentBase::getEquipmentTypeId,
                 ids.isEmpty() ? Collections.singleton("__NO_MONITORING_TYPE__") : ids);
     }

+ 73 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/Monitoring/MonitoringExportServiceContractTest.java

@@ -0,0 +1,73 @@
+package com.zksy.WaterSupply.Monitoring;
+
+import com.zksy.WaterSupply.Monitoring.service.impl.MonitoringExportServiceImpl;
+import com.zksy.base.domain.vo.FlowMonitoringVO;
+import com.zksy.base.domain.vo.LeakageMonitoringVO;
+import com.zksy.base.domain.vo.PressureMonitoringVO;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.time.LocalDateTime;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MonitoringExportServiceContractTest {
+
+    @Test
+    void monitoringExportsCreateTypedExcelSheetsWithTraceableDeviceFields() throws Exception {
+        MonitoringExportServiceImpl service = new MonitoringExportServiceImpl();
+
+        ByteArrayOutputStream flowOutput = new ByteArrayOutputStream();
+        FlowMonitoringVO flow = new FlowMonitoringVO();
+        flow.setEquipmentId("flow-id");
+        flow.setEquipmentCode("WS-FLOW-1");
+        flow.setEquipmentName("流量监测点1");
+        flow.setEquipmentLocation("测试位置");
+        flow.setEquipmentTypeName("流量监测");
+        flow.setDataSource("雷达流量计");
+        flow.setInstantFlow("30");
+        flow.setMonitorTime(LocalDateTime.of(2026, 9, 1, 12, 0));
+        service.writeFlow(flowOutput, Collections.singletonList(flow));
+
+        ByteArrayOutputStream pressureOutput = new ByteArrayOutputStream();
+        PressureMonitoringVO pressure = new PressureMonitoringVO();
+        pressure.setEquipmentId("pressure-id");
+        pressure.setEquipmentCode("WS-PR-1");
+        pressure.setEquipmentName("压力监测点1");
+        pressure.setPressureValue(0.42D);
+        pressure.setMonitorTime(LocalDateTime.of(2026, 9, 1, 12, 0));
+        service.writePressure(pressureOutput, Collections.singletonList(pressure));
+
+        ByteArrayOutputStream leakageOutput = new ByteArrayOutputStream();
+        LeakageMonitoringVO leakage = new LeakageMonitoringVO();
+        leakage.setEquipmentId("leakage-id");
+        leakage.setEquipmentCode("WS-LK-1");
+        leakage.setEquipmentName("漏失监测点1");
+        leakage.setPower(12D);
+        leakage.setMonitorTime(LocalDateTime.of(2026, 9, 1, 12, 0));
+        service.writeLeakage(leakageOutput, Collections.singletonList(leakage));
+
+        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(flowOutput.toByteArray()))) {
+            Sheet sheet = workbook.getSheet("流量监测");
+            assertEquals("设备编码", sheet.getRow(0).getCell(1).getStringCellValue());
+            assertEquals("WS-FLOW-1", sheet.getRow(1).getCell(1).getStringCellValue());
+            assertEquals("采集时间", sheet.getRow(0).getCell(10).getStringCellValue());
+            assertEquals("2026-09-01 12:00:00", sheet.getRow(1).getCell(10).getStringCellValue());
+        }
+        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(pressureOutput.toByteArray()))) {
+            Sheet sheet = workbook.getSheet("压力监测");
+            assertEquals("WS-PR-1", sheet.getRow(1).getCell(1).getStringCellValue());
+            assertEquals(0.42D, sheet.getRow(1).getCell(5).getNumericCellValue());
+        }
+        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(leakageOutput.toByteArray()))) {
+            Sheet sheet = workbook.getSheet("漏失监测");
+            assertEquals("WS-LK-1", sheet.getRow(1).getCell(1).getStringCellValue());
+            assertEquals(12D, sheet.getRow(1).getCell(5).getNumericCellValue());
+        }
+    }
+}

+ 290 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/MonitoringServiceImplTest.java

@@ -0,0 +1,290 @@
+package com.zksy.base.service.impl;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.audio.domain.NoiseInfo;
+import com.zksy.base.audio.mapper.NoiseInfoMapper;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.vo.FlowMonitoringVO;
+import com.zksy.base.domain.vo.LeakageMonitoringVO;
+import com.zksy.base.domain.vo.PressureMonitoringVO;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.base.pressure.domain.FirefightingPressure;
+import com.zksy.base.pressure.mapper.FirefightingPressureMapper;
+import com.zksy.base.radar.domain.RadarData;
+import com.zksy.base.radar.mapper.RadarDataMapper;
+import com.zksy.base.telemetry.domain.TelemetryData;
+import com.zksy.base.telemetry.mapper.TelemetryDataMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MonitoringServiceImplTest {
+
+    private static final LocalDateTime BASE_TIME = LocalDateTime.of(2026, 9, 1, 12, 0);
+
+    private MonitoringServiceImpl service;
+    private EquipmentBaseMapper equipmentBaseMapper;
+    private EquipmentTypeMapper equipmentTypeMapper;
+    private RadarDataMapper radarDataMapper;
+    private TelemetryDataMapper telemetryDataMapper;
+    private FirefightingPressureMapper pressureMapper;
+    private NoiseInfoMapper noiseMapper;
+
+    @BeforeEach
+    void setUp() {
+        service = new MonitoringServiceImpl();
+        equipmentBaseMapper = mock(EquipmentBaseMapper.class);
+        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
+        radarDataMapper = mock(RadarDataMapper.class);
+        telemetryDataMapper = mock(TelemetryDataMapper.class);
+        pressureMapper = mock(FirefightingPressureMapper.class);
+        noiseMapper = mock(NoiseInfoMapper.class);
+
+        ReflectionTestUtils.setField(service, "equipmentBaseMapper", equipmentBaseMapper);
+        ReflectionTestUtils.setField(service, "equipmentTypeMapper", equipmentTypeMapper);
+        ReflectionTestUtils.setField(service, "radarDataMapper", radarDataMapper);
+        ReflectionTestUtils.setField(service, "telemetryDataMapper", telemetryDataMapper);
+        ReflectionTestUtils.setField(service, "firefightingPressureMapper", pressureMapper);
+        ReflectionTestUtils.setField(service, "noiseInfoMapper", noiseMapper);
+
+        when(equipmentTypeMapper.selectList(any())).thenReturn(Arrays.asList(
+                type("flow-type", "flow-type", "流量监测"),
+                type("pressure-type", "pressure-type", "压力监测"),
+                type("leakage-type", "leakage-type", "漏失监测")
+        ));
+        when(radarDataMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(telemetryDataMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(pressureMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(noiseMapper.selectList(any())).thenReturn(Collections.emptyList());
+    }
+
+    @Test
+    void flowPaginationTotalsFollowMeasurementFilteringAndKeepDeviceFields() {
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Arrays.asList(
+                device("flow-1", "WS-FLOW-1", "流量监测点1", "flow-type", BASE_TIME),
+                device("flow-2", "WS-FLOW-2", "流量监测点2", "flow-type", BASE_TIME.minusMinutes(1))
+        ));
+        when(radarDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                radar("WS-FLOW-1", "100", BASE_TIME),
+                radar("WS-FLOW-1", "30", BASE_TIME.minusHours(1)),
+                radar("WS-FLOW-2", "5", BASE_TIME.minusMinutes(2))
+        ));
+
+        Page<FlowMonitoringVO> page = service.getFlowMonitorPage(
+                1, 10, null, null, null, null, null, 20D, 40D);
+
+        assertEquals(1, page.getTotal());
+        assertEquals(1, page.getRecords().size());
+        FlowMonitoringVO row = page.getRecords().get(0);
+        assertEquals("flow-1", row.getEquipmentId());
+        assertEquals("WS-FLOW-1", row.getEquipmentCode());
+        assertEquals("流量监测点1", row.getEquipmentName());
+        assertEquals("流量监测", row.getEquipmentTypeName());
+        assertEquals("30", row.getInstantFlow());
+        assertEquals(BASE_TIME.minusHours(1), row.getMonitorTime());
+    }
+
+    @Test
+    void pressurePaginationTotalsFollowMeasurementFiltering() {
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Arrays.asList(
+                device("pressure-1", "WS-PR-1", "压力监测点1", "pressure-type", BASE_TIME),
+                device("pressure-2", "WS-PR-2", "压力监测点2", "pressure-type", BASE_TIME.minusMinutes(1)),
+                device("pressure-3", "WS-PR-3", "压力监测点3", "pressure-type", BASE_TIME.minusMinutes(2))
+        ));
+        when(pressureMapper.selectList(any())).thenReturn(Arrays.asList(
+                pressure("WS-PR-1", 0.4D, BASE_TIME),
+                pressure("WS-PR-2", 0.9D, BASE_TIME.minusMinutes(1)),
+                pressure("WS-PR-3", 0.3D, BASE_TIME.minusMinutes(2))
+        ));
+
+        Page<PressureMonitoringVO> page = service.getPressureMonitorPage(
+                1, 10, null, null, null, null, null, 0.35D, 0.5D);
+
+        assertEquals(1, page.getTotal());
+        assertEquals(1, page.getRecords().size());
+        assertEquals("pressure-1", page.getRecords().get(0).getEquipmentId());
+        assertEquals(0.4D, page.getRecords().get(0).getPressureValue());
+    }
+
+    @Test
+    void leakagePaginationExcludesDevicesWithoutQualifyingMeasurements() {
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Arrays.asList(
+                device("leakage-1", "WS-LK-1", "漏失监测点1", "leakage-type", BASE_TIME),
+                device("leakage-2", "WS-LK-2", "漏失监测点2", "leakage-type", BASE_TIME.minusMinutes(1))
+        ));
+        when(noiseMapper.selectList(any())).thenReturn(Collections.singletonList(
+                noise("WS-LK-1", 12D, BASE_TIME)
+        ));
+
+        Page<LeakageMonitoringVO> page = service.getLeakageMonitorPage(
+                1, 10, null, null, null, null, null, null, null);
+
+        assertEquals(1, page.getTotal());
+        assertEquals(1, page.getRecords().size());
+        assertEquals("leakage-1", page.getRecords().get(0).getEquipmentId());
+        assertEquals(12D, page.getRecords().get(0).getPower());
+        assertEquals(BASE_TIME, page.getRecords().get(0).getMonitorTime());
+    }
+
+    @Test
+    void monitoringPagesReturnAnEmptyPageWhenThereIsNoMeasurementData() {
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(
+                device("flow-empty", "WS-FLOW-EMPTY", "无数据监测点", "flow-type", BASE_TIME)
+        ));
+
+        Page<FlowMonitoringVO> flow = service.getFlowMonitorPage(1, 10, null, null, null, null, null, null, null);
+        assertEquals(0, flow.getTotal());
+        assertTrue(flow.getRecords().isEmpty());
+
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(
+                device("pressure-empty", "WS-PR-EMPTY", "无数据监测点", "pressure-type", BASE_TIME)
+        ));
+        Page<PressureMonitoringVO> pressure = service.getPressureMonitorPage(1, 10, null, null, null, null, null, null, null);
+        assertEquals(0, pressure.getTotal());
+        assertTrue(pressure.getRecords().isEmpty());
+
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(
+                device("leakage-empty", "WS-LK-EMPTY", "无数据监测点", "leakage-type", BASE_TIME)
+        ));
+        Page<LeakageMonitoringVO> leakage = service.getLeakageMonitorPage(1, 10, null, null, null, null, null, null, null);
+        assertEquals(0, leakage.getTotal());
+        assertTrue(leakage.getRecords().isEmpty());
+    }
+
+    @Test
+    void trendEndpointsUseTheRequestedTimeRangeForAllMonitoringTypes() {
+        LocalDateTime start = BASE_TIME.minusHours(2);
+        LocalDateTime end = BASE_TIME;
+
+        EquipmentBase flowDevice = device("flow-1", "WS-FLOW-1", "流量监测点1", "flow-type", BASE_TIME);
+        when(equipmentBaseMapper.selectById("flow-1")).thenReturn(flowDevice);
+        when(radarDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                radar("WS-FLOW-1", "10", BASE_TIME.minusHours(3)),
+                radar("WS-FLOW-1", "20", BASE_TIME.minusHours(1)),
+                radar("WS-FLOW-1", "30", BASE_TIME.plusHours(1))
+        ));
+        List<Map<String, Object>> flow = service.getFlowTrend("flow-1", start, end);
+        assertEquals(1, flow.size());
+        assertEquals("20", flow.get(0).get("instantFlow"));
+        assertEquals(BASE_TIME.minusHours(1), flow.get(0).get("time"));
+        assertEquals("WS-FLOW-1", flow.get(0).get("equipmentCode"));
+
+        EquipmentBase pressureDevice = device("pressure-1", "WS-PR-1", "压力监测点1", "pressure-type", BASE_TIME);
+        when(equipmentBaseMapper.selectById("pressure-1")).thenReturn(pressureDevice);
+        when(pressureMapper.selectList(any())).thenReturn(Arrays.asList(
+                pressure("WS-PR-1", 0.1D, BASE_TIME.minusHours(3)),
+                pressure("WS-PR-1", 0.2D, BASE_TIME.minusHours(1)),
+                pressure("WS-PR-1", 0.3D, BASE_TIME.plusHours(1))
+        ));
+        List<Map<String, Object>> pressure = service.getPressureTrend("pressure-1", start, end);
+        assertEquals(1, pressure.size());
+        assertEquals(0.2D, pressure.get(0).get("pressureValue"));
+        assertEquals("WS-PR-1", pressure.get(0).get("equipmentCode"));
+
+        EquipmentBase leakageDevice = device("leakage-1", "WS-LK-1", "漏失监测点1", "leakage-type", BASE_TIME);
+        when(equipmentBaseMapper.selectById("leakage-1")).thenReturn(leakageDevice);
+        when(noiseMapper.selectList(any())).thenReturn(Arrays.asList(
+                noise("WS-LK-1", 1D, BASE_TIME.minusHours(3)),
+                noise("WS-LK-1", 2D, BASE_TIME.minusHours(1)),
+                noise("WS-LK-1", 3D, BASE_TIME.plusHours(1))
+        ));
+        List<Map<String, Object>> leakage = service.getLeakageTrend("leakage-1", start, end);
+        assertEquals(1, leakage.size());
+        assertEquals(2D, leakage.get(0).get("power"));
+        assertEquals("WS-LK-1", leakage.get(0).get("equipmentCode"));
+    }
+
+    @Test
+    void trendEndpointWithOnlyAnEndTimeDoesNotApplyTheDefault24HourStart() {
+        EquipmentBase device = device("flow-1", "WS-FLOW-1", "流量监测点1", "flow-type", BASE_TIME);
+        when(equipmentBaseMapper.selectById("flow-1")).thenReturn(device);
+        when(radarDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                radar("WS-FLOW-1", "10", BASE_TIME.minusDays(3)),
+                radar("WS-FLOW-1", "20", BASE_TIME)
+        ));
+
+        List<Map<String, Object>> trend = service.getFlowTrend("flow-1", null, BASE_TIME);
+
+        assertEquals(2, trend.size());
+        assertEquals("10", trend.get(0).get("instantFlow"));
+        assertEquals("20", trend.get(1).get("instantFlow"));
+    }
+
+    @Test
+    void requestedPageBeyondMeasurementResultsKeepsTheFilteredTotal() {
+        when(equipmentBaseMapper.selectList(any())).thenReturn(Collections.singletonList(
+                device("flow-1", "WS-FLOW-1", "流量监测点1", "flow-type", BASE_TIME)
+        ));
+        when(radarDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                radar("WS-FLOW-1", "10", BASE_TIME)
+        ));
+
+        Page<FlowMonitoringVO> page = service.getFlowMonitorPage(2, 10, null, null, null, null, null, null, null);
+
+        assertEquals(1, page.getTotal());
+        assertTrue(page.getRecords().isEmpty());
+    }
+
+    private static EquipmentType type(String id, String typeId, String name) {
+        EquipmentType type = new EquipmentType();
+        type.setId(id);
+        type.setTypeId(typeId);
+        type.setTypeName(name);
+        return type;
+    }
+
+    private static EquipmentBase device(String id, String code, String name, String typeId, LocalDateTime createTime) {
+        EquipmentBase device = new EquipmentBase();
+        device.setEquipmentId(id);
+        device.setEquipmentCode(code);
+        device.setEquipmentName(name);
+        device.setEquipmentTypeId(typeId);
+        device.setEquipmentLocation("测试位置-" + code);
+        device.setCreateTime(createTime);
+        return device;
+    }
+
+    private static RadarData radar(String code, String flow, LocalDateTime time) {
+        RadarData data = new RadarData();
+        data.setDeviceCode(code);
+        data.setMeter1InstantFlow(flow);
+        data.setFlowSpeed("1.2");
+        data.setCreateTime(time);
+        return data;
+    }
+
+    private static FirefightingPressure pressure(String code, Double value, LocalDateTime time) {
+        FirefightingPressure data = new FirefightingPressure();
+        data.setTelemeteringStation(code);
+        data.setPressureValue(value);
+        data.setCreateTime(time);
+        return data;
+    }
+
+    private static NoiseInfo noise(String code, Double power, LocalDateTime time) {
+        NoiseInfo data = new NoiseInfo();
+        data.setEncode(code);
+        data.setPower(power);
+        data.setGain(1D);
+        data.setRms(10);
+        data.setCenterFrequency(1000D);
+        data.setCreateTime(time);
+        return data;
+    }
+}