设备运行总览

main
liutao 1 week ago
parent b85e6e7360
commit 719cfb797e

@ -1,37 +1,46 @@
package cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceOperationOverviewRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceOperationRateTrendRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceOperationRecordPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceOperationRecordRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceOperationRecordSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceTotalTimeRecordReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceTotalTimeRecordRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
import cn.iocoder.yudao.module.iot.service.deviceoperationrecord.DeviceOperationRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.*;
import javax.validation.*;
import javax.servlet.http.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.io.IOException;
import java.util.List;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
import cn.iocoder.yudao.module.iot.service.deviceoperationrecord.DeviceOperationRecordService;
@Tag(name = "管理后台 - 运行记录")
@RestController
@RequestMapping("/iot/device-operation-record")
@ -87,12 +96,11 @@ public class DeviceOperationRecordController {
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportDeviceOperationRecordExcel(@Valid DeviceOperationRecordPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<DeviceOperationRecordDO> list = deviceOperationRecordService.getDeviceOperationRecordPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "运行记录.xls", "数据", DeviceOperationRecordRespVO.class,
BeanUtils.toBean(list, DeviceOperationRecordRespVO.class));
BeanUtils.toBean(list, DeviceOperationRecordRespVO.class));
}
@GetMapping("/deviceOperationPage")
@ -111,45 +119,44 @@ public class DeviceOperationRecordController {
}
@GetMapping("/deviceRateTrendByDeviceId")
@Operation(summary = "根据设备ID查询某个设备开机率和稼动率")
@Operation(summary = "根据设备 ID 查询单设备开机率和利用率趋势")
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:query')")
public CommonResult<List<DeviceOperationRateTrendRespVO>> getDeviceRateTrendByDeviceId(@RequestParam("deviceId") Long deviceId,
@RequestParam(value = "period", required = false) String period) {
public CommonResult<List<DeviceOperationRateTrendRespVO>> getDeviceRateTrendByDeviceId(
@RequestParam("deviceId") Long deviceId,
@RequestParam(value = "period", required = false) String period) {
return success(deviceOperationRecordService.getDeviceRateTrendByDeviceId(deviceId, period));
}
@GetMapping("/deviceOperationList")
@Operation(summary = "产线设备运行开机率/稼动率-大屏")
@Operation(summary = "产线设备运行开机率/利用率大屏")
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:query')")
public CommonResult<List<DeviceTotalTimeRecordRespVO>> deviceOperationList(@Valid DeviceTotalTimeRecordReqVO pageReqVO) {
List<DeviceTotalTimeRecordRespVO> deviceTotalTimeRecordRespVOList = deviceOperationRecordService.deviceOperationList(pageReqVO);
return success(deviceTotalTimeRecordRespVOList);
return success(deviceOperationRecordService.deviceOperationList(pageReqVO));
}
@GetMapping("/runOverview")
@Operation(summary = "设备运行总览")
@Operation(
summary = "设备运行总览",
description = "根据设备 ID 集合、统计时间范围和时间轴分页参数,返回运行总览指标卡、按小时状态分布、状态汇总以及设备时间轴数据"
)
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:query')")
public CommonResult<DeviceOperationOverviewRespVO> runOverview(@Valid DeviceTotalTimeRecordReqVO pageReqVO) {
return success(deviceOperationRecordService.runOverview(pageReqVO));
}
@GetMapping("/export-device-operation-report")
@Operation(summary = "导出设备运行报表记录 Excel")
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportDeviceOperationReport(@Valid DeviceTotalTimeRecordReqVO pageReqVO,
HttpServletResponse response) throws IOException {
HttpServletResponse response) throws IOException {
PageResult<DeviceTotalTimeRecordRespVO> pageResult = deviceOperationRecordService.deviceOperationPage(pageReqVO);
// 设置响应头
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode("设备运行报表记录.xls", "UTF-8"));
response.setHeader("Content-Encoding", "identity");
// 导出Excel
String fileName = String.format("设备运行报表记录_%s.xls", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
// 导出 Excel
ExcelUtils.write(response, fileName, "数据", DeviceTotalTimeRecordRespVO.class,pageResult.getList());
String fileName = String.format("设备运行报表记录_%s.xls",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
ExcelUtils.write(response, fileName, "数据", DeviceTotalTimeRecordRespVO.class, pageResult.getList());
}
}

@ -0,0 +1,127 @@
package cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 设备运行总览 Response VO")
@Data
public class DeviceOperationOverviewRespVO {
@Schema(description = "指标卡数据")
private List<MetricItem> metrics;
@Schema(description = "按小时统计的状态分布,单位为百分比")
private List<HourlyStatusItem> hourlyStatus;
@Schema(description = "状态汇总数据")
private List<SummaryItem> summary;
@Schema(description = "汇总总时长,单位小时")
private Double summaryTotalHours;
@Schema(description = "设备时间轴数据")
private List<TimelineRowItem> timelineRows;
@Schema(description = "设备总数")
private Integer totalDevices;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "指标卡项")
public static class MetricItem {
@Schema(description = "指标 key例如 utilizationRate、powerOnRate")
private String key;
@Schema(description = "前端图标标识")
private String icon;
@Schema(description = "指标值")
private Double value;
@Schema(description = "指标单位")
private String unit;
@Schema(description = "对比变化值,当前默认返回 0")
private Double change;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "小时状态分布项")
public static class HourlyStatusItem {
@Schema(description = "小时刻度,例如 08:00")
private String hour;
@Schema(description = "运行占比,百分比")
private Double running;
@Schema(description = "待机占比,百分比")
private Double standby;
@Schema(description = "故障占比,百分比")
private Double fault;
@Schema(description = "离线占比,百分比")
private Double offline;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "状态汇总项")
public static class SummaryItem {
@Schema(description = "状态编码running/standby/fault/offline")
private String status;
@Schema(description = "状态占比,百分比")
private Double percent;
@Schema(description = "状态累计时长,单位小时")
private Double hours;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "设备时间轴行")
public static class TimelineRowItem {
@Schema(description = "设备 ID")
private String id;
@Schema(description = "设备名称")
private String name;
@Schema(description = "设备利用率,百分比")
private Double utilizationRate;
@Schema(description = "时间轴状态段")
private List<TimelineSegmentItem> segments;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "时间轴状态段")
public static class TimelineSegmentItem {
@Schema(description = "状态编码running/standby/fault/offline")
private String status;
@Schema(description = "段开始位置,按 24 小时刻度折算")
private Double startHour;
@Schema(description = "段结束位置,按 24 小时刻度折算")
private Double endHour;
}
}

@ -1,35 +1,38 @@
package cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 运行记录分页 Request VO")
@Schema(description = "管理后台 - 设备运行统计请求 VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class DeviceTotalTimeRecordReqVO extends PageParam{
public class DeviceTotalTimeRecordReqVO extends PageParam {
@Schema(description = "时间区间LAST_WEEK/THIS_WEEK/LAST_7_DAYS/LAST_MONTH/THIS_MONTH")
@Schema(description = "快捷时间范围,支持 LAST_WEEK、THIS_WEEK、LAST_7_DAYS、LAST_MONTH、THIS_MONTH")
private String period;
@Schema(description = "设备编码")
@Schema(description = "设备编码,模糊匹配")
private String deviceCode;
@Schema(description = "设备名称")
@Schema(description = "设备名称,模糊匹配")
private String deviceName;
@Schema(description = "开始时间")
@Schema(description = "统计开始时间,格式 yyyy-MM-dd HH:mm:ss")
private String startTime;
@Schema(description = "结束时间")
@Schema(description = "统计结束时间,格式 yyyy-MM-dd HH:mm:ss")
private String endTime;
@Schema(description = "ids导出集合用")
@Schema(description = "设备 ID 集合,使用英文逗号分隔,例如 1,2,3")
private String ids;
@Schema(description = "时间轴页码,默认 1")
private Integer timelinePageNo;
@Schema(description = "时间轴每页条数,默认 10")
private Integer timelinePageSize;
}

@ -2748,7 +2748,7 @@ public class TDengineService {
List<Object> params = new ArrayList<>();
sql.append("SELECT ")
.append("HOUR(_wstart) AS hourValue, ")
.append("TO_CHAR(_wstart, 'HH24') 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, ")
@ -2786,6 +2786,57 @@ public class TDengineService {
}
}
@DS("tdengine")
public List<DeviceOperationRecordDO> queryDeviceOperationTimeline(List<Long> deviceIds, String startTime, String endTime) {
if (deviceIds == null || deviceIds.isEmpty()) {
return Collections.emptyList();
}
StringBuilder sql = new StringBuilder();
List<Object> params = new ArrayList<>();
sql.append("SELECT device_id, rule, create_time ")
.append("FROM besure_server.iot_device_operation_record ")
.append("WHERE deleted = 0 ");
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(") ");
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);
}
sql.append("ORDER BY device_id ASC, create_time ASC");
try {
return jdbcTemplate.query(sql.toString(), params.toArray(), (rs, rowNum) -> {
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
record.setDeviceId(rs.getLong("device_id"));
record.setRule(rs.getString("rule"));
Timestamp createTime = rs.getTimestamp("create_time");
if (createTime != null) {
record.setCreateTime(createTime.toLocalDateTime());
}
return record;
});
} catch (Exception e) {
log.error("TDengine 查询设备时间轴失败sql={}, params={}", sql, params, e);
return Collections.emptyList();
}
}

@ -452,6 +452,7 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
@Override
public DeviceOperationOverviewRespVO runOverview(DeviceTotalTimeRecordReqVO pageReqVO) {
//参数构造
applyPeriodIfNecessary(pageReqVO);
List<DeviceTotalTimeRecordRespVO> records = deviceOperationPageList(pageReqVO);
@ -487,6 +488,11 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
List<Long> deviceIds = parseIds(pageReqVO.getIds());
List<Map<String, Object>> hourlyRows = tDengineService.queryDeviceHourTrend(deviceIds, pageReqVO.getStartTime(), pageReqVO.getEndTime());
respVO.setHourlyStatus(buildHourlyStatus(hourlyRows));
//设备运行轨迹
List<DeviceOperationOverviewRespVO.TimelineRowItem> timelineRows =
buildTimelineRows(records, deviceIds, pageReqVO.getStartTime(), pageReqVO.getEndTime());
respVO.setTotalDevices(timelineRows.size());
respVO.setTimelineRows(paginateTimelineRows(timelineRows, pageReqVO.getTimelinePageNo(), pageReqVO.getTimelinePageSize()));
return respVO;
}
@ -552,6 +558,104 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
return Math.round(value * 100D) / 100D;
}
private List<DeviceOperationOverviewRespVO.TimelineRowItem> buildTimelineRows(List<DeviceTotalTimeRecordRespVO> records,
List<Long> deviceIds,
String startTime,
String endTime) {
if (records == null || records.isEmpty()) {
return Collections.emptyList();
}
Map<Long, List<DeviceOperationRecordDO>> recordMap = tDengineService.queryDeviceOperationTimeline(deviceIds, startTime, endTime)
.stream()
.filter(item -> item.getDeviceId() != null)
.collect(Collectors.groupingBy(DeviceOperationRecordDO::getDeviceId, LinkedHashMap::new, Collectors.toList()));
LocalDateTime start = LocalDateTime.parse(startTime, DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND));
LocalDateTime end = LocalDateTime.parse(endTime, DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND));
double totalSeconds = Math.max(1D, Duration.between(start, end).getSeconds());
return records.stream().map(record -> {
List<DeviceOperationOverviewRespVO.TimelineSegmentItem> segments = buildTimelineSegments(
recordMap.getOrDefault(record.getId(), Collections.emptyList()),
start,
end,
totalSeconds
);
return DeviceOperationOverviewRespVO.TimelineRowItem.builder()
.id(String.valueOf(record.getId()))
.name(record.getDeviceName())
.utilizationRate(parsePercentValue(record))
.segments(segments)
.build();
}).collect(Collectors.toList());
}
private List<DeviceOperationOverviewRespVO.TimelineRowItem> paginateTimelineRows(List<DeviceOperationOverviewRespVO.TimelineRowItem> rows,
Integer pageNo,
Integer pageSize) {
if (rows == null || rows.isEmpty()) {
return Collections.emptyList();
}
int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, rows.size());
int toIndex = Math.min(fromIndex + actualPageSize, rows.size());
return rows.subList(fromIndex, toIndex);
}
private List<DeviceOperationOverviewRespVO.TimelineSegmentItem> buildTimelineSegments(List<DeviceOperationRecordDO> timelineRecords,
LocalDateTime start,
LocalDateTime end,
double totalSeconds) {
if (timelineRecords == null || timelineRecords.isEmpty()) {
return Collections.emptyList();
}
List<DeviceOperationOverviewRespVO.TimelineSegmentItem> segments = new ArrayList<>();
for (int i = 0; i < timelineRecords.size(); i++) {
DeviceOperationRecordDO current = timelineRecords.get(i);
LocalDateTime currentTime = current.getCreateTime();
if (currentTime == null) {
continue;
}
LocalDateTime segmentStart = currentTime.isBefore(start) ? start : currentTime;
LocalDateTime segmentEnd = i + 1 < timelineRecords.size() && timelineRecords.get(i + 1).getCreateTime() != null
? timelineRecords.get(i + 1).getCreateTime()
: end;
if (segmentEnd.isAfter(end)) {
segmentEnd = end;
}
if (!segmentEnd.isAfter(segmentStart)) {
continue;
}
double startHour = Duration.between(start, segmentStart).getSeconds() / totalSeconds * 24D;
double endHour = Duration.between(start, segmentEnd).getSeconds() / totalSeconds * 24D;
segments.add(DeviceOperationOverviewRespVO.TimelineSegmentItem.builder()
.status(mapRuleToStatus(current.getRule()))
.startHour(round2(startHour))
.endHour(round2(endHour))
.build());
}
return segments;
}
private String mapRuleToStatus(String rule) {
if ("1".equals(rule)) {
return "running";
}
if ("2".equals(rule)) {
return "standby";
}
if ("3".equals(rule)) {
return "fault";
}
return "offline";
}
private double getDoubleValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {

Loading…
Cancel
Save