Commit 45889459b7f9f34b70ba3c071267c290905867e5
1 parent
af6e283f
✨ feat(im): 实现 im 的首页统计
Showing
10 changed files
with
687 additions
and
0 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
| ... | ... | @@ -236,6 +236,23 @@ public class LocalDateTimeUtils { |
| 236 | 236 | return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); |
| 237 | 237 | } |
| 238 | 238 | |
| 239 | + /** | |
| 240 | + * 获取最近 N 天的 0 点时刻序列(升序,含今天) | |
| 241 | + * <p> | |
| 242 | + * 例:getLatestDays(3) 返回 [前天 00:00, 昨天 00:00, 今天 00:00] | |
| 243 | + * | |
| 244 | + * @param days 天数(含今天) | |
| 245 | + * @return 升序的 LocalDateTime 列表 | |
| 246 | + */ | |
| 247 | + public static List<LocalDateTime> getLatestDays(int days) { | |
| 248 | + LocalDateTime today = getToday(); | |
| 249 | + List<LocalDateTime> dates = new ArrayList<>(days); | |
| 250 | + for (int i = days - 1; i >= 0; i--) { | |
| 251 | + dates.add(today.minusDays(i)); | |
| 252 | + } | |
| 253 | + return dates; | |
| 254 | + } | |
| 255 | + | |
| 239 | 256 | public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime, |
| 240 | 257 | LocalDateTime endTime, |
| 241 | 258 | Integer interval) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; | |
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo.*; | |
| 7 | +import cn.iocoder.yudao.module.im.service.statistics.ImStatisticsManagerService; | |
| 8 | +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; | |
| 9 | +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; | |
| 10 | +import io.swagger.v3.oas.annotations.Operation; | |
| 11 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 12 | +import io.swagger.v3.oas.annotations.tags.Tag; | |
| 13 | +import jakarta.annotation.Resource; | |
| 14 | +import jakarta.validation.constraints.Max; | |
| 15 | +import jakarta.validation.constraints.Min; | |
| 16 | +import org.springframework.security.access.prepost.PreAuthorize; | |
| 17 | +import org.springframework.validation.annotation.Validated; | |
| 18 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 19 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 20 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 21 | +import org.springframework.web.bind.annotation.RestController; | |
| 22 | + | |
| 23 | +import java.time.LocalDate; | |
| 24 | +import java.time.LocalDateTime; | |
| 25 | +import java.util.*; | |
| 26 | + | |
| 27 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 28 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | |
| 29 | + | |
| 30 | +@Tag(name = "管理后台 - IM 数据看板") | |
| 31 | +@RestController | |
| 32 | +@RequestMapping("/im/manager/statistics") | |
| 33 | +@Validated | |
| 34 | +public class ImStatisticsManagerController { | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 群规模分桶名称的展示顺序 | |
| 38 | + */ | |
| 39 | + private static final List<String> GROUP_SIZE_BUCKETS = Arrays.asList("1-9 人", "10-49 人", "50-199 人", "200+ 人"); | |
| 40 | + /** | |
| 41 | + * 看板分布默认时间窗口(天) | |
| 42 | + */ | |
| 43 | + private static final int DISTRIBUTION_WINDOW_DAYS = 30; | |
| 44 | + /** | |
| 45 | + * TOP 发送者数量 | |
| 46 | + */ | |
| 47 | + private static final int TOP_SENDER_LIMIT = 10; | |
| 48 | + | |
| 49 | + @Resource | |
| 50 | + private ImStatisticsManagerService statisticsService; | |
| 51 | + @Resource | |
| 52 | + private AdminUserApi adminUserApi; | |
| 53 | + | |
| 54 | + @GetMapping("/overview") | |
| 55 | + @Operation(summary = "获得数据概览") | |
| 56 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 57 | + public CommonResult<ImStatisticsManagerOverviewRespVO> getOverview() { | |
| 58 | + LocalDateTime todayBegin = LocalDate.now().atStartOfDay(); | |
| 59 | + LocalDateTime tomorrowBegin = todayBegin.plusDays(1); | |
| 60 | + LocalDateTime yesterdayBegin = todayBegin.minusDays(1); | |
| 61 | + // 周活/月活定义为 N 天滚动窗口(含今天) | |
| 62 | + LocalDateTime weekBegin = todayBegin.minusDays(6); | |
| 63 | + LocalDateTime monthBegin = todayBegin.minusDays(29); | |
| 64 | + return success(new ImStatisticsManagerOverviewRespVO() | |
| 65 | + .setTotalUser(statisticsService.getTotalUserCount()) | |
| 66 | + .setNewUserToday(statisticsService.getNewUserCount(todayBegin, tomorrowBegin)) | |
| 67 | + .setTotalGroup(statisticsService.getTotalGroupCount()) | |
| 68 | + .setNewGroupToday(statisticsService.getNewGroupCount(todayBegin, tomorrowBegin)) | |
| 69 | + .setActiveUserDaily(statisticsService.getActiveUserCount(todayBegin, tomorrowBegin)) | |
| 70 | + .setActiveUserWeekly(statisticsService.getActiveUserCount(weekBegin, tomorrowBegin)) | |
| 71 | + .setActiveUserMonthly(statisticsService.getActiveUserCount(monthBegin, tomorrowBegin)) | |
| 72 | + .setPrivateMessageToday(statisticsService.getPrivateMessageCount(todayBegin, tomorrowBegin)) | |
| 73 | + .setGroupMessageToday(statisticsService.getGroupMessageCount(todayBegin, tomorrowBegin)) | |
| 74 | + .setPrivateMessageYesterday(statisticsService.getPrivateMessageCount(yesterdayBegin, todayBegin)) | |
| 75 | + .setGroupMessageYesterday(statisticsService.getGroupMessageCount(yesterdayBegin, todayBegin))); | |
| 76 | + } | |
| 77 | + | |
| 78 | + @GetMapping("/message-trend") | |
| 79 | + @Operation(summary = "获得消息趋势(私聊 / 群聊双线)") | |
| 80 | + @Parameter(name = "days", description = "回看天数(含今日)", example = "7") | |
| 81 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 82 | + public CommonResult<ImStatisticsManagerTrendRespVO> getMessageTrend( | |
| 83 | + @RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) { | |
| 84 | + List<LocalDateTime> dates = LocalDateTimeUtils.getLatestDays(days); | |
| 85 | + LocalDateTime beginTime = dates.get(0); | |
| 86 | + LocalDateTime endTime = dates.get(days - 1).plusDays(1); | |
| 87 | + Map<LocalDateTime, Long> privateMap = statisticsService.getPrivateMessageDailyCountMap(beginTime, endTime); | |
| 88 | + Map<LocalDateTime, Long> groupMap = statisticsService.getGroupMessageDailyCountMap(beginTime, endTime); | |
| 89 | + // 转换格式 | |
| 90 | + Map<String, List<Long>> series = new LinkedHashMap<>(); | |
| 91 | + series.put("private", alignSeries(dates, privateMap)); | |
| 92 | + series.put("group", alignSeries(dates, groupMap)); | |
| 93 | + return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series)); | |
| 94 | + } | |
| 95 | + | |
| 96 | + @GetMapping("/user-trend") | |
| 97 | + @Operation(summary = "获得用户趋势(新增注册 / 日活双线)") | |
| 98 | + @Parameter(name = "days", description = "回看天数(含今日)", example = "7") | |
| 99 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 100 | + public CommonResult<ImStatisticsManagerTrendRespVO> getUserTrend( | |
| 101 | + @RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) { | |
| 102 | + List<LocalDateTime> dates = LocalDateTimeUtils.getLatestDays(days); | |
| 103 | + LocalDateTime beginTime = dates.get(0); | |
| 104 | + LocalDateTime endTime = dates.get(days - 1).plusDays(1); | |
| 105 | + Map<LocalDateTime, Long> registerMap = statisticsService.getNewUserDailyCountMap(beginTime, endTime); | |
| 106 | + Map<LocalDateTime, Long> activeMap = statisticsService.getActiveUserDailyCountMap(beginTime, endTime); | |
| 107 | + // 转换格式 | |
| 108 | + Map<String, List<Long>> series = new LinkedHashMap<>(); | |
| 109 | + series.put("register", alignSeries(dates, registerMap)); | |
| 110 | + series.put("active", alignSeries(dates, activeMap)); | |
| 111 | + return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series)); | |
| 112 | + } | |
| 113 | + | |
| 114 | + @GetMapping("/message-type-distribution") | |
| 115 | + @Operation(summary = "获得消息类型分布(最近 30 天)") | |
| 116 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 117 | + public CommonResult<List<ImStatisticsManagerMessageTypeRespVO>> getMessageTypeDistribution() { | |
| 118 | + LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay(); | |
| 119 | + LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS); | |
| 120 | + Map<Integer, Long> typeCountMap = statisticsService.getMessageTypeCountMap(beginTime, endTime); | |
| 121 | + // 转换格式 | |
| 122 | + return success(convertList(typeCountMap.entrySet(), entry -> new ImStatisticsManagerMessageTypeRespVO() | |
| 123 | + .setType(entry.getKey()).setValue(entry.getValue()))); | |
| 124 | + } | |
| 125 | + | |
| 126 | + @GetMapping("/group-size-distribution") | |
| 127 | + @Operation(summary = "获得群规模分布") | |
| 128 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 129 | + public CommonResult<List<ImStatisticsManagerGroupSizeRespVO>> getGroupSizeDistribution() { | |
| 130 | + Map<String, Long> groupSizeMap = statisticsService.getGroupSizeCountMap(); | |
| 131 | + // 转换格式 | |
| 132 | + return success(convertList(GROUP_SIZE_BUCKETS, bucket -> new ImStatisticsManagerGroupSizeRespVO() | |
| 133 | + .setRange(bucket).setCount(groupSizeMap.getOrDefault(bucket, 0L)))); | |
| 134 | + } | |
| 135 | + | |
| 136 | + @GetMapping("/top-senders") | |
| 137 | + @Operation(summary = "获得消息 TOP 发送者(最近 30 天)") | |
| 138 | + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") | |
| 139 | + public CommonResult<List<ImStatisticsManagerTopSenderRespVO>> getTopSenders() { | |
| 140 | + LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay(); | |
| 141 | + LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS); | |
| 142 | + Map<Long, Long> topSenderMap = statisticsService.getTopSenderCountMap(beginTime, endTime, TOP_SENDER_LIMIT); | |
| 143 | + // TOP 发送者:批量回填昵称 | |
| 144 | + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(topSenderMap.keySet()); | |
| 145 | + return success(convertList(topSenderMap.entrySet(), entry -> { | |
| 146 | + ImStatisticsManagerTopSenderRespVO item = new ImStatisticsManagerTopSenderRespVO() | |
| 147 | + .setUserId(entry.getKey()).setMessageCount(entry.getValue()); | |
| 148 | + MapUtils.findAndThen(userMap, entry.getKey(), user -> item.setNickname(user.getNickname())); | |
| 149 | + return item; | |
| 150 | + })); | |
| 151 | + } | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * 把每日聚合 Map 对齐到 dates 序列;缺失天补 0 | |
| 155 | + */ | |
| 156 | + private static List<Long> alignSeries(List<LocalDateTime> dates, Map<LocalDateTime, Long> dailyMap) { | |
| 157 | + return convertList(dates, date -> dailyMap.getOrDefault(date, 0L)); | |
| 158 | + } | |
| 159 | + | |
| 160 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +@Schema(description = "管理后台 - IM 数据看板群规模分布项 Response VO") | |
| 7 | +@Data | |
| 8 | +public class ImStatisticsManagerGroupSizeRespVO { | |
| 9 | + | |
| 10 | + @Schema(description = "区间名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1-9 人") | |
| 11 | + private String range; | |
| 12 | + | |
| 13 | + @Schema(description = "群数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "320") | |
| 14 | + private Long count; | |
| 15 | + | |
| 16 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +@Schema(description = "管理后台 - IM 数据看板消息类型分布项 Response VO") | |
| 7 | +@Data | |
| 8 | +public class ImStatisticsManagerMessageTypeRespVO { | |
| 9 | + | |
| 10 | + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 11 | + private Integer type; // 参见 ImMessageTypeEnum 枚举类 | |
| 12 | + | |
| 13 | + @Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000") | |
| 14 | + private Long value; | |
| 15 | + | |
| 16 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +@Schema(description = "管理后台 - IM 数据看板概览 Response VO") | |
| 7 | +@Data | |
| 8 | +public class ImStatisticsManagerOverviewRespVO { | |
| 9 | + | |
| 10 | + @Schema(description = "用户总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12345") | |
| 11 | + private Long totalUser; | |
| 12 | + | |
| 13 | + @Schema(description = "今日新增用户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23") | |
| 14 | + private Long newUserToday; | |
| 15 | + | |
| 16 | + @Schema(description = "群总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "678") | |
| 17 | + private Long totalGroup; | |
| 18 | + | |
| 19 | + @Schema(description = "今日新建群数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") | |
| 20 | + private Long newGroupToday; | |
| 21 | + | |
| 22 | + @Schema(description = "日活用户(今日发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1023") | |
| 23 | + private Long activeUserDaily; | |
| 24 | + | |
| 25 | + @Schema(description = "周活用户(最近 7 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "4567") | |
| 26 | + private Long activeUserWeekly; | |
| 27 | + | |
| 28 | + @Schema(description = "月活用户(最近 30 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "8901") | |
| 29 | + private Long activeUserMonthly; | |
| 30 | + | |
| 31 | + @Schema(description = "今日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8765") | |
| 32 | + private Long privateMessageToday; | |
| 33 | + | |
| 34 | + @Schema(description = "今日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3210") | |
| 35 | + private Long groupMessageToday; | |
| 36 | + | |
| 37 | + @Schema(description = "昨日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7890") | |
| 38 | + private Long privateMessageYesterday; | |
| 39 | + | |
| 40 | + @Schema(description = "昨日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3000") | |
| 41 | + private Long groupMessageYesterday; | |
| 42 | + | |
| 43 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +@Schema(description = "管理后台 - IM 数据看板 TOP 发送者项 Response VO") | |
| 7 | +@Data | |
| 8 | +public class ImStatisticsManagerTopSenderRespVO { | |
| 9 | + | |
| 10 | + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 11 | + private Long userId; | |
| 12 | + | |
| 13 | + @Schema(description = "用户昵称", example = "张三") | |
| 14 | + private String nickname; | |
| 15 | + | |
| 16 | + @Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1500") | |
| 17 | + private Long messageCount; | |
| 18 | + | |
| 19 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +@Schema(description = "管理后台 - IM 数据看板趋势 Response VO") | |
| 11 | +@Data | |
| 12 | +public class ImStatisticsManagerTrendRespVO { | |
| 13 | + | |
| 14 | + @Schema(description = "横轴日期序列(每天 00:00:00,由前端按需取日期部分)", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 15 | + private List<LocalDateTime> dates; | |
| 16 | + | |
| 17 | + @Schema(description = "数据系列:key 为系列名(如 private/group 或 register/active),value 为与 dates 等长的计数数组", | |
| 18 | + requiredMode = Schema.RequiredMode.REQUIRED) | |
| 19 | + private Map<String, List<Long>> series; | |
| 20 | + | |
| 21 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.statistics; | |
| 2 | + | |
| 3 | +import org.apache.ibatis.annotations.Mapper; | |
| 4 | +import org.apache.ibatis.annotations.Param; | |
| 5 | +import org.apache.ibatis.annotations.Select; | |
| 6 | + | |
| 7 | +import java.time.LocalDateTime; | |
| 8 | +import java.util.List; | |
| 9 | +import java.util.Map; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * IM 数据看板 Mapper | |
| 13 | + * <p> | |
| 14 | + * 独立于业务 Mapper:所有统计 SQL 集中在此,仅服务于 manager 看板,不被其它业务调用,保持各业务 Mapper / Service 不受统计需求污染。 | |
| 15 | + * | |
| 16 | + * @author 芋道源码 | |
| 17 | + */ | |
| 18 | +@Mapper | |
| 19 | +public interface ImStatisticsManagerMapper { | |
| 20 | + | |
| 21 | + // ==================== 用户 ==================== | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 用户总数(system_users) | |
| 25 | + */ | |
| 26 | + @Select("SELECT COUNT(*) FROM system_users WHERE deleted = 0") | |
| 27 | + Long selectTotalUserCount(); | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 区间内新增用户数 | |
| 31 | + */ | |
| 32 | + @Select("SELECT COUNT(*) FROM system_users " + | |
| 33 | + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime}") | |
| 34 | + Long selectNewUserCount(@Param("beginTime") LocalDateTime beginTime, | |
| 35 | + @Param("endTime") LocalDateTime endTime); | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * 区间内活跃用户数:私聊或群聊发过消息的去重用户数 | |
| 39 | + */ | |
| 40 | + @Select("SELECT COUNT(DISTINCT user_id) FROM (" + | |
| 41 | + " SELECT sender_id AS user_id FROM im_private_message " + | |
| 42 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 43 | + " UNION ALL " + | |
| 44 | + " SELECT sender_id AS user_id FROM im_group_message " + | |
| 45 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 46 | + ") t") | |
| 47 | + Long selectActiveUserCount(@Param("beginTime") LocalDateTime beginTime, | |
| 48 | + @Param("endTime") LocalDateTime endTime); | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 区间内每日新增用户数(按天分组) | |
| 52 | + * | |
| 53 | + * @return [{date: "yyyy-MM-dd", count: 123}, ...] | |
| 54 | + */ | |
| 55 | + @Select("SELECT DATE(create_time) AS date, COUNT(*) AS count FROM system_users " + | |
| 56 | + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " + | |
| 57 | + "GROUP BY DATE(create_time)") | |
| 58 | + List<Map<String, Object>> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime, | |
| 59 | + @Param("endTime") LocalDateTime endTime); | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) | |
| 63 | + */ | |
| 64 | + @Select("SELECT day AS date, COUNT(DISTINCT user_id) AS count FROM (" + | |
| 65 | + " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_private_message " + | |
| 66 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 67 | + " UNION ALL " + | |
| 68 | + " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_group_message " + | |
| 69 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 70 | + ") t GROUP BY day") | |
| 71 | + List<Map<String, Object>> selectActiveUserDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); | |
| 72 | + | |
| 73 | + // ==================== 群 ==================== | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 群总数(im_group) | |
| 77 | + */ | |
| 78 | + @Select("SELECT COUNT(*) FROM im_group WHERE deleted = 0") | |
| 79 | + Long selectTotalGroupCount(); | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 区间内新建群数 | |
| 83 | + */ | |
| 84 | + @Select("SELECT COUNT(*) FROM im_group " + | |
| 85 | + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime}") | |
| 86 | + Long selectNewGroupCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); | |
| 87 | + | |
| 88 | + /** | |
| 89 | + * 群规模分布(按群成员数分桶) | |
| 90 | + * | |
| 91 | + * @return [{range: "1-9 人", count: 123}, ...] | |
| 92 | + */ | |
| 93 | + @Select("SELECT " + | |
| 94 | + " CASE " + | |
| 95 | + " WHEN cnt < 10 THEN '1-9 人' " + | |
| 96 | + " WHEN cnt < 50 THEN '10-49 人' " + | |
| 97 | + " WHEN cnt < 200 THEN '50-199 人' " + | |
| 98 | + " ELSE '200+ 人' " + | |
| 99 | + " END AS `range`, COUNT(*) AS count " + | |
| 100 | + "FROM (" + | |
| 101 | + " SELECT g.id, COUNT(m.id) AS cnt " + | |
| 102 | + " FROM im_group g " + | |
| 103 | + " LEFT JOIN im_group_member m ON m.group_id = g.id AND m.status = 0 AND m.deleted = 0 " + | |
| 104 | + " WHERE g.deleted = 0 " + | |
| 105 | + " GROUP BY g.id" + | |
| 106 | + ") t " + | |
| 107 | + "GROUP BY `range`") | |
| 108 | + List<Map<String, Object>> selectGroupSizeDistribution(); | |
| 109 | + | |
| 110 | + // ==================== 消息 ==================== | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 区间内私聊消息数 | |
| 114 | + */ | |
| 115 | + @Select("SELECT COUNT(*) FROM im_private_message " + | |
| 116 | + "WHERE send_time >= #{beginTime} AND send_time < #{endTime}") | |
| 117 | + Long selectPrivateMessageCount(@Param("beginTime") LocalDateTime beginTime, | |
| 118 | + @Param("endTime") LocalDateTime endTime); | |
| 119 | + | |
| 120 | + /** | |
| 121 | + * 区间内群聊消息数 | |
| 122 | + */ | |
| 123 | + @Select("SELECT COUNT(*) FROM im_group_message " + | |
| 124 | + "WHERE send_time >= #{beginTime} AND send_time < #{endTime}") | |
| 125 | + Long selectGroupMessageCount(@Param("beginTime") LocalDateTime beginTime, | |
| 126 | + @Param("endTime") LocalDateTime endTime); | |
| 127 | + | |
| 128 | + /** | |
| 129 | + * 区间内每日私聊消息数(按天分组) | |
| 130 | + */ | |
| 131 | + @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_private_message " + | |
| 132 | + "WHERE send_time >= #{beginTime} AND send_time < #{endTime} " + | |
| 133 | + "GROUP BY DATE(send_time)") | |
| 134 | + List<Map<String, Object>> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, | |
| 135 | + @Param("endTime") LocalDateTime endTime); | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 区间内每日群聊消息数(按天分组) | |
| 139 | + */ | |
| 140 | + @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_group_message " + | |
| 141 | + "WHERE send_time >= #{beginTime} AND send_time < #{endTime} " + | |
| 142 | + "GROUP BY DATE(send_time)") | |
| 143 | + List<Map<String, Object>> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, | |
| 144 | + @Param("endTime") LocalDateTime endTime); | |
| 145 | + | |
| 146 | + /** | |
| 147 | + * 区间内消息类型分布(私聊+群聊合并) | |
| 148 | + * | |
| 149 | + * @return [{type: 0, count: 123}, ...] | |
| 150 | + */ | |
| 151 | + @Select("SELECT type, COUNT(*) AS count FROM (" + | |
| 152 | + " SELECT type FROM im_private_message " + | |
| 153 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 154 | + " UNION ALL " + | |
| 155 | + " SELECT type FROM im_group_message " + | |
| 156 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 157 | + ") t GROUP BY type") | |
| 158 | + List<Map<String, Object>> selectMessageTypeDistribution(@Param("beginTime") LocalDateTime beginTime, | |
| 159 | + @Param("endTime") LocalDateTime endTime); | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 区间内 TOP 发送者(私聊+群聊合并,按消息数倒序) | |
| 163 | + * | |
| 164 | + * @return [{userId: 1024, messageCount: 1500}, ...] | |
| 165 | + */ | |
| 166 | + @Select("SELECT user_id AS userId, COUNT(*) AS messageCount FROM (" + | |
| 167 | + " SELECT sender_id AS user_id FROM im_private_message " + | |
| 168 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 169 | + " UNION ALL " + | |
| 170 | + " SELECT sender_id AS user_id FROM im_group_message " + | |
| 171 | + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + | |
| 172 | + ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}") | |
| 173 | + List<Map<String, Object>> selectTopSenders(@Param("beginTime") LocalDateTime beginTime, | |
| 174 | + @Param("endTime") LocalDateTime endTime, | |
| 175 | + @Param("limit") int limit); | |
| 176 | + | |
| 177 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.statistics; | |
| 2 | + | |
| 3 | +import java.time.LocalDateTime; | |
| 4 | +import java.util.Map; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * IM 数据看板 Service 接口 | |
| 8 | + * <p> | |
| 9 | + * 仅服务于 manager 后台统计页,独立于业务 Service,避免污染。 | |
| 10 | + * 返回的均为聚合后的简单结构,由 Controller 负责 VO 装配与昵称回填。 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +public interface ImStatisticsManagerService { | |
| 15 | + | |
| 16 | + // ==================== 用户 ==================== | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 获取用户总数 | |
| 20 | + */ | |
| 21 | + Long getTotalUserCount(); | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 获取区间内新增用户数 | |
| 25 | + */ | |
| 26 | + Long getNewUserCount(LocalDateTime beginTime, LocalDateTime endTime); | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 获取区间内活跃用户数(私聊+群聊去重) | |
| 30 | + */ | |
| 31 | + Long getActiveUserCount(LocalDateTime beginTime, LocalDateTime endTime); | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 获取区间内每日新增用户数 Map(key 为 yyyy-MM-dd 日期) | |
| 35 | + */ | |
| 36 | + Map<LocalDateTime, Long> getNewUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * 获取区间内每日活跃用户数 Map | |
| 40 | + */ | |
| 41 | + Map<LocalDateTime, Long> getActiveUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); | |
| 42 | + | |
| 43 | + // ==================== 群 ==================== | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * 获取群总数 | |
| 47 | + */ | |
| 48 | + Long getTotalGroupCount(); | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 获取区间内新建群数 | |
| 52 | + */ | |
| 53 | + Long getNewGroupCount(LocalDateTime beginTime, LocalDateTime endTime); | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * 获取群规模分布 Map(key 为分桶名) | |
| 57 | + */ | |
| 58 | + Map<String, Long> getGroupSizeCountMap(); | |
| 59 | + | |
| 60 | + // ==================== 消息 ==================== | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 获取区间内私聊消息数 | |
| 64 | + */ | |
| 65 | + Long getPrivateMessageCount(LocalDateTime beginTime, LocalDateTime endTime); | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 获取区间内群聊消息数 | |
| 69 | + */ | |
| 70 | + Long getGroupMessageCount(LocalDateTime beginTime, LocalDateTime endTime); | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 获取区间内每日私聊消息数 Map | |
| 74 | + */ | |
| 75 | + Map<LocalDateTime, Long> getPrivateMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); | |
| 76 | + | |
| 77 | + /** | |
| 78 | + * 获取区间内每日群聊消息数 Map | |
| 79 | + */ | |
| 80 | + Map<LocalDateTime, Long> getGroupMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 获取区间内消息类型分布 Map(key 为消息类型) | |
| 84 | + */ | |
| 85 | + Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime); | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 获取区间内 TOP 发送者 Map(key 为 userId,value 为消息数;按消息数倒序) | |
| 89 | + */ | |
| 90 | + Map<Long, Long> getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit); | |
| 91 | + | |
| 92 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.statistics; | |
| 2 | + | |
| 3 | +import cn.hutool.core.convert.Convert; | |
| 4 | +import cn.iocoder.yudao.module.im.dal.mysql.statistics.ImStatisticsManagerMapper; | |
| 5 | +import jakarta.annotation.Resource; | |
| 6 | +import org.springframework.stereotype.Service; | |
| 7 | +import org.springframework.validation.annotation.Validated; | |
| 8 | + | |
| 9 | +import java.time.LocalDate; | |
| 10 | +import java.time.LocalDateTime; | |
| 11 | +import java.util.List; | |
| 12 | +import java.util.Map; | |
| 13 | + | |
| 14 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * IM 数据看板 Service 实现类 | |
| 18 | + * | |
| 19 | + * @author 芋道源码 | |
| 20 | + */ | |
| 21 | +@Service | |
| 22 | +@Validated | |
| 23 | +public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerService { | |
| 24 | + | |
| 25 | + @Resource | |
| 26 | + private ImStatisticsManagerMapper statisticsMapper; | |
| 27 | + | |
| 28 | + // ==================== 用户 ==================== | |
| 29 | + | |
| 30 | + @Override | |
| 31 | + public Long getTotalUserCount() { | |
| 32 | + return statisticsMapper.selectTotalUserCount(); | |
| 33 | + } | |
| 34 | + | |
| 35 | + @Override | |
| 36 | + public Long getNewUserCount(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 37 | + return statisticsMapper.selectNewUserCount(beginTime, endTime); | |
| 38 | + } | |
| 39 | + | |
| 40 | + @Override | |
| 41 | + public Long getActiveUserCount(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 42 | + return statisticsMapper.selectActiveUserCount(beginTime, endTime); | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public Map<LocalDateTime, Long> getNewUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 47 | + List<Map<String, Object>> rows = statisticsMapper.selectNewUserDailyCount(beginTime, endTime); | |
| 48 | + return toDailyCountMap(rows); | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Override | |
| 52 | + public Map<LocalDateTime, Long> getActiveUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 53 | + List<Map<String, Object>> rows = statisticsMapper.selectActiveUserDailyCount(beginTime, endTime); | |
| 54 | + return toDailyCountMap(rows); | |
| 55 | + } | |
| 56 | + | |
| 57 | + // ==================== 群 ==================== | |
| 58 | + | |
| 59 | + @Override | |
| 60 | + public Long getTotalGroupCount() { | |
| 61 | + return statisticsMapper.selectTotalGroupCount(); | |
| 62 | + } | |
| 63 | + | |
| 64 | + @Override | |
| 65 | + public Long getNewGroupCount(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 66 | + return statisticsMapper.selectNewGroupCount(beginTime, endTime); | |
| 67 | + } | |
| 68 | + | |
| 69 | + @Override | |
| 70 | + public Map<String, Long> getGroupSizeCountMap() { | |
| 71 | + List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution(); | |
| 72 | + return convertMap(rows, | |
| 73 | + row -> (String) row.get("range"), | |
| 74 | + row -> (Long) row.get("count")); | |
| 75 | + } | |
| 76 | + | |
| 77 | + // ==================== 消息 ==================== | |
| 78 | + | |
| 79 | + @Override | |
| 80 | + public Long getPrivateMessageCount(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 81 | + return statisticsMapper.selectPrivateMessageCount(beginTime, endTime); | |
| 82 | + } | |
| 83 | + | |
| 84 | + @Override | |
| 85 | + public Long getGroupMessageCount(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 86 | + return statisticsMapper.selectGroupMessageCount(beginTime, endTime); | |
| 87 | + } | |
| 88 | + | |
| 89 | + @Override | |
| 90 | + public Map<LocalDateTime, Long> getPrivateMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 91 | + List<Map<String, Object>> rows = statisticsMapper.selectPrivateMessageDailyCount(beginTime, endTime); | |
| 92 | + return toDailyCountMap(rows); | |
| 93 | + } | |
| 94 | + | |
| 95 | + @Override | |
| 96 | + public Map<LocalDateTime, Long> getGroupMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 97 | + List<Map<String, Object>> rows = statisticsMapper.selectGroupMessageDailyCount(beginTime, endTime); | |
| 98 | + return toDailyCountMap(rows); | |
| 99 | + } | |
| 100 | + | |
| 101 | + @Override | |
| 102 | + public Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime) { | |
| 103 | + List<Map<String, Object>> rows = statisticsMapper.selectMessageTypeDistribution(beginTime, endTime); | |
| 104 | + return convertMap(rows, | |
| 105 | + row -> (Integer) row.get("type"), | |
| 106 | + row -> (Long) row.get("count")); | |
| 107 | + } | |
| 108 | + | |
| 109 | + @Override | |
| 110 | + public Map<Long, Long> getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit) { | |
| 111 | + List<Map<String, Object>> rows = statisticsMapper.selectTopSenders(beginTime, endTime, limit); | |
| 112 | + return convertMap(rows, | |
| 113 | + row -> (Long) row.get("userId"), | |
| 114 | + row -> (Long) row.get("messageCount")); | |
| 115 | + } | |
| 116 | + | |
| 117 | + /** | |
| 118 | + * 把 [{date, count}] 行映射为 {LocalDateTime -> Long}; | |
| 119 | + */ | |
| 120 | + private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) { | |
| 121 | + return convertMap(rows, | |
| 122 | + row -> Convert.convert(LocalDate.class, row.get("date")).atStartOfDay(), | |
| 123 | + row -> (Long) row.get("count")); | |
| 124 | + } | |
| 125 | + | |
| 126 | +} | ... | ... |