This commit is contained in:
alwayssuper 2025-04-16 11:08:33 +08:00
parent d83af87f9f
commit 5832600d0b
8 changed files with 188 additions and 69 deletions

View File

@ -16,6 +16,7 @@ import java.util.Arrays;
@AllArgsConstructor
public enum DateIntervalEnum implements ArrayValuable<Integer> {
HOUR(0, "小时"),
DAY(1, ""),
WEEK(2, ""),
MONTH(3, ""),

View File

@ -1,8 +1,9 @@
package cn.iocoder.yudao.module.iot.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@ -10,18 +11,24 @@ import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService;
import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Tag(name = "管理后台 - IoT 数据统计")
@RestController
@ -49,7 +56,7 @@ public class IotStatisticsController {
respVO.setDeviceMessageCount(deviceLogService.getDeviceLogCount(null));
// 1.2 获取今日新增数量
// TODO @super使用 LocalDateTimeUtils.getToday()
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
LocalDateTime todayStart = LocalDateTimeUtils.getToday();
respVO.setProductCategoryTodayCount(productCategoryService.getProductCategoryCount(todayStart));
respVO.setProductTodayCount(productService.getProductCount(todayStart));
respVO.setDeviceTodayCount(deviceService.getDeviceCount(todayStart));
@ -70,10 +77,16 @@ public class IotStatisticsController {
@GetMapping("/get-log-summary")
@Operation(summary = "获取 IoT 设备上下行消息数据统计")
public CommonResult<IotStatisticsDeviceMessageSummaryRespVO> getIotStatisticsDeviceMessageSummary(
@Valid IotStatisticsReqVO reqVO) {
@Parameter(description = "查询起始时间戳(毫秒)", required = true, example = "1658460600000") @RequestParam(required = true) Long startTime,
@Parameter(description = "查询结束时间戳(毫秒)", required = true, example = "1754888399000") @RequestParam(required = true) Long endTime) {
// 当时间范围过大 前端图表组件会产生问题 所以不以小时返回 以天返回 判断时间跨度是否大于30天
long thirtyDaysInMillis = TimeUnit.DAYS.toMillis(30);
boolean isLongTimeSpan = (endTime - startTime) > thirtyDaysInMillis;
return success(new IotStatisticsDeviceMessageSummaryRespVO()
.setDownstreamCounts(deviceLogService.getDeviceLogUpCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime()))
.setDownstreamCounts((deviceLogService.getDeviceLogDownCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime()))));
.setStatType(isLongTimeSpan ? DateIntervalEnum.DAY.getInterval() : DateIntervalEnum.HOUR.getInterval())
.setUpstreamCounts(deviceLogService.getDeviceLogCountByHour(true, null, startTime, endTime))
.setDownstreamCounts(deviceLogService.getDeviceLogCountByHour(false, null, startTime, endTime)));
}
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo;
import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -10,10 +12,14 @@ import java.util.Map;
@Data
public class IotStatisticsDeviceMessageSummaryRespVO {
@Schema(description = "每小时上行数据数量统计")
@Schema(description = "时间间隔类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@InEnum(value = DateIntervalEnum.class, message = "时间间隔类型")
private Integer statType;
@Schema(description = "上行数据数量统计")
private List<Map<Long, Integer>> upstreamCounts;
@Schema(description = "每小时下行数据数量统计")
@Schema(description = "下行数据数量统计")
private List<Map<Long, Integer>> downstreamCounts;
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - IoT 统计 Request VO")
@Data
public class IotStatisticsReqVO {
// TODO @super前端传递的时候还是通过 startTime endTime 传递后端转成 Long
@Schema(description = "查询起始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1658486600000")
@NotNull(message = "查询起始时间不能为空")
private Long startTime;
@Schema(description = "查询结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1758486600000")
@NotNull(message = "查询结束时间不能为空")
private Long endTime;
}

View File

@ -90,6 +90,7 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
* @return 设备数量统计列表
*/
// TODO @super通过 mybatis-plus 来写哈然后返回 Map 貌似就行了
// TODO 讨论试了一下 mybatis-plus select("state as `key`", "count(1) as `value`") 有点啰嗦感觉不如放在 xml 但是放 xml 里又会增加冗余
List<Map<String, Object>> selectDeviceCountMapByProductId();
// TODO @super通过 mybatis-plus 来写哈然后返回 Map 貌似就行了

View File

@ -59,6 +59,7 @@ public interface IotDeviceLogMapper {
// TODO @super1上行下行不写在 mapper 而是通过参数传递这样selectDeviceLogUpCountByHourselectDeviceLogDownCountByHour 可以合并
// TODO @super2不能只基于 identifier 来计算而是要 type + identifier 成对
// TODO 感觉 type + identifier 这块目前还没固定 这里打算等后续等大体不变了再参照艿哥的建议方式修改
/**
* 查询每个小时设备上行消息数量
*/

View File

@ -48,28 +48,30 @@ public interface IotDeviceLogService {
Long getDeviceLogCount(@Nullable LocalDateTime createTime);
// TODO @superdeviceKey 是不是用不上哈
// TODO 讨论这个 deviceKey 是打算用来查看每个设备上下行数据时的预留参数
/**
* 获得每个小时设备上行消息数量统计
* 获得每个小时设备消息数量统计
*
* @param upstream 消息上下行标识
* @param deviceKey 设备标识
* @param startTime 开始时间
* @param endTime 结束时间
* @return key: 时间戳, value: 消息数量
*/
List<Map<Long, Integer>> getDeviceLogUpCountByHour(@Nullable String deviceKey,
List<Map<Long, Integer>> getDeviceLogCountByHour(@Nullable Boolean upstream, @Nullable String deviceKey,
@Nullable Long startTime,
@Nullable Long endTime);
/**
* 获得每个小时设备下行消息数量统计
*
* @param deviceKey 设备标识
* @param startTime 开始时间
* @param endTime 结束时间
* @return key: 时间戳, value: 消息数量
*/
List<Map<Long, Integer>> getDeviceLogDownCountByHour(@Nullable String deviceKey,
@Nullable Long startTime,
@Nullable Long endTime);
// /**
// * 获得每个小时设备下行消息数量统计
// *
// * @param deviceKey 设备标识
// * @param startTime 开始时间
// * @param endTime 结束时间
// * @return key: 时间戳, value: 消息数量
// */
// List<Map<Long, Integer>> getDeviceLogDownCountByHour(@Nullable String deviceKey,
// @Nullable Long startTime,
// @Nullable Long endTime);
}

View File

@ -18,9 +18,12 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@ -75,37 +78,150 @@ public class IotDeviceLogServiceImpl implements IotDeviceLogService {
return deviceLogMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null);
}
// TODO:这俩方法看看后续要不要抽到 utils
/**
* 根据起始和结束时间戳生成每小时时间戳的列表
*
* @param startTimestamp 开始时间戳毫秒
* @param endTimestamp 结束时间戳毫秒
* @return 每小时时间戳的列表
*/
public List<Long> generateHourlyTimestamps(Long startTimestamp, Long endTimestamp) {
// 转换为Instant
Instant startInstant = Instant.ofEpochMilli(startTimestamp);
Instant endInstant = Instant.ofEpochMilli(endTimestamp);
// 将起始时间调整为整点小时
Instant alignedStart = startInstant.truncatedTo(ChronoUnit.HOURS);
// 计算需要多少个小时
long hoursBetween = ChronoUnit.HOURS.between(alignedStart, endInstant);
// 如果结束时间有分钟秒的部分需要多加一个小时
if (!endInstant.equals(endInstant.truncatedTo(ChronoUnit.HOURS))) {
hoursBetween++;
}
// 生成每小时时间戳列表
List<Long> hourlyTimestamps = new ArrayList<>();
for (int i = 0; i <= hoursBetween; i++) {
Instant currentHour = alignedStart.plus(i, ChronoUnit.HOURS);
hourlyTimestamps.add(currentHour.toEpochMilli());
}
return hourlyTimestamps;
}
/**
* 根据起始和结束时间戳生成每天时间戳的列表
*
* @param startTimestamp 开始时间戳毫秒
* @param endTimestamp 结束时间戳毫秒
* @return 每天时间戳的列表
*/
public List<Long> generateDailyTimestamps(Long startTimestamp, Long endTimestamp) {
// 转换为Instant
Instant startInstant = Instant.ofEpochMilli(startTimestamp);
Instant endInstant = Instant.ofEpochMilli(endTimestamp);
// 将起始时间调整为一天的开始
Instant alignedStart = startInstant.truncatedTo(ChronoUnit.DAYS);
// 计算需要多少天
long daysBetween = ChronoUnit.DAYS.between(alignedStart, endInstant);
// 如果结束时间不是在一天的开始需要多加一天
if (!endInstant.equals(endInstant.truncatedTo(ChronoUnit.DAYS))) {
daysBetween++;
}
// 生成每天时间戳列表
List<Long> dailyTimestamps = new ArrayList<>();
for (int i = 0; i <= daysBetween; i++) {
Instant currentDay = alignedStart.plus(i, ChronoUnit.DAYS);
dailyTimestamps.add(currentDay.toEpochMilli());
}
return dailyTimestamps;
}
// TODO @super加一个参数Boolean upstreamtrue 上行false 下行null 不过滤
// TODO 这块后续和 mapper 方法一起再做进一步改进
@Override
public List<Map<Long, Integer>> getDeviceLogUpCountByHour(String deviceKey, Long startTime, Long endTime) {
public List<Map<Long, Integer>> getDeviceLogCountByHour(Boolean upstream, String deviceKey, Long startTime, Long endTime) {
// TODO @super不能只基于数据库统计因为有一些小时可能出现没数据的情况导致前端展示的图是不全的可以参考 CrmStatisticsCustomerService 来实现
List<Map<String, Object>> list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime);
return list.stream()
.map(map -> {
// 从Timestamp获取时间戳
Timestamp timestamp = (Timestamp) map.get("time");
Long timeMillis = timestamp.getTime();
// 消息数量转换
Integer count = ((Number) map.get("data")).intValue();
return Map.of(timeMillis, count);
})
// TODO 讨论因为 tdengine 的时间数据要转为时间戳所有没有复用 CrmStatisticsCustomerService 中调用的 utils 方法 还有目前时间戳与 LocalDateTime 的转换还是比较混乱艿哥这里要不要统一全转成 LocalDateTime 输出现在是将时间戳交给前端去处理了
// 获取数据库中的统计数据
List<Map<String, Object>> list = null;
if(upstream == true){
list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime);
}
else {
list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime);
}
// 将数据库返回的结果转换成Map结构
Map<Long, Integer> hourlyCountMap = list.stream()
.collect(Collectors.toMap(
map -> ((Timestamp) map.get("time")).getTime(),
map -> ((Number) map.get("data")).intValue()
));
// 当时间范围过大 前端图表组件会产生问题 所以以天返回 判断时间跨度是否大于30天30天 * 24小时 * 60分钟 * 60秒 * 1000毫秒
long thirtyDaysInMillis = TimeUnit.DAYS.toMillis(30);
boolean isLongTimeSpan = (endTime - startTime) > thirtyDaysInMillis;
if (isLongTimeSpan) {
//TODO: 这里的 SQL 等后续一块优化时 改成分查小时还是查天
// 按天统计 - 先生成每天的时间戳列表
List<Long> allDailyTimestamps = generateDailyTimestamps(startTime, endTime);
// 将小时数据按天进行合并
Map<Long, Integer> dailyCountMap = new HashMap<>();
// 遍历每个小时的数据将其归入对应的天
hourlyCountMap.forEach((hourTimestamp, count) -> {
// 将小时时间戳转为当天开始的时间戳
Instant hourInstant = Instant.ofEpochMilli(hourTimestamp);
Instant dayInstant = hourInstant.truncatedTo(ChronoUnit.DAYS);
Long dayTimestamp = dayInstant.toEpochMilli();
// 累加到对应的天
dailyCountMap.merge(dayTimestamp, count, Integer::sum);
});
// 确保每天都有数据没有的填充0
return allDailyTimestamps.stream()
.map(timestamp -> Map.of(timestamp, dailyCountMap.getOrDefault(timestamp, 0)))
.collect(Collectors.toList());
} else {
// 按小时统计 - 生成所有小时的时间戳列表
List<Long> allHourlyTimestamps = generateHourlyTimestamps(startTime, endTime);
// 确保每个小时都有数据没有的填充0
return allHourlyTimestamps.stream()
.map(timestamp -> Map.of(timestamp, hourlyCountMap.getOrDefault(timestamp, 0)))
.collect(Collectors.toList());
}
}
// TODO @supergetDeviceLogDownCountByHour 融合到 getDeviceLogUpCountByHour
@Override
public List<Map<Long, Integer>> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) {
List<Map<String, Object>> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime);
return list.stream()
.map(map -> {
// 从Timestamp获取时间戳
Timestamp timestamp = (Timestamp) map.get("time");
Long timeMillis = timestamp.getTime();
// 消息数量转换
Integer count = ((Number) map.get("data")).intValue();
return Map.of(timeMillis, count);
})
.collect(Collectors.toList());
}
// @Override
// public List<Map<Long, Integer>> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) {
// // 获取数据库中的统计数据
// List<Map<String, Object>> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime);
// Map<Long, Integer> hourlyCountMap = list.stream()
// .collect(Collectors.toMap(
// map -> ((Timestamp) map.get("time")).getTime(),
// map -> ((Number) map.get("data")).intValue()
// ));
//
// // 生成所有小时的时间戳列表
// List<Long> allHourlyTimestamps = generateHourlyTimestamps(startTime, endTime);
//
// // 确保每个小时都有数据没有的填充0
// return allHourlyTimestamps.stream()
// .map(timestamp -> Map.of(timestamp, hourlyCountMap.getOrDefault(timestamp, 0)))
// .collect(Collectors.toList());
// }
}