diff --git a/os-ems/src/main/java/com/os/ems/record/controller/RecordIotenvInstantController.java b/os-ems/src/main/java/com/os/ems/record/controller/RecordIotenvInstantController.java index c0748e7..2b105da 100644 --- a/os-ems/src/main/java/com/os/ems/record/controller/RecordIotenvInstantController.java +++ b/os-ems/src/main/java/com/os/ems/record/controller/RecordIotenvInstantController.java @@ -1,14 +1,16 @@ package com.os.ems.record.controller; +import java.math.BigDecimal; import java.text.ParseException; -import java.util.Date; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletResponse; +import com.os.common.exception.ServiceException; + import com.os.common.core.page.PageDomain; import com.os.common.core.page.TableSupport; -import org.checkerframework.checker.units.qual.A; -import org.springframework.format.annotation.DateTimeFormat; +import com.os.common.utils.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -23,7 +25,7 @@ import com.os.common.core.page.TableDataInfo; /** * 物联网数据Controller - * + * * @author zch * @date 2025-04-28 */ @@ -34,6 +36,9 @@ public class RecordIotenvInstantController extends BaseController @Autowired private IRecordIotenvInstantService recordIotenvInstantService; + /** 最大导出记录数限制(10万条) */ + private static final int MAX_EXPORT_RECORDS = 100000; + /** * 查询物联网数据列表 */ @@ -48,15 +53,196 @@ public class RecordIotenvInstantController extends BaseController /** - * 导出物联网数据列表 + * 导出物联网数据列表(支持按日期分表查询、多设备批量导出、动态列导出) */ @Log(title = "物联网数据", businessType = BusinessType.EXPORT) @PostMapping("/export") - public void export(HttpServletResponse response, RecordIotenvInstant recordIotenvInstant)throws ParseException + public void export(HttpServletResponse response, RecordIotenvInstant recordIotenvInstant) throws ParseException { - List list = recordIotenvInstantService.selectRecordIotenvInstantList(recordIotenvInstant); + // 校验时间范围参数 + Map params = recordIotenvInstant.getParams(); + if (params == null || params.get("beginRecordTime") == null || params.get("endRecordTime") == null) { + throw new ServiceException("导出失败:请选择记录时间范围"); + } + + // 解析设备类型参数(用于动态导出列和数据过滤) + Set monitorTypes = parseMonitorTypes(params); + + // 使用专用导出方法(性能优化:移除ORDER BY,支持多设备批量导出) + List list = recordIotenvInstantService.selectRecordIotenvInstantListForExport(recordIotenvInstant); + + // 根据设备类型过滤无效数据(零值和超范围值) + list = filterInvalidData(list, monitorTypes); + + // 添加数据量检查,防止导出数据量过大导致内存溢出 + if (list.size() > MAX_EXPORT_RECORDS) { + throw new ServiceException("导出数据量过大(" + list.size() + "条),请缩小时间范围或减少设备数量"); + } + + // 根据设备类型动态选择导出列 + String[] includeColumns = getExportColumnsByType(monitorTypes); ExcelUtil util = new ExcelUtil(RecordIotenvInstant.class); - util.exportExcel(response, list, "物联网数据数据"); + // 使用showColumn方法指定需要导出的列 + util.showColumn(includeColumns); + util.exportExcel(response, list, "物联网数据"); + } + + /** + * 解析设备类型参数 + */ + private Set parseMonitorTypes(Map params) { + Set types = new HashSet<>(); + Object monitorTypesObj = params.get("monitorTypes"); + if (monitorTypesObj != null && StringUtils.isNotEmpty(monitorTypesObj.toString())) { + String[] typeStrs = monitorTypesObj.toString().split(","); + for (String typeStr : typeStrs) { + try { + types.add(Integer.parseInt(typeStr.trim())); + } catch (NumberFormatException ignored) { + } + } + } + return types; + } + + /** + * 根据设备类型过滤无效数据 + * 过滤规则(使用OR关系,满足任一类型的有效性即保留): + * 1. 温度设备(type=5):温度>0且<=79 + * 2. 温湿度设备(type=6):温度或湿度任一>0且<=79 + * 3. 噪声设备(type=7):噪声>0且<=79 + * 4. 振动设备(type=10):振动相关字段任一>0 + */ + private List filterInvalidData(List list, Set monitorTypes) { + if (monitorTypes.isEmpty()) { + // 未指定类型时,保留有任何有效数据的记录 + return list.stream().filter(this::hasValidData).collect(Collectors.toList()); + } + + // 单一类型:严格按该类型过滤 + if (monitorTypes.size() == 1) { + Integer type = monitorTypes.iterator().next(); + return list.stream().filter(record -> isValidForType(record, type)).collect(Collectors.toList()); + } + + // 多类型:使用OR关系,满足任一选中类型的有效性即保留 + return list.stream().filter(record -> { + for (Integer type : monitorTypes) { + if (isValidForType(record, type)) { + return true; + } + } + return false; + }).collect(Collectors.toList()); + } + + /** 判断记录对于指定类型是否有效 */ + private boolean isValidForType(RecordIotenvInstant record, Integer type) { + switch (type) { + case 5: // 温度设备 + return isValidTemperature(record.getTemperature()); + case 6: // 温湿度设备 + return isValidTemperature(record.getTemperature()) || isValidHumidity(record.getHumidity()); + case 7: // 噪声设备 + return isValidNoise(record.getNoise()); + case 10: // 振动设备 + return hasValidVibrationData(record); + default: + return hasValidData(record); + } + } + + /** 判断温度是否有效:>0且<=79 */ + private boolean isValidTemperature(BigDecimal temperature) { + return temperature != null && temperature.compareTo(BigDecimal.ZERO) > 0 && temperature.compareTo(new BigDecimal("79")) <= 0; + } + + /** 判断湿度是否有效:>0且<=79 */ + private boolean isValidHumidity(BigDecimal humidity) { + return humidity != null && humidity.compareTo(BigDecimal.ZERO) > 0 && humidity.compareTo(new BigDecimal("79")) <= 0; + } + + /** 判断噪声是否有效:>0且<=79 */ + private boolean isValidNoise(BigDecimal noise) { + return noise != null && noise.compareTo(BigDecimal.ZERO) > 0 && noise.compareTo(new BigDecimal("79")) <= 0; + } + + /** 判断是否有有效的振动数据 */ + private boolean hasValidVibrationData(RecordIotenvInstant record) { + return (record.getVibrationSpeed() != null && record.getVibrationSpeed().compareTo(BigDecimal.ZERO) > 0) || + (record.getVibrationDisplacement() != null && record.getVibrationDisplacement().compareTo(BigDecimal.ZERO) > 0) || + (record.getVibrationAcceleration() != null && record.getVibrationAcceleration().compareTo(BigDecimal.ZERO) > 0) || + (record.getVibrationTemp() != null && record.getVibrationTemp().compareTo(BigDecimal.ZERO) > 0); + } + + /** 判断记录是否有任何有效数据 */ + private boolean hasValidData(RecordIotenvInstant record) { + return isValidTemperature(record.getTemperature()) || + isValidHumidity(record.getHumidity()) || + isValidNoise(record.getNoise()) || + hasValidVibrationData(record); + } + + /** + * 根据设备类型获取导出列 + * 列顺序:设备编号、设备名称、能源类型数据列、记录时间 + * type=5: 温度设备 -> 设备编号、设备名称、温度、记录时间 + * type=6: 温湿度设备 -> 设备编号、设备名称、温度、湿度、记录时间 + * type=7: 噪声设备 -> 设备编号、设备名称、噪声、记录时间 + * type=10: 振动设备 -> 设备编号、设备名称、振动速度、振动位移、振动加速度、振动温度、记录时间 + * 混合类型: 导出所有列 + */ + private String[] getExportColumnsByType(Set monitorTypes) { + List columns = new ArrayList<>(); + // 固定列:设备编号、设备名称 + columns.add("monitorCode"); + columns.add("monitorName"); + + if (monitorTypes.isEmpty() || monitorTypes.size() > 1) { + // 混合类型或未指定:导出所有数据列 + columns.add("temperature"); + columns.add("humidity"); + columns.add("noise"); + columns.add("vibrationSpeed"); + columns.add("vibrationDisplacement"); + columns.add("vibrationAcceleration"); + columns.add("vibrationTemp"); + } else { + // 单一类型:只导出对应的列 + Integer type = monitorTypes.iterator().next(); + switch (type) { + case 5: // 温度设备 + columns.add("temperature"); + break; + case 6: // 温湿度设备 + columns.add("temperature"); + columns.add("humidity"); + break; + case 7: // 噪声设备 + columns.add("noise"); + break; + case 10: // 振动设备 + columns.add("vibrationSpeed"); + columns.add("vibrationDisplacement"); + columns.add("vibrationAcceleration"); + columns.add("vibrationTemp"); + break; + default: + // 其他类型:导出所有列 + columns.add("temperature"); + columns.add("humidity"); + columns.add("noise"); + columns.add("vibrationSpeed"); + columns.add("vibrationDisplacement"); + columns.add("vibrationAcceleration"); + columns.add("vibrationTemp"); + break; + } + } + + // 固定列:记录时间 + columns.add("recodeTime"); + return columns.toArray(new String[0]); } /** diff --git a/os-ems/src/main/java/com/os/ems/record/domain/RecordIotenvInstant.java b/os-ems/src/main/java/com/os/ems/record/domain/RecordIotenvInstant.java index 8e586ad..61742bf 100644 --- a/os-ems/src/main/java/com/os/ems/record/domain/RecordIotenvInstant.java +++ b/os-ems/src/main/java/com/os/ems/record/domain/RecordIotenvInstant.java @@ -27,9 +27,19 @@ public class RecordIotenvInstant extends BaseEntity private Long objid; /** 计量设备编号 */ - @Excel(name = "计量设备编号") + // @Excel(name = "计量设备编号") private String monitorId; + + /** 计量设备编号 */ + @Excel(name = "计量设备编号") + private String monitorCode; + + /** 设备名称 */ + @Excel(name = "设备名称") + private String monitorName; + + /** 温度 */ @Excel(name = "温度") private BigDecimal temperature; @@ -68,7 +78,7 @@ public class RecordIotenvInstant extends BaseEntity /** 采集时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - @Excel(name = "采集时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + // @Excel(name = "采集时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date collectTime; /** 记录时间 */ @@ -79,12 +89,7 @@ public class RecordIotenvInstant extends BaseEntity private String[] monitorIds; - //关联查询设备名称 - private String monitorName; - /** 计量设备编号 */ - @Excel(name = "计量设备编号") - private String monitorCode; /** 能源类型 */ private Long monitorType; diff --git a/os-ems/src/main/java/com/os/ems/record/mapper/RecordIotenvInstantMapper.java b/os-ems/src/main/java/com/os/ems/record/mapper/RecordIotenvInstantMapper.java index 365d9db..c054471 100644 --- a/os-ems/src/main/java/com/os/ems/record/mapper/RecordIotenvInstantMapper.java +++ b/os-ems/src/main/java/com/os/ems/record/mapper/RecordIotenvInstantMapper.java @@ -158,11 +158,25 @@ public interface RecordIotenvInstantMapper /** * 按时间间隔采样查询:每个时间窗口选择最新的一条数据 * 用于减少数据点数量,提升前端渲染性能 - * + * * @param tableNames 表名列表 * @param recordIotenvInstant 查询条件(包含samplingInterval采样间隔) * @return 采样后的物联网数据集合 */ List selectRecordIotenvInstantListFromTablesWithSampling(@Param("tableNames") List tableNames, @Param("recordIotenvInstant") RecordIotenvInstant recordIotenvInstant); + + /** + * 从多个表查询物联网数据列表(专用导出) + * 与普通查询的区别: + * 1. 移除ORDER BY子句,提升大数据量导出性能 + * 2. 支持多设备批量导出 + * 3. 自动过滤虚拟设备 + * + * @param tableNames 表名列表 + * @param recordIotenvInstant 查询条件 + * @return 物联网数据集合 + */ + List selectRecordIotenvInstantListForExport(@Param("tableNames") List tableNames, + @Param("recordIotenvInstant") RecordIotenvInstant recordIotenvInstant); } diff --git a/os-ems/src/main/java/com/os/ems/record/service/IRecordIotenvInstantService.java b/os-ems/src/main/java/com/os/ems/record/service/IRecordIotenvInstantService.java index e64839d..ac5271c 100644 --- a/os-ems/src/main/java/com/os/ems/record/service/IRecordIotenvInstantService.java +++ b/os-ems/src/main/java/com/os/ems/record/service/IRecordIotenvInstantService.java @@ -78,10 +78,22 @@ public interface IRecordIotenvInstantService /** * 根据父节点ID查询子设备的最新数据 - * + * * @param parentId 父节点ID * @return 子设备最新数据列表 */ public List selectRecordListByParentId(Long parentId); + /** + * 查询物联网数据列表(专用导出) + * 与普通查询的区别: + * 1. 使用专用的导出Mapper方法,移除ORDER BY提升性能 + * 2. 支持多设备批量导出 + * 3. 自动过滤虚拟设备 + * + * @param recordIotenvInstant 物联网数据 + * @return 物联网数据集合 + */ + public List selectRecordIotenvInstantListForExport(RecordIotenvInstant recordIotenvInstant) throws ParseException; + } diff --git a/os-ems/src/main/java/com/os/ems/record/service/impl/RecordIotenvInstantServiceImpl.java b/os-ems/src/main/java/com/os/ems/record/service/impl/RecordIotenvInstantServiceImpl.java index f9d8940..4883b0e 100644 --- a/os-ems/src/main/java/com/os/ems/record/service/impl/RecordIotenvInstantServiceImpl.java +++ b/os-ems/src/main/java/com/os/ems/record/service/impl/RecordIotenvInstantServiceImpl.java @@ -8,6 +8,7 @@ import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.regex.Pattern; import com.github.pagehelper.PageInfo; import com.os.common.constant.HttpStatus; @@ -29,7 +30,7 @@ import com.os.ems.record.service.IRecordIotenvInstantService; * @date 2025-04-28 */ @Service -public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantService +public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantService { @Autowired private RecordIotenvInstantMapper recordIotenvInstantMapper; @@ -37,6 +38,9 @@ public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantServi @Autowired private EmsBaseMonitorInfoMapper emsBaseMonitorInfoMapper; + /** 表名格式校验正则表达式(record_iotenv_instant_yyyyMMdd) */ + private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^record_iotenv_instant_\\d{8}$"); + /** * 查询物联网数据 * @@ -159,6 +163,12 @@ public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantServi while (!currentDate.after(endDateOnly)) { String tableSuffix = tableFormat.format(currentDate); String tableName = "record_iotenv_instant_" + tableSuffix; + + // 添加表名格式校验,防止SQL注入 + if (!TABLE_NAME_PATTERN.matcher(tableName).matches()) { + throw new ServiceException("非法的表名格式: " + tableName); + } + if (isTableExists(tableName)){ tableNames.add(tableName); } @@ -452,7 +462,7 @@ public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantServi /** * 获取当天日期对应的分表名 - * + * * @return 分表名 */ private String getTodayTableName() { @@ -461,5 +471,49 @@ public class RecordIotenvInstantServiceImpl implements IRecordIotenvInstantServi String dateSuffix = today.format(formatter); return "record_iotenv_instant_" + dateSuffix; } - + + /** + * 查询物联网数据列表(专用导出) + * 与普通查询的区别: + * 1. 使用专用的导出Mapper方法,移除ORDER BY提升性能 + * 2. 支持多设备批量导出 + * 3. 自动过滤虚拟设备 + * + * @param recordIotenvInstant 物联网数据 + * @return 物联网数据集合 + */ + @Override + public List selectRecordIotenvInstantListForExport(RecordIotenvInstant recordIotenvInstant) throws ParseException { + Map params = recordIotenvInstant.getParams(); + + // 添加null检查,防止空指针异常 + if (params == null) { + throw new ServiceException("导出参数不能为空"); + } + + Object beginTimeObj = params.get("beginRecordTime"); + Object endTimeObj = params.get("endRecordTime"); + + if (beginTimeObj == null || endTimeObj == null) { + throw new ServiceException("导出时间范围不能为空"); + } + + String beginTimeStr = beginTimeObj.toString(); + String endTimeStr = endTimeObj.toString(); + + // 解析日期 + SimpleDateFormat fullFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date beginDate = fullFormat.parse(beginTimeStr); + Date endDate = fullFormat.parse(endTimeStr); + + // 获取需要查询的表名列表 + List tableNames = getTableNamesByDateRange(beginDate, endDate); + if (tableNames.isEmpty()) { + return new ArrayList<>(); + } + + // 使用专用导出查询(无ORDER BY,性能优化) + return recordIotenvInstantMapper.selectRecordIotenvInstantListForExport(tableNames, recordIotenvInstant); + } + } diff --git a/os-ems/src/main/resources/mapper/ems/record/RecordIotenvInstantMapper.xml b/os-ems/src/main/resources/mapper/ems/record/RecordIotenvInstantMapper.xml index d2a5bc1..b281635 100644 --- a/os-ems/src/main/resources/mapper/ems/record/RecordIotenvInstantMapper.xml +++ b/os-ems/src/main/resources/mapper/ems/record/RecordIotenvInstantMapper.xml @@ -19,6 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -165,7 +166,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (t.temperature IS NULL OR t.temperature BETWEEN 0 AND 79) AND (t.humidity IS NULL OR t.humidity BETWEEN 0 AND 79) AND (t.noise IS NULL OR t.noise BETWEEN 0 AND 79) - + + +-- AND (ebmi.is_ammeter IS NULL OR ebmi.is_ammeter != '0') + AND ( @@ -266,13 +270,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{monitorId} - + AND (t.temperature IS NULL OR t.temperature BETWEEN 0 AND 79) AND (t.humidity IS NULL OR t.humidity BETWEEN 0 AND 79) AND (t.noise IS NULL OR t.noise BETWEEN 0 AND 79) - + + +-- AND (ebmi.is_ammeter IS NULL OR ebmi.is_ammeter != '0') + AND ( @@ -334,13 +341,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{monitorId} - + AND (t.temperature IS NULL OR t.temperature BETWEEN 0 AND 79) AND (t.humidity IS NULL OR t.humidity BETWEEN 0 AND 79) AND (t.noise IS NULL OR t.noise BETWEEN 0 AND 79) - + + +-- AND (ebmi.is_ammeter IS NULL OR ebmi.is_ammeter != '0') + AND ( @@ -447,7 +457,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND (t.temperature IS NULL OR t.temperature BETWEEN 0 AND 79) AND (t.humidity IS NULL OR t.humidity BETWEEN 0 AND 79) AND (t.noise IS NULL OR t.noise BETWEEN 0 AND 79) - + + +-- AND (ebmi2.is_ammeter IS NULL OR ebmi2.is_ammeter != '0') + AND ( @@ -469,4 +482,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ORDER BY monitorId ASC, recodeTime ASC + + + + \ No newline at end of file