diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index 287cda2..6be9ff0 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -236,6 +236,23 @@ public class LocalDateTimeUtils { return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); } + /** + * 获取最近 N 天的 0 点时刻序列(升序,含今天) + *

+ * 例:getLatestDays(3) 返回 [前天 00:00, 昨天 00:00, 今天 00:00] + * + * @param days 天数(含今天) + * @return 升序的 LocalDateTime 列表 + */ + public static List getLatestDays(int days) { + LocalDateTime today = getToday(); + List dates = new ArrayList<>(days); + for (int i = days - 1; i >= 0; i--) { + dates.add(today.minusDays(i)); + } + return dates; + } + public static List getDateRangeList(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java new file mode 100644 index 0000000..82403e3 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo.*; +import cn.iocoder.yudao.module.im.service.statistics.ImStatisticsManagerService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.constraints.Max; +import jakarta.validation.constraints.Min; +import org.springframework.security.access.prepost.PreAuthorize; +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.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IM 数据看板") +@RestController +@RequestMapping("/im/manager/statistics") +@Validated +public class ImStatisticsManagerController { + + /** + * 群规模分桶名称的展示顺序 + */ + private static final List GROUP_SIZE_BUCKETS = Arrays.asList("1-9 人", "10-49 人", "50-199 人", "200+ 人"); + /** + * 看板分布默认时间窗口(天) + */ + private static final int DISTRIBUTION_WINDOW_DAYS = 30; + /** + * TOP 发送者数量 + */ + private static final int TOP_SENDER_LIMIT = 10; + + @Resource + private ImStatisticsManagerService statisticsService; + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/overview") + @Operation(summary = "获得数据概览") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult getOverview() { + LocalDateTime todayBegin = LocalDate.now().atStartOfDay(); + LocalDateTime tomorrowBegin = todayBegin.plusDays(1); + LocalDateTime yesterdayBegin = todayBegin.minusDays(1); + // 周活/月活定义为 N 天滚动窗口(含今天) + LocalDateTime weekBegin = todayBegin.minusDays(6); + LocalDateTime monthBegin = todayBegin.minusDays(29); + return success(new ImStatisticsManagerOverviewRespVO() + .setTotalUser(statisticsService.getTotalUserCount()) + .setNewUserToday(statisticsService.getNewUserCount(todayBegin, tomorrowBegin)) + .setTotalGroup(statisticsService.getTotalGroupCount()) + .setNewGroupToday(statisticsService.getNewGroupCount(todayBegin, tomorrowBegin)) + .setActiveUserDaily(statisticsService.getActiveUserCount(todayBegin, tomorrowBegin)) + .setActiveUserWeekly(statisticsService.getActiveUserCount(weekBegin, tomorrowBegin)) + .setActiveUserMonthly(statisticsService.getActiveUserCount(monthBegin, tomorrowBegin)) + .setPrivateMessageToday(statisticsService.getPrivateMessageCount(todayBegin, tomorrowBegin)) + .setGroupMessageToday(statisticsService.getGroupMessageCount(todayBegin, tomorrowBegin)) + .setPrivateMessageYesterday(statisticsService.getPrivateMessageCount(yesterdayBegin, todayBegin)) + .setGroupMessageYesterday(statisticsService.getGroupMessageCount(yesterdayBegin, todayBegin))); + } + + @GetMapping("/message-trend") + @Operation(summary = "获得消息趋势(私聊 / 群聊双线)") + @Parameter(name = "days", description = "回看天数(含今日)", example = "7") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult getMessageTrend( + @RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) { + List dates = LocalDateTimeUtils.getLatestDays(days); + LocalDateTime beginTime = dates.get(0); + LocalDateTime endTime = dates.get(days - 1).plusDays(1); + Map privateMap = statisticsService.getPrivateMessageDailyCountMap(beginTime, endTime); + Map groupMap = statisticsService.getGroupMessageDailyCountMap(beginTime, endTime); + // 转换格式 + Map> series = new LinkedHashMap<>(); + series.put("private", alignSeries(dates, privateMap)); + series.put("group", alignSeries(dates, groupMap)); + return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series)); + } + + @GetMapping("/user-trend") + @Operation(summary = "获得用户趋势(新增注册 / 日活双线)") + @Parameter(name = "days", description = "回看天数(含今日)", example = "7") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult getUserTrend( + @RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) { + List dates = LocalDateTimeUtils.getLatestDays(days); + LocalDateTime beginTime = dates.get(0); + LocalDateTime endTime = dates.get(days - 1).plusDays(1); + Map registerMap = statisticsService.getNewUserDailyCountMap(beginTime, endTime); + Map activeMap = statisticsService.getActiveUserDailyCountMap(beginTime, endTime); + // 转换格式 + Map> series = new LinkedHashMap<>(); + series.put("register", alignSeries(dates, registerMap)); + series.put("active", alignSeries(dates, activeMap)); + return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series)); + } + + @GetMapping("/message-type-distribution") + @Operation(summary = "获得消息类型分布(最近 30 天)") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult> getMessageTypeDistribution() { + LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay(); + LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS); + Map typeCountMap = statisticsService.getMessageTypeCountMap(beginTime, endTime); + // 转换格式 + return success(convertList(typeCountMap.entrySet(), entry -> new ImStatisticsManagerMessageTypeRespVO() + .setType(entry.getKey()).setValue(entry.getValue()))); + } + + @GetMapping("/group-size-distribution") + @Operation(summary = "获得群规模分布") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult> getGroupSizeDistribution() { + Map groupSizeMap = statisticsService.getGroupSizeCountMap(); + // 转换格式 + return success(convertList(GROUP_SIZE_BUCKETS, bucket -> new ImStatisticsManagerGroupSizeRespVO() + .setRange(bucket).setCount(groupSizeMap.getOrDefault(bucket, 0L)))); + } + + @GetMapping("/top-senders") + @Operation(summary = "获得消息 TOP 发送者(最近 30 天)") + @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')") + public CommonResult> getTopSenders() { + LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay(); + LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS); + Map topSenderMap = statisticsService.getTopSenderCountMap(beginTime, endTime, TOP_SENDER_LIMIT); + // TOP 发送者:批量回填昵称 + Map userMap = adminUserApi.getUserMap(topSenderMap.keySet()); + return success(convertList(topSenderMap.entrySet(), entry -> { + ImStatisticsManagerTopSenderRespVO item = new ImStatisticsManagerTopSenderRespVO() + .setUserId(entry.getKey()).setMessageCount(entry.getValue()); + MapUtils.findAndThen(userMap, entry.getKey(), user -> item.setNickname(user.getNickname())); + return item; + })); + } + + /** + * 把每日聚合 Map 对齐到 dates 序列;缺失天补 0 + */ + private static List alignSeries(List dates, Map dailyMap) { + return convertList(dates, date -> dailyMap.getOrDefault(date, 0L)); + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java new file mode 100644 index 0000000..064f0de --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IM 数据看板群规模分布项 Response VO") +@Data +public class ImStatisticsManagerGroupSizeRespVO { + + @Schema(description = "区间名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1-9 人") + private String range; + + @Schema(description = "群数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "320") + private Long count; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java new file mode 100644 index 0000000..2da0827 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IM 数据看板消息类型分布项 Response VO") +@Data +public class ImStatisticsManagerMessageTypeRespVO { + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer type; // 参见 ImMessageTypeEnum 枚举类 + + @Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000") + private Long value; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java new file mode 100644 index 0000000..dc9e713 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IM 数据看板概览 Response VO") +@Data +public class ImStatisticsManagerOverviewRespVO { + + @Schema(description = "用户总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12345") + private Long totalUser; + + @Schema(description = "今日新增用户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23") + private Long newUserToday; + + @Schema(description = "群总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "678") + private Long totalGroup; + + @Schema(description = "今日新建群数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + private Long newGroupToday; + + @Schema(description = "日活用户(今日发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1023") + private Long activeUserDaily; + + @Schema(description = "周活用户(最近 7 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "4567") + private Long activeUserWeekly; + + @Schema(description = "月活用户(最近 30 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "8901") + private Long activeUserMonthly; + + @Schema(description = "今日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8765") + private Long privateMessageToday; + + @Schema(description = "今日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3210") + private Long groupMessageToday; + + @Schema(description = "昨日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7890") + private Long privateMessageYesterday; + + @Schema(description = "昨日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3000") + private Long groupMessageYesterday; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java new file mode 100644 index 0000000..2f06158 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IM 数据看板 TOP 发送者项 Response VO") +@Data +public class ImStatisticsManagerTopSenderRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "用户昵称", example = "张三") + private String nickname; + + @Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1500") + private Long messageCount; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java new file mode 100644 index 0000000..e08d980 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - IM 数据看板趋势 Response VO") +@Data +public class ImStatisticsManagerTrendRespVO { + + @Schema(description = "横轴日期序列(每天 00:00:00,由前端按需取日期部分)", requiredMode = Schema.RequiredMode.REQUIRED) + private List dates; + + @Schema(description = "数据系列:key 为系列名(如 private/group 或 register/active),value 为与 dates 等长的计数数组", + requiredMode = Schema.RequiredMode.REQUIRED) + private Map> series; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java new file mode 100644 index 0000000..75aa197 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java @@ -0,0 +1,177 @@ +package cn.iocoder.yudao.module.im.dal.mysql.statistics; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * IM 数据看板 Mapper + *

+ * 独立于业务 Mapper:所有统计 SQL 集中在此,仅服务于 manager 看板,不被其它业务调用,保持各业务 Mapper / Service 不受统计需求污染。 + * + * @author 芋道源码 + */ +@Mapper +public interface ImStatisticsManagerMapper { + + // ==================== 用户 ==================== + + /** + * 用户总数(system_users) + */ + @Select("SELECT COUNT(*) FROM system_users WHERE deleted = 0") + Long selectTotalUserCount(); + + /** + * 区间内新增用户数 + */ + @Select("SELECT COUNT(*) FROM system_users " + + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime}") + Long selectNewUserCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内活跃用户数:私聊或群聊发过消息的去重用户数 + */ + @Select("SELECT COUNT(DISTINCT user_id) FROM (" + + " SELECT sender_id AS user_id FROM im_private_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + " UNION ALL " + + " SELECT sender_id AS user_id FROM im_group_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + ") t") + Long selectActiveUserCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内每日新增用户数(按天分组) + * + * @return [{date: "yyyy-MM-dd", count: 123}, ...] + */ + @Select("SELECT DATE(create_time) AS date, COUNT(*) AS count FROM system_users " + + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " + + "GROUP BY DATE(create_time)") + List> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) + */ + @Select("SELECT day AS date, COUNT(DISTINCT user_id) AS count FROM (" + + " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_private_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + " UNION ALL " + + " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_group_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + ") t GROUP BY day") + List> selectActiveUserDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); + + // ==================== 群 ==================== + + /** + * 群总数(im_group) + */ + @Select("SELECT COUNT(*) FROM im_group WHERE deleted = 0") + Long selectTotalGroupCount(); + + /** + * 区间内新建群数 + */ + @Select("SELECT COUNT(*) FROM im_group " + + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime}") + Long selectNewGroupCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); + + /** + * 群规模分布(按群成员数分桶) + * + * @return [{range: "1-9 人", count: 123}, ...] + */ + @Select("SELECT " + + " CASE " + + " WHEN cnt < 10 THEN '1-9 人' " + + " WHEN cnt < 50 THEN '10-49 人' " + + " WHEN cnt < 200 THEN '50-199 人' " + + " ELSE '200+ 人' " + + " END AS `range`, COUNT(*) AS count " + + "FROM (" + + " SELECT g.id, COUNT(m.id) AS cnt " + + " FROM im_group g " + + " LEFT JOIN im_group_member m ON m.group_id = g.id AND m.status = 0 AND m.deleted = 0 " + + " WHERE g.deleted = 0 " + + " GROUP BY g.id" + + ") t " + + "GROUP BY `range`") + List> selectGroupSizeDistribution(); + + // ==================== 消息 ==================== + + /** + * 区间内私聊消息数 + */ + @Select("SELECT COUNT(*) FROM im_private_message " + + "WHERE send_time >= #{beginTime} AND send_time < #{endTime}") + Long selectPrivateMessageCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内群聊消息数 + */ + @Select("SELECT COUNT(*) FROM im_group_message " + + "WHERE send_time >= #{beginTime} AND send_time < #{endTime}") + Long selectGroupMessageCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内每日私聊消息数(按天分组) + */ + @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_private_message " + + "WHERE send_time >= #{beginTime} AND send_time < #{endTime} " + + "GROUP BY DATE(send_time)") + List> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内每日群聊消息数(按天分组) + */ + @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_group_message " + + "WHERE send_time >= #{beginTime} AND send_time < #{endTime} " + + "GROUP BY DATE(send_time)") + List> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内消息类型分布(私聊+群聊合并) + * + * @return [{type: 0, count: 123}, ...] + */ + @Select("SELECT type, COUNT(*) AS count FROM (" + + " SELECT type FROM im_private_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + " UNION ALL " + + " SELECT type FROM im_group_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + ") t GROUP BY type") + List> selectMessageTypeDistribution(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 区间内 TOP 发送者(私聊+群聊合并,按消息数倒序) + * + * @return [{userId: 1024, messageCount: 1500}, ...] + */ + @Select("SELECT user_id AS userId, COUNT(*) AS messageCount FROM (" + + " SELECT sender_id AS user_id FROM im_private_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + " UNION ALL " + + " SELECT sender_id AS user_id FROM im_group_message " + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" + + ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}") + List> selectTopSenders(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime, + @Param("limit") int limit); + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerService.java new file mode 100644 index 0000000..5337bff --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerService.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.im.service.statistics; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * IM 数据看板 Service 接口 + *

+ * 仅服务于 manager 后台统计页,独立于业务 Service,避免污染。 + * 返回的均为聚合后的简单结构,由 Controller 负责 VO 装配与昵称回填。 + * + * @author 芋道源码 + */ +public interface ImStatisticsManagerService { + + // ==================== 用户 ==================== + + /** + * 获取用户总数 + */ + Long getTotalUserCount(); + + /** + * 获取区间内新增用户数 + */ + Long getNewUserCount(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内活跃用户数(私聊+群聊去重) + */ + Long getActiveUserCount(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内每日新增用户数 Map(key 为 yyyy-MM-dd 日期) + */ + Map getNewUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内每日活跃用户数 Map + */ + Map getActiveUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); + + // ==================== 群 ==================== + + /** + * 获取群总数 + */ + Long getTotalGroupCount(); + + /** + * 获取区间内新建群数 + */ + Long getNewGroupCount(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取群规模分布 Map(key 为分桶名) + */ + Map getGroupSizeCountMap(); + + // ==================== 消息 ==================== + + /** + * 获取区间内私聊消息数 + */ + Long getPrivateMessageCount(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内群聊消息数 + */ + Long getGroupMessageCount(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内每日私聊消息数 Map + */ + Map getPrivateMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内每日群聊消息数 Map + */ + Map getGroupMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内消息类型分布 Map(key 为消息类型) + */ + Map getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获取区间内 TOP 发送者 Map(key 为 userId,value 为消息数;按消息数倒序) + */ + Map getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit); + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java new file mode 100644 index 0000000..da4e3fb --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.im.service.statistics; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.module.im.dal.mysql.statistics.ImStatisticsManagerMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IM 数据看板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerService { + + @Resource + private ImStatisticsManagerMapper statisticsMapper; + + // ==================== 用户 ==================== + + @Override + public Long getTotalUserCount() { + return statisticsMapper.selectTotalUserCount(); + } + + @Override + public Long getNewUserCount(LocalDateTime beginTime, LocalDateTime endTime) { + return statisticsMapper.selectNewUserCount(beginTime, endTime); + } + + @Override + public Long getActiveUserCount(LocalDateTime beginTime, LocalDateTime endTime) { + return statisticsMapper.selectActiveUserCount(beginTime, endTime); + } + + @Override + public Map getNewUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { + List> rows = statisticsMapper.selectNewUserDailyCount(beginTime, endTime); + return toDailyCountMap(rows); + } + + @Override + public Map getActiveUserDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { + List> rows = statisticsMapper.selectActiveUserDailyCount(beginTime, endTime); + return toDailyCountMap(rows); + } + + // ==================== 群 ==================== + + @Override + public Long getTotalGroupCount() { + return statisticsMapper.selectTotalGroupCount(); + } + + @Override + public Long getNewGroupCount(LocalDateTime beginTime, LocalDateTime endTime) { + return statisticsMapper.selectNewGroupCount(beginTime, endTime); + } + + @Override + public Map getGroupSizeCountMap() { + List> rows = statisticsMapper.selectGroupSizeDistribution(); + return convertMap(rows, + row -> (String) row.get("range"), + row -> (Long) row.get("count")); + } + + // ==================== 消息 ==================== + + @Override + public Long getPrivateMessageCount(LocalDateTime beginTime, LocalDateTime endTime) { + return statisticsMapper.selectPrivateMessageCount(beginTime, endTime); + } + + @Override + public Long getGroupMessageCount(LocalDateTime beginTime, LocalDateTime endTime) { + return statisticsMapper.selectGroupMessageCount(beginTime, endTime); + } + + @Override + public Map getPrivateMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { + List> rows = statisticsMapper.selectPrivateMessageDailyCount(beginTime, endTime); + return toDailyCountMap(rows); + } + + @Override + public Map getGroupMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime) { + List> rows = statisticsMapper.selectGroupMessageDailyCount(beginTime, endTime); + return toDailyCountMap(rows); + } + + @Override + public Map getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime) { + List> rows = statisticsMapper.selectMessageTypeDistribution(beginTime, endTime); + return convertMap(rows, + row -> (Integer) row.get("type"), + row -> (Long) row.get("count")); + } + + @Override + public Map getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit) { + List> rows = statisticsMapper.selectTopSenders(beginTime, endTime, limit); + return convertMap(rows, + row -> (Long) row.get("userId"), + row -> (Long) row.get("messageCount")); + } + + /** + * 把 [{date, count}] 行映射为 {LocalDateTime -> Long}; + */ + private static Map toDailyCountMap(List> rows) { + return convertMap(rows, + row -> Convert.convert(LocalDate.class, row.get("date")).atStartOfDay(), + row -> (Long) row.get("count")); + } + +}