diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/DeviceOperationRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/DeviceOperationRecordController.java index f74ee3a5a..277a8888c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/DeviceOperationRecordController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/deviceoperationrecord/DeviceOperationRecordController.java @@ -127,6 +127,13 @@ public class DeviceOperationRecordController { return success(deviceTotalTimeRecordRespVOList); } + @GetMapping("/runOverview") + @Operation(summary = "设备运行总览") + @PreAuthorize("@ss.hasPermission('iot:device-operation-record:query')") + public CommonResult runOverview(@Valid DeviceTotalTimeRecordReqVO pageReqVO) { + return success(deviceOperationRecordService.runOverview(pageReqVO)); + } + @GetMapping("/export-device-operation-report") @Operation(summary = "导出设备运行报表记录 Excel") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java index d6e991f6e..d9c58da51 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java @@ -2742,6 +2742,50 @@ public class TDengineService { } } + @DS("tdengine") + public List> queryDeviceHourTrend(List deviceIds, String startTime, String endTime) { + StringBuilder sql = new StringBuilder(); + List params = new ArrayList<>(); + + sql.append("SELECT ") + .append("HOUR(_wstart) AS hourValue, ") + .append("SUM(CASE WHEN rule = '0' THEN 1 ELSE 0 END) * 60 AS totalOfflineTime, ") + .append("SUM(CASE WHEN rule = '1' THEN 1 ELSE 0 END) * 60 AS totalRunningTime, ") + .append("SUM(CASE WHEN rule = '2' THEN 1 ELSE 0 END) * 60 AS totalStandbyTime, ") + .append("SUM(CASE WHEN rule = '3' THEN 1 ELSE 0 END) * 60 AS totalFaultTime ") + .append("FROM besure_server.iot_device_operation_record ") + .append("WHERE deleted = 0 "); + + if (startTime != null && !startTime.trim().isEmpty()) { + sql.append("AND create_time >= ? "); + params.add(startTime); + } + if (endTime != null && !endTime.trim().isEmpty()) { + sql.append("AND create_time <= ? "); + params.add(endTime); + } + if (deviceIds != null && !deviceIds.isEmpty()) { + sql.append("AND device_id IN ("); + for (int i = 0; i < deviceIds.size(); i++) { + if (i > 0) { + sql.append(", "); + } + sql.append("?"); + params.add(deviceIds.get(i)); + } + sql.append(") "); + } + + sql.append("INTERVAL(1h) ORDER BY _wstart ASC"); + + try { + return jdbcTemplate.queryForList(sql.toString(), params.toArray()); + } catch (Exception e) { + log.error("TDengine 查询设备小时趋势失败,sql={}, params={}", sql, params, e); + return Collections.emptyList(); + } + } + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordService.java index 154c9c91b..974902659 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordService.java @@ -59,4 +59,6 @@ public interface DeviceOperationRecordService { List getDeviceRateTrendByDeviceId(Long deviceId, String period); List deviceOperationList(@Valid DeviceTotalTimeRecordReqVO pageReqVO); + + DeviceOperationOverviewRespVO runOverview(@Valid DeviceTotalTimeRecordReqVO pageReqVO); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java index 25d5ea992..4f31f99a1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/deviceoperationrecord/DeviceOperationRecordServiceImpl.java @@ -16,7 +16,6 @@ import org.springframework.transaction.annotation.Transactional; import java.time.*; import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.stream.Collectors; @@ -451,6 +450,46 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe return lineList; } + @Override + public DeviceOperationOverviewRespVO runOverview(DeviceTotalTimeRecordReqVO pageReqVO) { + applyPeriodIfNecessary(pageReqVO); + + List records = deviceOperationPageList(pageReqVO); + DeviceOperationOverviewRespVO respVO = new DeviceOperationOverviewRespVO(); + + double totalRunningHours = records.stream().mapToDouble(DeviceTotalTimeRecordRespVO::getTotalRunningTime).sum(); + double totalStandbyHours = records.stream().mapToDouble(DeviceTotalTimeRecordRespVO::getTotalStandbyTime).sum(); + double totalFaultHours = records.stream().mapToDouble(DeviceTotalTimeRecordRespVO::getTotalFaultTime).sum(); + double totalOfflineHours = records.stream().mapToDouble(DeviceTotalTimeRecordRespVO::getTotalOfflineTime).sum(); + + double onlineHours = totalRunningHours + totalStandbyHours + totalFaultHours; + double totalHours = onlineHours + totalOfflineHours; + double utilizationRate = onlineHours > 0 ? totalRunningHours / onlineHours * 100 : 0D; + double powerOnRate = totalHours > 0 ? onlineHours / totalHours * 100 : 0D; + double faultRate = onlineHours > 0 ? totalFaultHours / onlineHours * 100 : 0D; + double standbyRate = onlineHours > 0 ? totalStandbyHours / onlineHours * 100 : 0D; + + respVO.setMetrics(Arrays.asList( + DeviceOperationOverviewRespVO.MetricItem.builder().key("utilizationRate").icon("ep:pie-chart").value(round2(utilizationRate)).unit("%").change(0D).build(), + DeviceOperationOverviewRespVO.MetricItem.builder().key("powerOnRate").icon("ep:video-play").value(round2(powerOnRate)).unit("%").change(0D).build(), + DeviceOperationOverviewRespVO.MetricItem.builder().key("faultRate").icon("ep:warning").value(round2(faultRate)).unit("%").change(0D).build(), + DeviceOperationOverviewRespVO.MetricItem.builder().key("standbyRate").icon("ep:timer").value(round2(standbyRate)).unit("%").change(0D).build() + )); + + respVO.setSummary(Arrays.asList( + DeviceOperationOverviewRespVO.SummaryItem.builder().status("running").percent(toPercent(totalRunningHours, totalHours)).hours(round2(totalRunningHours)).build(), + DeviceOperationOverviewRespVO.SummaryItem.builder().status("standby").percent(toPercent(totalStandbyHours, totalHours)).hours(round2(totalStandbyHours)).build(), + DeviceOperationOverviewRespVO.SummaryItem.builder().status("fault").percent(toPercent(totalFaultHours, totalHours)).hours(round2(totalFaultHours)).build(), + DeviceOperationOverviewRespVO.SummaryItem.builder().status("offline").percent(toPercent(totalOfflineHours, totalHours)).hours(round2(totalOfflineHours)).build() + )); + respVO.setSummaryTotalHours(round2(totalHours)); + + List deviceIds = parseIds(pageReqVO.getIds()); + List> hourlyRows = tDengineService.queryDeviceHourTrend(deviceIds, pageReqVO.getStartTime(), pageReqVO.getEndTime()); + respVO.setHourlyStatus(buildHourlyStatus(hourlyRows)); + return respVO; + } + private double parsePercentValue(DeviceTotalTimeRecordRespVO record) { if (record == null || StringUtils.isBlank(record.getUtilizationRate())) { return 0D; @@ -458,6 +497,88 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe return Double.parseDouble(record.getUtilizationRate().replace("%", "").trim()); } + private List parseIds(String ids) { + if (StringUtils.isBlank(ids)) { + return Collections.emptyList(); + } + return Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(Long::valueOf) + .distinct() + .collect(Collectors.toList()); + } + + private List buildHourlyStatus(List> hourlyRows) { + Map bucketMap = new LinkedHashMap<>(); + for (int i = 0; i < 24; i++) { + String hour = String.format("%02d:00", i); + bucketMap.put(hour, new HourBucket(hour)); + } + + for (Map row : hourlyRows) { + Integer hourValue = getIntValue(row, "hourValue"); + if (hourValue == null || hourValue < 0 || hourValue > 23) { + continue; + } + String hour = String.format("%02d:00", hourValue); + HourBucket bucket = bucketMap.get(hour); + if (bucket == null) { + continue; + } + bucket.offline += getDoubleValue(row, "totalofflinetime"); + bucket.running += getDoubleValue(row, "totalrunningtime"); + bucket.standby += getDoubleValue(row, "totalstandbytime"); + bucket.fault += getDoubleValue(row, "totalfaulttime"); + } + + return bucketMap.values().stream().map(bucket -> { + double total = bucket.running + bucket.standby + bucket.fault + bucket.offline; + return DeviceOperationOverviewRespVO.HourlyStatusItem.builder() + .hour(bucket.hour) + .running(total > 0 ? round2(bucket.running / total * 100) : 0D) + .standby(total > 0 ? round2(bucket.standby / total * 100) : 0D) + .fault(total > 0 ? round2(bucket.fault / total * 100) : 0D) + .offline(total > 0 ? round2(bucket.offline / total * 100) : 0D) + .build(); + }).collect(Collectors.toList()); + } + + private double toPercent(double numerator, double denominator) { + return denominator > 0 ? round2(numerator / denominator * 100) : 0D; + } + + private double round2(double value) { + return Math.round(value * 100D) / 100D; + } + + private double getDoubleValue(Map map, String key) { + Object value = map.get(key); + if (value == null) { + return 0D; + } + if (value instanceof Number) { + return ((Number) value).doubleValue() / 3600D; + } + try { + return Double.parseDouble(String.valueOf(value)) / 3600D; + } catch (Exception e) { + return 0D; + } + } + + private static class HourBucket { + private final String hour; + private double running; + private double standby; + private double fault; + private double offline; + + private HourBucket(String hour) { + this.hour = hour; + } + } + private void calculateAndSetConvertedValues( List records, DeviceTotalTimeRecordReqVO reqVO) {