feat(ems/report): 新增电线图接口

新增物联网环境监测数据按日分表查询功能,包含以下核心组件:
1. IotEnvMonitorQuery 查询参数封装对象
2. IotEnvMonitorTypeEnum 设备类型枚举
3. IIotEnvMonitorDataService 服务接口及实现
4. IotEnvMonitorDataMapper 数据访问层
5. IotEnvMonitorDataController REST接口

实现功能:
- 支持按日期路由到对应分表查询
- 按设备类型动态裁剪返回字段
- 提供批量查询设备最新数据接口
- 支持时间范围和多条件组合查询
- 新增振动设备参数选择功能
main
zch 3 months ago
parent 887324cb7d
commit 97f8958427

@ -0,0 +1,207 @@
package org.dromara.ems.record.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.ems.common.web.EmsBaseController;
import org.dromara.ems.record.domain.bo.IotEnvMonitorQuery;
import org.dromara.ems.record.service.IIotEnvMonitorDataService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* Controller
*
* <p> RecordIotenvInstantController
*
* 1. GET /latest
* 2. GET /list
* 3. POST /latestBatch monitorIds
* 4. GET /byType
* </p>
*
* <p> ems/record:recordIotenvInstant:list
* </p>
*
* @author zch
* @date 2026-04-09
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/ems/record/iotEnvMonitorData")
public class IotEnvMonitorDataController extends EmsBaseController {
private final IIotEnvMonitorDataService iotEnvMonitorDataService;
/**
*
*
* <p>
* 1. monitorIds
* 2. 退
* 3. monitor_type Map </p>
*
* @return
*/
@SaCheckPermission("ems/record:recordIotenvInstant:list")
@Log(title = "物联环境监测-所有设备最新值查询", businessType = BusinessType.OTHER)
@GetMapping("/latestAll")
public R<?> getLatestAllMonitorData() {
try {
List<Map<String, Object>> list = iotEnvMonitorDataService.queryLatestAllMonitorData();
return success(list);
} catch (ServiceException e) {
logger.warn("查询所有设备最新数据失败,原因:{}", e.getMessage());
return error(e.getMessage());
} catch (Exception e) {
logger.error("查询所有设备最新数据异常", e);
return error("查询失败,请稍后重试或联系管理员");
}
}
/**
*
*
* <p></p>
*
*
* GET /ems/record/iotEnvMonitorData/latest?date=2026-04-09&monitorId=E0001_2200
*
* type=5
* {
* "code": 200,
* "data": {
* "objid": 2042107207071436800,
* "monitorId": "E0001_2200",
* "monitorCode": "E0001_2200",
* "monitorName": "7.47.01",
* "monitorType": 5,
* "energyName": "温度",
* "temperature": 44.87,
* "collectTime": "2026-04-09 13:08:20",
* "recodeTime": "2026-04-09 13:07:53"
* }
* }
*
* @param query datemonitorId
* @return Map Map
*/
@SaCheckPermission("ems/record:recordIotenvInstant:list")
@Log(title = "物联环境监测-按日分表查询", businessType = BusinessType.OTHER)
@GetMapping("/latest")
public R<?> getLatestByMonitorIdAndDate(IotEnvMonitorQuery query) {
try {
Map<String, Object> data = iotEnvMonitorDataService.queryLatestByMonitorIdAndDate(query);
return success(data);
} catch (ServiceException e) {
// 业务异常(参数校验失败、日期格式错误等)直接返回给前端
logger.warn("查询单设备最新数据失败,参数:{},原因:{}", query, e.getMessage());
return error(e.getMessage());
} catch (Exception e) {
// 非预期异常统一脱敏避免将表名、SQL 信息暴露给前端
logger.error("查询单设备最新数据异常,参数:{}", query, e);
return error("查询失败,请稍后重试或联系管理员");
}
}
/**
*
*
* <p> monitorId / monitorType /
* monitor_type </p>
*
*
* GET /ems/record/iotEnvMonitorData/list?date=2026-04-09&monitorType=5
*
*
* GET /ems/record/iotEnvMonitorData/list?date=2026-04-09&startTime=2026-04-09 08:00:00&endTime=2026-04-09 18:00:00
*
* @param query date
* @return
*/
@SaCheckPermission("ems/record:recordIotenvInstant:list")
@Log(title = "物联环境监测-按日分表查询", businessType = BusinessType.OTHER)
@GetMapping("/list")
public R<?> getDataListByDate(IotEnvMonitorQuery query) {
try {
List<Map<String, Object>> list = iotEnvMonitorDataService.queryDataListByDate(query);
return success(list);
} catch (ServiceException e) {
logger.warn("查询日期数据列表失败,参数:{},原因:{}", query, e.getMessage());
return error(e.getMessage());
} catch (Exception e) {
logger.error("查询日期数据列表异常,参数:{}", query, e);
return error("查询失败,请稍后重试或联系管理员");
}
}
/**
*
*
* <p>使 POST monitorIds GET URL
* </p>
*
*
* POST /ems/record/iotEnvMonitorData/latestBatch
* Content-Type: application/json
* Body:
* {
* "date": "2026-04-09",
* "monitorIds": ["E0001_2200", "E0001_1900", "E0001_1400"]
* }
*
* @param query datemonitorIds RequestBody
* @return
*/
@SaCheckPermission("ems/record:recordIotenvInstant:list")
@Log(title = "物联环境监测-按日分表批量查询", businessType = BusinessType.OTHER)
@PostMapping("/latestBatch")
public R<?> getLatestBatchByDate(@RequestBody IotEnvMonitorQuery query) {
try {
List<Map<String, Object>> list = iotEnvMonitorDataService.queryLatestByMonitorIdsAndDate(query);
return success(list);
} catch (ServiceException e) {
logger.warn("批量查询设备最新数据失败,原因:{}", e.getMessage());
return error(e.getMessage());
} catch (Exception e) {
logger.error("批量查询设备最新数据异常", e);
return error("查询失败,请稍后重试或联系管理员");
}
}
/**
*
*
* <p>线
* monitorType </p>
*
*
* GET /ems/record/iotEnvMonitorData/byType?date=2026-04-09&monitorType=10
*
*
* GET /ems/record/iotEnvMonitorData/byType?date=2026-04-09&monitorType=10&vibrationParam=vibrationSpeed
*
* @param query datemonitorType
* @return
*/
@SaCheckPermission("ems/record:recordIotenvInstant:list")
@Log(title = "物联环境监测-按类型按日分表查询", businessType = BusinessType.OTHER)
@GetMapping("/byType")
public R<?> getDataListByMonitorType(IotEnvMonitorQuery query) {
try {
List<Map<String, Object>> list = iotEnvMonitorDataService.queryDataListByMonitorType(query);
return success(list);
} catch (ServiceException e) {
logger.warn("按设备类型查询数据失败,原因:{}", e.getMessage());
return error(e.getMessage());
} catch (Exception e) {
logger.error("按设备类型查询数据异常", e);
return error("查询失败,请稍后重试或联系管理员");
}
}
}

@ -0,0 +1,72 @@
package org.dromara.ems.record.domain.bo;
import lombok.Data;
import java.util.List;
/**
*
*
* <p> RecordIotenvInstant
* "按日分表查询"</p>
*
* @author zch
* @date 2026-04-09
*/
@Data
public class IotEnvMonitorQuery {
/**
*
* yyyy-MM-dd yyyyMMdd
* record_iotenv_instant_yyyyMMdd
*
*/
private String date;
/**
*
* ems_base_monitor_info.monitor_code / record_iotenv_instant.monitorId
* monitorIds monitorIds monitorId
*/
private String monitorId;
/**
*
* ems_base_monitor_info.monitor_code
* monitorIds monitorId
* 使 POST URL
*/
private List<String> monitorIds;
/**
*
* ems_base_monitor_info.monitor_type
* 5= / 6=湿 / 7= / 8= / 9= / 10=
*
*/
private Integer monitorType;
/**
*
* yyyy-MM-dd HH:mm:ss
* recodeTime
* 使 selectRecordIotenvInstantList
*/
private String startTime;
/**
*
* yyyy-MM-dd HH:mm:ss
* startTime 使
*/
private String endTime;
/**
* type=10
* vibrationSpeed / vibrationDisplacement / vibrationAcceleration / vibrationTemp
*
* Service
*/
private String vibrationParam;
}

@ -0,0 +1,87 @@
package org.dromara.ems.record.enums;
/**
*
* energyName
*
* ems_base_monitor_info.monitor_type
*
* @author zch
* @date 2026-04-09
*/
public enum IotEnvMonitorTypeEnum {
/** 温度设备(只采集 temperature */
TEMPERATURE(5, "温度"),
/** 温湿度设备(采集 temperature + humidity */
TEMPERATURE_HUMIDITY(6, "温湿度"),
/** 噪声设备(只采集 noise */
NOISE(7, "噪声"),
/** 照度设备(只采集 illuminance */
ILLUMINANCE(8, "照度"),
/** 气体浓度设备(只采集 concentration */
CONCENTRATION(9, "气体浓度"),
/** 振动设备(采集 vibration_speed / vibration_displacement / vibration_acceleration / vibration_temp */
VIBRATION(10, "振动"),
/**
*
* Service 线
*/
UNKNOWN(-1, "未知");
/** 设备类型编码(对应 ems_base_monitor_info.monitor_type */
private final int code;
/** 设备类型中文名称(用于接口返回 energyName 字段) */
private final String energyName;
IotEnvMonitorTypeEnum(int code, String energyName) {
this.code = code;
this.energyName = energyName;
}
public int getCode() {
return code;
}
public String getEnergyName() {
return energyName;
}
/**
*
* null UNKNOWN
* NPE
*
* @param code ems_base_monitor_info.monitor_type
* @return UNKNOWN
*/
public static IotEnvMonitorTypeEnum fromCode(Integer code) {
if (code == null) {
return UNKNOWN;
}
for (IotEnvMonitorTypeEnum item : values()) {
if (item.code == code) {
return item;
}
}
// 未识别类型返回兜底枚举,业务层按 UNKNOWN 处理
return UNKNOWN;
}
/**
*
*
* @param code
* @return "未知"
*/
public static String getEnergyNameByCode(Integer code) {
return fromCode(code).getEnergyName();
}
}

@ -0,0 +1,102 @@
package org.dromara.ems.record.mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* Mapper
*
* <p> RecordIotenvInstantMapper
* 使 Map Service monitor_type </p>
*
* <p>
* tableName Service
* record_iotenv_instant_yyyyMMdd</p>
*
* @author zch
* @date 2026-04-09
*/
public interface IotEnvMonitorDataMapper {
/**
* SQL Server INFORMATION_SCHEMA
*
* <p>
* SQL SQL </p>
*
* @param tableName record_iotenv_instant_yyyyMMdd
* @return 1 0
*/
Integer checkDailyTableExists(@Param("tableName") String tableName);
/**
* ems_base_monitor_info
*
* <p>Service monitor_type
* SQL CASE WHEN </p>
*
* @param tableName record_iotenv_instant_yyyyMMdd
* @param monitorId null
* @param monitorIds
* @param monitorType null
* @param startTime Service null
* @param endTime Service null
* @return Map
*/
List<Map<String, Object>> selectRawDataByDate(
@Param("tableName") String tableName,
@Param("monitorId") String monitorId,
@Param("monitorIds") List<String> monitorIds,
@Param("monitorType") Integer monitorType,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime
);
/**
* TOP 1 recodeTime DESC, objid DESC
*
* <p></p>
*
* @param tableName
* @param monitorId monitorId
* @return Map null
*/
Map<String, Object> selectLatestRawByMonitorId(
@Param("tableName") String tableName,
@Param("monitorId") String monitorId
);
/**
*
*
* <p>使 ROW_NUMBER() OVER (PARTITION BY monitorId ORDER BY recodeTime DESC, objid DESC)
* SQL N+1 </p>
*
* @param tableName
* @param monitorIds Service
* @return Map monitorId
*/
List<Map<String, Object>> selectLatestRawByMonitorIds(
@Param("tableName") String tableName,
@Param("monitorIds") List<String> monitorIds
);
/**
*
*
* <p>
* Service monitorIds
* recodeTime 退</p>
*
* @param tableNames -> 退
* @param monitorIds Service
* @return Map Service
*/
List<Map<String, Object>> selectLatestRawByMonitorIdsFromTables(
@Param("tableNames") List<String> tableNames,
@Param("monitorIds") List<String> monitorIds
);
}

@ -0,0 +1,76 @@
package org.dromara.ems.record.service;
import org.dromara.ems.record.domain.bo.IotEnvMonitorQuery;
import java.util.List;
import java.util.Map;
/**
* Service
*
* <p> IRecordIotenvInstantService
* Map Service monitor_type
* </p>
*
* @author zch
* @date 2026-04-09
*/
public interface IIotEnvMonitorDataService {
/**
*
*
* <p> monitorIds
* RecordIotenvInstantServiceImpl 退
* monitor_type </p>
*
* @return
*/
List<Map<String, Object>> queryLatestAllMonitorData();
/**
*
*
* <p> date record_iotenv_instant_yyyyMMdd
* recodeTime monitor_type </p>
*
* @param query datemonitorId
* @return Map Map
*/
Map<String, Object> queryLatestByMonitorIdAndDate(IotEnvMonitorQuery query);
/**
*
*
* <p> monitorId / monitorIds / monitorType /
* monitor_type </p>
*
* <p>date
* </p>
*
* @param query date
* @return
*/
List<Map<String, Object>> queryDataListByDate(IotEnvMonitorQuery query);
/**
*
*
* <p>使 ROW_NUMBER() SQL
* N+1 monitor_type </p>
*
* @param query datemonitorIds
* @return
*/
List<Map<String, Object>> queryLatestByMonitorIdsAndDate(IotEnvMonitorQuery query);
/**
*
*
* <p> monitorType </p>
*
* @param query datemonitorType
* @return
*/
List<Map<String, Object>> queryDataListByMonitorType(IotEnvMonitorQuery query);
}

@ -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-0120261301 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:002026/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;
}
}
}

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.ems.record.mapper.IotEnvMonitorDataMapper">
<!--
检查指定分表是否存在于 SQL Server 数据库。
使用 INFORMATION_SCHEMA.TABLES 做兼容查询,比 sys.objects 可读性更好。
tableName 由 Service 层经过正则白名单校验后以 #{} 预编译参数传入,防止 SQL 注入。
-->
<select id="checkDailyTableExists" resultType="java.lang.Integer">
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = #{tableName}
</select>
<!--
查询指定日分表的原始数据列表,关联 ems_base_monitor_info 获取设备基础信息。
动态表名说明:
${tableName} 使用字符串替换注入表名,这是 MyBatis 动态表名的标准做法。
tableName 在 Service 层已经过严格校验:
1. 日期格式校验yyyy-MM-dd 或 yyyyMMdd
2. 白名单正则校验(^record_iotenv_instant_\d{8}$
3. 不允许任何外部原始字符串直接传入
因此此处 ${tableName} 的注入风险已在 Service 层消除。
返回全量字段Service 层根据 monitor_type 动态裁剪返回给前端的字段集合,
此方案比在 SQL 中用 CASE WHEN 裁剪更易维护,且便于后续按需扩展。
-->
<select id="selectRawDataByDate" resultType="java.util.LinkedHashMap">
SELECT
t.objid,
t.monitorId,
t.temperature,
t.humidity,
t.illuminance,
t.noise,
t.concentration,
t.vibration_speed AS vibrationSpeed,
t.vibration_displacement AS vibrationDisplacement,
t.vibration_acceleration AS vibrationAcceleration,
t.vibration_temp AS vibrationTemp,
t.collectTime,
t.recodeTime,
COALESCE(ebmi.monitor_code, t.monitorId) AS monitorCode,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ebmi.monitor_type AS monitorType
FROM ${tableName} t
LEFT JOIN ems_base_monitor_info ebmi ON t.monitorId = ebmi.monitor_code
<where>
<!-- monitorIds 优先级高于 monitorId避免两个条件叠加后把结果误过滤为空 -->
<choose>
<when test="monitorIds != null and monitorIds.size() > 0">
AND t.monitorId IN
<foreach collection="monitorIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
<when test="monitorId != null and monitorId != ''">
AND t.monitorId = #{monitorId}
</when>
</choose>
<!-- 按设备类型过滤(关联设备基础信息表的 monitor_type 字段) -->
<if test="monitorType != null">
AND ebmi.monitor_type = #{monitorType}
</if>
<!-- 按记录时间起始过滤(单日表内时间范围查询,不支持跨天) -->
<if test="startTime != null">
AND t.recodeTime &gt;= #{startTime}
</if>
<!-- 按记录时间结束过滤 -->
<if test="endTime != null">
AND t.recodeTime &lt;= #{endTime}
</if>
</where>
ORDER BY t.recodeTime DESC, t.objid DESC
</select>
<!--
查询单个设备在指定日分表中的最新一条数据TOP 1
适用于实时监控、设备详情页展示单设备当前最新状态的场景。
动态表名安全处理同上(由 Service 层校验后注入)。
-->
<select id="selectLatestRawByMonitorId" resultType="java.util.LinkedHashMap">
SELECT TOP (1)
t.objid,
t.monitorId,
t.temperature,
t.humidity,
t.illuminance,
t.noise,
t.concentration,
t.vibration_speed AS vibrationSpeed,
t.vibration_displacement AS vibrationDisplacement,
t.vibration_acceleration AS vibrationAcceleration,
t.vibration_temp AS vibrationTemp,
t.collectTime,
t.recodeTime,
COALESCE(ebmi.monitor_code, t.monitorId) AS monitorCode,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ebmi.monitor_type AS monitorType
FROM ${tableName} t
LEFT JOIN ems_base_monitor_info ebmi ON t.monitorId = ebmi.monitor_code
WHERE t.monitorId = #{monitorId}
ORDER BY t.recodeTime DESC, t.objid DESC
</select>
<!--
批量查询多个设备在指定日分表中各自的最新一条数据。
实现原理:
使用 ROW_NUMBER() OVER (PARTITION BY t.monitorId ORDER BY t.recodeTime DESC, t.objid DESC)
在一条 SQL 中完成"每个设备取最新行"的逻辑比对每个设备单独发起查询N+1性能更优。
外层过滤 rn = 1 即取每个设备的最新一条。
动态表名安全处理同上(由 Service 层校验后注入)。
-->
<select id="selectLatestRawByMonitorIds" resultType="java.util.LinkedHashMap">
SELECT
objid,
monitorId,
temperature,
humidity,
illuminance,
noise,
concentration,
vibrationSpeed,
vibrationDisplacement,
vibrationAcceleration,
vibrationTemp,
collectTime,
recodeTime,
monitorCode,
monitorName,
monitorType
FROM (
SELECT
t.objid,
t.monitorId,
t.temperature,
t.humidity,
t.illuminance,
t.noise,
t.concentration,
t.vibration_speed AS vibrationSpeed,
t.vibration_displacement AS vibrationDisplacement,
t.vibration_acceleration AS vibrationAcceleration,
t.vibration_temp AS vibrationTemp,
t.collectTime,
t.recodeTime,
COALESCE(ebmi.monitor_code, t.monitorId) AS monitorCode,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ebmi.monitor_type AS monitorType,
<!-- ROW_NUMBER 分区排序,每个 monitorId 独立取最新一行 -->
ROW_NUMBER() OVER (
PARTITION BY t.monitorId
ORDER BY t.recodeTime DESC, t.objid DESC
) AS rn
FROM ${tableName} t
LEFT JOIN ems_base_monitor_info ebmi ON t.monitorId = ebmi.monitor_code
WHERE t.monitorId IN
<foreach collection="monitorIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
) ranked
WHERE ranked.rn = 1
ORDER BY monitorId ASC
</select>
<!--
从多个最近可用分表中批量查询多个设备各自最新一条数据。
设计意图:
1. 复用现有“最近可用分表回退”思路,让无参接口不需要前端传日期也能拿到最新数据。
2. 仍然坚持由 Service 层先收集 monitorIds再用 IN 做白名单过滤,
避免直接把整张分表全量扫一遍给前端。
3. 使用 ROW_NUMBER() 做跨表分组取最新,保证“同一设备跨天时只取最新一条”。
-->
<select id="selectLatestRawByMonitorIdsFromTables" resultType="java.util.LinkedHashMap">
WITH all_data AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT
t.objid,
t.monitorId,
t.temperature,
t.humidity,
t.illuminance,
t.noise,
t.concentration,
t.vibration_speed AS vibrationSpeed,
t.vibration_displacement AS vibrationDisplacement,
t.vibration_acceleration AS vibrationAcceleration,
t.vibration_temp AS vibrationTemp,
t.collectTime,
t.recodeTime,
COALESCE(ebmi.monitor_code, t.monitorId) AS monitorCode,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ebmi.monitor_type AS monitorType
FROM ${tableName} t
LEFT JOIN ems_base_monitor_info ebmi ON t.monitorId = ebmi.monitor_code
WHERE t.monitorId IN
<foreach collection="monitorIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</foreach>
)
SELECT
objid,
monitorId,
temperature,
humidity,
illuminance,
noise,
concentration,
vibrationSpeed,
vibrationDisplacement,
vibrationAcceleration,
vibrationTemp,
collectTime,
recodeTime,
monitorCode,
monitorName,
monitorType
FROM (
SELECT
all_data.*,
ROW_NUMBER() OVER (
PARTITION BY all_data.monitorId
ORDER BY all_data.recodeTime DESC, all_data.objid DESC
) AS rn
FROM all_data
) ranked
WHERE ranked.rn = 1
</select>
</mapper>
Loading…
Cancel
Save