20260423:数据分析

This commit is contained in:
zhugongping 2026-04-23 20:52:05 +08:00
parent 30a8100d64
commit 1586645838
10 changed files with 1116 additions and 599 deletions

View File

@ -44,7 +44,7 @@ public class DeviceJobServiceImpl implements IDeviceJobService {
/**
* 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
*/
@PostConstruct
//@PostConstruct
public void init() throws SchedulerException, TaskException {
scheduler.clear();
// 设备定时任务

View File

@ -9,7 +9,10 @@ import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.iot.model.AlertCountVO;
import com.fastbee.iot.model.DataQualityVO;
import com.fastbee.iot.model.DeviceHistoryParam;
import com.fastbee.iot.model.DeviceStatsVO;
import com.fastbee.iot.model.HistogramVO;
import com.fastbee.iot.model.SubDeviceCompareParam;
import com.fastbee.iot.model.ThingsModelLogCountVO;
import com.fastbee.iot.model.dto.ThingsModelDTO;
@ -68,6 +71,48 @@ public class DataCenterController {
return AjaxResult.success(jsonObject);
}
@ApiOperation("查询设备物模型统计指标(最大/最小/均值/累计/数据量/最新值),支持网关设备")
@PreAuthorize("@ss.hasPermi('dataCenter:analysis:list')")
@PostMapping("/deviceStats")
public AjaxResult deviceStats(@RequestBody DeviceHistoryParam deviceHistoryParam) {
if (deviceHistoryParam.getDeviceId() == null) {
return AjaxResult.error("请选择设备");
}
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
}
List<DeviceStatsVO> list = dataCenterService.deviceStats(deviceHistoryParam);
return AjaxResult.success(list);
}
@ApiOperation("数据质量分析:上报间隔估算、缺数时间段、连续重复值检测、质量评分、7×24热力图")
@PreAuthorize("@ss.hasPermi('dataCenter:analysis:list')")
@PostMapping("/deviceDataQuality")
public AjaxResult deviceDataQuality(@RequestBody DeviceHistoryParam deviceHistoryParam) {
if (deviceHistoryParam.getDeviceId() == null) {
return AjaxResult.error("请选择设备");
}
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
}
List<DataQualityVO> list = dataCenterService.deviceDataQuality(deviceHistoryParam);
return AjaxResult.success(list);
}
@ApiOperation("值分布直方图:统计每个物模型标识符在查询时段内的值区间分布频次")
@PreAuthorize("@ss.hasPermi('dataCenter:analysis:list')")
@PostMapping("/deviceHistogram")
public AjaxResult deviceHistogram(@RequestBody DeviceHistoryParam deviceHistoryParam) {
if (deviceHistoryParam.getDeviceId() == null) {
return AjaxResult.error("请选择设备");
}
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
}
List<HistogramVO> list = dataCenterService.deviceHistogram(deviceHistoryParam);
return AjaxResult.success(list);
}
@ApiOperation("网关产品-可导入的子产品物模型列表(按identifier去重)")
@PreAuthorize("@ss.hasPermi('iot:model:list')")
@GetMapping("/subProductThingsModel")

View File

@ -22,22 +22,66 @@ public class DeviceDeferredCommand {
// 执行失败
public static final int STATUS_FAILED = 3;
/**
* 主键
*/
private Long id;
/**
* 设备序列号
*/
private String serialNumber;
/**
* 主题名称
*/
private String topicName;
/**
* 消息体hex
*/
private String messageHex;
/**
* 数据类型
*/
private String dataType;
/**
* 任务类型
*/
private Integer taskType;
/**
* 状态(0:待执行,1:执行中,2:执行成功,3:执行失败)
*/
private Integer status;
/**
* 消息体hash值
*/
private String payloadHash;
/**
* 错误信息
*/
private String errMsg;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 过期时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
/**
* 调度时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date dispatchTime;
}

View File

@ -0,0 +1,62 @@
package com.fastbee.iot.model;
import lombok.Data;
import java.util.List;
/**
* 设备物模型数据质量 VO
* 包含:上报间隔估算、缺数时间段列表、重复值统计
*/
@Data
public class DataQualityVO {
/** 标识符 */
private String identifier;
/** 物模型名称 */
private String modelName;
/** 所属设备序列号 */
private String serialNumber;
/** 时间范围内实际数据量 */
private Long totalCount;
/** 估算平均上报间隔(秒) */
private Long avgIntervalSeconds;
/** 缺数时间段列表(间隔超过 5× 平均间隔视为缺数) */
private List<MissingSegment> missingSegments;
/** 缺数段数量 */
private Integer missingCount;
/** 缺数总时长(分钟) */
private Long totalMissingMinutes;
/** 连续重复值数量 */
private Long repeatCount;
/** 连续重复率(0~100) */
private Double repeatRate;
/** 数据质量评分(0~100,综合缺数率和重复率计算) */
private Integer qualityScore;
/**
* 7×24 上报热力图数据,每项为 [星期几(0=周日~6=周六), 小时(0~23), 数据量]
* 用于 ECharts heatmap 的 data 字段
*/
private List<List<Integer>> heatmapData;
@Data
public static class MissingSegment {
/** 缺数段开始时间(上一条记录时间) */
private String gapStart;
/** 缺数段结束时间(下一条记录时间) */
private String gapEnd;
/** 缺数时长(分钟) */
private Long durationMinutes;
}
}

View File

@ -0,0 +1,41 @@
package com.fastbee.iot.model;
import lombok.Data;
/**
* 设备物模型统计指标 VO
* 包含查询时间范围内某一标识符的最大/最小/均值/累计值/数据量/最新值
*/
@Data
public class DeviceStatsVO {
/** 标识符 */
private String identifier;
/** 物模型名称 */
private String modelName;
/** 所属设备序列号(网关子设备与网关本身 SN 不同) */
private String serialNumber;
/** 最大值(仅数值型属性有效) */
private String maxValue;
/** 最小值(仅数值型属性有效) */
private String minValue;
/** 平均值(仅数值型属性有效) */
private String avgValue;
/** 累计值(仅数值型属性有效) */
private String sumValue;
/** 数据量(时间范围内上报次数) */
private Long dataCount;
/** 最新值(时间范围内最后一条记录的值) */
private String lastValue;
/** 最新值对应的时间 */
private String lastTime;
}

View File

@ -0,0 +1,42 @@
package com.fastbee.iot.model;
import lombok.Data;
import java.util.List;
/**
* 设备物模型值分布直方图 VO
*/
@Data
public class HistogramVO {
/** 标识符 */
private String identifier;
/** 物模型名称 */
private String modelName;
/** 所属设备序列号 */
private String serialNumber;
/** 分桶列表 */
private List<Bucket> buckets;
/** 非数值条数(布尔/枚举/字符串类型) */
private Long nonNumericCount;
/** 非数值枚举分布(key=值, value=次数),用于布尔/枚举类型 */
private java.util.Map<String, Long> enumDistribution;
@Data
public static class Bucket {
/** 区间标签,如 "18.00 ~ 20.00" */
private String label;
/** 区间下界(含) */
private Double rangeMin;
/** 区间上界(不含,最后一个桶含) */
private Double rangeMax;
/** 落在该区间的数据量 */
private Long count;
}
}

View File

@ -3,7 +3,10 @@ package com.fastbee.iot.service;
import com.alibaba.fastjson2.JSONObject;
import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.iot.model.AlertCountVO;
import com.fastbee.iot.model.DataQualityVO;
import com.fastbee.iot.model.DeviceHistoryParam;
import com.fastbee.iot.model.DeviceStatsVO;
import com.fastbee.iot.model.HistogramVO;
import com.fastbee.iot.model.SubDeviceCompareParam;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.ThingsModelLogCountVO;
@ -100,4 +103,31 @@ public interface DataCenterService {
* @return 仅计算物模型的 DTO 列表
*/
List<ThingsModelDTO> listGatewayCalculatedModels(Long gwDeviceId);
/**
* 查询设备物模型在时间范围内的统计指标(最大/最小/均值/累计/数据量/最新值)
* 支持网关设备(自动按子设备 SN 分组查询)
*
* @param param deviceId、serialNumber、identifierList(可选)、beginTime、endTime
* @return 每个标识符对应的统计指标列表
*/
List<DeviceStatsVO> deviceStats(DeviceHistoryParam param);
/**
* 数据质量分析:上报间隔估算、缺数时间段、连续重复值检测、质量评分
* 支持网关设备(自动按子设备 SN 分组查询)
*
* @param param deviceId、serialNumber、identifierList(可选)、beginTime、endTime
* @return 每个标识符对应的数据质量报告
*/
List<DataQualityVO> deviceDataQuality(DeviceHistoryParam param);
/**
* 值分布直方图:按标识符统计值在各区间的频次分布
* 支持网关设备(自动按子设备 SN 分组查询)
*
* @param param deviceId、serialNumber、identifierList(可选)、beginTime、endTime
* @return 每个标识符对应的直方图数据
*/
List<HistogramVO> deviceHistogram(DeviceHistoryParam param);
}

View File

@ -14,6 +14,10 @@ import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.model.param.DataCenterParam;
import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
import com.fastbee.iot.service.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
@ -59,60 +63,82 @@ public class DataCenterServiceImpl implements DataCenterService {
@Override
public List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam) {
List<JSONObject> resultList = new ArrayList<>();
List<DeviceHistoryParam.IdentifierVO> identifierVOList = new ArrayList<>();
// snToIdentifiers: 每个 serialNumber 对应该 SN 下需要查询的标识符列表
// 对于普通设备只有一个 SN;对于网关设备,自身与各子设备 SN 不同,需分别查询
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers = new LinkedHashMap<>();
List<String> allIdentifiers = new ArrayList<>();
if (CollectionUtils.isEmpty(deviceHistoryParam.getIdentifierList())) {
// 未指定标识符时,从 listThingsModel 获取(已携带每个物模型真实的 serialNumber)
List<ThingsModelDTO> list = deviceService.listThingsModel(deviceHistoryParam.getDeviceId());
for (ThingsModelDTO thingsModelDTO : list) {
DeviceHistoryParam.IdentifierVO identifierVO = new DeviceHistoryParam.IdentifierVO();
identifierVO.setIdentifier(thingsModelDTO.getIdentifier());
identifierVO.setType(thingsModelDTO.getType());
identifierVOList.add(identifierVO);
for (ThingsModelDTO dto : list) {
DeviceHistoryParam.IdentifierVO vo = new DeviceHistoryParam.IdentifierVO();
vo.setIdentifier(dto.getIdentifier());
vo.setType(dto.getType());
String sn = StringUtils.isNotEmpty(dto.getSerialNumber())
? dto.getSerialNumber()
: deviceHistoryParam.getSerialNumber();
snToIdentifiers.computeIfAbsent(sn, k -> new ArrayList<>()).add(vo);
if (!allIdentifiers.contains(dto.getIdentifier())) {
allIdentifiers.add(dto.getIdentifier());
}
}
} else {
identifierVOList = deviceHistoryParam.getIdentifierList();
// 显式传入标识符列表时统一使用请求中的 serialNumber
snToIdentifiers.put(deviceHistoryParam.getSerialNumber(), deviceHistoryParam.getIdentifierList());
deviceHistoryParam.getIdentifierList()
.forEach(vo -> allIdentifiers.add(vo.getIdentifier()));
}
if (CollectionUtils.isEmpty(identifierVOList)) {
return resultList;
if (snToIdentifiers.isEmpty()) {
return new ArrayList<>();
}
List<String> identifierList = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier)
.collect(Collectors.toList());
List<HistoryModel> historyModelList = new ArrayList<>();
for (Map.Entry<String, List<DeviceHistoryParam.IdentifierVO>> entry : snToIdentifiers.entrySet()) {
String sn = entry.getKey();
List<DeviceHistoryParam.IdentifierVO> identifierVOList = entry.getValue();
List<String> propertyIdentifierList = identifierVOList.stream().filter(t -> 1 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(propertyIdentifierList)) {
DeviceLog deviceLog = new DeviceLog();
deviceLog.setIdentityList(propertyIdentifierList);
deviceLog.setSerialNumber(deviceHistoryParam.getSerialNumber());
deviceLog.setSerialNumber(sn);
deviceLog.setBeginTime(deviceHistoryParam.getBeginTime());
deviceLog.setEndTime(deviceHistoryParam.getEndTime());
historyModelList.addAll(deviceLogService.listHistory(deviceLog));
}
List<String> functionIdentifierList = identifierVOList.stream().filter(t -> 2 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(functionIdentifierList)) {
FunctionLog functionLog = new FunctionLog();
functionLog.setIdentifyList(functionIdentifierList);
functionLog.setSerialNumber(deviceHistoryParam.getSerialNumber());
functionLog
.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime()));
functionLog.setEndTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getEndTime()));
functionLog.setSerialNumber(sn);
functionLog.setBeginTime(
DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime()));
functionLog.setEndTime(
DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getEndTime()));
historyModelList.addAll(functionLogService.listHistory(functionLog));
}
List<String> eventIdentifierList = identifierVOList.stream().filter(t -> 3 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(eventIdentifierList)) {
EventLog eventLog = new EventLog();
eventLog.setIdentityList(eventIdentifierList);
eventLog.setSerialNumber(deviceHistoryParam.getSerialNumber());
eventLog.setSerialNumber(sn);
Map<String, Object> params = new HashMap<>(2);
params.put("beginTime", deviceHistoryParam.getBeginTime());
params.put("endTime", deviceHistoryParam.getEndTime());
eventLog.setParams(params);
historyModelList.addAll(eventLogService.listHistory(eventLog));
}
}
historyModelList.sort(Comparator.comparing(HistoryModel::getTime));
return this.handleData(identifierList, historyModelList);
return this.handleData(allIdentifiers, historyModelList);
}
private List<JSONObject> handleData(List<String> identifierList, List<HistoryModel> historyModelList) {
@ -510,10 +536,353 @@ public class DataCenterServiceImpl implements DataCenterService {
return result;
}
@Override
public List<DataQualityVO> deviceDataQuality(DeviceHistoryParam param) {
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers = new LinkedHashMap<>();
Map<String, String> identifierToModelName = new LinkedHashMap<>();
Map<String, String> identifierToSn = new LinkedHashMap<>();
buildSnGrouping(param, snToIdentifiers, identifierToModelName, identifierToSn);
if (snToIdentifiers.isEmpty()) {
return new ArrayList<>();
}
// 按 identifier 聚合原始历史(仅属性类型)
Map<String, List<HistoryModel>> rawByIdentifier = fetchRawByIdentifier(param, snToIdentifiers, true, false);
List<DataQualityVO> result = new ArrayList<>();
for (Map.Entry<String, String> entry : identifierToSn.entrySet()) {
String identifier = entry.getKey();
List<HistoryModel> hList = rawByIdentifier.getOrDefault(identifier, new ArrayList<>());
// 按时间升序排列
hList.sort(Comparator.comparing(HistoryModel::getTime, Comparator.nullsLast(Comparator.naturalOrder())));
DataQualityVO vo = new DataQualityVO();
vo.setIdentifier(identifier);
vo.setModelName(identifierToModelName.getOrDefault(identifier, identifier));
vo.setSerialNumber(entry.getValue());
vo.setTotalCount((long) hList.size());
if (hList.size() >= 2) {
// 计算所有相邻记录的时间差(秒)
List<Long> gaps = new ArrayList<>();
for (int i = 1; i < hList.size(); i++) {
Date t1 = hList.get(i - 1).getTime();
Date t2 = hList.get(i).getTime();
if (t1 != null && t2 != null) {
gaps.add((t2.getTime() - t1.getTime()) / 1000L);
}
}
if (!gaps.isEmpty()) {
// 平均上报间隔(秒)
long avgGap = (long) gaps.stream().mapToLong(Long::longValue).average().orElse(0);
vo.setAvgIntervalSeconds(avgGap);
// 缺数检测阈值:最大取平均间隔的5倍或5分钟,最小不超过1小时
long threshold = Math.max(avgGap * 5, 300L);
threshold = Math.min(threshold, 3600L);
List<DataQualityVO.MissingSegment> segments = new ArrayList<>();
long totalMissingSeconds = 0;
for (int i = 0; i < gaps.size(); i++) {
if (gaps.get(i) > threshold) {
DataQualityVO.MissingSegment seg = new DataQualityVO.MissingSegment();
seg.setGapStart(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, hList.get(i).getTime()));
seg.setGapEnd(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, hList.get(i + 1).getTime()));
seg.setDurationMinutes(gaps.get(i) / 60);
segments.add(seg);
totalMissingSeconds += gaps.get(i);
}
}
vo.setMissingSegments(segments);
vo.setMissingCount(segments.size());
vo.setTotalMissingMinutes(totalMissingSeconds / 60);
}
} else {
vo.setMissingSegments(new ArrayList<>());
vo.setMissingCount(0);
vo.setTotalMissingMinutes(0L);
}
// 连续重复值检测
long repeatCount = 0;
for (int i = 1; i < hList.size(); i++) {
String prev = hList.get(i - 1).getValue();
String curr = hList.get(i).getValue();
if (prev != null && prev.equals(curr)) {
repeatCount++;
}
}
vo.setRepeatCount(repeatCount);
if (hList.size() > 1) {
vo.setRepeatRate(Math.round(repeatCount * 10000.0 / (hList.size() - 1)) / 100.0);
} else {
vo.setRepeatRate(0.0);
}
// 质量评分(0~100):缺数越多扣分,重复率越高扣分
int score = 100;
if (vo.getMissingCount() != null && vo.getMissingCount() > 0) {
// 每段缺数扣5分,最多扣50分
score -= Math.min(vo.getMissingCount() * 5, 50);
}
if (vo.getRepeatRate() != null && vo.getRepeatRate() > 5) {
score -= (int) Math.min((vo.getRepeatRate() - 5) / 2, 30);
}
vo.setQualityScore(Math.max(score, 0));
// 7×24 热力图:[dayOfWeek(0-6), hour(0-23), count]
int[][] heatmap = new int[7][24];
Calendar cal = Calendar.getInstance();
for (HistoryModel h : hList) {
if (h.getTime() != null) {
cal.setTime(h.getTime());
int dow = cal.get(Calendar.DAY_OF_WEEK) - 1; // 0=周日
int hour = cal.get(Calendar.HOUR_OF_DAY);
heatmap[dow][hour]++;
}
}
List<List<Integer>> heatmapData = new ArrayList<>();
for (int d = 0; d < 7; d++) {
for (int h = 0; h < 24; h++) {
if (heatmap[d][h] > 0) {
List<Integer> point = new ArrayList<>();
point.add(h); // ECharts x = 小时
point.add(d); // ECharts y = 星期
point.add(heatmap[d][h]);
heatmapData.add(point);
}
}
}
vo.setHeatmapData(heatmapData);
result.add(vo);
}
return result;
}
@Override
public List<HistogramVO> deviceHistogram(DeviceHistoryParam param) {
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers = new LinkedHashMap<>();
Map<String, String> identifierToModelName = new LinkedHashMap<>();
Map<String, String> identifierToSn = new LinkedHashMap<>();
buildSnGrouping(param, snToIdentifiers, identifierToModelName, identifierToSn);
if (snToIdentifiers.isEmpty()) {
return new ArrayList<>();
}
Map<String, List<HistoryModel>> rawByIdentifier = fetchRawByIdentifier(param, snToIdentifiers, true, false);
List<HistogramVO> result = new ArrayList<>();
for (Map.Entry<String, String> entry : identifierToSn.entrySet()) {
String identifier = entry.getKey();
List<HistoryModel> hList = rawByIdentifier.getOrDefault(identifier, new ArrayList<>());
HistogramVO vo = new HistogramVO();
vo.setIdentifier(identifier);
vo.setModelName(identifierToModelName.getOrDefault(identifier, identifier));
vo.setSerialNumber(entry.getValue());
// 尝试数值解析
List<Double> numericValues = new ArrayList<>();
Map<String, Long> enumDist = new LinkedHashMap<>();
long nonNumericCount = 0;
for (HistoryModel h : hList) {
if (h.getValue() == null || h.getValue().trim().isEmpty()) {
continue;
}
try {
numericValues.add(Double.parseDouble(h.getValue().trim()));
} catch (NumberFormatException e) {
nonNumericCount++;
enumDist.merge(h.getValue().trim(), 1L, (a, b) -> a + b);
}
}
vo.setNonNumericCount(nonNumericCount);
if (!numericValues.isEmpty()) {
double min = numericValues.stream().mapToDouble(Double::doubleValue).min().orElse(0);
double max = numericValues.stream().mapToDouble(Double::doubleValue).max().orElse(0);
int bucketCount = 10;
double range = max - min;
// 若所有值相同,设一个区间宽度
double bucketWidth = (range == 0) ? 1.0 : range / bucketCount;
long[] counts = new long[bucketCount];
for (double v : numericValues) {
int idx = (range == 0) ? 0 : (int) Math.min((v - min) / bucketWidth, bucketCount - 1);
counts[idx]++;
}
List<HistogramVO.Bucket> buckets = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
HistogramVO.Bucket bucket = new HistogramVO.Bucket();
double lo = min + i * bucketWidth;
double hi = min + (i + 1) * bucketWidth;
bucket.setRangeMin(Math.round(lo * 100.0) / 100.0);
bucket.setRangeMax(Math.round(hi * 100.0) / 100.0);
bucket.setLabel(String.format("%.2f~%.2f", lo, hi));
bucket.setCount(counts[i]);
buckets.add(bucket);
}
vo.setBuckets(buckets);
} else if (!enumDist.isEmpty()) {
// 全部是枚举/布尔值,返回枚举分布
vo.setEnumDistribution(enumDist);
vo.setBuckets(new ArrayList<>());
} else {
vo.setBuckets(new ArrayList<>());
}
result.add(vo);
}
return result;
}
/**
* 公共辅助:按 SN 分组,构建 snToIdentifiers / identifierToModelName / identifierToSn
*/
private void buildSnGrouping(DeviceHistoryParam param,
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers,
Map<String, String> identifierToModelName,
Map<String, String> identifierToSn) {
if (CollectionUtils.isEmpty(param.getIdentifierList())) {
List<ThingsModelDTO> list = deviceService.listThingsModel(param.getDeviceId());
for (ThingsModelDTO dto : list) {
String sn = StringUtils.isNotEmpty(dto.getSerialNumber())
? dto.getSerialNumber() : param.getSerialNumber();
DeviceHistoryParam.IdentifierVO vo = new DeviceHistoryParam.IdentifierVO();
vo.setIdentifier(dto.getIdentifier());
vo.setType(dto.getType());
snToIdentifiers.computeIfAbsent(sn, k -> new ArrayList<>()).add(vo);
identifierToModelName.putIfAbsent(dto.getIdentifier(), dto.getModelName());
identifierToSn.putIfAbsent(dto.getIdentifier(), sn);
}
} else {
snToIdentifiers.put(param.getSerialNumber(), param.getIdentifierList());
param.getIdentifierList().forEach(vo -> {
identifierToSn.put(vo.getIdentifier(), param.getSerialNumber());
});
}
}
/**
* 公共辅助:按 snToIdentifiers 分组查询原始历史,返回 identifier -> List<HistoryModel>
*
* @param includeProperty 是否查属性日志(type=1)
* @param includeFunction 是否查功能日志(type=2)
*/
private Map<String, List<HistoryModel>> fetchRawByIdentifier(DeviceHistoryParam param,
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers,
boolean includeProperty, boolean includeFunction) {
Map<String, List<HistoryModel>> rawByIdentifier = new LinkedHashMap<>();
for (Map.Entry<String, List<DeviceHistoryParam.IdentifierVO>> entry : snToIdentifiers.entrySet()) {
String sn = entry.getKey();
List<DeviceHistoryParam.IdentifierVO> vos = entry.getValue();
if (includeProperty) {
List<String> propIds = vos.stream().filter(v -> Integer.valueOf(1).equals(v.getType()))
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(propIds)) {
DeviceLog dl = new DeviceLog();
dl.setSerialNumber(sn);
dl.setIdentityList(propIds);
dl.setBeginTime(param.getBeginTime());
dl.setEndTime(param.getEndTime());
for (HistoryModel h : deviceLogService.listHistory(dl)) {
rawByIdentifier.computeIfAbsent(h.getIdentity(), k -> new ArrayList<>()).add(h);
}
}
}
if (includeFunction) {
List<String> funcIds = vos.stream().filter(v -> Integer.valueOf(2).equals(v.getType()))
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(funcIds)) {
FunctionLog fl = new FunctionLog();
fl.setSerialNumber(sn);
fl.setIdentifyList(funcIds);
fl.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, param.getBeginTime()));
fl.setEndTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, param.getEndTime()));
for (HistoryModel h : functionLogService.listHistory(fl)) {
rawByIdentifier.computeIfAbsent(h.getIdentity(), k -> new ArrayList<>()).add(h);
}
}
}
}
return rawByIdentifier;
}
private Map<String, String> parseCrossDeviceRefs(String formula) {
return CalculateUtils.parseCrossDeviceRefs(formula);
}
@Override
public List<DeviceStatsVO> deviceStats(DeviceHistoryParam param) {
Map<String, List<DeviceHistoryParam.IdentifierVO>> snToIdentifiers = new LinkedHashMap<>();
Map<String, String> identifierToModelName = new LinkedHashMap<>();
Map<String, String> identifierToSn = new LinkedHashMap<>();
buildSnGrouping(param, snToIdentifiers, identifierToModelName, identifierToSn);
if (snToIdentifiers.isEmpty()) {
return new ArrayList<>();
}
Map<String, List<HistoryModel>> rawByIdentifier = fetchRawByIdentifier(param, snToIdentifiers, true, true);
// 计算每个 identifier 的统计指标
List<DeviceStatsVO> result = new ArrayList<>();
for (Map.Entry<String, String> entry : identifierToSn.entrySet()) {
String identifier = entry.getKey();
String sn = entry.getValue();
List<HistoryModel> hList = rawByIdentifier.getOrDefault(identifier, new ArrayList<>());
DeviceStatsVO vo = new DeviceStatsVO();
vo.setIdentifier(identifier);
vo.setModelName(identifierToModelName.getOrDefault(identifier, identifier));
vo.setSerialNumber(sn);
vo.setDataCount((long) hList.size());
if (!hList.isEmpty()) {
// listHistory 返回 desc 排序,第一条即最新
HistoryModel latest = hList.get(0);
vo.setLastValue(latest.getValue());
if (latest.getTime() != null) {
vo.setLastTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, latest.getTime()));
}
// 数值统计
BigDecimal max = null, min = null, sum = BigDecimal.ZERO;
int numCount = 0;
for (HistoryModel h : hList) {
if (h.getValue() == null || h.getValue().isEmpty()) {
continue;
}
try {
BigDecimal v = new BigDecimal(h.getValue().trim());
if (max == null || v.compareTo(max) > 0) {
max = v;
}
if (min == null || v.compareTo(min) < 0) {
min = v;
}
sum = sum.add(v);
numCount++;
} catch (NumberFormatException ignored) {
// 非数值型(布尔、枚举、字符串等)跳过数值计算
}
}
if (numCount > 0) {
vo.setMaxValue(stripTrailingZeros(max));
vo.setMinValue(stripTrailingZeros(min));
vo.setAvgValue(stripTrailingZeros(sum.divide(BigDecimal.valueOf(numCount), 4, RoundingMode.HALF_UP)));
vo.setSumValue(stripTrailingZeros(sum));
}
}
result.add(vo);
}
return result;
}
private String stripTrailingZeros(BigDecimal value) {
if (value == null) {
return null;
}
return value.stripTrailingZeros().toPlainString();
}
private List<Date> buildMinuteSlots(String beginTime, String endTime) {
Date start = DateUtils.parseDate(beginTime);
Date end = DateUtils.parseDate(endTime);

View File

@ -45,6 +45,33 @@ export function getDataCenterCountThingsModelInvoke(query) {
});
}
// 查询设备物模型统计指标(最大/最小/均值/累计/数据量/最新值)
export function getDataCenterDeviceStats(data) {
return request({
url: '/data/center/deviceStats',
method: 'post',
data: data,
});
}
// 数据质量分析(上报间隔、缺数时间段、重复值检测、质量评分、7×24热力图)
export function getDataCenterDeviceDataQuality(data) {
return request({
url: '/data/center/deviceDataQuality',
method: 'post',
data: data,
});
}
// 值分布直方图
export function getDataCenterDeviceHistogram(data) {
return request({
url: '/data/center/deviceHistogram',
method: 'post',
data: data,
});
}
// 导出设备历史数据Excel
export function exportDeviceHistory(data) {
return request({

File diff suppressed because it is too large Load Diff