feat(wm-production): #62 在线监测列表与多维筛选
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
-- =============================================
|
||||
-- 智慧水务管理系统 - 在线监测列表 DDL
|
||||
-- 版本: V3
|
||||
-- 功能: 在线监测设备 + 实时数据 + 多维筛选
|
||||
-- =============================================
|
||||
|
||||
-- ==================== 在线监测设备 ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_monitor_device (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
device_code VARCHAR(50) NOT NULL UNIQUE, -- 设备编号
|
||||
device_name VARCHAR(200) NOT NULL, -- 设备名称
|
||||
device_type VARCHAR(30) NOT NULL, -- 设备类型: flow/pressure/level/quality
|
||||
area VARCHAR(50) NOT NULL, -- 所属区域
|
||||
location VARCHAR(300), -- 安装位置描述
|
||||
lng DECIMAL(12, 8), -- 经度
|
||||
lat DECIMAL(12, 8), -- 纬度
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'offline', -- 设备状态: online/offline/fault/abnormal
|
||||
last_report_time TIMESTAMP, -- 最后上报时间
|
||||
brand VARCHAR(100), -- 品牌/型号
|
||||
install_time TIMESTAMP, -- 安装时间
|
||||
remark VARCHAR(500), -- 备注
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE prod_monitor_device IS '在线监测设备表';
|
||||
COMMENT ON COLUMN prod_monitor_device.device_type IS '设备类型: flow(流量计)/pressure(压力计)/level(液位计)/quality(水质仪)';
|
||||
COMMENT ON COLUMN prod_monitor_device.status IS '设备状态: online(在线)/offline(离线)/fault(故障)/abnormal(数据异常)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_device_area ON prod_monitor_device(area);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_device_type ON prod_monitor_device(device_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_device_status ON prod_monitor_device(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_device_report ON prod_monitor_device(last_report_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_device_code_name ON prod_monitor_device(device_code, device_name);
|
||||
|
||||
-- ==================== 监测实时数据 ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_monitor_realtime_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
device_id BIGINT NOT NULL REFERENCES prod_monitor_device(id), -- 关联设备
|
||||
device_code VARCHAR(50) NOT NULL, -- 设备编号(冗余)
|
||||
metric_key VARCHAR(50) NOT NULL, -- 参数类型: flow/pressure/level/turbidity/ph/residual_chlorine/temperature
|
||||
metric_value DECIMAL(14, 4) NOT NULL, -- 实时值
|
||||
unit VARCHAR(20), -- 单位
|
||||
threshold_high DECIMAL(14, 4), -- 阈值上限
|
||||
threshold_low DECIMAL(14, 4), -- 阈值下限
|
||||
is_abnormal SMALLINT DEFAULT 0, -- 是否异常: 0正常 1异常
|
||||
collect_time TIMESTAMP NOT NULL, -- 采集时间
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE prod_monitor_realtime_data IS '监测实时数据表';
|
||||
COMMENT ON COLUMN prod_monitor_realtime_data.metric_key IS '参数类型: flow(流量)/pressure(压力)/level(液位)/turbidity(浊度)/ph(pH值)/residual_chlorine(余氯)/temperature(温度)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_realtime_device_id ON prod_monitor_realtime_data(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_realtime_device_code ON prod_monitor_realtime_data(device_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_realtime_metric_key ON prod_monitor_realtime_data(metric_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_realtime_collect_time ON prod_monitor_realtime_data(collect_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_realtime_device_metric ON prod_monitor_realtime_data(device_id, metric_key, collect_time DESC);
|
||||
|
||||
-- ==================== 初始化数据(示例) ====================
|
||||
|
||||
INSERT INTO prod_monitor_device (device_code, device_name, device_type, area, location, lng, lat, status, last_report_time, brand)
|
||||
VALUES
|
||||
('MON-FLOW-001', '一号泵站出口流量计', 'flow', '一体化水厂', '一号泵站出口', 82.07123456, 44.84567890, 'online', NOW(), 'E+H Promag 50'),
|
||||
('MON-FLOW-002', '二号泵站出口流量计', 'flow', '一体化水厂', '二号泵站出口', 82.07234567, 44.84678901, 'online', NOW(), 'E+H Promag 50'),
|
||||
('MON-PRES-001', '管网压力监测点A', 'pressure', '管网一区', '人民路DN300', 82.06890123, 44.84234567, 'online', NOW(), 'WIKA S-20'),
|
||||
('MON-PRES-002', '管网压力监测点B', 'pressure', '管网一区', '建设路DN200', 82.06901234, 44.84345678, 'offline', NOW() - INTERVAL '2 hours', 'WIKA S-20'),
|
||||
('MON-LEV-001', '清水池液位计', 'level', '一体化水厂', '清水池', 82.07156789, 44.84501234, 'online', NOW(), 'VEGA VEGAPULS 64'),
|
||||
('MON-LEV-002', '沉淀池液位计', 'level', '一体化水厂', '沉淀池', 82.07167890, 44.84512345, 'fault', NOW() - INTERVAL '30 minutes', 'VEGA VEGAPULS 64'),
|
||||
('MON-QUAL-001', '出厂水质监测仪', 'quality', '一体化水厂', '出厂水口', 82.07178901, 44.84523456, 'online', NOW(), 'HACH sc200'),
|
||||
('MON-QUAL-002', '管网末梢水质仪', 'quality', '管网二区', '末梢检测点', 82.06543210, 44.83987654, 'abnormal',NOW(), 'HACH sc200'),
|
||||
('MON-FLOW-003', '三号泵站流量计', 'flow', '管网二区', '三号泵站', 82.06654321, 44.84098765, 'online', NOW(), 'E+H Promag 10'),
|
||||
('MON-PRES-003', '高位水池压力计', 'pressure', '管网三区', '高位水池出口', 82.07345678, 44.84789012, 'online', NOW(), 'WIKA S-20')
|
||||
ON CONFLICT (device_code) DO NOTHING;
|
||||
|
||||
INSERT INTO prod_monitor_realtime_data (device_id, device_code, metric_key, metric_value, unit, threshold_high, threshold_low, is_abnormal, collect_time)
|
||||
SELECT d.id, d.device_code, m.metric_key, m.metric_value, m.unit, m.threshold_high, m.threshold_low, m.is_abnormal, NOW()
|
||||
FROM prod_monitor_device d
|
||||
CROSS JOIN (VALUES
|
||||
('flow', 125.50, 'm³/h', 200.0, 10.0, 0),
|
||||
('pressure', 0.35, 'MPa', 0.6, 0.15, 0),
|
||||
('level', 3.80, 'm', 5.0, 0.5, 0),
|
||||
('turbidity', 0.45, 'NTU', 1.0, NULL, 0),
|
||||
('ph', 7.20, '', 8.5, 6.5, 0),
|
||||
('residual_chlorine', 0.35, 'mg/L', 0.8, 0.05, 0)
|
||||
) AS m(metric_key, metric_value, unit, threshold_high, threshold_low, is_abnormal)
|
||||
WHERE d.status = 'online'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.dto.MonitorExportRequest;
|
||||
import com.water.production.dto.MonitorQueryRequest;
|
||||
import com.water.production.service.MonitorExportService;
|
||||
import com.water.production.service.MonitorListService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 在线监测列表 Controller
|
||||
* 提供设备列表、实时数据、统计、导出等接口
|
||||
*/
|
||||
@Tag(name = "在线监测管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/monitor")
|
||||
@RequiredArgsConstructor
|
||||
public class MonitorController {
|
||||
|
||||
private final MonitorListService monitorListService;
|
||||
private final MonitorExportService monitorExportService;
|
||||
|
||||
// ========== 1. 设备列表(分页 + 多维筛选) ==========
|
||||
@Operation(summary = "在线监测设备列表(分页+多维筛选)")
|
||||
@GetMapping("/list")
|
||||
public R<Map<String, Object>> list(MonitorQueryRequest request) {
|
||||
return R.ok(monitorListService.queryDeviceList(request));
|
||||
}
|
||||
|
||||
// ========== 2. 设备详情 ==========
|
||||
@Operation(summary = "获取设备详情")
|
||||
@GetMapping("/device/{deviceId}")
|
||||
public R<Map<String, Object>> deviceDetail(@PathVariable Long deviceId) {
|
||||
Map<String, Object> detail = monitorListService.getDeviceDetail(deviceId);
|
||||
if (detail.isEmpty()) {
|
||||
return R.fail(404, "设备不存在");
|
||||
}
|
||||
return R.ok(detail);
|
||||
}
|
||||
|
||||
// ========== 3. 设备实时数据 ==========
|
||||
@Operation(summary = "获取设备实时监测数据")
|
||||
@GetMapping("/device/{deviceId}/realtime")
|
||||
public R<List<Map<String, Object>>> deviceRealtime(@PathVariable Long deviceId) {
|
||||
return R.ok(monitorListService.getDeviceRealtimeData(deviceId));
|
||||
}
|
||||
|
||||
// ========== 4. 设备统计概览 ==========
|
||||
@Operation(summary = "监测设备统计概览(按状态/类型/区域分组)")
|
||||
@GetMapping("/statistics")
|
||||
public R<Map<String, Object>> statistics() {
|
||||
return R.ok(monitorListService.getDeviceStatistics());
|
||||
}
|
||||
|
||||
// ========== 5. 更新设备状态 ==========
|
||||
@Operation(summary = "更新设备状态(online/offline/fault/abnormal)")
|
||||
@PutMapping("/device/{deviceId}/status")
|
||||
public R<String> updateStatus(@PathVariable Long deviceId, @RequestParam String status) {
|
||||
monitorListService.updateDeviceStatus(deviceId, status);
|
||||
return R.ok("状态已更新");
|
||||
}
|
||||
|
||||
// ========== 6. 获取区域列表 ==========
|
||||
@Operation(summary = "获取所有监测区域列表")
|
||||
@GetMapping("/areas")
|
||||
public R<List<String>> areas() {
|
||||
return R.ok(monitorListService.getAreaList());
|
||||
}
|
||||
|
||||
// ========== 7. 获取设备类型列表 ==========
|
||||
@Operation(summary = "获取所有设备类型列表")
|
||||
@GetMapping("/device-types")
|
||||
public R<List<String>> deviceTypes() {
|
||||
return R.ok(monitorListService.getDeviceTypeList());
|
||||
}
|
||||
|
||||
// ========== 8. 导出 Excel ==========
|
||||
@Operation(summary = "导出在线监测数据(Excel)")
|
||||
@PostMapping("/export/excel")
|
||||
public ResponseEntity<byte[]> exportExcel(@RequestBody MonitorExportRequest request) {
|
||||
request.setFormat("excel");
|
||||
byte[] data = monitorExportService.exportExcel(request);
|
||||
String filename = URLEncoder.encode("在线监测数据.xlsx", StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.body(data);
|
||||
}
|
||||
|
||||
// ========== 9. 导出 CSV ==========
|
||||
@Operation(summary = "导出在线监测数据(CSV)")
|
||||
@PostMapping("/export/csv")
|
||||
public ResponseEntity<byte[]> exportCsv(@RequestBody MonitorExportRequest request) {
|
||||
request.setFormat("csv");
|
||||
byte[] data = monitorExportService.exportCsv(request);
|
||||
String filename = URLEncoder.encode("在线监测数据.csv", StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||
.contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
|
||||
.body(data);
|
||||
}
|
||||
|
||||
// ========== 10. 通用导出(按 format 参数自动选择) ==========
|
||||
@Operation(summary = "通用导出(format=excel|csv)")
|
||||
@PostMapping("/export")
|
||||
public ResponseEntity<byte[]> export(@RequestBody MonitorExportRequest request) {
|
||||
if ("csv".equalsIgnoreCase(request.getFormat())) {
|
||||
return exportCsv(request);
|
||||
}
|
||||
return exportExcel(request);
|
||||
}
|
||||
|
||||
// ========== 11. 批量获取实时数据 ==========
|
||||
@Operation(summary = "批量获取多设备实时数据")
|
||||
@PostMapping("/realtime/batch")
|
||||
public R<List<Map<String, Object>>> batchRealtime(@RequestBody List<Long> deviceIds) {
|
||||
return R.ok(monitorListService.getBatchRealtimeData(deviceIds));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.water.production.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在线监测数据导出请求
|
||||
*/
|
||||
@Data
|
||||
public class MonitorExportRequest {
|
||||
|
||||
/** 导出格式: excel/csv */
|
||||
private String format = "excel";
|
||||
|
||||
/** 区域 */
|
||||
private String area;
|
||||
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
|
||||
/** 设备状态 */
|
||||
private String status;
|
||||
|
||||
/** 关键词 */
|
||||
private String keyword;
|
||||
|
||||
/** 开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 指定导出的设备ID列表(可选,为空则按筛选条件导出) */
|
||||
private List<Long> deviceIds;
|
||||
|
||||
/** 是否包含实时数据 */
|
||||
private Boolean includeRealtime = true;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.water.production.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 在线监测列表查询请求
|
||||
*/
|
||||
@Data
|
||||
public class MonitorQueryRequest {
|
||||
|
||||
/** 区域 */
|
||||
private String area;
|
||||
|
||||
/** 设备类型: flow/pressure/level/quality */
|
||||
private String deviceType;
|
||||
|
||||
/** 设备状态: online/offline/fault/abnormal */
|
||||
private String status;
|
||||
|
||||
/** 关键词(设备编号/名称模糊搜索) */
|
||||
private String keyword;
|
||||
|
||||
/** 开始时间(最后上报时间范围) */
|
||||
private String startTime;
|
||||
|
||||
/** 结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 排序字段: deviceCode/deviceName/status/lastReportTime */
|
||||
private String sortField;
|
||||
|
||||
/** 排序方向: asc/desc */
|
||||
private String sortOrder;
|
||||
|
||||
/** 页码(默认1) */
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/** 每页条数(默认20) */
|
||||
private Integer pageSize = 20;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 在线监测设备实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_monitor_device")
|
||||
public class MonitorDevice {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 设备编号 */
|
||||
private String deviceCode;
|
||||
|
||||
/** 设备名称 */
|
||||
private String deviceName;
|
||||
|
||||
/** 设备类型: flow/pressure/level/quality */
|
||||
private String deviceType;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
/** 安装位置 */
|
||||
private String location;
|
||||
|
||||
/** 经度 */
|
||||
private BigDecimal lng;
|
||||
|
||||
/** 纬度 */
|
||||
private BigDecimal lat;
|
||||
|
||||
/** 设备状态: online/offline/fault/abnormal */
|
||||
private String status;
|
||||
|
||||
/** 最后上报时间 */
|
||||
private LocalDateTime lastReportTime;
|
||||
|
||||
/** 设备品牌/型号 */
|
||||
private String brand;
|
||||
|
||||
/** 安装日期 */
|
||||
private LocalDateTime installTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 监测实时数据实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_monitor_realtime_data")
|
||||
public class MonitorRealtimeData {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联设备ID */
|
||||
private Long deviceId;
|
||||
|
||||
/** 设备编号 */
|
||||
private String deviceCode;
|
||||
|
||||
/** 监测参数类型: flow/pressure/level/turbidity/ph/residual_chlorine/temperature */
|
||||
private String metricKey;
|
||||
|
||||
/** 实时值 */
|
||||
private BigDecimal metricValue;
|
||||
|
||||
/** 单位 */
|
||||
private String unit;
|
||||
|
||||
/** 阈值上限 */
|
||||
private BigDecimal thresholdHigh;
|
||||
|
||||
/** 阈值下限 */
|
||||
private BigDecimal thresholdLow;
|
||||
|
||||
/** 是否异常: 0正常 1异常 */
|
||||
private Integer isAbnormal;
|
||||
|
||||
/** 采集时间 */
|
||||
private LocalDateTime collectTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.MonitorDevice;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 在线监测设备 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MonitorDeviceMapper extends BaseMapper<MonitorDevice> {
|
||||
|
||||
/**
|
||||
* 查询设备列表(含最新实时数据)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT d.*, " +
|
||||
" (SELECT COUNT(*) FROM prod_monitor_realtime_data r WHERE r.device_id = d.id) AS metric_count " +
|
||||
"FROM prod_monitor_device d " +
|
||||
"WHERE 1=1 " +
|
||||
"<if test='area != null and area != \"\"'> AND d.area = #{area}</if> " +
|
||||
"<if test='deviceType != null and deviceType != \"\"'> AND d.device_type = #{deviceType}</if> " +
|
||||
"<if test='status != null and status != \"\"'> AND d.status = #{status}</if> " +
|
||||
"<if test='keyword != null and keyword != \"\"'> AND (d.device_code ILIKE CONCAT('%',#{keyword},'%') OR d.device_name ILIKE CONCAT('%',#{keyword},'%'))</if> " +
|
||||
"<if test='startTime != null and startTime != \"\"'> AND d.last_report_time >= #{startTime}::timestamp</if> " +
|
||||
"<if test='endTime != null and endTime != \"\"'> AND d.last_report_time <= #{endTime}::timestamp</if> " +
|
||||
"<if test='sortField == \"deviceCode\"'> ORDER BY d.device_code ${sortOrder}</if> " +
|
||||
"<if test='sortField == \"deviceName\"'> ORDER BY d.device_name ${sortOrder}</if> " +
|
||||
"<if test='sortField == \"status\"'> ORDER BY d.status ${sortOrder}</if> " +
|
||||
"<if test='sortField == \"lastReportTime\"'> ORDER BY d.last_report_time ${sortOrder}</if> " +
|
||||
"<if test='sortField == null or sortField == \"\"'> ORDER BY d.last_report_time DESC</if> " +
|
||||
"</script>")
|
||||
List<Map<String, Object>> selectDeviceListWithMetrics(
|
||||
@Param("area") String area,
|
||||
@Param("deviceType") String deviceType,
|
||||
@Param("status") String status,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("startTime") String startTime,
|
||||
@Param("endTime") String endTime,
|
||||
@Param("sortField") String sortField,
|
||||
@Param("sortOrder") String sortOrder);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.MonitorRealtimeData;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 监测实时数据 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MonitorRealtimeDataMapper extends BaseMapper<MonitorRealtimeData> {
|
||||
|
||||
/**
|
||||
* 获取指定设备的最新实时数据(所有指标)
|
||||
*/
|
||||
@Select("SELECT r.* FROM prod_monitor_realtime_data r " +
|
||||
"INNER JOIN (SELECT device_id, metric_key, MAX(collect_time) AS max_time " +
|
||||
" FROM prod_monitor_realtime_data WHERE device_id = #{deviceId} GROUP BY device_id, metric_key) latest " +
|
||||
"ON r.device_id = latest.device_id AND r.metric_key = latest.metric_key AND r.collect_time = latest.max_time")
|
||||
List<MonitorRealtimeData> selectLatestByDeviceId(@Param("deviceId") Long deviceId);
|
||||
|
||||
/**
|
||||
* 批量获取多个设备的最新实时数据
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT r.* FROM prod_monitor_realtime_data r " +
|
||||
"INNER JOIN (SELECT device_id, metric_key, MAX(collect_time) AS max_time " +
|
||||
" FROM prod_monitor_realtime_data " +
|
||||
" WHERE device_id IN " +
|
||||
" <foreach item='id' collection='deviceIds' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
" GROUP BY device_id, metric_key) latest " +
|
||||
"ON r.device_id = latest.device_id AND r.metric_key = latest.metric_key AND r.collect_time = latest.max_time " +
|
||||
"ORDER BY r.device_id, r.metric_key" +
|
||||
"</script>")
|
||||
List<Map<String, Object>> selectLatestBatch(@Param("deviceIds") List<Long> deviceIds);
|
||||
|
||||
/**
|
||||
* 按设备ID统计异常数据数量
|
||||
*/
|
||||
@Select("SELECT device_id, COUNT(*) AS cnt FROM prod_monitor_realtime_data " +
|
||||
"WHERE is_abnormal = 1 AND device_id IN " +
|
||||
"<script><foreach item='id' collection='deviceIds' open='(' separator=',' close=')'>#{id}</foreach></script> " +
|
||||
"GROUP BY device_id")
|
||||
List<Map<String, Object>> countAbnormalByDevices(@Param("deviceIds") List<Long> deviceIds);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.water.production.dto.MonitorExportRequest;
|
||||
import com.water.production.dto.MonitorQueryRequest;
|
||||
import com.water.production.entity.MonitorDevice;
|
||||
import com.water.production.entity.MonitorRealtimeData;
|
||||
import com.water.production.mapper.MonitorRealtimeDataMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 在线监测数据导出服务
|
||||
* 支持 Excel / CSV 格式导出
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MonitorExportService {
|
||||
|
||||
private final MonitorListService monitorListService;
|
||||
private final MonitorRealtimeDataMapper realtimeDataMapper;
|
||||
|
||||
/**
|
||||
* 导出 Excel 格式
|
||||
*/
|
||||
public byte[] exportExcel(MonitorExportRequest req) {
|
||||
List<MonitorDevice> devices = getDevicesForExport(req);
|
||||
if (devices.isEmpty()) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 构建导出数据
|
||||
List<List<String>> head = buildExcelHead();
|
||||
List<List<Object>> data = buildExcelData(devices, req.getIncludeRealtime());
|
||||
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
EasyExcel.write(bos)
|
||||
.sheet("在线监测数据")
|
||||
.head(head.stream()
|
||||
.map(row -> row.stream()
|
||||
.map(c -> (String) c)
|
||||
.collect(Collectors.toList()))
|
||||
.collect(Collectors.toList()))
|
||||
.doWrite(data);
|
||||
return bos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
log.error("Excel 导出失败", e);
|
||||
throw new RuntimeException("Excel 导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 CSV 格式
|
||||
*/
|
||||
public byte[] exportCsv(MonitorExportRequest req) {
|
||||
List<MonitorDevice> devices = getDevicesForExport(req);
|
||||
if (devices.isEmpty()) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(bos, StandardCharsets.UTF_8))) {
|
||||
// BOM for Excel UTF-8
|
||||
bos.write(0xEF);
|
||||
bos.write(0xBB);
|
||||
bos.write(0xBF);
|
||||
|
||||
// CSV header
|
||||
writer.println("设备编号,设备名称,设备类型,区域,安装位置,状态,最后上报时间,流量(m³/h),压力(MPa),液位(m),浊度(NTU),pH,余氯(mg/L)");
|
||||
|
||||
for (MonitorDevice device : devices) {
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(csvEsc(device.getDeviceCode())).append(",");
|
||||
line.append(csvEsc(device.getDeviceName())).append(",");
|
||||
line.append(csvEsc(device.getDeviceType())).append(",");
|
||||
line.append(csvEsc(device.getArea())).append(",");
|
||||
line.append(csvEsc(device.getLocation())).append(",");
|
||||
line.append(csvEsc(device.getStatus())).append(",");
|
||||
line.append(device.getLastReportTime() != null ? device.getLastReportTime().toString() : "");
|
||||
|
||||
if (Boolean.TRUE.equals(req.getIncludeRealtime())) {
|
||||
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||
Map<String, String> metricMap = new LinkedHashMap<>();
|
||||
for (MonitorRealtimeData data : realtimeList) {
|
||||
metricMap.put(data.getMetricKey(),
|
||||
data.getMetricValue() != null ? data.getMetricValue().toPlainString() : "");
|
||||
}
|
||||
line.append(",").append(metricMap.getOrDefault("flow", ""));
|
||||
line.append(",").append(metricMap.getOrDefault("pressure", ""));
|
||||
line.append(",").append(metricMap.getOrDefault("level", ""));
|
||||
line.append(",").append(metricMap.getOrDefault("turbidity", ""));
|
||||
line.append(",").append(metricMap.getOrDefault("ph", ""));
|
||||
line.append(",").append(metricMap.getOrDefault("residual_chlorine", ""));
|
||||
}
|
||||
|
||||
writer.println(line);
|
||||
}
|
||||
|
||||
writer.flush();
|
||||
return bos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
log.error("CSV 导出失败", e);
|
||||
throw new RuntimeException("CSV 导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<MonitorDevice> getDevicesForExport(MonitorExportRequest req) {
|
||||
// 如果指定了设备ID列表,直接按ID查询
|
||||
if (req.getDeviceIds() != null && !req.getDeviceIds().isEmpty()) {
|
||||
MonitorQueryRequest queryReq = new MonitorQueryRequest();
|
||||
queryReq.setPageSize(10000);
|
||||
return monitorListService.queryDevicesForExport(queryReq).stream()
|
||||
.filter(d -> req.getDeviceIds().contains(d.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 按筛选条件查询
|
||||
MonitorQueryRequest queryReq = new MonitorQueryRequest();
|
||||
queryReq.setArea(req.getArea());
|
||||
queryReq.setDeviceType(req.getDeviceType());
|
||||
queryReq.setStatus(req.getStatus());
|
||||
queryReq.setKeyword(req.getKeyword());
|
||||
queryReq.setStartTime(req.getStartTime());
|
||||
queryReq.setEndTime(req.getEndTime());
|
||||
queryReq.setPageSize(10000);
|
||||
return monitorListService.queryDevicesForExport(queryReq);
|
||||
}
|
||||
|
||||
private List<List<String>> buildExcelHead() {
|
||||
List<List<String>> head = new ArrayList<>();
|
||||
head.add(List.of("设备编号"));
|
||||
head.add(List.of("设备名称"));
|
||||
head.add(List.of("设备类型"));
|
||||
head.add(List.of("区域"));
|
||||
head.add(List.of("安装位置"));
|
||||
head.add(List.of("状态"));
|
||||
head.add(List.of("最后上报时间"));
|
||||
head.add(List.of("流量(m³/h)"));
|
||||
head.add(List.of("压力(MPa)"));
|
||||
head.add(List.of("液位(m)"));
|
||||
head.add(List.of("浊度(NTU)"));
|
||||
head.add(List.of("pH"));
|
||||
head.add(List.of("余氯(mg/L)"));
|
||||
return head;
|
||||
}
|
||||
|
||||
private List<List<Object>> buildExcelData(List<MonitorDevice> devices, Boolean includeRealtime) {
|
||||
List<List<Object>> data = new ArrayList<>();
|
||||
for (MonitorDevice device : devices) {
|
||||
List<Object> row = new ArrayList<>();
|
||||
row.add(device.getDeviceCode());
|
||||
row.add(device.getDeviceName());
|
||||
row.add(device.getDeviceType());
|
||||
row.add(device.getArea());
|
||||
row.add(device.getLocation());
|
||||
row.add(formatStatus(device.getStatus()));
|
||||
row.add(device.getLastReportTime() != null ? device.getLastReportTime().toString() : "");
|
||||
|
||||
if (Boolean.TRUE.equals(includeRealtime)) {
|
||||
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||
Map<String, Object> metricMap = new LinkedHashMap<>();
|
||||
for (MonitorRealtimeData d : realtimeList) {
|
||||
metricMap.put(d.getMetricKey(), d.getMetricValue());
|
||||
}
|
||||
row.add(metricMap.getOrDefault("flow", ""));
|
||||
row.add(metricMap.getOrDefault("pressure", ""));
|
||||
row.add(metricMap.getOrDefault("level", ""));
|
||||
row.add(metricMap.getOrDefault("turbidity", ""));
|
||||
row.add(metricMap.getOrDefault("ph", ""));
|
||||
row.add(metricMap.getOrDefault("residual_chlorine", ""));
|
||||
}
|
||||
|
||||
data.add(row);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private String formatStatus(String status) {
|
||||
if (status == null) return "未知";
|
||||
return switch (status) {
|
||||
case "online" -> "在线";
|
||||
case "offline" -> "离线";
|
||||
case "fault" -> "故障";
|
||||
case "abnormal" -> "数据异常";
|
||||
default -> status;
|
||||
};
|
||||
}
|
||||
|
||||
private String csvEsc(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.water.production.dto.MonitorQueryRequest;
|
||||
import com.water.production.entity.MonitorDevice;
|
||||
import com.water.production.entity.MonitorRealtimeData;
|
||||
import com.water.production.mapper.MonitorDeviceMapper;
|
||||
import com.water.production.mapper.MonitorRealtimeDataMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 在线监测列表服务
|
||||
* 提供设备列表查询、实时数据获取、设备详情、统计等功能
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MonitorListService {
|
||||
|
||||
private final MonitorDeviceMapper deviceMapper;
|
||||
private final MonitorRealtimeDataMapper realtimeDataMapper;
|
||||
|
||||
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 分页查询在线监测设备列表(含最新实时数据)
|
||||
*/
|
||||
public Map<String, Object> queryDeviceList(MonitorQueryRequest req) {
|
||||
// 使用 MyBatis-Plus 分页
|
||||
Page<MonitorDevice> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||
if (req.getArea() != null && !req.getArea().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getArea, req.getArea());
|
||||
}
|
||||
if (req.getDeviceType() != null && !req.getDeviceType().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getDeviceType, req.getDeviceType());
|
||||
}
|
||||
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getStatus, req.getStatus());
|
||||
}
|
||||
if (req.getKeyword() != null && !req.getKeyword().isEmpty()) {
|
||||
wrapper.and(w -> w.like(MonitorDevice::getDeviceCode, req.getKeyword())
|
||||
.or().like(MonitorDevice::getDeviceName, req.getKeyword()));
|
||||
}
|
||||
if (req.getStartTime() != null && !req.getStartTime().isEmpty()) {
|
||||
wrapper.ge(MonitorDevice::getLastReportTime, LocalDateTime.parse(req.getStartTime(), DTF));
|
||||
}
|
||||
if (req.getEndTime() != null && !req.getEndTime().isEmpty()) {
|
||||
wrapper.le(MonitorDevice::getLastReportTime, LocalDateTime.parse(req.getEndTime(), DTF));
|
||||
}
|
||||
|
||||
// 排序
|
||||
String sortField = req.getSortField();
|
||||
boolean isAsc = "asc".equalsIgnoreCase(req.getSortOrder());
|
||||
if ("deviceCode".equals(sortField)) {
|
||||
wrapper.orderBy(true, isAsc, MonitorDevice::getDeviceCode);
|
||||
} else if ("deviceName".equals(sortField)) {
|
||||
wrapper.orderBy(true, isAsc, MonitorDevice::getDeviceName);
|
||||
} else if ("status".equals(sortField)) {
|
||||
wrapper.orderBy(true, isAsc, MonitorDevice::getStatus);
|
||||
} else {
|
||||
wrapper.orderByDesc(MonitorDevice::getLastReportTime);
|
||||
}
|
||||
|
||||
Page<MonitorDevice> result = deviceMapper.selectPage(page, wrapper);
|
||||
|
||||
// 填充实时数据
|
||||
List<Map<String, Object>> deviceList = new ArrayList<>();
|
||||
for (MonitorDevice device : result.getRecords()) {
|
||||
Map<String, Object> deviceMap = new LinkedHashMap<>();
|
||||
deviceMap.put("id", device.getId());
|
||||
deviceMap.put("deviceCode", device.getDeviceCode());
|
||||
deviceMap.put("deviceName", device.getDeviceName());
|
||||
deviceMap.put("deviceType", device.getDeviceType());
|
||||
deviceMap.put("area", device.getArea());
|
||||
deviceMap.put("location", device.getLocation());
|
||||
deviceMap.put("lng", device.getLng());
|
||||
deviceMap.put("lat", device.getLat());
|
||||
deviceMap.put("status", device.getStatus());
|
||||
deviceMap.put("lastReportTime", device.getLastReportTime());
|
||||
deviceMap.put("brand", device.getBrand());
|
||||
|
||||
// 获取最新实时数据
|
||||
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||
Map<String, Object> realtimeMap = new LinkedHashMap<>();
|
||||
for (MonitorRealtimeData data : realtimeList) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("metricKey", data.getMetricKey());
|
||||
item.put("value", data.getMetricValue());
|
||||
item.put("unit", data.getUnit());
|
||||
item.put("thresholdHigh", data.getThresholdHigh());
|
||||
item.put("thresholdLow", data.getThresholdLow());
|
||||
item.put("isAbnormal", data.getIsAbnormal());
|
||||
item.put("collectTime", data.getCollectTime());
|
||||
realtimeMap.put(data.getMetricKey(), item);
|
||||
}
|
||||
deviceMap.put("realtimeData", realtimeMap);
|
||||
deviceList.add(deviceMap);
|
||||
}
|
||||
|
||||
Map<String, Object> pageResult = new LinkedHashMap<>();
|
||||
pageResult.put("records", deviceList);
|
||||
pageResult.put("total", result.getTotal());
|
||||
pageResult.put("pageNum", result.getCurrent());
|
||||
pageResult.put("pageSize", result.getSize());
|
||||
pageResult.put("pages", result.getPages());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个设备详情(含实时数据)
|
||||
*/
|
||||
public Map<String, Object> getDeviceDetail(Long deviceId) {
|
||||
MonitorDevice device = deviceMapper.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("id", device.getId());
|
||||
detail.put("deviceCode", device.getDeviceCode());
|
||||
detail.put("deviceName", device.getDeviceName());
|
||||
detail.put("deviceType", device.getDeviceType());
|
||||
detail.put("area", device.getArea());
|
||||
detail.put("location", device.getLocation());
|
||||
detail.put("lng", device.getLng());
|
||||
detail.put("lat", device.getLat());
|
||||
detail.put("status", device.getStatus());
|
||||
detail.put("lastReportTime", device.getLastReportTime());
|
||||
detail.put("brand", device.getBrand());
|
||||
detail.put("installTime", device.getInstallTime());
|
||||
detail.put("remark", device.getRemark());
|
||||
|
||||
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(deviceId);
|
||||
detail.put("realtimeData", realtimeList);
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备实时数据(多参数)
|
||||
*/
|
||||
public List<Map<String, Object>> getDeviceRealtimeData(Long deviceId) {
|
||||
List<MonitorRealtimeData> list = realtimeDataMapper.selectLatestByDeviceId(deviceId);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (MonitorRealtimeData data : list) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("metricKey", data.getMetricKey());
|
||||
map.put("value", data.getMetricValue());
|
||||
map.put("unit", data.getUnit());
|
||||
map.put("thresholdHigh", data.getThresholdHigh());
|
||||
map.put("thresholdLow", data.getThresholdLow());
|
||||
map.put("isAbnormal", data.getIsAbnormal());
|
||||
map.put("collectTime", data.getCollectTime());
|
||||
result.add(map);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备统计概览(按状态、类型、区域分组)
|
||||
*/
|
||||
public Map<String, Object> getDeviceStatistics() {
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
|
||||
// 按状态统计
|
||||
List<MonitorDevice> allDevices = deviceMapper.selectList(null);
|
||||
Map<String, Long> statusCount = allDevices.stream()
|
||||
.collect(Collectors.groupingBy(d -> d.getStatus() != null ? d.getStatus() : "unknown", Collectors.counting()));
|
||||
stats.put("statusDistribution", statusCount);
|
||||
stats.put("totalDevices", allDevices.size());
|
||||
stats.put("onlineCount", statusCount.getOrDefault("online", 0L));
|
||||
stats.put("offlineCount", statusCount.getOrDefault("offline", 0L));
|
||||
stats.put("faultCount", statusCount.getOrDefault("fault", 0L));
|
||||
stats.put("abnormalCount", statusCount.getOrDefault("abnormal", 0L));
|
||||
|
||||
// 按类型统计
|
||||
Map<String, Long> typeCount = allDevices.stream()
|
||||
.collect(Collectors.groupingBy(d -> d.getDeviceType() != null ? d.getDeviceType() : "unknown", Collectors.counting()));
|
||||
stats.put("typeDistribution", typeCount);
|
||||
|
||||
// 按区域统计
|
||||
Map<String, Long> areaCount = allDevices.stream()
|
||||
.collect(Collectors.groupingBy(d -> d.getArea() != null ? d.getArea() : "unknown", Collectors.counting()));
|
||||
stats.put("areaDistribution", areaCount);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备状态
|
||||
*/
|
||||
public void updateDeviceStatus(Long deviceId, String status) {
|
||||
MonitorDevice device = new MonitorDevice();
|
||||
device.setId(deviceId);
|
||||
device.setStatus(status);
|
||||
device.setUpdatedTime(LocalDateTime.now());
|
||||
deviceMapper.updateById(device);
|
||||
log.info("设备 {} 状态更新为 {}", deviceId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的区域列表
|
||||
*/
|
||||
public List<String> getAreaList() {
|
||||
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(MonitorDevice::getArea).groupBy(MonitorDevice::getArea);
|
||||
return deviceMapper.selectList(wrapper).stream()
|
||||
.map(MonitorDevice::getArea)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的设备类型列表
|
||||
*/
|
||||
public List<String> getDeviceTypeList() {
|
||||
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(MonitorDevice::getDeviceType).groupBy(MonitorDevice::getDeviceType);
|
||||
return deviceMapper.selectList(wrapper).stream()
|
||||
.map(MonitorDevice::getDeviceType)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取设备实时数据(用于导出)
|
||||
*/
|
||||
public List<Map<String, Object>> getBatchRealtimeData(List<Long> deviceIds) {
|
||||
if (deviceIds == null || deviceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return realtimeDataMapper.selectLatestBatch(deviceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据筛选条件查询设备列表(不分页,用于导出)
|
||||
*/
|
||||
public List<MonitorDevice> queryDevicesForExport(MonitorQueryRequest req) {
|
||||
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||
if (req.getArea() != null && !req.getArea().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getArea, req.getArea());
|
||||
}
|
||||
if (req.getDeviceType() != null && !req.getDeviceType().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getDeviceType, req.getDeviceType());
|
||||
}
|
||||
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
|
||||
wrapper.eq(MonitorDevice::getStatus, req.getStatus());
|
||||
}
|
||||
if (req.getKeyword() != null && !req.getKeyword().isEmpty()) {
|
||||
wrapper.and(w -> w.like(MonitorDevice::getDeviceCode, req.getKeyword())
|
||||
.or().like(MonitorDevice::getDeviceName, req.getKeyword()));
|
||||
}
|
||||
wrapper.orderByDesc(MonitorDevice::getLastReportTime);
|
||||
return deviceMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.water.production.dto.MonitorExportRequest;
|
||||
import com.water.production.dto.MonitorQueryRequest;
|
||||
import com.water.production.entity.MonitorDevice;
|
||||
import com.water.production.entity.MonitorRealtimeData;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 在线监测列表单元测试
|
||||
* 覆盖实体、DTO、查询筛选逻辑、导出格式、状态判断等
|
||||
*/
|
||||
class MonitorListServiceTest {
|
||||
|
||||
// ========== 1. 实体构建测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("MonitorDevice 实体字段完整性")
|
||||
void testMonitorDeviceEntity() {
|
||||
MonitorDevice device = new MonitorDevice();
|
||||
device.setId(1L);
|
||||
device.setDeviceCode("MON-FLOW-001");
|
||||
device.setDeviceName("一号泵站出口流量计");
|
||||
device.setDeviceType("flow");
|
||||
device.setArea("一体化水厂");
|
||||
device.setLocation("一号泵站出口");
|
||||
device.setLng(new BigDecimal("82.07123456"));
|
||||
device.setLat(new BigDecimal("44.84567890"));
|
||||
device.setStatus("online");
|
||||
device.setLastReportTime(LocalDateTime.of(2026, 6, 14, 16, 0, 0));
|
||||
device.setBrand("E+H Promag 50");
|
||||
device.setInstallTime(LocalDateTime.of(2025, 1, 15, 10, 0, 0));
|
||||
device.setRemark("正常运行");
|
||||
|
||||
assertEquals(1L, device.getId());
|
||||
assertEquals("MON-FLOW-001", device.getDeviceCode());
|
||||
assertEquals("flow", device.getDeviceType());
|
||||
assertEquals("online", device.getStatus());
|
||||
assertEquals(new BigDecimal("82.07123456"), device.getLng());
|
||||
assertNotNull(device.getLastReportTime());
|
||||
assertEquals("E+H Promag 50", device.getBrand());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MonitorRealtimeData 实体字段完整性")
|
||||
void testMonitorRealtimeDataEntity() {
|
||||
MonitorRealtimeData data = new MonitorRealtimeData();
|
||||
data.setId(1L);
|
||||
data.setDeviceId(1L);
|
||||
data.setDeviceCode("MON-FLOW-001");
|
||||
data.setMetricKey("flow");
|
||||
data.setMetricValue(new BigDecimal("125.5000"));
|
||||
data.setUnit("m³/h");
|
||||
data.setThresholdHigh(new BigDecimal("200.0000"));
|
||||
data.setThresholdLow(new BigDecimal("10.0000"));
|
||||
data.setIsAbnormal(0);
|
||||
data.setCollectTime(LocalDateTime.of(2026, 6, 14, 15, 55, 0));
|
||||
|
||||
assertEquals("flow", data.getMetricKey());
|
||||
assertEquals(new BigDecimal("125.5000"), data.getMetricValue());
|
||||
assertEquals("m³/h", data.getUnit());
|
||||
assertEquals(0, data.getIsAbnormal());
|
||||
assertNotNull(data.getCollectTime());
|
||||
}
|
||||
|
||||
// ========== 2. DTO 默认值测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("MonitorQueryRequest 默认分页参数")
|
||||
void testQueryRequestDefaults() {
|
||||
MonitorQueryRequest req = new MonitorQueryRequest();
|
||||
assertEquals(1, req.getPageNum());
|
||||
assertEquals(20, req.getPageSize());
|
||||
assertNull(req.getArea());
|
||||
assertNull(req.getDeviceType());
|
||||
assertNull(req.getStatus());
|
||||
assertNull(req.getSortField());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MonitorExportRequest 默认格式为 excel")
|
||||
void testExportRequestDefaults() {
|
||||
MonitorExportRequest req = new MonitorExportRequest();
|
||||
assertEquals("excel", req.getFormat());
|
||||
assertTrue(req.getIncludeRealtime());
|
||||
assertNull(req.getDeviceIds());
|
||||
}
|
||||
|
||||
// ========== 3. 状态判断逻辑测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("设备状态格式化逻辑")
|
||||
void testStatusFormatting() {
|
||||
// 模拟 Controller 中的状态格式化
|
||||
Map<String, String> statusMap = Map.of(
|
||||
"online", "在线",
|
||||
"offline", "离线",
|
||||
"fault", "故障",
|
||||
"abnormal", "数据异常"
|
||||
);
|
||||
|
||||
assertEquals("在线", statusMap.get("online"));
|
||||
assertEquals("离线", statusMap.get("offline"));
|
||||
assertEquals("故障", statusMap.get("fault"));
|
||||
assertEquals("数据异常", statusMap.get("abnormal"));
|
||||
assertNull(statusMap.get("unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("设备异常判定逻辑")
|
||||
void testAbnormalDetection() {
|
||||
// 模拟异常判定:值超出阈值范围
|
||||
BigDecimal value = new BigDecimal("250.00");
|
||||
BigDecimal high = new BigDecimal("200.00");
|
||||
BigDecimal low = new BigDecimal("10.00");
|
||||
|
||||
boolean isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||
|| (low != null && value.compareTo(low) < 0);
|
||||
assertTrue(isAbnormal, "超出上限应判定为异常");
|
||||
|
||||
// 正常范围
|
||||
value = new BigDecimal("125.50");
|
||||
isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||
|| (low != null && value.compareTo(low) < 0);
|
||||
assertFalse(isAbnormal, "正常范围不应判定为异常");
|
||||
|
||||
// 低于下限
|
||||
value = new BigDecimal("5.00");
|
||||
isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||
|| (low != null && value.compareTo(low) < 0);
|
||||
assertTrue(isAbnormal, "低于下限应判定为异常");
|
||||
}
|
||||
|
||||
// ========== 4. 筛选逻辑测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("多维度筛选条件构建")
|
||||
void testMultiDimensionFilter() {
|
||||
// 模拟设备列表
|
||||
List<MonitorDevice> devices = buildMockDevices();
|
||||
|
||||
// 按区域筛选
|
||||
List<MonitorDevice> filtered = devices.stream()
|
||||
.filter(d -> "一体化水厂".equals(d.getArea()))
|
||||
.collect(Collectors.toList());
|
||||
assertEquals(3, filtered.size());
|
||||
|
||||
// 按设备类型筛选
|
||||
filtered = devices.stream()
|
||||
.filter(d -> "pressure".equals(d.getDeviceType()))
|
||||
.collect(Collectors.toList());
|
||||
assertEquals(2, filtered.size());
|
||||
|
||||
// 按状态筛选
|
||||
filtered = devices.stream()
|
||||
.filter(d -> "online".equals(d.getStatus()))
|
||||
.collect(Collectors.toList());
|
||||
assertEquals(3, filtered.size());
|
||||
|
||||
// 组合筛选:区域 + 类型
|
||||
filtered = devices.stream()
|
||||
.filter(d -> "一体化水厂".equals(d.getArea()) && "flow".equals(d.getDeviceType()))
|
||||
.collect(Collectors.toList());
|
||||
assertEquals(2, filtered.size());
|
||||
|
||||
// 关键词搜索
|
||||
String keyword = "流量计";
|
||||
filtered = devices.stream()
|
||||
.filter(d -> (d.getDeviceName() != null && d.getDeviceName().contains(keyword))
|
||||
|| (d.getDeviceCode() != null && d.getDeviceCode().contains(keyword)))
|
||||
.collect(Collectors.toList());
|
||||
assertEquals(2, filtered.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("排序逻辑测试")
|
||||
void testSortLogic() {
|
||||
List<MonitorDevice> devices = buildMockDevices();
|
||||
|
||||
// 按设备编号升序
|
||||
devices.sort(Comparator.comparing(MonitorDevice::getDeviceCode));
|
||||
assertEquals("MON-FLOW-001", devices.get(0).getDeviceCode());
|
||||
|
||||
// 按设备编号降序
|
||||
devices.sort(Comparator.comparing(MonitorDevice::getDeviceCode).reversed());
|
||||
assertEquals("MON-QUAL-001", devices.get(0).getDeviceCode());
|
||||
|
||||
// 按最后上报时间降序(最新在前)
|
||||
devices.sort(Comparator.comparing(MonitorDevice::getLastReportTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
assertNotNull(devices.get(0).getLastReportTime());
|
||||
}
|
||||
|
||||
// ========== 5. 统计逻辑测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("设备统计聚合逻辑")
|
||||
void testDeviceStatistics() {
|
||||
List<MonitorDevice> devices = buildMockDevices();
|
||||
|
||||
// 按状态统计
|
||||
Map<String, Long> statusCount = devices.stream()
|
||||
.collect(Collectors.groupingBy(MonitorDevice::getStatus, Collectors.counting()));
|
||||
assertEquals(3L, statusCount.get("online"));
|
||||
assertEquals(1L, statusCount.get("offline"));
|
||||
assertEquals(1L, statusCount.get("fault"));
|
||||
|
||||
// 按类型统计
|
||||
Map<String, Long> typeCount = devices.stream()
|
||||
.collect(Collectors.groupingBy(MonitorDevice::getDeviceType, Collectors.counting()));
|
||||
assertEquals(2L, typeCount.get("flow"));
|
||||
assertEquals(2L, typeCount.get("pressure"));
|
||||
assertEquals(1L, typeCount.get("quality"));
|
||||
|
||||
// 按区域统计
|
||||
Map<String, Long> areaCount = devices.stream()
|
||||
.collect(Collectors.groupingBy(MonitorDevice::getArea, Collectors.counting()));
|
||||
assertEquals(3L, areaCount.get("一体化水厂"));
|
||||
assertEquals(2L, areaCount.get("管网一区"));
|
||||
}
|
||||
|
||||
// ========== 6. 分页计算测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("分页参数计算")
|
||||
void testPagination() {
|
||||
int total = 50;
|
||||
int pageSize = 20;
|
||||
|
||||
int pages = (int) Math.ceil((double) total / pageSize);
|
||||
assertEquals(3, pages);
|
||||
|
||||
// 第1页偏移量
|
||||
int offset = (1 - 1) * pageSize;
|
||||
assertEquals(0, offset);
|
||||
|
||||
// 第3页偏移量
|
||||
offset = (3 - 1) * pageSize;
|
||||
assertEquals(40, offset);
|
||||
|
||||
// 整除情况
|
||||
total = 40;
|
||||
pages = (int) Math.ceil((double) total / pageSize);
|
||||
assertEquals(2, pages);
|
||||
}
|
||||
|
||||
// ========== 7. CSV 转义测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("CSV 字段转义逻辑")
|
||||
void testCsvEscaping() {
|
||||
// 包含逗号
|
||||
String value = "一号泵站,出口";
|
||||
String escaped = csvEsc(value);
|
||||
assertEquals("\"一号泵站,出口\"", escaped);
|
||||
|
||||
// 包含引号
|
||||
value = "含\"引号\"的文本";
|
||||
escaped = csvEsc(value);
|
||||
assertEquals("\"含\"\"引号\"\"的文本\"", escaped);
|
||||
|
||||
// 普通文本
|
||||
value = "正常文本";
|
||||
escaped = csvEsc(value);
|
||||
assertEquals("正常文本", escaped);
|
||||
|
||||
// null
|
||||
escaped = csvEsc(null);
|
||||
assertEquals("", escaped);
|
||||
}
|
||||
|
||||
// ========== 8. 实时数据结构测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("多参数实时数据结构完整性")
|
||||
void testRealtimeDataStructure() {
|
||||
// 模拟多参数实时数据
|
||||
List<String> expectedMetrics = List.of("flow", "pressure", "level", "turbidity", "ph", "residual_chlorine");
|
||||
Map<String, MonitorRealtimeData> metricMap = new LinkedHashMap<>();
|
||||
|
||||
for (String metric : expectedMetrics) {
|
||||
MonitorRealtimeData data = new MonitorRealtimeData();
|
||||
data.setDeviceId(1L);
|
||||
data.setMetricKey(metric);
|
||||
data.setMetricValue(new BigDecimal("100.00"));
|
||||
data.setUnit("unit_" + metric);
|
||||
data.setCollectTime(LocalDateTime.now());
|
||||
metricMap.put(metric, data);
|
||||
}
|
||||
|
||||
assertEquals(6, metricMap.size());
|
||||
assertTrue(metricMap.containsKey("flow"));
|
||||
assertTrue(metricMap.containsKey("pressure"));
|
||||
assertTrue(metricMap.containsKey("level"));
|
||||
assertTrue(metricMap.containsKey("turbidity"));
|
||||
assertTrue(metricMap.containsKey("ph"));
|
||||
assertTrue(metricMap.containsKey("residual_chlorine"));
|
||||
|
||||
// 验证各参数值
|
||||
for (String metric : expectedMetrics) {
|
||||
assertNotNull(metricMap.get(metric).getMetricValue());
|
||||
assertEquals(metric, metricMap.get(metric).getMetricKey());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 9. 设备在线率计算 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("设备在线率计算")
|
||||
void testOnlineRateCalculation() {
|
||||
List<MonitorDevice> devices = buildMockDevices();
|
||||
int total = devices.size();
|
||||
long onlineCount = devices.stream().filter(d -> "online".equals(d.getStatus())).count();
|
||||
|
||||
BigDecimal onlineRate = total > 0
|
||||
? BigDecimal.valueOf(onlineCount * 100.0 / total).setScale(1, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
assertEquals(5, total);
|
||||
assertEquals(3, onlineCount);
|
||||
assertEquals(new BigDecimal("60.0"), onlineRate);
|
||||
}
|
||||
|
||||
// ========== 10. 导出数据量限制测试 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("导出最大数据量限制")
|
||||
void testExportMaxLimit() {
|
||||
MonitorExportRequest req = new MonitorExportRequest();
|
||||
int maxExportLimit = 10000;
|
||||
|
||||
// 模拟大量设备
|
||||
List<MonitorDevice> devices = new ArrayList<>();
|
||||
for (int i = 0; i < 15000; i++) {
|
||||
MonitorDevice d = new MonitorDevice();
|
||||
d.setId((long) (i + 1));
|
||||
d.setDeviceCode("MON-" + String.format("%05d", i));
|
||||
devices.add(d);
|
||||
}
|
||||
|
||||
// 导出应限制在 maxExportLimit 内
|
||||
List<MonitorDevice> exportList = devices.size() > maxExportLimit
|
||||
? devices.subList(0, maxExportLimit)
|
||||
: devices;
|
||||
|
||||
assertEquals(maxExportLimit, exportList.size());
|
||||
assertTrue(devices.size() > maxExportLimit);
|
||||
}
|
||||
|
||||
// ========== Helper Methods ==========
|
||||
|
||||
private List<MonitorDevice> buildMockDevices() {
|
||||
List<MonitorDevice> devices = new ArrayList<>();
|
||||
|
||||
MonitorDevice d1 = new MonitorDevice();
|
||||
d1.setId(1L);
|
||||
d1.setDeviceCode("MON-FLOW-001");
|
||||
d1.setDeviceName("一号泵站出口流量计");
|
||||
d1.setDeviceType("flow");
|
||||
d1.setArea("一体化水厂");
|
||||
d1.setStatus("online");
|
||||
d1.setLastReportTime(LocalDateTime.of(2026, 6, 14, 16, 0, 0));
|
||||
devices.add(d1);
|
||||
|
||||
MonitorDevice d2 = new MonitorDevice();
|
||||
d2.setId(2L);
|
||||
d2.setDeviceCode("MON-FLOW-002");
|
||||
d2.setDeviceName("二号泵站流量计");
|
||||
d2.setDeviceType("flow");
|
||||
d2.setArea("一体化水厂");
|
||||
d2.setStatus("online");
|
||||
d2.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 50, 0));
|
||||
devices.add(d2);
|
||||
|
||||
MonitorDevice d3 = new MonitorDevice();
|
||||
d3.setId(3L);
|
||||
d3.setDeviceCode("MON-PRES-001");
|
||||
d3.setDeviceName("管网压力监测点A");
|
||||
d3.setDeviceType("pressure");
|
||||
d3.setArea("管网一区");
|
||||
d3.setStatus("online");
|
||||
d3.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 55, 0));
|
||||
devices.add(d3);
|
||||
|
||||
MonitorDevice d4 = new MonitorDevice();
|
||||
d4.setId(4L);
|
||||
d4.setDeviceCode("MON-PRES-002");
|
||||
d4.setDeviceName("管网压力监测点B");
|
||||
d4.setDeviceType("pressure");
|
||||
d4.setArea("管网一区");
|
||||
d4.setStatus("offline");
|
||||
d4.setLastReportTime(LocalDateTime.of(2026, 6, 14, 14, 0, 0));
|
||||
devices.add(d4);
|
||||
|
||||
MonitorDevice d5 = new MonitorDevice();
|
||||
d5.setId(5L);
|
||||
d5.setDeviceCode("MON-QUAL-001");
|
||||
d5.setDeviceName("出厂水质监测仪");
|
||||
d5.setDeviceType("quality");
|
||||
d5.setArea("一体化水厂");
|
||||
d5.setStatus("fault");
|
||||
d5.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 30, 0));
|
||||
devices.add(d5);
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
private String csvEsc(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user