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