diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java index 8d6a791784..3b311f93a6 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java @@ -16,6 +16,7 @@ import java.util.Arrays; @AllArgsConstructor public enum DateIntervalEnum implements ArrayValuable { + HOUR(0, "小时"), DAY(1, "天"), WEEK(2, "周"), MONTH(3, "月"), diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java index a9c195656c..160e91f707 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java @@ -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 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))); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java index 15d2abccc6..d431ec0b60 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java @@ -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> upstreamCounts; - @Schema(description = "每小时下行数据数量统计") + @Schema(description = "下行数据数量统计") private List> downstreamCounts; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java deleted file mode 100644 index 741f77f3a4..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java +++ /dev/null @@ -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; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java index 785af92551..42855b4841 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -90,6 +90,7 @@ public interface IotDeviceMapper extends BaseMapperX { * @return 设备数量统计列表 */ // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! + // TODO 讨论:试了一下 mybatis-plus 如:select("state as `key`", "count(1) as `value`") 有点啰嗦感觉不如放在 xml 里,但是放 xml 里又会增加冗余 List> selectDeviceCountMapByProductId(); // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java index 96741e6095..a3ee65243f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java @@ -59,6 +59,7 @@ public interface IotDeviceLogMapper { // TODO @super:1)上行、下行,不写在 mapper 里,而是通过参数传递,这样,selectDeviceLogUpCountByHour、selectDeviceLogDownCountByHour 可以合并; // TODO @super:2)不能只基于 identifier 来计算,而是要 type + identifier 成对 + // TODO 感觉 type + identifier 这块目前还没固定 这里打算等后续等大体不变了再参照艿哥的建议方式修改 /** * 查询每个小时设备上行消息数量 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java index b79732911d..cd27f89b0e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java @@ -48,28 +48,30 @@ public interface IotDeviceLogService { Long getDeviceLogCount(@Nullable LocalDateTime createTime); // TODO @super:deviceKey 是不是用不上哈? + // TODO 讨论:这个 deviceKey 是打算用来查看每个设备上下行数据时的预留参数 /** - * 获得每个小时设备上行消息数量统计 + * 获得每个小时设备消息数量统计 * + * @param upstream 消息上下行标识 * @param deviceKey 设备标识 * @param startTime 开始时间 * @param endTime 结束时间 * @return key: 时间戳, value: 消息数量 */ - List> getDeviceLogUpCountByHour(@Nullable String deviceKey, + List> getDeviceLogCountByHour(@Nullable Boolean upstream, @Nullable String deviceKey, @Nullable Long startTime, @Nullable Long endTime); - /** - * 获得每个小时设备下行消息数量统计 - * - * @param deviceKey 设备标识 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return key: 时间戳, value: 消息数量 - */ - List> getDeviceLogDownCountByHour(@Nullable String deviceKey, - @Nullable Long startTime, - @Nullable Long endTime); +// /** +// * 获得每个小时设备下行消息数量统计 +// * +// * @param deviceKey 设备标识 +// * @param startTime 开始时间 +// * @param endTime 结束时间 +// * @return key: 时间戳, value: 消息数量 +// */ +// List> getDeviceLogDownCountByHour(@Nullable String deviceKey, +// @Nullable Long startTime, +// @Nullable Long endTime); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java index 1df4d4cd44..28403c727c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java @@ -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 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 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 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 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 upstream:true 上行,false 下行,null 不过滤 + // TODO 这块后续和 mapper 方法一起再做进一步改进 @Override - public List> getDeviceLogUpCountByHour(String deviceKey, Long startTime, Long endTime) { + public List> getDeviceLogCountByHour(Boolean upstream, String deviceKey, Long startTime, Long endTime) { // TODO @super:不能只基于数据库统计。因为有一些小时,可能出现没数据的情况,导致前端展示的图是不全的。可以参考 CrmStatisticsCustomerService 来实现 - List> 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); - }) - .collect(Collectors.toList()); + // TODO 讨论:因为 tdengine 的时间数据要转为时间戳,所有没有复用 CrmStatisticsCustomerService 中调用的 utils 方法 还有目前时间戳与 LocalDateTime 的转换还是比较混乱,艿哥这里要不要统一全转成 LocalDateTime 输出,现在是将时间戳交给前端去处理了 + + // 获取数据库中的统计数据 + List> list = null; + if(upstream == true){ + list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime); + } + else { + list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime); + } + + // 将数据库返回的结果转换成Map结构 + Map 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 allDailyTimestamps = generateDailyTimestamps(startTime, endTime); + + // 将小时数据按天进行合并 + Map 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 allHourlyTimestamps = generateHourlyTimestamps(startTime, endTime); + + // 确保每个小时都有数据,没有的填充0 + return allHourlyTimestamps.stream() + .map(timestamp -> Map.of(timestamp, hourlyCountMap.getOrDefault(timestamp, 0))) + .collect(Collectors.toList()); + } } // TODO @super:getDeviceLogDownCountByHour 融合到 getDeviceLogUpCountByHour - @Override - public List> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) { - List> 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> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) { +// // 获取数据库中的统计数据 +// List> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime); +// Map hourlyCountMap = list.stream() +// .collect(Collectors.toMap( +// map -> ((Timestamp) map.get("time")).getTime(), +// map -> ((Number) map.get("data")).intValue() +// )); +// +// // 生成所有小时的时间戳列表 +// List allHourlyTimestamps = generateHourlyTimestamps(startTime, endTime); +// +// // 确保每个小时都有数据,没有的填充0 +// return allHourlyTimestamps.stream() +// .map(timestamp -> Map.of(timestamp, hourlyCountMap.getOrDefault(timestamp, 0))) +// .collect(Collectors.toList()); +// } }