Commit 45889459b7f9f34b70ba3c071267c290905867e5

Authored by YunaiV
1 parent af6e283f

✨ feat(im): 实现 im 的首页统计

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 +}
... ...