From 97f89584272172a9766f572dcaad19a486e809c4 Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 9 Apr 2026 15:41:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(ems/report):=20=E6=96=B0=E5=A2=9E=E7=94=B5?= =?UTF-8?q?=E7=BA=BF=E5=9B=BE=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增物联网环境监测数据按日分表查询功能,包含以下核心组件: 1. IotEnvMonitorQuery 查询参数封装对象 2. IotEnvMonitorTypeEnum 设备类型枚举 3. IIotEnvMonitorDataService 服务接口及实现 4. IotEnvMonitorDataMapper 数据访问层 5. IotEnvMonitorDataController REST接口 实现功能: - 支持按日期路由到对应分表查询 - 按设备类型动态裁剪返回字段 - 提供批量查询设备最新数据接口 - 支持时间范围和多条件组合查询 - 新增振动设备参数选择功能 --- .../IotEnvMonitorDataController.java | 207 ++++++ .../record/domain/bo/IotEnvMonitorQuery.java | 72 ++ .../record/enums/IotEnvMonitorTypeEnum.java | 87 +++ .../mapper/IotEnvMonitorDataMapper.java | 102 +++ .../service/IIotEnvMonitorDataService.java | 76 ++ .../impl/IotEnvMonitorDataServiceImpl.java | 673 ++++++++++++++++++ .../ems/record/IotEnvMonitorDataMapper.xml | 238 +++++++ 7 files changed, 1455 insertions(+) create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/controller/IotEnvMonitorDataController.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/IotEnvMonitorQuery.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/enums/IotEnvMonitorTypeEnum.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/mapper/IotEnvMonitorDataMapper.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/service/IIotEnvMonitorDataService.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/service/impl/IotEnvMonitorDataServiceImpl.java create mode 100644 ruoyi-ems/src/main/resources/mapper/ems/record/IotEnvMonitorDataMapper.xml diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/controller/IotEnvMonitorDataController.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/controller/IotEnvMonitorDataController.java new file mode 100644 index 0000000..1ed3108 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/controller/IotEnvMonitorDataController.java @@ -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 + * + *

独立新增,不修改已有 RecordIotenvInstantController。 + * 提供以下接口能力: + * 1. 查询指定日期指定设备最新一条数据(GET /latest) + * 2. 查询指定日期设备数据列表,支持多条件过滤(GET /list) + * 3. 批量查询指定日期多设备最新数据(POST /latestBatch,支持较大 monitorIds 列表) + * 4. 按设备类型查询指定日期数据(GET /byType) + *

+ * + *

接口权限复用已有权限标识 ems/record:recordIotenvInstant:list, + * 待根据项目实际权限体系调整为独立权限标识。

+ * + * @author zch + * @date 2026-04-09 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/ems/record/iotEnvMonitorData") +public class IotEnvMonitorDataController extends EmsBaseController { + + private final IIotEnvMonitorDataService iotEnvMonitorDataService; + + /** + * 无参查询所有设备各自的最新一条监测数据。 + * + *

前端无需传任何参数,后端会自动: + * 1. 从设备主数据表收集全部 monitorIds + * 2. 回退到最近存在的日分表中取每个设备最新一条 + * 3. 按 monitor_type 裁剪字段后返回 Map 列表

+ * + * @return 所有设备最新值列表;若暂无有效分表则返回设备骨架列表 + */ + @SaCheckPermission("ems/record:recordIotenvInstant:list") + @Log(title = "物联环境监测-所有设备最新值查询", businessType = BusinessType.OTHER) + @GetMapping("/latestAll") + public R getLatestAllMonitorData() { + try { + List> 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("查询失败,请稍后重试或联系管理员"); + } + } + + /** + * 查询指定日期指定设备的最新一条监测数据。 + * + *

适用于设备详情页、实时监控面板展示单设备当前最新状态的场景。

+ * + * 请求示例: + * 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 查询参数(必传字段:date、monitorId) + * @return 裁剪后的设备数据 Map,分表不存在或无数据时返回空 Map + */ + @SaCheckPermission("ems/record:recordIotenvInstant:list") + @Log(title = "物联环境监测-按日分表查询", businessType = BusinessType.OTHER) + @GetMapping("/latest") + public R getLatestByMonitorIdAndDate(IotEnvMonitorQuery query) { + try { + Map 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("查询失败,请稍后重试或联系管理员"); + } + } + + /** + * 查询指定日期的监测数据列表。 + * + *

支持按 monitorId / monitorType / 时间范围等条件组合查询, + * 每条记录根据各自的 monitor_type 动态裁剪返回字段。

+ * + * 请求示例(按设备类型查询): + * 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> 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("查询失败,请稍后重试或联系管理员"); + } + } + + /** + * 批量查询指定日期多个设备各自的最新一条监测数据。 + * + *

使用 POST 方式以支持较大的 monitorIds 列表,避免 GET 请求 URL 超长限制。 + * 适用于看板页需要同时展示多个设备最新状态的场景。

+ * + * 请求示例: + * POST /ems/record/iotEnvMonitorData/latestBatch + * Content-Type: application/json + * Body: + * { + * "date": "2026-04-09", + * "monitorIds": ["E0001_2200", "E0001_1900", "E0001_1400"] + * } + * + * @param query 查询参数(必传字段:date、monitorIds,通过 RequestBody 传入) + * @return 每个设备最新一条裁剪后的数据列表 + */ + @SaCheckPermission("ems/record:recordIotenvInstant:list") + @Log(title = "物联环境监测-按日分表批量查询", businessType = BusinessType.OTHER) + @PostMapping("/latestBatch") + public R getLatestBatchByDate(@RequestBody IotEnvMonitorQuery query) { + try { + List> 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("查询失败,请稍后重试或联系管理员"); + } + } + + /** + * 按设备类型查询指定日期的监测数据列表。 + * + *

适用于历史曲线页面按设备类型筛选展示数据的场景, + * 返回字段集合完全由 monitorType 决定(纯净返回,无无关字段)。

+ * + * 请求示例(查询振动设备数据): + * 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 查询参数(必传字段:date、monitorType) + * @return 该类型设备的数据列表,分表不存在时返回空列表 + */ + @SaCheckPermission("ems/record:recordIotenvInstant:list") + @Log(title = "物联环境监测-按类型按日分表查询", businessType = BusinessType.OTHER) + @GetMapping("/byType") + public R getDataListByMonitorType(IotEnvMonitorQuery query) { + try { + List> 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("查询失败,请稍后重试或联系管理员"); + } + } +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/IotEnvMonitorQuery.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/IotEnvMonitorQuery.java new file mode 100644 index 0000000..1626e1b --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/IotEnvMonitorQuery.java @@ -0,0 +1,72 @@ +package org.dromara.ems.record.domain.bo; + +import lombok.Data; + +import java.util.List; + +/** + * 物联网环境监测数据查询参数封装对象(按日分表查询专用) + * + *

该对象作为新增独立接口的入参,不依赖原 RecordIotenvInstant 实体, + * 职责单一,仅用于"按日分表查询"场景,避免与已有查询逻辑产生耦合。

+ * + * @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 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; +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/enums/IotEnvMonitorTypeEnum.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/enums/IotEnvMonitorTypeEnum.java new file mode 100644 index 0000000..42f3c66 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/enums/IotEnvMonitorTypeEnum.java @@ -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(); + } +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/mapper/IotEnvMonitorDataMapper.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/mapper/IotEnvMonitorDataMapper.java new file mode 100644 index 0000000..be5f06a --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/mapper/IotEnvMonitorDataMapper.java @@ -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 接口 + * + *

独立新增,不修改已有 RecordIotenvInstantMapper。 + * 所有方法均使用 Map 作为返回类型,由 Service 层根据 monitor_type 动态裁剪字段后再返回前端。

+ * + *

动态表名安全说明: + * tableName 参数在 Service 层经过严格的日期格式解析和白名单正则校验后才传入, + * 格式固定为 record_iotenv_instant_yyyyMMdd,拒绝任何外部原始字符串直接注入。

+ * + * @author zch + * @date 2026-04-09 + */ +public interface IotEnvMonitorDataMapper { + + /** + * 检查指定日分表是否存在于数据库(SQL Server INFORMATION_SCHEMA 查询) + * + *

查询前必须先做此判断,分表不存在时直接返回空集合, + * 避免动态 SQL 执行时因表名不存在而抛出 SQL 异常。

+ * + * @param tableName 已经白名单校验的分表名(格式:record_iotenv_instant_yyyyMMdd) + * @return 存在则返回 1,不存在返回 0 + */ + Integer checkDailyTableExists(@Param("tableName") String tableName); + + /** + * 查询指定日分表中的原始数据列表,关联 ems_base_monitor_info 获取设备信息。 + * + *

返回全量字段原始数据,Service 层根据每行的 monitor_type 按需裁剪字段, + * 此方式比在 SQL 中做 CASE WHEN 裁剪更稳妥、更易维护。

+ * + * @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> selectRawDataByDate( + @Param("tableName") String tableName, + @Param("monitorId") String monitorId, + @Param("monitorIds") List monitorIds, + @Param("monitorType") Integer monitorType, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime + ); + + /** + * 查询指定日分表中单个设备的最新一条原始数据(TOP 1,按 recodeTime DESC, objid DESC)。 + * + *

适用于实时监控面板展示单设备最新状态的场景。

+ * + * @param tableName 已校验合法的分表名 + * @param monitorId 设备编号(精确匹配 monitorId 字段) + * @return 该设备最新一条数据 Map;分表中无该设备数据时返回 null + */ + Map selectLatestRawByMonitorId( + @Param("tableName") String tableName, + @Param("monitorId") String monitorId + ); + + /** + * 批量查询指定日分表中多个设备各自的最新一条原始数据。 + * + *

使用 ROW_NUMBER() OVER (PARTITION BY monitorId ORDER BY recodeTime DESC, objid DESC) + * 在一条 SQL 中完成各设备取最新行的操作,避免对每个设备单独发起查询造成 N+1 问题。

+ * + * @param tableName 已校验合法的分表名 + * @param monitorIds 设备编号列表(至少需要包含一个元素,空列表由 Service 层拦截) + * @return 每个设备最新一条数据 Map 列表,按 monitorId 升序排列 + */ + List> selectLatestRawByMonitorIds( + @Param("tableName") String tableName, + @Param("monitorIds") List monitorIds + ); + + /** + * 从多个最近可用分表中批量查询多个设备各自的最新一条原始数据。 + * + *

该方法主要服务于“无参获取所有设备最新值”的新接口: + * 先由 Service 层收集设备主数据里的 monitorIds,再在最近存在的分表中做跨表聚合, + * 最终为每个设备取 recodeTime 最新的一条,避免前端自己感知回退到哪张分表。

+ * + * @param tableNames 已校验合法的分表名列表,顺序通常为“当天 -> 往前回退” + * @param monitorIds 设备编号列表,由 Service 层从设备主数据表收集并去重 + * @return 命中最新数据的原始 Map 列表;未命中的设备由 Service 层补设备骨架 + */ + List> selectLatestRawByMonitorIdsFromTables( + @Param("tableNames") List tableNames, + @Param("monitorIds") List monitorIds + ); +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/service/IIotEnvMonitorDataService.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/service/IIotEnvMonitorDataService.java new file mode 100644 index 0000000..8ba7ada --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/service/IIotEnvMonitorDataService.java @@ -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 接口 + * + *

独立新增,不修改已有 IRecordIotenvInstantService 及其实现。 + * 所有方法返回 Map 类型,Service 层负责根据 monitor_type 动态裁剪字段, + * 避免不同设备类型的无关字段出现在接口响应中。

+ * + * @author zch + * @date 2026-04-09 + */ +public interface IIotEnvMonitorDataService { + + /** + * 无参查询所有设备各自最新一条监测数据。 + * + *

接口内部会先从设备主数据表中收集 monitorIds,再参考现有 + * RecordIotenvInstantServiceImpl 的“最近可用分表回退”策略, + * 从最近存在的日分表中为每个设备取最新一条数据,并按 monitor_type 裁剪字段。

+ * + * @return 所有设备最新一条裁剪后的数据列表;若暂无有效分表则返回设备骨架列表 + */ + List> queryLatestAllMonitorData(); + + /** + * 查询指定日期指定设备的最新一条监测数据。 + * + *

根据 date 参数自动路由到对应日分表(record_iotenv_instant_yyyyMMdd), + * 取该设备 recodeTime 最新的一条记录,并按 monitor_type 裁剪返回字段。

+ * + * @param query 查询参数(必传字段:date、monitorId) + * @return 裁剪后的设备数据 Map;分表不存在或该设备无数据时返回空 Map + */ + Map queryLatestByMonitorIdAndDate(IotEnvMonitorQuery query); + + /** + * 查询指定日期的监测数据列表。 + * + *

支持按 monitorId / monitorIds / monitorType / 时间范围等条件组合过滤, + * 每条记录按各自的 monitor_type 独立裁剪返回字段。

+ * + *

第一阶段只支持单天查询(date 对应一张分表); + * 跨天聚合查询可在第二阶段通过多次调用或扩展新接口实现。

+ * + * @param query 查询参数(必传字段:date,其余条件选传) + * @return 裁剪后的数据列表;分表不存在时返回空列表 + */ + List> queryDataListByDate(IotEnvMonitorQuery query); + + /** + * 批量查询指定日期多个设备各自的最新一条监测数据。 + * + *

使用 ROW_NUMBER() 分区取最新行,一次 SQL 查询完成多个设备的最新数据获取, + * 避免 N+1 查询问题。每条记录按各自 monitor_type 裁剪返回字段。

+ * + * @param query 查询参数(必传字段:date、monitorIds) + * @return 每个设备最新一条裁剪后的数据列表;分表不存在时返回空列表 + */ + List> queryLatestByMonitorIdsAndDate(IotEnvMonitorQuery query); + + /** + * 按设备类型查询指定日期的监测数据列表。 + * + *

仅返回该设备类型对应的字段,返回结果字段集合由 monitorType 决定。

+ * + * @param query 查询参数(必传字段:date、monitorType) + * @return 该类型设备的数据列表;分表不存在时返回空列表 + */ + List> queryDataListByMonitorType(IotEnvMonitorQuery query); +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/service/impl/IotEnvMonitorDataServiceImpl.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/service/impl/IotEnvMonitorDataServiceImpl.java new file mode 100644 index 0000000..67ba004 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/service/impl/IotEnvMonitorDataServiceImpl.java @@ -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 实现 + * + *

核心职责: + * 1. 校验查询参数(必填项、格式合法性) + * 2. 将前端传入的日期字符串解析为 LocalDate,再通过 RecordIotenvPartitionService 生成白名单分表名 + * 3. 判断目标分表是否存在(不存在时优雅返回空集合,不报 SQL 异常) + * 4. 调用 Mapper 查询原始数据(全量字段) + * 5. 根据每条记录的 monitor_type 裁剪返回字段,补充 energyName 中文名称 + * 6. 组装并返回 Map 类型结果 + *

+ * + * @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> queryLatestAllMonitorData() { + // 先从设备主数据表收集 monitorIds,保证“全设备最新值”与现有首页口径一致 + List baseMonitorInfos = listAllMonitorInfos(); + if (baseMonitorInfos.isEmpty()) { + return Collections.emptyList(); + } + + List monitorIds = extractMonitorIds(baseMonitorInfos); + if (monitorIds.isEmpty()) { + return Collections.emptyList(); + } + + // 参考现有实时看板做法:优先当天,若当天无表则向前回退到最近存在分表 + List tableNames = recordIotenvPartitionService.resolveLatestAvailableTables(LATEST_TABLE_LOOKBACK_DAYS); + if (tableNames.isEmpty()) { + // 没有有效分表时仍返回设备骨架,避免前端因为空集合导致整页结构断层 + return buildLatestSnapshot(baseMonitorInfos, Collections.emptyMap()); + } + + List> rawList = iotEnvMonitorDataMapper.selectLatestRawByMonitorIdsFromTables( + tableNames, monitorIds + ); + return buildLatestSnapshot(baseMonitorInfos, indexRawDataByMonitorId(rawList)); + } + + /** + * 查询指定日期指定设备的最新一条监测数据 + */ + @Override + public Map 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 rawData = iotEnvMonitorDataMapper.selectLatestRawByMonitorId( + tableName, monitorId + ); + if (rawData == null || rawData.isEmpty()) { + return Collections.emptyMap(); + } + + // 按设备类型裁剪字段并补充 energyName + return trimFieldsByMonitorType(rawData, query.getVibrationParam()); + } + + /** + * 查询指定日期的监测数据列表(支持多条件过滤) + */ + @Override + public List> 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> rawList = iotEnvMonitorDataMapper.selectRawDataByDate( + tableName, + monitorFilter.monitorId, + monitorFilter.monitorIds, + query.getMonitorType(), + timeRange.startTime, + timeRange.endTime + ); + + // 对每条记录按设备类型裁剪字段 + return trimListByMonitorType(rawList, query.getVibrationParam()); + } + + /** + * 批量查询指定日期多个设备各自最新一条监测数据 + */ + @Override + public List> queryLatestByMonitorIdsAndDate(IotEnvMonitorQuery query) { + validateDateRequired(query); + List monitorIds = normalizeMonitorIds(query.getMonitorIds()); + if (monitorIds.isEmpty()) { + throw new ServiceException("设备编号列表 monitorIds 不能为空"); + } + + String tableName = buildAndValidateTableName(query.getDate()); + if (!isDailyTableExists(tableName)) { + return Collections.emptyList(); + } + + List> rawList = iotEnvMonitorDataMapper.selectLatestRawByMonitorIds( + tableName, monitorIds + ); + return trimListByMonitorType(rawList, query.getVibrationParam()); + } + + /** + * 按设备类型查询指定日期的监测数据列表 + */ + @Override + public List> 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> 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 生成经白名单校验的分表名。 + * + *

安全说明: + * 分表名永远由服务端根据合法日期自动生成,绝不允许前端原始字符串直接拼入 SQL。 + * RecordIotenvPartitionService.buildTableName() 内部有正则白名单双重校验 + * (格式:^record_iotenv_instant_\d{8}$),任何不符合格式的输入都会抛出 ServiceException。

+ * + * @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 条件。 + * + *

评审要求明确了“以 monitorIds 列表为主”,因此当列表非空时会主动忽略 monitorId, + * 避免出现 monitorId = ? AND monitorId IN (?) 叠加后误过滤为空的情况。

+ */ + private MonitorFilter resolveMonitorFilter(IotEnvMonitorQuery query) { + List monitorIds = normalizeMonitorIds(query.getMonitorIds()); + if (!monitorIds.isEmpty()) { + return new MonitorFilter(null, monitorIds); + } + return new MonitorFilter(trimToNull(query.getMonitorId()), Collections.emptyList()); + } + + /** + * 规范化 monitorIds:去空白、去重、保序。 + * + *

这样做的业务价值是:既能减少 SQL IN 条件里的重复值,又能避免前端数组里夹杂空串时 + * 误判为“有值但查不到数据”,排查起来更直接。

+ */ + private List normalizeMonitorIds(List monitorIds) { + if (monitorIds == null || monitorIds.isEmpty()) { + return Collections.emptyList(); + } + LinkedHashSet 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); + } + + /** + * 解析可选的记录时间范围。 + * + *

这里参照现有 RecordIotenvInstantServiceImpl 的思路,在 Service 层先把字符串解析成时间对象, + * 再校验起止顺序,避免把格式错误或颠倒顺序的问题拖到数据库层才暴露。

+ */ + 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); + } + + /** + * 严格解析时间范围参数。 + * + *

仅接受 yyyy-MM-dd HH:mm:ss,像 2026-04-09 8:00、2026/04/09 08:00:00 + * 这类非约定格式都直接 Fail Fast,避免接口口径被不同前端页面偷偷拉歪。

+ */ + 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 listAllMonitorInfos() { + List monitorInfos = emsBaseMonitorInfoMapper.selectEmsBaseMonitorInfoList( + new EmsBaseMonitorInfo() + ); + if (monitorInfos == null || monitorInfos.isEmpty()) { + return Collections.emptyList(); + } + + LinkedHashMap 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 extractMonitorIds(List baseMonitorInfos) { + List monitorIds = new ArrayList<>(baseMonitorInfos.size()); + for (EmsBaseMonitorInfo baseMonitorInfo : baseMonitorInfos) { + if (trimToNull(baseMonitorInfo.getMonitorCode()) != null) { + monitorIds.add(baseMonitorInfo.getMonitorCode()); + } + } + return monitorIds; + } + + /** + * 将原始实时数据按 monitorId 建索引,方便后续与设备主数据做一一补齐。 + */ + private Map> indexRawDataByMonitorId(List> rawList) { + if (rawList == null || rawList.isEmpty()) { + return Collections.emptyMap(); + } + Map> rawDataMap = new HashMap<>(rawList.size()); + for (Map raw : rawList) { + String monitorId = trimToNull(raw.get("monitorId") == null ? null : raw.get("monitorId").toString()); + if (monitorId != null) { + rawDataMap.putIfAbsent(monitorId, raw); + } + } + return rawDataMap; + } + + /** + * 按设备主数据顺序构建“所有设备最新值”结果。 + * + *

即便某设备当前没有实时数据,也会保留设备骨架并按 monitor_type 裁剪字段, + * 这样前端调用无参接口时可以稳定拿到完整设备集合。

+ */ + private List> buildLatestSnapshot( + List baseMonitorInfos, Map> rawDataMap + ) { + List> result = new ArrayList<>(baseMonitorInfos.size()); + for (EmsBaseMonitorInfo baseMonitorInfo : baseMonitorInfos) { + Map raw = buildSnapshotRawData(baseMonitorInfo, rawDataMap.get(baseMonitorInfo.getMonitorCode())); + result.add(trimFieldsByMonitorType(raw, null)); + } + return result; + } + + /** + * 合并设备主数据与实时原始数据。 + * + *

主数据负责补足 monitorCode/monitorName/monitorType,实时数据负责补测量值和时间戳; + * 这样可以兼容“设备存在但当前无最新值”的场景。

+ */ + private Map buildSnapshotRawData(EmsBaseMonitorInfo baseMonitorInfo, Map 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 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 raw, String key) { + return raw == null ? null : raw.get(key); + } + + /** + * 批量裁剪:对列表中每条记录按各自的 monitor_type 裁剪字段。 + * + * @param rawList 原始数据列表 + * @param vibrationParam 振动参数选择(仅振动设备有效) + * @return 裁剪后的数据列表 + */ + private List> trimListByMonitorType( + List> rawList, String vibrationParam + ) { + if (rawList == null || rawList.isEmpty()) { + return Collections.emptyList(); + } + List> result = new ArrayList<>(rawList.size()); + for (Map raw : rawList) { + result.add(trimFieldsByMonitorType(raw, vibrationParam)); + } + return result; + } + + /** + * 按设备类型裁剪返回字段的核心方法。 + * + *

设计原则: + * 1. 先提取公共字段(设备标识 + 时间字段),所有设备类型都包含 + * 2. 再根据 monitor_type 追加对应的测量字段 + * 3. 不相关的测量字段不放入结果 Map,避免前端渲染时处理大量 null 值 + * 4. 兜底逻辑:未识别类型返回全量字段,保证新设备类型接入后不丢数据

+ * + *

纯净返回方案(默认):仅返回对应设备类型的测量字段。 + * 若需要兼容旧前端(旧接口期望全量字段),可在此方法中补充 null 填充逻辑(兼容方案)。

+ * + * @param raw 原始数据行(含所有字段) + * @param vibrationParam 振动参数选择(vibrationSpeed/vibrationDisplacement/vibrationAcceleration/vibrationTemp) + * @return 裁剪后只包含该设备类型有业务意义字段的 Map + */ + private Map trimFieldsByMonitorType( + Map raw, String vibrationParam + ) { + // 从原始数据中读取设备类型编码 + Object monitorTypeObj = raw.get("monitorType"); + Integer monitorType = (monitorTypeObj == null) ? null : ((Number) monitorTypeObj).intValue(); + IotEnvMonitorTypeEnum typeEnum = IotEnvMonitorTypeEnum.fromCode(monitorType); + + // 构建结果 Map,使用 LinkedHashMap 保持字段顺序(便于前端调试和日志可读性) + Map 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 result, Map 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 monitorIds; + + private MonitorFilter(String monitorId, List 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; + } + } +} diff --git a/ruoyi-ems/src/main/resources/mapper/ems/record/IotEnvMonitorDataMapper.xml b/ruoyi-ems/src/main/resources/mapper/ems/record/IotEnvMonitorDataMapper.xml new file mode 100644 index 0000000..0ba1c73 --- /dev/null +++ b/ruoyi-ems/src/main/resources/mapper/ems/record/IotEnvMonitorDataMapper.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + +