|
|
|
|
@ -0,0 +1,673 @@
|
|
|
|
|
package org.dromara.ems.record.service.impl;
|
|
|
|
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import org.dromara.common.core.exception.ServiceException;
|
|
|
|
|
import org.dromara.ems.base.domain.EmsBaseMonitorInfo;
|
|
|
|
|
import org.dromara.ems.base.mapper.EmsBaseMonitorInfoMapper;
|
|
|
|
|
import org.dromara.ems.record.domain.bo.IotEnvMonitorQuery;
|
|
|
|
|
import org.dromara.ems.record.enums.IotEnvMonitorTypeEnum;
|
|
|
|
|
import org.dromara.ems.record.mapper.IotEnvMonitorDataMapper;
|
|
|
|
|
import org.dromara.ems.record.service.IIotEnvMonitorDataService;
|
|
|
|
|
import org.dromara.ems.record.service.RecordIotenvPartitionService;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDate;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneId;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.time.format.DateTimeFormatterBuilder;
|
|
|
|
|
import java.time.format.DateTimeParseException;
|
|
|
|
|
import java.time.format.ResolverStyle;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 物联网环境监测数据按日分表查询 Service 实现
|
|
|
|
|
*
|
|
|
|
|
* <p>核心职责:
|
|
|
|
|
* 1. 校验查询参数(必填项、格式合法性)
|
|
|
|
|
* 2. 将前端传入的日期字符串解析为 LocalDate,再通过 RecordIotenvPartitionService 生成白名单分表名
|
|
|
|
|
* 3. 判断目标分表是否存在(不存在时优雅返回空集合,不报 SQL 异常)
|
|
|
|
|
* 4. 调用 Mapper 查询原始数据(全量字段)
|
|
|
|
|
* 5. 根据每条记录的 monitor_type 裁剪返回字段,补充 energyName 中文名称
|
|
|
|
|
* 6. 组装并返回 Map 类型结果
|
|
|
|
|
* </p>
|
|
|
|
|
*
|
|
|
|
|
* @author zch
|
|
|
|
|
* @date 2026-04-09
|
|
|
|
|
*/
|
|
|
|
|
@Service
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
public class IotEnvMonitorDataServiceImpl implements IIotEnvMonitorDataService {
|
|
|
|
|
|
|
|
|
|
/** 复用现有最新值回退策略,只回看最近 7 天内存在的日分表 */
|
|
|
|
|
private static final int LATEST_TABLE_LOOKBACK_DAYS = 7;
|
|
|
|
|
|
|
|
|
|
/** 严格日期格式:前端若传 2026-02-29 这类非法日期,必须直接报错,不能被静默纠正 */
|
|
|
|
|
private static final DateTimeFormatter STRICT_DATE_FORMATTER = new DateTimeFormatterBuilder()
|
|
|
|
|
.appendPattern("uuuu-MM-dd")
|
|
|
|
|
.toFormatter()
|
|
|
|
|
.withResolverStyle(ResolverStyle.STRICT);
|
|
|
|
|
|
|
|
|
|
/** 紧凑日期格式同样使用严格解析,避免非法日期被自动回滚到上一个月末 */
|
|
|
|
|
private static final DateTimeFormatter STRICT_COMPACT_DATE_FORMATTER = new DateTimeFormatterBuilder()
|
|
|
|
|
.appendPattern("uuuuMMdd")
|
|
|
|
|
.toFormatter()
|
|
|
|
|
.withResolverStyle(ResolverStyle.STRICT);
|
|
|
|
|
|
|
|
|
|
/** 时间范围只接受秒级完整格式,避免不同页面传参粒度不一致导致查询口径漂移 */
|
|
|
|
|
private static final DateTimeFormatter STRICT_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
|
|
|
|
|
.appendPattern("uuuu-MM-dd HH:mm:ss")
|
|
|
|
|
.toFormatter()
|
|
|
|
|
.withResolverStyle(ResolverStyle.STRICT);
|
|
|
|
|
|
|
|
|
|
/** 与现有 RecordIotenvInstantServiceImpl 保持一致,统一按系统时区把时间对象落库查询 */
|
|
|
|
|
private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();
|
|
|
|
|
|
|
|
|
|
/** 独立新增的 Mapper,不复用 RecordIotenvInstantMapper,保持职责隔离 */
|
|
|
|
|
private final IotEnvMonitorDataMapper iotEnvMonitorDataMapper;
|
|
|
|
|
|
|
|
|
|
/** 设备主数据来源,用于无参接口先收集 monitorIds,并在缺实时数据时补设备骨架 */
|
|
|
|
|
private final EmsBaseMonitorInfoMapper emsBaseMonitorInfoMapper;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 复用已有分区路由服务的 buildTableName() 方法,保证分表名生成规则与全系统一致,
|
|
|
|
|
* 避免出现两套命名规则导致分表路由口径不一致的问题。
|
|
|
|
|
*/
|
|
|
|
|
private final RecordIotenvPartitionService recordIotenvPartitionService;
|
|
|
|
|
|
|
|
|
|
// ======================================================================
|
|
|
|
|
// 接口方法实现
|
|
|
|
|
// ======================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 无参查询所有设备各自最新一条监测数据
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<Map<String, Object>> queryLatestAllMonitorData() {
|
|
|
|
|
// 先从设备主数据表收集 monitorIds,保证“全设备最新值”与现有首页口径一致
|
|
|
|
|
List<EmsBaseMonitorInfo> baseMonitorInfos = listAllMonitorInfos();
|
|
|
|
|
if (baseMonitorInfos.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<String> monitorIds = extractMonitorIds(baseMonitorInfos);
|
|
|
|
|
if (monitorIds.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 参考现有实时看板做法:优先当天,若当天无表则向前回退到最近存在分表
|
|
|
|
|
List<String> tableNames = recordIotenvPartitionService.resolveLatestAvailableTables(LATEST_TABLE_LOOKBACK_DAYS);
|
|
|
|
|
if (tableNames.isEmpty()) {
|
|
|
|
|
// 没有有效分表时仍返回设备骨架,避免前端因为空集合导致整页结构断层
|
|
|
|
|
return buildLatestSnapshot(baseMonitorInfos, Collections.emptyMap());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Map<String, Object>> rawList = iotEnvMonitorDataMapper.selectLatestRawByMonitorIdsFromTables(
|
|
|
|
|
tableNames, monitorIds
|
|
|
|
|
);
|
|
|
|
|
return buildLatestSnapshot(baseMonitorInfos, indexRawDataByMonitorId(rawList));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询指定日期指定设备的最新一条监测数据
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public Map<String, Object> queryLatestByMonitorIdAndDate(IotEnvMonitorQuery query) {
|
|
|
|
|
// 校验必填参数
|
|
|
|
|
validateDateRequired(query);
|
|
|
|
|
String monitorId = trimToNull(query.getMonitorId());
|
|
|
|
|
if (monitorId == null) {
|
|
|
|
|
throw new ServiceException("设备编号 monitorId 不能为空");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析日期并生成经过白名单校验的分表名
|
|
|
|
|
String tableName = buildAndValidateTableName(query.getDate());
|
|
|
|
|
|
|
|
|
|
// 分表不存在时优雅返回空 Map,避免直接抛 SQL 异常暴露内部结构
|
|
|
|
|
if (!isDailyTableExists(tableName)) {
|
|
|
|
|
return Collections.emptyMap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查询原始数据(单条,TOP 1)
|
|
|
|
|
Map<String, Object> rawData = iotEnvMonitorDataMapper.selectLatestRawByMonitorId(
|
|
|
|
|
tableName, monitorId
|
|
|
|
|
);
|
|
|
|
|
if (rawData == null || rawData.isEmpty()) {
|
|
|
|
|
return Collections.emptyMap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 按设备类型裁剪字段并补充 energyName
|
|
|
|
|
return trimFieldsByMonitorType(rawData, query.getVibrationParam());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询指定日期的监测数据列表(支持多条件过滤)
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<Map<String, Object>> queryDataListByDate(IotEnvMonitorQuery query) {
|
|
|
|
|
validateDateRequired(query);
|
|
|
|
|
MonitorFilter monitorFilter = resolveMonitorFilter(query);
|
|
|
|
|
TimeRange timeRange = resolveOptionalRecordTimeRange(query);
|
|
|
|
|
|
|
|
|
|
String tableName = buildAndValidateTableName(query.getDate());
|
|
|
|
|
if (!isDailyTableExists(tableName)) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 调用 Mapper 查询原始数据列表
|
|
|
|
|
List<Map<String, Object>> rawList = iotEnvMonitorDataMapper.selectRawDataByDate(
|
|
|
|
|
tableName,
|
|
|
|
|
monitorFilter.monitorId,
|
|
|
|
|
monitorFilter.monitorIds,
|
|
|
|
|
query.getMonitorType(),
|
|
|
|
|
timeRange.startTime,
|
|
|
|
|
timeRange.endTime
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 对每条记录按设备类型裁剪字段
|
|
|
|
|
return trimListByMonitorType(rawList, query.getVibrationParam());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量查询指定日期多个设备各自最新一条监测数据
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<Map<String, Object>> queryLatestByMonitorIdsAndDate(IotEnvMonitorQuery query) {
|
|
|
|
|
validateDateRequired(query);
|
|
|
|
|
List<String> monitorIds = normalizeMonitorIds(query.getMonitorIds());
|
|
|
|
|
if (monitorIds.isEmpty()) {
|
|
|
|
|
throw new ServiceException("设备编号列表 monitorIds 不能为空");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String tableName = buildAndValidateTableName(query.getDate());
|
|
|
|
|
if (!isDailyTableExists(tableName)) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Map<String, Object>> rawList = iotEnvMonitorDataMapper.selectLatestRawByMonitorIds(
|
|
|
|
|
tableName, monitorIds
|
|
|
|
|
);
|
|
|
|
|
return trimListByMonitorType(rawList, query.getVibrationParam());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按设备类型查询指定日期的监测数据列表
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<Map<String, Object>> queryDataListByMonitorType(IotEnvMonitorQuery query) {
|
|
|
|
|
validateDateRequired(query);
|
|
|
|
|
if (query.getMonitorType() == null) {
|
|
|
|
|
throw new ServiceException("设备类型 monitorType 不能为空");
|
|
|
|
|
}
|
|
|
|
|
TimeRange timeRange = resolveOptionalRecordTimeRange(query);
|
|
|
|
|
|
|
|
|
|
String tableName = buildAndValidateTableName(query.getDate());
|
|
|
|
|
if (!isDailyTableExists(tableName)) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Map<String, Object>> rawList = iotEnvMonitorDataMapper.selectRawDataByDate(
|
|
|
|
|
tableName,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
query.getMonitorType(),
|
|
|
|
|
timeRange.startTime,
|
|
|
|
|
timeRange.endTime
|
|
|
|
|
);
|
|
|
|
|
return trimListByMonitorType(rawList, query.getVibrationParam());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================================================================
|
|
|
|
|
// 私有工具方法
|
|
|
|
|
// ======================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验 date 参数是否存在且非空
|
|
|
|
|
*/
|
|
|
|
|
private void validateDateRequired(IotEnvMonitorQuery query) {
|
|
|
|
|
if (query == null) {
|
|
|
|
|
throw new ServiceException("查询参数不能为空");
|
|
|
|
|
}
|
|
|
|
|
if (isBlank(query.getDate())) {
|
|
|
|
|
throw new ServiceException("查询日期 date 不能为空,支持格式:yyyy-MM-dd 或 yyyyMMdd");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析日期字符串并通过 RecordIotenvPartitionService 生成经白名单校验的分表名。
|
|
|
|
|
*
|
|
|
|
|
* <p>安全说明:
|
|
|
|
|
* 分表名永远由服务端根据合法日期自动生成,绝不允许前端原始字符串直接拼入 SQL。
|
|
|
|
|
* RecordIotenvPartitionService.buildTableName() 内部有正则白名单双重校验
|
|
|
|
|
* (格式:^record_iotenv_instant_\d{8}$),任何不符合格式的输入都会抛出 ServiceException。</p>
|
|
|
|
|
*
|
|
|
|
|
* @param dateStr 前端传入的日期字符串(yyyy-MM-dd 或 yyyyMMdd)
|
|
|
|
|
* @return 已校验合法的分表名
|
|
|
|
|
*/
|
|
|
|
|
private String buildAndValidateTableName(String dateStr) {
|
|
|
|
|
LocalDate localDate = parseDateString(dateStr);
|
|
|
|
|
// buildTableName 内部会再次做正则校验,双重保险
|
|
|
|
|
return recordIotenvPartitionService.buildTableName(localDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析日期字符串,支持 yyyy-MM-dd 和 yyyyMMdd 两种格式。
|
|
|
|
|
* 严格解析,非法日期(如 2026-13-01、20261301)会直接抛出 ServiceException。
|
|
|
|
|
*
|
|
|
|
|
* @param dateStr 日期字符串
|
|
|
|
|
* @return LocalDate 对象
|
|
|
|
|
*/
|
|
|
|
|
private LocalDate parseDateString(String dateStr) {
|
|
|
|
|
String trimmed = dateStr.trim();
|
|
|
|
|
// 先尝试 yyyy-MM-dd 格式(前端日期选择器最常见输出格式)
|
|
|
|
|
try {
|
|
|
|
|
return LocalDate.parse(trimmed, STRICT_DATE_FORMATTER);
|
|
|
|
|
} catch (DateTimeParseException ignored) {
|
|
|
|
|
// 继续尝试 yyyyMMdd 格式
|
|
|
|
|
}
|
|
|
|
|
// 再尝试 yyyyMMdd 格式(紧凑型日期字符串)
|
|
|
|
|
try {
|
|
|
|
|
return LocalDate.parse(trimmed, STRICT_COMPACT_DATE_FORMATTER);
|
|
|
|
|
} catch (DateTimeParseException e) {
|
|
|
|
|
throw new ServiceException(
|
|
|
|
|
"日期格式不正确,仅支持 yyyy-MM-dd 或 yyyyMMdd,实际收到:" + dateStr
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 检查指定分表是否存在于数据库。
|
|
|
|
|
* 使用新 Mapper 的 checkDailyTableExists 方法查询 INFORMATION_SCHEMA.TABLES。
|
|
|
|
|
*
|
|
|
|
|
* @param tableName 已经白名单校验过的分表名
|
|
|
|
|
* @return true 表示分表存在
|
|
|
|
|
*/
|
|
|
|
|
private boolean isDailyTableExists(String tableName) {
|
|
|
|
|
Integer count = iotEnvMonitorDataMapper.checkDailyTableExists(tableName);
|
|
|
|
|
return count != null && count > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 归一化 monitor 条件。
|
|
|
|
|
*
|
|
|
|
|
* <p>评审要求明确了“以 monitorIds 列表为主”,因此当列表非空时会主动忽略 monitorId,
|
|
|
|
|
* 避免出现 monitorId = ? AND monitorId IN (?) 叠加后误过滤为空的情况。</p>
|
|
|
|
|
*/
|
|
|
|
|
private MonitorFilter resolveMonitorFilter(IotEnvMonitorQuery query) {
|
|
|
|
|
List<String> monitorIds = normalizeMonitorIds(query.getMonitorIds());
|
|
|
|
|
if (!monitorIds.isEmpty()) {
|
|
|
|
|
return new MonitorFilter(null, monitorIds);
|
|
|
|
|
}
|
|
|
|
|
return new MonitorFilter(trimToNull(query.getMonitorId()), Collections.emptyList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 规范化 monitorIds:去空白、去重、保序。
|
|
|
|
|
*
|
|
|
|
|
* <p>这样做的业务价值是:既能减少 SQL IN 条件里的重复值,又能避免前端数组里夹杂空串时
|
|
|
|
|
* 误判为“有值但查不到数据”,排查起来更直接。</p>
|
|
|
|
|
*/
|
|
|
|
|
private List<String> normalizeMonitorIds(List<String> monitorIds) {
|
|
|
|
|
if (monitorIds == null || monitorIds.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
|
|
|
|
for (String monitorId : monitorIds) {
|
|
|
|
|
String trimmedMonitorId = trimToNull(monitorId);
|
|
|
|
|
if (trimmedMonitorId != null) {
|
|
|
|
|
normalized.add(trimmedMonitorId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (normalized.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
return new ArrayList<>(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析可选的记录时间范围。
|
|
|
|
|
*
|
|
|
|
|
* <p>这里参照现有 RecordIotenvInstantServiceImpl 的思路,在 Service 层先把字符串解析成时间对象,
|
|
|
|
|
* 再校验起止顺序,避免把格式错误或颠倒顺序的问题拖到数据库层才暴露。</p>
|
|
|
|
|
*/
|
|
|
|
|
private TimeRange resolveOptionalRecordTimeRange(IotEnvMonitorQuery query) {
|
|
|
|
|
Date startTime = parseOptionalDateTime(query.getStartTime(), "开始记录时间");
|
|
|
|
|
Date endTime = parseOptionalDateTime(query.getEndTime(), "结束记录时间");
|
|
|
|
|
if (startTime != null && endTime != null && startTime.after(endTime)) {
|
|
|
|
|
throw new ServiceException("开始记录时间不能晚于结束记录时间");
|
|
|
|
|
}
|
|
|
|
|
return new TimeRange(startTime, endTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 严格解析时间范围参数。
|
|
|
|
|
*
|
|
|
|
|
* <p>仅接受 yyyy-MM-dd HH:mm:ss,像 2026-04-09 8:00、2026/04/09 08:00:00
|
|
|
|
|
* 这类非约定格式都直接 Fail Fast,避免接口口径被不同前端页面偷偷拉歪。</p>
|
|
|
|
|
*/
|
|
|
|
|
private Date parseOptionalDateTime(String dateTimeStr, String fieldName) {
|
|
|
|
|
String trimmed = trimToNull(dateTimeStr);
|
|
|
|
|
if (trimmed == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, STRICT_DATE_TIME_FORMATTER);
|
|
|
|
|
return Date.from(localDateTime.atZone(SYSTEM_ZONE_ID).toInstant());
|
|
|
|
|
} catch (DateTimeParseException e) {
|
|
|
|
|
throw new ServiceException(fieldName + "格式不正确,请使用 yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询设备主数据表并去掉空编码、重复编码。
|
|
|
|
|
*/
|
|
|
|
|
private List<EmsBaseMonitorInfo> listAllMonitorInfos() {
|
|
|
|
|
List<EmsBaseMonitorInfo> monitorInfos = emsBaseMonitorInfoMapper.selectEmsBaseMonitorInfoList(
|
|
|
|
|
new EmsBaseMonitorInfo()
|
|
|
|
|
);
|
|
|
|
|
if (monitorInfos == null || monitorInfos.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LinkedHashMap<String, EmsBaseMonitorInfo> distinctMonitorMap = new LinkedHashMap<>();
|
|
|
|
|
for (EmsBaseMonitorInfo monitorInfo : monitorInfos) {
|
|
|
|
|
String monitorCode = trimToNull(monitorInfo.getMonitorCode());
|
|
|
|
|
if (monitorCode == null || distinctMonitorMap.containsKey(monitorCode)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
monitorInfo.setMonitorCode(monitorCode);
|
|
|
|
|
distinctMonitorMap.put(monitorCode, monitorInfo);
|
|
|
|
|
}
|
|
|
|
|
return new ArrayList<>(distinctMonitorMap.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从设备主数据中提取 monitorIds 列表。
|
|
|
|
|
*/
|
|
|
|
|
private List<String> extractMonitorIds(List<EmsBaseMonitorInfo> baseMonitorInfos) {
|
|
|
|
|
List<String> monitorIds = new ArrayList<>(baseMonitorInfos.size());
|
|
|
|
|
for (EmsBaseMonitorInfo baseMonitorInfo : baseMonitorInfos) {
|
|
|
|
|
if (trimToNull(baseMonitorInfo.getMonitorCode()) != null) {
|
|
|
|
|
monitorIds.add(baseMonitorInfo.getMonitorCode());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return monitorIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将原始实时数据按 monitorId 建索引,方便后续与设备主数据做一一补齐。
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, Map<String, Object>> indexRawDataByMonitorId(List<Map<String, Object>> rawList) {
|
|
|
|
|
if (rawList == null || rawList.isEmpty()) {
|
|
|
|
|
return Collections.emptyMap();
|
|
|
|
|
}
|
|
|
|
|
Map<String, Map<String, Object>> rawDataMap = new HashMap<>(rawList.size());
|
|
|
|
|
for (Map<String, Object> raw : rawList) {
|
|
|
|
|
String monitorId = trimToNull(raw.get("monitorId") == null ? null : raw.get("monitorId").toString());
|
|
|
|
|
if (monitorId != null) {
|
|
|
|
|
rawDataMap.putIfAbsent(monitorId, raw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return rawDataMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按设备主数据顺序构建“所有设备最新值”结果。
|
|
|
|
|
*
|
|
|
|
|
* <p>即便某设备当前没有实时数据,也会保留设备骨架并按 monitor_type 裁剪字段,
|
|
|
|
|
* 这样前端调用无参接口时可以稳定拿到完整设备集合。</p>
|
|
|
|
|
*/
|
|
|
|
|
private List<Map<String, Object>> buildLatestSnapshot(
|
|
|
|
|
List<EmsBaseMonitorInfo> baseMonitorInfos, Map<String, Map<String, Object>> rawDataMap
|
|
|
|
|
) {
|
|
|
|
|
List<Map<String, Object>> result = new ArrayList<>(baseMonitorInfos.size());
|
|
|
|
|
for (EmsBaseMonitorInfo baseMonitorInfo : baseMonitorInfos) {
|
|
|
|
|
Map<String, Object> raw = buildSnapshotRawData(baseMonitorInfo, rawDataMap.get(baseMonitorInfo.getMonitorCode()));
|
|
|
|
|
result.add(trimFieldsByMonitorType(raw, null));
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 合并设备主数据与实时原始数据。
|
|
|
|
|
*
|
|
|
|
|
* <p>主数据负责补足 monitorCode/monitorName/monitorType,实时数据负责补测量值和时间戳;
|
|
|
|
|
* 这样可以兼容“设备存在但当前无最新值”的场景。</p>
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, Object> buildSnapshotRawData(EmsBaseMonitorInfo baseMonitorInfo, Map<String, Object> latestRaw) {
|
|
|
|
|
String monitorCode = baseMonitorInfo.getMonitorCode();
|
|
|
|
|
String monitorName = trimToNull(baseMonitorInfo.getMonitorName());
|
|
|
|
|
if (monitorName == null && latestRaw != null && latestRaw.get("monitorName") != null) {
|
|
|
|
|
monitorName = latestRaw.get("monitorName").toString();
|
|
|
|
|
}
|
|
|
|
|
Object monitorType = baseMonitorInfo.getMonitorType() != null
|
|
|
|
|
? baseMonitorInfo.getMonitorType()
|
|
|
|
|
: getRawValue(latestRaw, "monitorType");
|
|
|
|
|
|
|
|
|
|
Map<String, Object> raw = new LinkedHashMap<>();
|
|
|
|
|
raw.put("objid", getRawValue(latestRaw, "objid"));
|
|
|
|
|
raw.put("monitorId", monitorCode);
|
|
|
|
|
raw.put("monitorCode", monitorCode);
|
|
|
|
|
raw.put("monitorName", monitorName != null ? monitorName : monitorCode);
|
|
|
|
|
raw.put("monitorType", monitorType);
|
|
|
|
|
raw.put("temperature", getRawValue(latestRaw, "temperature"));
|
|
|
|
|
raw.put("humidity", getRawValue(latestRaw, "humidity"));
|
|
|
|
|
raw.put("illuminance", getRawValue(latestRaw, "illuminance"));
|
|
|
|
|
raw.put("noise", getRawValue(latestRaw, "noise"));
|
|
|
|
|
raw.put("concentration", getRawValue(latestRaw, "concentration"));
|
|
|
|
|
raw.put("vibrationSpeed", getRawValue(latestRaw, "vibrationSpeed"));
|
|
|
|
|
raw.put("vibrationDisplacement", getRawValue(latestRaw, "vibrationDisplacement"));
|
|
|
|
|
raw.put("vibrationAcceleration", getRawValue(latestRaw, "vibrationAcceleration"));
|
|
|
|
|
raw.put("vibrationTemp", getRawValue(latestRaw, "vibrationTemp"));
|
|
|
|
|
raw.put("collectTime", getRawValue(latestRaw, "collectTime"));
|
|
|
|
|
raw.put("recodeTime", getRawValue(latestRaw, "recodeTime"));
|
|
|
|
|
return raw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 读取原始 Map 值,兼容 latestRaw 为空的设备骨架场景。
|
|
|
|
|
*/
|
|
|
|
|
private Object getRawValue(Map<String, Object> raw, String key) {
|
|
|
|
|
return raw == null ? null : raw.get(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量裁剪:对列表中每条记录按各自的 monitor_type 裁剪字段。
|
|
|
|
|
*
|
|
|
|
|
* @param rawList 原始数据列表
|
|
|
|
|
* @param vibrationParam 振动参数选择(仅振动设备有效)
|
|
|
|
|
* @return 裁剪后的数据列表
|
|
|
|
|
*/
|
|
|
|
|
private List<Map<String, Object>> trimListByMonitorType(
|
|
|
|
|
List<Map<String, Object>> rawList, String vibrationParam
|
|
|
|
|
) {
|
|
|
|
|
if (rawList == null || rawList.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
List<Map<String, Object>> result = new ArrayList<>(rawList.size());
|
|
|
|
|
for (Map<String, Object> raw : rawList) {
|
|
|
|
|
result.add(trimFieldsByMonitorType(raw, vibrationParam));
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按设备类型裁剪返回字段的核心方法。
|
|
|
|
|
*
|
|
|
|
|
* <p>设计原则:
|
|
|
|
|
* 1. 先提取公共字段(设备标识 + 时间字段),所有设备类型都包含
|
|
|
|
|
* 2. 再根据 monitor_type 追加对应的测量字段
|
|
|
|
|
* 3. 不相关的测量字段不放入结果 Map,避免前端渲染时处理大量 null 值
|
|
|
|
|
* 4. 兜底逻辑:未识别类型返回全量字段,保证新设备类型接入后不丢数据</p>
|
|
|
|
|
*
|
|
|
|
|
* <p>纯净返回方案(默认):仅返回对应设备类型的测量字段。
|
|
|
|
|
* 若需要兼容旧前端(旧接口期望全量字段),可在此方法中补充 null 填充逻辑(兼容方案)。</p>
|
|
|
|
|
*
|
|
|
|
|
* @param raw 原始数据行(含所有字段)
|
|
|
|
|
* @param vibrationParam 振动参数选择(vibrationSpeed/vibrationDisplacement/vibrationAcceleration/vibrationTemp)
|
|
|
|
|
* @return 裁剪后只包含该设备类型有业务意义字段的 Map
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, Object> trimFieldsByMonitorType(
|
|
|
|
|
Map<String, Object> raw, String vibrationParam
|
|
|
|
|
) {
|
|
|
|
|
// 从原始数据中读取设备类型编码
|
|
|
|
|
Object monitorTypeObj = raw.get("monitorType");
|
|
|
|
|
Integer monitorType = (monitorTypeObj == null) ? null : ((Number) monitorTypeObj).intValue();
|
|
|
|
|
IotEnvMonitorTypeEnum typeEnum = IotEnvMonitorTypeEnum.fromCode(monitorType);
|
|
|
|
|
|
|
|
|
|
// 构建结果 Map,使用 LinkedHashMap 保持字段顺序(便于前端调试和日志可读性)
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
// ---- 公共标识字段:所有设备类型都必须返回 ----
|
|
|
|
|
result.put("objid", raw.get("objid"));
|
|
|
|
|
result.put("monitorId", raw.get("monitorId"));
|
|
|
|
|
result.put("monitorCode", raw.get("monitorCode"));
|
|
|
|
|
result.put("monitorName", raw.get("monitorName"));
|
|
|
|
|
result.put("monitorType", monitorType);
|
|
|
|
|
// energyName 由枚举根据 monitor_type 自动映射,不依赖数据库字段
|
|
|
|
|
result.put("energyName", typeEnum.getEnergyName());
|
|
|
|
|
|
|
|
|
|
// ---- 按设备类型追加对应测量字段(纯净返回方案)----
|
|
|
|
|
switch (typeEnum) {
|
|
|
|
|
case TEMPERATURE:
|
|
|
|
|
// 温度设备(type=5):只返回温度字段
|
|
|
|
|
result.put("temperature", raw.get("temperature"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TEMPERATURE_HUMIDITY:
|
|
|
|
|
// 温湿度设备(type=6):返回温度和湿度两个字段
|
|
|
|
|
result.put("temperature", raw.get("temperature"));
|
|
|
|
|
result.put("humidity", raw.get("humidity"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case NOISE:
|
|
|
|
|
// 噪声设备(type=7):只返回噪声字段
|
|
|
|
|
result.put("noise", raw.get("noise"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ILLUMINANCE:
|
|
|
|
|
// 照度设备(type=8):只返回照度字段
|
|
|
|
|
result.put("illuminance", raw.get("illuminance"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CONCENTRATION:
|
|
|
|
|
// 气体浓度设备(type=9):只返回浓度字段
|
|
|
|
|
result.put("concentration", raw.get("concentration"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case VIBRATION:
|
|
|
|
|
// 振动设备(type=10):若指定了 vibrationParam 则只返回该单一振动参数,
|
|
|
|
|
// 否则返回全部四个振动字段
|
|
|
|
|
if (!isBlank(vibrationParam)) {
|
|
|
|
|
appendSingleVibrationField(result, raw, vibrationParam);
|
|
|
|
|
} else {
|
|
|
|
|
result.put("vibrationSpeed", raw.get("vibrationSpeed"));
|
|
|
|
|
result.put("vibrationDisplacement", raw.get("vibrationDisplacement"));
|
|
|
|
|
result.put("vibrationAcceleration", raw.get("vibrationAcceleration"));
|
|
|
|
|
result.put("vibrationTemp", raw.get("vibrationTemp"));
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// 兜底:未识别类型返回全量测量字段,避免新设备类型上线后数据全部被过滤丢失。
|
|
|
|
|
// 此处统一记录到日志中,便于后续排查是否有新设备类型未在枚举中登记。
|
|
|
|
|
result.put("temperature", raw.get("temperature"));
|
|
|
|
|
result.put("humidity", raw.get("humidity"));
|
|
|
|
|
result.put("illuminance", raw.get("illuminance"));
|
|
|
|
|
result.put("noise", raw.get("noise"));
|
|
|
|
|
result.put("concentration", raw.get("concentration"));
|
|
|
|
|
result.put("vibrationSpeed", raw.get("vibrationSpeed"));
|
|
|
|
|
result.put("vibrationDisplacement", raw.get("vibrationDisplacement"));
|
|
|
|
|
result.put("vibrationAcceleration", raw.get("vibrationAcceleration"));
|
|
|
|
|
result.put("vibrationTemp", raw.get("vibrationTemp"));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- 公共时间字段:所有设备类型都必须返回 ----
|
|
|
|
|
result.put("collectTime", raw.get("collectTime"));
|
|
|
|
|
result.put("recodeTime", raw.get("recodeTime"));
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 仅追加振动设备指定的单一振动参数字段。
|
|
|
|
|
* 若 vibrationParam 不在已知列表中,则退化为返回全部振动字段(兜底保护)。
|
|
|
|
|
*
|
|
|
|
|
* @param result 待填充的结果 Map
|
|
|
|
|
* @param raw 原始数据行
|
|
|
|
|
* @param vibrationParam 振动参数名称
|
|
|
|
|
*/
|
|
|
|
|
private void appendSingleVibrationField(
|
|
|
|
|
Map<String, Object> result, Map<String, Object> raw, String vibrationParam
|
|
|
|
|
) {
|
|
|
|
|
switch (vibrationParam) {
|
|
|
|
|
case "vibrationSpeed":
|
|
|
|
|
result.put("vibrationSpeed", raw.get("vibrationSpeed"));
|
|
|
|
|
break;
|
|
|
|
|
case "vibrationDisplacement":
|
|
|
|
|
result.put("vibrationDisplacement", raw.get("vibrationDisplacement"));
|
|
|
|
|
break;
|
|
|
|
|
case "vibrationAcceleration":
|
|
|
|
|
result.put("vibrationAcceleration", raw.get("vibrationAcceleration"));
|
|
|
|
|
break;
|
|
|
|
|
case "vibrationTemp":
|
|
|
|
|
result.put("vibrationTemp", raw.get("vibrationTemp"));
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// 未知振动参数名时返回全部振动字段,避免前端拿到空 Map 报错
|
|
|
|
|
result.put("vibrationSpeed", raw.get("vibrationSpeed"));
|
|
|
|
|
result.put("vibrationDisplacement", raw.get("vibrationDisplacement"));
|
|
|
|
|
result.put("vibrationAcceleration", raw.get("vibrationAcceleration"));
|
|
|
|
|
result.put("vibrationTemp", raw.get("vibrationTemp"));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断字符串是否为空(null 或纯空白字符)
|
|
|
|
|
*
|
|
|
|
|
* @param str 待判断字符串
|
|
|
|
|
* @return true 表示空
|
|
|
|
|
*/
|
|
|
|
|
private boolean isBlank(String str) {
|
|
|
|
|
return str == null || str.trim().isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 去掉首尾空白后返回非空字符串;若为空则统一返回 null。
|
|
|
|
|
*/
|
|
|
|
|
private String trimToNull(String str) {
|
|
|
|
|
if (str == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
String trimmed = str.trim();
|
|
|
|
|
return trimmed.isEmpty() ? null : trimmed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* monitor 条件归一化结果。
|
|
|
|
|
*/
|
|
|
|
|
private static final class MonitorFilter {
|
|
|
|
|
private final String monitorId;
|
|
|
|
|
private final List<String> monitorIds;
|
|
|
|
|
|
|
|
|
|
private MonitorFilter(String monitorId, List<String> monitorIds) {
|
|
|
|
|
this.monitorId = monitorId;
|
|
|
|
|
this.monitorIds = monitorIds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 可选时间范围解析结果。
|
|
|
|
|
*/
|
|
|
|
|
private static final class TimeRange {
|
|
|
|
|
private final Date startTime;
|
|
|
|
|
private final Date endTime;
|
|
|
|
|
|
|
|
|
|
private TimeRange(Date startTime, Date endTime) {
|
|
|
|
|
this.startTime = startTime;
|
|
|
|
|
this.endTime = endTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|