{
return CollUtil.getFirst(list);
}
+ /**
+ * 获取满足条件的最新一条记录
+ *
+ * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
+ */
+ default T selectLastOne(LambdaQueryWrapper queryWrapper) {
+ return CollUtil.getLast(selectList(queryWrapper));
+ }
default Long selectCount() {
return selectCount(new QueryWrapper<>());
diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml
index 4730c850c..4b97b5a8c 100644
--- a/yudao-gateway/src/main/resources/application.yaml
+++ b/yudao-gateway/src/main/resources/application.yaml
@@ -209,6 +209,13 @@ spring:
- Path=/admin-api/wms/**
filters:
- RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
+ ## im-server 服务
+ - id: im-admin-api # 路由的编号
+ uri: grayLb://im-server
+ predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
+ - Path=/admin-api/im/**
+ filters:
+ - RewritePath=/admin-api/im/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
default-filters:
@@ -274,6 +281,9 @@ knife4j:
- name: wms-server
service-name: wms-server
url: /admin-api/wms/v3/api-docs
+ - name: im-server
+ service-name: im-server
+ url: /admin-api/im/v3/api-docs
--- #################### 芋道相关配置 ####################
diff --git a/yudao-module-im/pom.xml b/yudao-module-im/pom.xml
new file mode 100644
index 000000000..7afd4e0fb
--- /dev/null
+++ b/yudao-module-im/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ cn.iocoder.cloud
+ yudao
+ ${revision}
+
+ 4.0.0
+
+ yudao-module-im-api
+ yudao-module-im-server
+
+ yudao-module-im
+ pom
+
+ ${project.artifactId}
+
+ im 模块,我们放即时通讯业务。
+ 例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
+
+
+
diff --git a/yudao-module-im/yudao-module-im-api/pom.xml b/yudao-module-im/yudao-module-im-api/pom.xml
new file mode 100644
index 000000000..8b8b84651
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-api/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-module-im
+ ${revision}
+
+ 4.0.0
+ yudao-module-im-api
+ jar
+
+ ${project.artifactId}
+
+ im 模块 API,暴露给其它模块调用
+
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ true
+
+
+
+
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java
new file mode 100644
index 000000000..49380970f
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * @author anhaohao
+ * @since 2024/3/9 下午8:59
+ */
+package cn.iocoder.yudao.module.im.api;
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/Dockerfile b/yudao-module-im/yudao-module-im-server/Dockerfile
new file mode 100644
index 000000000..1c32ca50a
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/Dockerfile
@@ -0,0 +1,19 @@
+## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
+## 感谢复旦核博士的建议!灰子哥,牛皮!
+FROM eclipse-temurin:21-jre
+
+## 创建目录,并使用它作为工作目录
+RUN mkdir -p /yudao-module-im-server
+WORKDIR /yudao-module-im-server
+## 将后端项目的 Jar 文件,复制到镜像中
+COPY ./target/yudao-module-im-server.jar app.jar
+
+## 设置 TZ 时区
+## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
+ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m"
+
+## 暴露后端项目的 48093 端口
+EXPOSE 48093
+
+## 启动后端项目
+CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar
diff --git a/yudao-module-im/yudao-module-im-server/pom.xml b/yudao-module-im/yudao-module-im-server/pom.xml
new file mode 100644
index 000000000..17a010f33
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/pom.xml
@@ -0,0 +1,156 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-module-im
+ ${revision}
+
+ 4.0.0
+ yudao-module-im-server
+ jar
+
+ ${project.artifactId}
+
+ im 模块,我们放即时通讯业务。
+ 例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
+
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-env
+
+
+
+
+ cn.iocoder.cloud
+ yudao-module-im-api
+ ${revision}
+
+
+ cn.iocoder.cloud
+ yudao-module-system-api
+ ${revision}
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-tenant
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-websocket
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mybatis
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-redis
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-rpc
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-job
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mq
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-test
+ test
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-excel
+
+
+
+
+ com.github.houbb
+ sensitive-word
+
+
+
+
+ com.belerweb
+ pinyin4j
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-monitor
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ ${project.artifactId}
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/ImServerApplication.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/ImServerApplication.java
new file mode 100644
index 000000000..184eb7a48
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/ImServerApplication.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.im;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * 项目的启动类
+ *
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ *
+ * @author 芋道源码
+ */
+@SpringBootApplication
+public class ImServerApplication {
+
+ public static void main(String[] args) {
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+
+ SpringApplication.run(ImServerApplication.class, args);
+
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/channel/ImChannelMaterialController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/channel/ImChannelMaterialController.java
new file mode 100644
index 000000000..e2427a130
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/channel/ImChannelMaterialController.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.channel;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
+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 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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "用户 APP - IM 频道素材")
+@RestController
+@RequestMapping("/im/channel/material")
+@Validated
+public class ImChannelMaterialController {
+
+ @Resource
+ private ImChannelMaterialService channelMaterialService;
+
+ @GetMapping("/get")
+ @Operation(summary = "获取素材详情;用于客户端点击图文卡片渲染详情页")
+ @Parameter(name = "id", description = "素材编号", required = true, example = "1024")
+ public CommonResult getMaterial(@RequestParam("id") Long id) {
+ ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(id);
+ return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFacePackController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFacePackController.java
new file mode 100644
index 000000000..ea908242b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFacePackController.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.im.controller.admin.face;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.face.vo.pack.ImFacePackUserRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
+import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
+import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+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.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+
+@Tag(name = "管理后台 - IM 表情包")
+@RestController
+@RequestMapping("/im/face-pack")
+@Validated
+public class ImFacePackController {
+
+ @Resource
+ private ImFacePackService facePackService;
+ @Resource
+ private ImFacePackItemService facePackItemService;
+
+ @GetMapping("/list")
+ @Operation(summary = "获得启用的表情包列表(含表情)")
+ public CommonResult> getFacePackList() {
+ // 1.1 拉所有启用表情包
+ List packs = facePackService.getEnabledFacePackList();
+ if (packs.isEmpty()) {
+ return success(List.of());
+ }
+ // 1.2 拉这些包下所有启用表情,按 packId 分组
+ List items = facePackItemService.getEnabledItemListByPackIds(
+ convertList(packs, ImFacePackDO::getId));
+ Map> itemsByPackId = convertMultiMap(items, ImFacePackItemDO::getPackId);
+
+ // 2. 拼装:BeanUtils 把 pack 字段映射 + 自己塞 items
+ List result = convertList(packs, pack -> {
+ ImFacePackUserRespVO vo = BeanUtils.toBean(pack, ImFacePackUserRespVO.class);
+ vo.setItems(BeanUtils.toBean(itemsByPackId.getOrDefault(pack.getId(), List.of()), ImFacePackUserRespVO.Item.class));
+ return vo;
+ });
+ return success(result);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFaceUserItemController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFaceUserItemController.java
new file mode 100644
index 000000000..7f27c173c
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/ImFaceUserItemController.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.im.controller.admin.face;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
+import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - IM 个人表情")
+@RestController
+@RequestMapping("/im/face-user-item")
+@Validated
+public class ImFaceUserItemController {
+
+ @Resource
+ private ImFaceUserItemService faceUserItemService;
+
+ @GetMapping("/list")
+ @Operation(summary = "获得我的个人表情列表")
+ public CommonResult> getFaceUserItemList() {
+ List items = faceUserItemService.getFaceUserItemList(getLoginUserId());
+ return success(BeanUtils.toBean(items, ImFaceUserItemRespVO.class));
+ }
+
+ @PostMapping("/create")
+ @Operation(summary = "添加个人表情")
+ public CommonResult createFaceUserItem(@Valid @RequestBody ImFaceUserItemSaveReqVO reqVO) {
+ return success(faceUserItemService.createFaceUserItem(getLoginUserId(), reqVO));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除个人表情")
+ @Parameter(name = "id", description = "编号", required = true, example = "4096")
+ public CommonResult deleteFaceUserItem(@RequestParam("id") Long id) {
+ faceUserItemService.deleteFaceUserItem(getLoginUserId(), id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/pack/ImFacePackUserRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/pack/ImFacePackUserRespVO.java
new file mode 100644
index 000000000..5a73d821c
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/pack/ImFacePackUserRespVO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.im.controller.admin.face.vo.pack;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "IM 表情包(用户端) Response VO")
+@Data
+public class ImFacePackUserRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
+ private String name;
+
+ @Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
+ private String icon;
+
+ @Schema(description = "表情列表", requiredMode = Schema.RequiredMode.REQUIRED)
+ private List- items;
+
+ @Schema(description = "IM 表情包项(用户端)")
+ @Data
+ public static class Item {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long id;
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/pack/cat-001.png")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer height;
+
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemRespVO.java
new file mode 100644
index 000000000..61be77857
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemRespVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "IM 个人表情 Response VO")
+@Data
+public class ImFaceUserItemRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
+ private Long id;
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/user/abc.gif")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer height;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemSaveReqVO.java
new file mode 100644
index 000000000..b9a508d00
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/face/vo/userItem/ImFaceUserItemSaveReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "IM 个人表情新增 Request VO")
+@Data
+public class ImFaceUserItemSaveReqVO {
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/user/abc.gif")
+ @NotBlank(message = "表情图 URL 不能为空")
+ @Size(max = 512, message = "表情图 URL 长度不能超过 512")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ @Size(max = 64, message = "表情名长度不能超过 64")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ @NotNull(message = "渲染宽度不能为空")
+ @Min(value = 1, message = "渲染宽度不能小于 1 像素")
+ @Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ @NotNull(message = "渲染高度不能为空")
+ @Min(value = 1, message = "渲染高度不能小于 1 像素")
+ @Max(value = 2048, message = "渲染高度不能大于 2048 像素")
+ private Integer height;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
new file mode 100644
index 000000000..43668f8d3
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
@@ -0,0 +1,127 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
+import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
+import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
+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.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - IM 好友")
+@RestController
+@RequestMapping("/im/friend")
+@Validated
+public class ImFriendController {
+
+ @Resource
+ private ImFriendService friendService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/list")
+ @Operation(summary = "获得当前登录用户的好友列表")
+ public CommonResult
> getMyFriendList() {
+ // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
+ List friends = friendService.getFriendList(getLoginUserId());
+ return success(buildFriendRespVOList(friends));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得好友详情")
+ @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
+ public CommonResult getFriend(@RequestParam("friendUserId") Long friendUserId) {
+ ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId);
+ return success(buildFriendRespVO(friend));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除好友(单向软删除)")
+ @Parameters({
+ @Parameter(description = "好友的用户编号", required = true, example = "2048"),
+ @Parameter(description = "是否级联清理本端相关数据(如私聊会话)")
+ })
+ public CommonResult deleteFriend(
+ @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
+ @RequestParam(value = "clear", required = false) Boolean clear) {
+ friendService.deleteFriend(getLoginUserId(), friendUserId, clear);
+ return success(true);
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新好友单边属性(备注 / 免打扰 / 联系人置顶)")
+ public CommonResult updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
+ friendService.updateFriend(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/block")
+ @Operation(summary = "拉黑好友(必须先是好友;单边屏蔽对方私聊消息)")
+ @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
+ public CommonResult blockFriend(
+ @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
+ friendService.blockFriend(getLoginUserId(), friendUserId);
+ return success(true);
+ }
+
+ @PutMapping("/unblock")
+ @Operation(summary = "移出黑名单")
+ @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
+ public CommonResult unblockFriend(
+ @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
+ friendService.unblockFriend(getLoginUserId(), friendUserId);
+ return success(true);
+ }
+
+ // ========== 私有方法:VO 组装 ==========
+
+ private List buildFriendRespVOList(Collection friends) {
+ if (CollUtil.isEmpty(friends)) {
+ return Collections.emptyList();
+ }
+ // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
+ Map userMap = adminUserApi.getUserMap(
+ convertList(friends, ImFriendDO::getFriendUserId));
+ return convertList(friends, friend -> {
+ ImFriendRespVO vo = BeanUtils.toBean(friend, ImFriendRespVO.class);
+ MapUtils.findAndThen(userMap, friend.getFriendUserId(), user ->
+ vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
+ // 备注 / 昵称的拼音,给前端做字母分桶 + 拼音搜索
+ vo.setDisplayNamePinyin(StrUtils.toPinyin(vo.getDisplayName()))
+ .setNicknamePinyin(StrUtils.toPinyin(vo.getNickname()));
+ return vo;
+ });
+ }
+
+ private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) {
+ if (friend == null) {
+ return null;
+ }
+ return CollUtil.getFirst(buildFriendRespVOList(singleton(friend)));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java
new file mode 100644
index 000000000..10b23519b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java
@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
+import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
+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.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+/**
+ * IM 好友申请记录 Controller
+ *
+ * @author 芋道源码
+ */
+@Tag(name = "管理后台 - IM 好友申请")
+@RestController
+@RequestMapping("/im/friend-request")
+@Validated
+public class ImFriendRequestController {
+
+ @Resource
+ private ImFriendRequestService friendRequestService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/apply")
+ @Operation(summary = "发起好友申请")
+ public CommonResult applyFriend(@Valid @RequestBody ImFriendRequestApplyReqVO reqVO) {
+ ImFriendRequestDO request = friendRequestService.applyFriend(getLoginUserId(), reqVO);
+ return success(request != null ? request.getId() : null);
+ }
+
+ @PutMapping("/agree")
+ @Operation(summary = "同意好友申请")
+ @Parameter(name = "id", description = "申请编号", required = true, example = "1024")
+ public CommonResult agreeFriendRequest(
+ @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
+ friendRequestService.agreeFriendRequest(getLoginUserId(), id);
+ return success(true);
+ }
+
+ @PutMapping("/refuse")
+ @Operation(summary = "拒绝好友申请")
+ public CommonResult refuseFriendRequest(
+ @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
+ @RequestParam(value = "handleContent", required = false)
+ @Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
+ friendRequestService.refuseFriendRequest(getLoginUserId(), id, handleContent);
+ return success(true);
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
+ public CommonResult> getMyFriendRequestList(
+ @Parameter(description = "当前列表最旧记录的 id;首页不传")
+ @RequestParam(value = "maxId", required = false) Long maxId,
+ @Parameter(description = "单次拉取条数", required = true)
+ @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
+ List list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
+ return success(buildList(list));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
+ @Parameter(name = "id", description = "申请记录编号", required = true)
+ public CommonResult getMyFriendRequest(@RequestParam("id") Long id) {
+ ImFriendRequestDO request = friendRequestService.getFriendRequest(id);
+ // 越权过滤:fromUser / toUser 必有一方是当前用户,否则当不存在返回 null
+ Long currentUserId = getLoginUserId();
+ if (request == null || (ObjUtil.notEqual(request.getFromUserId(), currentUserId)
+ && ObjUtil.notEqual(request.getToUserId(), currentUserId))) {
+ return success(null);
+ }
+ return success(CollUtil.getFirst(buildList(Collections.singletonList(request))));
+ }
+
+ // ========== 私有方法:VO 组装 ==========
+
+ private List buildList(List list) {
+ if (CollUtil.isEmpty(list)) {
+ return Collections.emptyList();
+ }
+ // 双向 OR 列表,userIds 取 from + to 两组并集
+ Set userIds = convertSetByFlatMap(list,
+ request -> Stream.of(request.getFromUserId(), request.getToUserId()));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ return convertList(list, request -> {
+ ImFriendRequestRespVO vo = BeanUtils.toBean(request, ImFriendRequestRespVO.class);
+ MapUtils.findAndThen(userMap, request.getFromUserId(), user ->
+ vo.setFromNickname(user.getNickname()).setFromAvatar(user.getAvatar()));
+ MapUtils.findAndThen(userMap, request.getToUserId(), user ->
+ vo.setToNickname(user.getNickname()).setToAvatar(user.getAvatar()));
+ return vo;
+ });
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java
new file mode 100644
index 000000000..2e17b5120
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * IM 好友 Response VO
+ *
+ * @author 芋道源码
+ */
+@Schema(description = "管理后台 - IM 好友 Response VO")
+@Data
+public class ImFriendRespVO {
+
+ @Schema(description = "关系记录编号", example = "1024")
+ private Long id;
+
+ @Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long friendUserId;
+
+ @Schema(description = "是否免打扰", example = "false")
+ private Boolean silent;
+
+ @Schema(description = "好友展示备注(仅自己可见)", example = "老张")
+ private String displayName;
+
+ @Schema(description = "好友展示备注的拼音(小写无空格)", example = "laozhang")
+ private String displayNamePinyin;
+
+ @Schema(description = "添加来源", example = "1")
+ private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
+
+ @Schema(description = "是否置顶联系人", example = "false")
+ private Boolean pinned;
+
+ @Schema(description = "是否拉黑(仅自己可见)", example = "false")
+ private Boolean blocked;
+
+ @Schema(description = "好友状态", example = "0")
+ private Integer status;
+
+ @Schema(description = "添加好友时间")
+ private LocalDateTime addTime;
+
+ @Schema(description = "删除好友时间")
+ private LocalDateTime deleteTime;
+
+ // ========== 下面是聚合字段,方便前端显示 ==========
+
+ @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道")
+ private String nickname;
+
+ @Schema(description = "好友昵称的拼音(小写无空格)", example = "yudao")
+ private String nicknamePinyin;
+
+ @Schema(description = "好友头像(实时聚合自 AdminUser)")
+ private String avatar;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendUpdateReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendUpdateReqVO.java
new file mode 100644
index 000000000..8eee96f3a
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendUpdateReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 好友更新 Request VO")
+@Data
+public class ImFriendUpdateReqVO {
+
+ @Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "好友用户编号不能为空")
+ private Long friendUserId;
+
+ @Schema(description = "是否免打扰;不传表示不修改", example = "true")
+ private Boolean silent;
+
+ @Schema(description = "好友展示备注(仅自己可见);不传表示不修改,传空串表示清空", example = "老张")
+ @Size(max = 16, message = "好友备注最多 16 个字符")
+ private String displayName;
+
+ @Schema(description = "是否置顶联系人;不传表示不修改", example = "true")
+ private Boolean pinned;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestApplyReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestApplyReqVO.java
new file mode 100644
index 000000000..fea58c1c4
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestApplyReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * IM 好友申请 - 发起 Request VO
+ *
+ * @author 芋道源码
+ */
+@Schema(description = "管理后台 - IM 好友申请发起 Request VO")
+@Data
+public class ImFriendRequestApplyReqVO {
+
+ @Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "接收方用户编号不能为空")
+ private Long toUserId;
+
+ @Schema(description = "申请理由", example = "我是芋艿(一种食材)")
+ @Size(max = 255, message = "申请理由最多 255 个字符")
+ private String applyContent;
+
+ @Schema(description = "对接收方的备注(仅自己可见)", example = "老张")
+ @Size(max = 16, message = "好友备注最多 16 个字符")
+ private String displayName;
+
+ @Schema(description = "添加来源", example = "1")
+ @InEnum(ImFriendAddSourceEnum.class)
+ private Integer addSource;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java
new file mode 100644
index 000000000..6f8f93d76
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * IM 好友申请 Response VO
+ *
+ * @author 芋道源码
+ */
+@Schema(description = "管理后台 - IM 好友申请 Response VO")
+@Data
+public class ImFriendRequestRespVO {
+
+ @Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ private Long fromUserId;
+
+ @Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Long toUserId;
+
+ @Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举
+
+ @Schema(description = "申请理由", example = "我是芋艿(一种食材)")
+ private String applyContent;
+
+ @Schema(description = "处理理由(接收方拒绝时可选填)", example = "暂不通过")
+ private String handleContent;
+
+ @Schema(description = "添加来源", example = "1")
+ private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
+
+ @Schema(description = "处理时间")
+ private LocalDateTime handleTime;
+
+ @Schema(description = "申请创建时间")
+ private LocalDateTime createTime;
+
+ // ========== 下面是聚合字段,方便前端显示 ==========
+
+ @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道")
+ private String fromNickname;
+
+ @Schema(description = "发起方头像(实时聚合自 AdminUser)")
+ private String fromAvatar;
+
+ @Schema(description = "接收方昵称(实时聚合自 AdminUser)", example = "老张")
+ private String toNickname;
+
+ @Schema(description = "接收方头像(实时聚合自 AdminUser)")
+ private String toAvatar;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
new file mode 100644
index 000000000..e9a7477b8
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
@@ -0,0 +1,206 @@
+package cn.iocoder.yudao.module.im.controller.admin.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - 群")
+@RestController
+@RequestMapping("/im/group")
+@Validated
+public class ImGroupController {
+
+ @Resource
+ private ImGroupService groupService;
+ @Resource
+ private ImGroupMemberService groupMemberService;
+ @Resource
+ private ImGroupMessageService groupMessageService;
+
+ // ==================== 群的写操作 ====================
+
+ @PostMapping("/create")
+ @Operation(summary = "创建群")
+ public CommonResult createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) {
+ ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId());
+ // 新建群必无 pinnedMessages,跳过关联回填
+ return success(BeanUtils.toBean(group, ImGroupRespVO.class));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新群")
+ public CommonResult updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) {
+ ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId());
+ return success(buildGroupRespVO(group, getLoginUserId()));
+ }
+
+ @DeleteMapping("/dissolve")
+ @Operation(summary = "解散群")
+ @Parameter(name = "id", description = "群编号", required = true)
+ public CommonResult dissolveGroup(@RequestParam("id") Long id) {
+ groupService.dissolveGroup(id, getLoginUserId());
+ return success(true);
+ }
+
+ // ==================== 群的读操作 ====================
+
+ @GetMapping("/get")
+ @Operation(summary = "获得群")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ public CommonResult getGroup(@RequestParam("id") Long id) {
+ ImGroupDO group = groupService.getGroup(id);
+ return success(buildGroupRespVO(group, getLoginUserId()));
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得当前登录用户的群列表")
+ public CommonResult> getMyGroupList() {
+ Long loginUserId = getLoginUserId();
+ List groups = groupService.getMyGroupList(loginUserId);
+ return success(buildGroupRespVOList(groups, loginUserId));
+ }
+
+ // ==================== 群成员的写操作 ====================
+
+ @PostMapping("/invite")
+ @Operation(summary = "邀请用户加入群")
+ public CommonResult inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) {
+ groupService.inviteGroupMember(getLoginUserId(), inviteReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/quit")
+ @Operation(summary = "退出群")
+ @Parameter(name = "groupId", description = "群编号", required = true)
+ public CommonResult quitGroup(@RequestParam("groupId") Long groupId) {
+ groupService.quitGroup(groupId, getLoginUserId());
+ return success(true);
+ }
+
+ @DeleteMapping("/kicking")
+ @Operation(summary = "移除群成员")
+ public CommonResult removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) {
+ groupService.removeGroupMember(getLoginUserId(), removeReqVO);
+ return success(true);
+ }
+
+ @PutMapping("/add-admin")
+ @Operation(summary = "添加群管理员")
+ public CommonResult addGroupAdmin(@Valid @RequestBody ImGroupAdminAddReqVO reqVO) {
+ groupService.addGroupAdmin(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/remove-admin")
+ @Operation(summary = "撤销群管理员")
+ public CommonResult removeGroupAdmin(@Valid @RequestBody ImGroupAdminRemoveReqVO reqVO) {
+ groupService.removeGroupAdmin(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/transfer-owner")
+ @Operation(summary = "转让群主")
+ public CommonResult transferGroupOwner(@Valid @RequestBody ImGroupTransferOwnerReqVO transferReqVO) {
+ groupService.transferGroupOwner(getLoginUserId(), transferReqVO);
+ return success(true);
+ }
+
+ // ==================== 群消息置顶 ====================
+
+ @PutMapping("/pin-message")
+ @Operation(summary = "置顶群消息(群主 / 管理员)")
+ public CommonResult pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
+ groupService.pinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
+ return success(true);
+ }
+
+ @PutMapping("/unpin-message")
+ @Operation(summary = "取消置顶群消息(群主 / 管理员)")
+ public CommonResult unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
+ groupService.unpinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
+ return success(true);
+ }
+
+ // ==================== 群禁言 ====================
+
+ @PutMapping("/mute-all")
+ @Operation(summary = "全群禁言 / 取消(群主 / 管理员)")
+ public CommonResult muteAll(@Valid @RequestBody ImGroupMuteAllReqVO reqVO) {
+ groupService.muteAll(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/mute-member")
+ @Operation(summary = "禁言成员")
+ public CommonResult muteMember(@Valid @RequestBody ImGroupMuteMemberReqVO reqVO) {
+ groupService.muteMember(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/cancel-mute-member")
+ @Operation(summary = "取消成员禁言")
+ public CommonResult cancelMuteMember(@Valid @RequestBody ImGroupCancelMuteMemberReqVO reqVO) {
+ groupService.cancelMuteMember(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */
+ private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) {
+ if (group == null) {
+ return null;
+ }
+ return buildGroupRespVOList(Collections.singletonList(group), loginUserId).get(0);
+ }
+
+ /**
+ * 群列表批量转 VO + 关联回填 pinnedMessages
+ *
+ * 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容
+ */
+ private List buildGroupRespVOList(List groups, Long loginUserId) {
+ if (CollUtil.isEmpty(groups)) {
+ return Collections.emptyList();
+ }
+ // 仅当前用户是有效成员的群才允许回填置顶消息
+ Set activeGroupIds = convertSet(
+ groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
+ Set allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
+ ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
+ Map messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
+ // 转换输出
+ return convertList(groups, group -> {
+ ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);
+ if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {
+ return vo;
+ }
+ // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过
+ List pinnedMesages = convertList(group.getPinnedMessageIds(), messageMap::get);
+ return vo.setPinnedMessages(BeanUtils.toBean(pinnedMesages, ImGroupMessageRespVO.class));
+ });
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
new file mode 100644
index 000000000..5584af6b4
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.module.im.controller.admin.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
+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.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
+
+@Tag(name = "管理后台 - 群成员")
+@RestController
+@RequestMapping("/im/group-member")
+@Validated
+public class ImGroupMemberController {
+
+ @Resource
+ private ImGroupMemberService groupMemberService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PutMapping("/update")
+ @Operation(summary = "更新群成员")
+ public CommonResult updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) {
+ groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得群成员")
+ @Parameters({
+ @Parameter(name = "id", description = "编号(与 groupId + userId 二选一)", example = "1024"),
+ @Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"),
+ @Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100")
+ })
+ public CommonResult getGroupMember(@RequestParam(value = "id", required = false) Long id,
+ @RequestParam(value = "groupId", required = false) Long groupId,
+ @RequestParam(value = "userId", required = false) Long userId) {
+ // 1. 查询群成员
+ ImGroupMemberDO member;
+ if (id != null) {
+ member = groupMemberService.getGroupMember(id);
+ } else if (groupId != null && userId != null) {
+ member = groupMemberService.getGroupMember(groupId, userId);
+ } else {
+ // 避免 selectByGroupIdAndUserId 收到 null 参数走全表扫 / 抛 SQL 异常
+ throw new IllegalArgumentException("参数缺失:需传 id 或 (groupId, userId)");
+ }
+ if (member == null) {
+ return success(null);
+ }
+
+ // 2. 校验当前登录用户是该成员所在群的有效成员
+ Long loginUserId = getLoginUserId();
+ groupMemberService.validateMemberInGroup(member.getGroupId(), loginUserId);
+
+ // 3. 转化 VO
+ ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
+ AdminUserRespDTO user = adminUserApi.getUser(member.getUserId()).getCheckedData();
+ if (user != null) {
+ memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar());
+ }
+ hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId);
+ return success(memberVO);
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得指定群的成员列表")
+ @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
+ public CommonResult> getGroupMemberList(@RequestParam("groupId") Long groupId) {
+ // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤)
+ // 说明:保留已退群成员,是为了前端展示历史消息时,仍能通过该接口拿到已退群成员的昵称 / 头像信息,避免显示为空
+ List members = groupMemberService.getGroupMemberListByGroupId(groupId);
+ // 1.2 校验当前登录用户是否为群的有效成员,非成员不可查看
+ Long loginUserId = getLoginUserId();
+ if (CollUtil.findOne(members, member -> loginUserId.equals(member.getUserId())
+ && CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())) == null) {
+ throw exception(GROUP_MEMBER_NOT_IN_GROUP);
+ }
+
+ // 2.批量聚合 AdminUser 信息(昵称 / 头像)
+ Map userMap = adminUserApi.getUserMap(
+ convertList(members, ImGroupMemberDO::getUserId));
+ return success(convertList(members, m -> {
+ ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class);
+ MapUtils.findAndThen(userMap, m.getUserId(), user ->
+ vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
+ hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId);
+ return vo;
+ }));
+ }
+
+ /**
+ * 非本人查看时,置空成员的私人设置字段(groupRemark / silent)
+ */
+ private void hidePrivateFieldsIfNotSelf(ImGroupMemberRespVO vo, Long memberUserId, Long loginUserId) {
+ if (ObjUtil.notEqual(loginUserId, memberUserId)) {
+ vo.setGroupRemark(null).setSilent(null);
+ }
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
new file mode 100644
index 000000000..440e30baf
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
@@ -0,0 +1,155 @@
+package cn.iocoder.yudao.module.im.controller.admin.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
+import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
+import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
+import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+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.Valid;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+/**
+ * IM 加群申请 Controller
+ *
+ * @author 芋道源码
+ */
+@Tag(name = "管理后台 - IM 加群申请")
+@RestController
+@RequestMapping("/im/group-request")
+@Validated
+public class ImGroupRequestController {
+
+ @Resource
+ private ImGroupRequestService groupRequestService;
+ @Resource
+ private ImGroupService groupService;
+ @Resource
+ private ImGroupMemberService groupMemberService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/apply")
+ @Operation(summary = "申请加群")
+ public CommonResult applyJoinGroup(@Valid @RequestBody ImGroupRequestApplyReqVO reqVO) {
+ ImGroupRequestDO request = groupRequestService.applyJoinGroup(getLoginUserId(), reqVO);
+ return success(request != null ? request.getId() : null);
+ }
+
+ @PutMapping("/agree")
+ @Operation(summary = "同意加群申请(群主或管理员)")
+ @Parameter(name = "id", description = "申请编号", required = true, example = "1024")
+ public CommonResult agreeGroupRequest(
+ @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
+ groupRequestService.agreeGroupRequest(getLoginUserId(), id);
+ return success(true);
+ }
+
+ @PutMapping("/refuse")
+ @Operation(summary = "拒绝加群申请(群主或管理员)")
+ public CommonResult refuseGroupRequest(
+ @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
+ @RequestParam(value = "handleContent", required = false)
+ @Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
+ groupRequestService.refuseGroupRequest(getLoginUserId(), id, handleContent);
+ return success(true);
+ }
+
+ @GetMapping("/unhandled-list")
+ @Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
+ public CommonResult> getUnhandledRequestList() {
+ List list = groupRequestService.getUnhandledRequestListByOwnerOrAdmin(getLoginUserId());
+ return success(buildVOList(list));
+ }
+
+ @GetMapping("/list-by-group")
+ @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
+ @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
+ public CommonResult> getGroupRequestListByGroupId(
+ @RequestParam("groupId") @NotNull(message = "群编号不能为空") Long groupId) {
+ List list = groupRequestService.getGroupRequestListByGroupId(getLoginUserId(), groupId);
+ return success(buildVOList(list));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
+ @Parameter(name = "id", description = "申请记录编号", required = true)
+ public CommonResult getGroupRequest(@RequestParam("id") Long id) {
+ ImGroupRequestDO request = groupRequestService.getGroupRequest(id);
+ if (request == null) {
+ return success(null);
+ }
+ // 越权过滤:申请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
+ Long currentUserId = getLoginUserId();
+ boolean canSee = ObjUtil.equal(request.getUserId(), currentUserId)
+ || ObjUtil.equal(request.getInviterUserId(), currentUserId)
+ || isGroupOwnerOrAdmin(request.getGroupId(), currentUserId);
+ if (!canSee) {
+ return success(null);
+ }
+
+ // 转换并返回
+ return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request))));
+ }
+
+ /**
+ * 当前用户是否该群的有效群主 / 管理员
+ */
+ private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) {
+ ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
+ return member != null
+ && !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())
+ && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole());
+ }
+
+ /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */
+ private List buildVOList(List list) {
+ if (CollUtil.isEmpty(list)) {
+ return Collections.emptyList();
+ }
+ // 1. 聚合 user / inviter 用户信息;convertSetByFlatMap 内部已过滤 null
+ Set userIds = convertSetByFlatMap(list,
+ request -> Stream.of(request.getUserId(), request.getInviterUserId()));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ // 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史)
+ Set groupIds = convertSet(list, ImGroupRequestDO::getGroupId);
+ Map groupMap = groupService.getGroupMap(groupIds);
+ return convertList(list, request -> {
+ ImGroupRequestRespVO vo = BeanUtils.toBean(request, ImGroupRequestRespVO.class);
+ MapUtils.findAndThen(userMap, request.getUserId(), user ->
+ vo.setUserNickname(user.getNickname()).setUserAvatar(user.getAvatar()));
+ MapUtils.findAndThen(userMap, request.getInviterUserId(), user ->
+ vo.setInviterNickname(user.getNickname()).setInviterAvatar(user.getAvatar()));
+ MapUtils.findAndThen(groupMap, request.getGroupId(), group ->
+ vo.setGroupName(group.getName()).setGroupAvatar(group.getAvatar()));
+ return vo;
+ });
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java
new file mode 100644
index 000000000..e4183f0c7
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 添加群管理员 Request VO")
+@Data
+public class ImGroupAdminAddReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
+ @NotEmpty(message = "目标用户编号列表不能为空")
+ private List userIds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java
new file mode 100644
index 000000000..7c35dfb95
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 撤销群管理员 Request VO")
+@Data
+public class ImGroupAdminRemoveReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
+ @NotEmpty(message = "目标用户编号列表不能为空")
+ private List userIds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java
new file mode 100644
index 000000000..2e2243abb
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 取消成员禁言 Request VO")
+@Data
+public class ImGroupCancelMuteMemberReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "用户编号不能为空")
+ private Long userId;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java
new file mode 100644
index 000000000..ec88708a8
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 群创建 Request VO")
+@Data
+public class ImGroupCreateReqVO {
+
+ @Schema(description = "群名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道技术交流群")
+ @NotBlank(message = "群名称不能为空")
+ @Size(max = 64, message = "群名称长度不能超过 64")
+ private String name;
+
+ @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]")
+ private List memberUserIds;
+
+ @Schema(description = "进群是否需群主 / 管理员审批;不传默认 false 自由进群", example = "false")
+ private Boolean joinApproval;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java
new file mode 100644
index 000000000..49c14ffe9
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 管理后台 - 群消息置顶 / 取消置顶 Request VO
+ */
+@Schema(description = "管理后台 - 群消息置顶 / 取消置顶 Request VO")
+@Data
+public class ImGroupMessagePinReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9527")
+ @NotNull(message = "消息编号不能为空")
+ private Long messageId;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java
new file mode 100644
index 000000000..0a23cb05c
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 全群禁言 / 取消 Request VO")
+@Data
+public class ImGroupMuteAllReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "是否全群禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "是否全群禁言不能为空")
+ private Boolean mutedAll;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java
new file mode 100644
index 000000000..184764860
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 成员禁言 Request VO")
+@Data
+public class ImGroupMuteMemberReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "被禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "用户编号不能为空")
+ private Long userId;
+
+ @Schema(description = "禁言时长(秒),0 表示永久禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "600")
+ @NotNull(message = "禁言时长不能为空")
+ @Min(value = 0, message = "禁言时长不能小于 0 秒")
+ private Integer mutedSeconds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupPageReqVO.java
new file mode 100644
index 000000000..fdb601639
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupPageReqVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import lombok.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 群分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ImGroupPageReqVO extends PageParam {
+
+ @Schema(description = "群名称", example = "芋艿")
+ private String name;
+
+ @Schema(description = "群主用户编号", example = "31460")
+ private Long ownerUserId;
+
+ @Schema(description = "群公告")
+ private String notice;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
new file mode 100644
index 000000000..2dac1467b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 群 Response VO")
+@Data
+public class ImGroupRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
+ private Long id;
+
+ @Schema(description = "群名称", example = "芋艿")
+ private String name;
+
+ @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460")
+ private Long ownerUserId;
+
+ @Schema(description = "群头像")
+ private String avatar;
+
+ @Schema(description = "群公告")
+ private String notice;
+
+ @Schema(description = "是否封禁")
+ private Boolean banned;
+
+ @Schema(description = "是否全群禁言")
+ private Boolean mutedAll;
+
+ @Schema(description = "进群是否需群主 / 管理员审批", example = "false")
+ private Boolean joinApproval;
+
+ @Schema(description = "封禁时间")
+ private LocalDateTime bannedTime;
+
+ @Schema(description = "群状态", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Integer status;
+
+ @Schema(description = "解散时间")
+ private LocalDateTime dissolvedTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+ @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
+ private List pinnedMessages;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupSaveReqVO.java
new file mode 100644
index 000000000..0275fd8ef
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupSaveReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - 群新增/修改 Request VO")
+@Data
+public class ImGroupSaveReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
+ private Long id;
+
+ @Schema(description = "群名称", example = "芋艿")
+ private String name;
+
+ @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460")
+ @NotNull(message = "群主用户编号不能为空")
+ private Long ownerUserId;
+
+ @Schema(description = "群头像")
+ private String avatar;
+
+ @Schema(description = "群公告")
+ private String notice;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java
new file mode 100644
index 000000000..226d07e48
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 群主转让 Request VO")
+@Data
+public class ImGroupTransferOwnerReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "新群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202")
+ @NotNull(message = "新群主用户编号不能为空")
+ private Long newOwnerUserId;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java
new file mode 100644
index 000000000..99bdfb358
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 群更新 Request VO")
+@Data
+public class ImGroupUpdateReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "群名称", example = "芋道技术交流群")
+ @Size(max = 64, message = "群名称长度不能超过 64")
+ private String name;
+
+ @Schema(description = "群头像")
+ @Size(max = 512, message = "群头像长度不能超过 512")
+ private String avatar;
+
+ @Schema(description = "群公告")
+ @Size(max = 2048, message = "群公告长度不能超过 2048")
+ private String notice;
+
+ @Schema(description = "进群是否需群主 / 管理员审批", example = "true")
+ private Boolean joinApproval;
+
+ @AssertTrue(message = "群名称不能为空")
+ @JsonIgnore
+ public boolean isNameValid() {
+ return name == null || StrUtil.isNotBlank(name);
+ }
+
+ @AssertTrue(message = "群头像不能为空")
+ @JsonIgnore
+ public boolean isAvatarValid() {
+ return avatar == null || StrUtil.isNotBlank(avatar);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberCreateReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberCreateReqVO.java
new file mode 100644
index 000000000..a486190fb
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberCreateReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 群成员邀请 Request VO")
+@Data
+public class ImGroupMemberCreateReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long groupId;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730")
+ @NotNull(message = "用户编号不能为空")
+ private Long userId;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberInviteReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberInviteReqVO.java
new file mode 100644
index 000000000..12cd09d37
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberInviteReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 群成员邀请 Request VO")
+@Data
+public class ImGroupMemberInviteReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
+ @NotNull(message = "群编号不能为空")
+ private Long groupId;
+
+ @Schema(description = "被邀请的用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
+ @NotEmpty(message = "被邀请的用户编号列表不能为空")
+ private List memberUserIds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRemoveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRemoveReqVO.java
new file mode 100644
index 000000000..9862b12d9
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRemoveReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 群成员移除 Request VO")
+@Data
+public class ImGroupMemberRemoveReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long groupId;
+
+ @Schema(description = "被移除的用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
+ @NotEmpty(message = "被移除的用户编号列表不能为空")
+ private List memberUserIds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java
new file mode 100644
index 000000000..ce095e134
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 群成员 Response VO")
+@Data
+public class ImGroupMemberRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17071")
+ private Long id;
+
+ @Schema(description = "群编号", example = "13279")
+ private Long groupId;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730")
+ private Long userId;
+
+ @Schema(description = "组内显示名", example = "芋艿")
+ private String displayUserName;
+
+ @Schema(description = "群备注", example = "核心群")
+ private String groupRemark;
+
+ @Schema(description = "是否免打扰")
+ private Boolean silent;
+
+ @Schema(description = "成员状态", example = "0")
+ private Integer status;
+
+ @Schema(description = "成员角色", example = "3")
+ private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类
+
+ @Schema(description = "入群时间")
+ private LocalDateTime joinTime;
+
+ @Schema(description = "退群时间")
+ private LocalDateTime quitTime;
+
+ @Schema(description = "禁言到期时间")
+ private LocalDateTime muteEndTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+ // ========== 关联 AdminUser 的字段 ==========
+
+ @Schema(description = "用户昵称", example = "芋道")
+ private String nickname;
+
+ @Schema(description = "用户头像")
+ private String avatar;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java
new file mode 100644
index 000000000..8c2c6fd1f
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Schema(description = "管理后台 - 群成员更新 Request VO")
+@Data
+@Accessors(chain = true)
+public class ImGroupMemberUpdateReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long groupId;
+
+ @Schema(description = "群内昵称", example = "芋头")
+ private String displayUserName;
+
+ @Schema(description = "群备注", example = "公司群")
+ private String groupRemark;
+
+ @Schema(description = "是否免打扰")
+ private Boolean silent;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestApplyReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestApplyReqVO.java
new file mode 100644
index 000000000..6f34a05c9
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestApplyReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.request;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 加群申请发起 Request VO")
+@Data
+public class ImGroupRequestApplyReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long groupId;
+
+ @Schema(description = "申请理由", example = "我是芋艿(一种食材)")
+ @Size(max = 255, message = "申请理由最多 255 个字符")
+ private String applyContent;
+
+ @Schema(description = "加入来源", example = "1")
+ @InEnum(ImGroupAddSourceEnum.class)
+ private Integer addSource;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java
new file mode 100644
index 000000000..587ca8d0f
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.im.controller.admin.group.vo.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 加群申请 Response VO")
+@Data
+public class ImGroupRequestRespVO {
+
+ @Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long groupId;
+
+ @Schema(description = "申请人 / 被邀请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ private Long userId;
+
+ @Schema(description = "邀请人用户编号;NULL 表示用户主动申请", example = "200")
+ private Long inviterUserId;
+
+ @Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举
+
+ @Schema(description = "申请理由", example = "我想加入这个群")
+ private String applyContent;
+
+ @Schema(description = "处理理由(拒绝时可选填)", example = "暂不通过")
+ private String handleContent;
+
+ @Schema(description = "处理人用户编号", example = "31460")
+ private Long handleUserId;
+
+ @Schema(description = "加入来源", example = "1")
+ private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举
+
+ @Schema(description = "处理时间")
+ private LocalDateTime handleTime;
+
+ @Schema(description = "申请创建时间")
+ private LocalDateTime createTime;
+
+ // ========== 下面是聚合字段,方便前端显示 ==========
+
+ @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道")
+ private String userNickname;
+
+ @Schema(description = "申请人 / 被邀请人头像(实时聚合自 AdminUser)")
+ private String userAvatar;
+
+ @Schema(description = "邀请人昵称(实时聚合自 AdminUser)", example = "老张")
+ private String inviterNickname;
+
+ @Schema(description = "邀请人头像(实时聚合自 AdminUser)")
+ private String inviterAvatar;
+
+ @Schema(description = "群名称(实时聚合自 ImGroup)", example = "芋道技术交流群")
+ private String groupName;
+
+ @Schema(description = "群头像(实时聚合自 ImGroup)")
+ private String groupAvatar;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelManagerController.java
new file mode 100644
index 000000000..2afc95ee2
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelManagerController.java
@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - IM 频道")
+@RestController
+@RequestMapping("/im/manager/channel")
+@Validated
+public class ImChannelManagerController {
+
+ @Resource
+ private ImChannelService channelService;
+
+ @PostMapping("/create")
+ @Operation(summary = "新增频道")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel:create')")
+ public CommonResult createChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
+ return success(channelService.createChannel(reqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改频道")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel:update')")
+ public CommonResult updateChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
+ channelService.updateChannel(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除频道")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel:delete')")
+ public CommonResult deleteChannel(@RequestParam("id") Long id) {
+ channelService.deleteChannel(id);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得频道分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
+ public CommonResult> getChannelPage(@Valid ImChannelPageReqVO pageReqVO) {
+ PageResult pageResult = channelService.getChannelPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, ImChannelRespVO.class));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得频道详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
+ public CommonResult getChannel(@RequestParam("id") Long id) {
+ ImChannelDO channel = channelService.getChannel(id);
+ return success(BeanUtils.toBean(channel, ImChannelRespVO.class));
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "获得启用的频道精简列表;前端表单选择频道时调用")
+ public CommonResult> getSimpleChannelList() {
+ // TODO DONE @AI:getChannelListByStatus 统一命名
+ List list = channelService.getChannelListByStatus(CommonStatusEnum.ENABLE.getStatus());
+ return success(BeanUtils.toBean(list, ImChannelRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelMaterialManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelMaterialManagerController.java
new file mode 100644
index 000000000..e6e027143
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelMaterialManagerController.java
@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - IM 频道素材")
+@RestController
+@RequestMapping("/im/manager/channel-material")
+@Validated
+public class ImChannelMaterialManagerController {
+
+ @Resource
+ private ImChannelMaterialService channelMaterialService;
+ @Resource
+ private ImChannelService channelService;
+
+ @PostMapping("/create")
+ @Operation(summary = "新增素材")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-material:create')")
+ public CommonResult createMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
+ return success(channelMaterialService.createMaterial(reqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改素材")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-material:update')")
+ public CommonResult updateMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
+ channelMaterialService.updateMaterial(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除素材")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-material:delete')")
+ public CommonResult deleteMaterial(@RequestParam("id") Long id) {
+ channelMaterialService.deleteMaterial(id);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得素材分页;含频道名回填")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
+ public CommonResult> getMaterialPage(@Valid ImChannelMaterialPageReqVO pageReqVO) {
+ PageResult pageResult = channelMaterialService.getMaterialPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 回填频道名
+ List channels = channelService.getChannelList(
+ convertSet(pageResult.getList(), ImChannelMaterialDO::getChannelId));
+ Map channelMap = convertMap(channels, ImChannelDO::getId);
+ return success(BeanUtils.toBean(pageResult, ImChannelMaterialRespVO.class, vo ->
+ MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()))));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得素材详情(含富文本正文)")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
+ public CommonResult getMaterial(@RequestParam("id") Long id) {
+ ImChannelMaterialDO material = channelMaterialService.getMaterial(id);
+ return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "获得指定频道下的素材精简列表;用于推送弹窗的素材下拉")
+ @Parameter(name = "channelId", description = "频道编号", required = true, example = "1")
+ public CommonResult> getSimpleMaterialList(@RequestParam("channelId") Long channelId) {
+ List list = channelMaterialService.getMaterialListByChannelId(channelId);
+ return success(BeanUtils.toBean(list, ImChannelMaterialRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelPageReqVO.java
new file mode 100644
index 000000000..51ac09af9
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelPageReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Schema(description = "管理后台 - IM 频道分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImChannelPageReqVO extends PageParam {
+
+ @Schema(description = "频道业务码", example = "system_notice")
+ private String code;
+
+ @Schema(description = "频道名称", example = "系统")
+ private String name;
+
+ @Schema(description = "状态", example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelRespVO.java
new file mode 100644
index 000000000..59fed78af
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelRespVO.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 频道 Response VO")
+@Data
+public class ImChannelRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "频道业务码", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
+ private String code;
+
+ @Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
+ private String name;
+
+ @Schema(description = "频道头像")
+ private String avatar;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelSaveReqVO.java
new file mode 100644
index 000000000..6e1b04283
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelSaveReqVO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 频道新增 / 修改 Request VO")
+@Data
+public class ImChannelSaveReqVO {
+
+ @Schema(description = "编号(修改时必填)", example = "1024")
+ private Long id;
+
+ @Schema(description = "频道业务码;唯一", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
+ @NotBlank(message = "频道编码不能为空")
+ @Size(max = 64, message = "频道编码长度不能超过 64")
+ @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "频道编码只能由小写字母 / 数字 / 下划线组成,且必须以字母开头")
+ private String code;
+
+ @Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
+ @NotBlank(message = "频道名称不能为空")
+ @Size(max = 64, message = "频道名称长度不能超过 64")
+ private String name;
+
+ @Schema(description = "频道头像", example = "https://cdn.example.com/channel/system_notice.png")
+ @Size(max = 512, message = "头像长度不能超过 512")
+ private String avatar;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "排序不能为空")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "状态不能为空")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialPageReqVO.java
new file mode 100644
index 000000000..1bb834b3b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialPageReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 频道素材分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImChannelMaterialPageReqVO extends PageParam {
+
+ @Schema(description = "频道编号", example = "1")
+ private Long channelId;
+
+ @Schema(description = "内容类型", example = "1")
+ private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
+
+ @Schema(description = "标题", example = "活动")
+ private String title;
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialRespVO.java
new file mode 100644
index 000000000..55aebed92
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialRespVO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 频道素材 Response VO")
+@Data
+public class ImChannelMaterialRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long channelId;
+
+ @Schema(description = "频道名称(关联查询填充)")
+ private String channelName;
+
+ @Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
+
+ @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String title;
+
+ @Schema(description = "封面图")
+ private String coverUrl;
+
+ @Schema(description = "摘要")
+ private String summary;
+
+ @Schema(description = "正文;富文本 HTML")
+ private String content;
+
+ @Schema(description = "跳转链接")
+ private String url;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialSaveReqVO.java
new file mode 100644
index 000000000..e30f35b91
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialSaveReqVO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 频道素材新增 / 修改 Request VO")
+@Data
+public class ImChannelMaterialSaveReqVO {
+
+ @Schema(description = "编号(修改时必填)", example = "1024")
+ private Long id;
+
+ @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "频道编号不能为空")
+ private Long channelId;
+
+ @Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "内容类型不能为空")
+ private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
+
+ @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "双十一活动来啦")
+ @NotBlank(message = "标题不能为空")
+ @Size(max = 128, message = "标题长度不能超过 128")
+ private String title;
+
+ @Schema(description = "封面图", example = "https://cdn.example.com/cover.png")
+ @Size(max = 512, message = "封面图长度不能超过 512")
+ private String coverUrl;
+
+ @Schema(description = "摘要", example = "全场五折,戳详情看玩法")
+ @Size(max = 255, message = "摘要长度不能超过 255")
+ private String summary;
+
+ @Schema(description = "正文;富文本 HTML")
+ private String content;
+
+ @Schema(description = "跳转链接;为空表示走客户端内置详情页", example = "https://example.com/activity/123")
+ @Size(max = 512, message = "跳转链接长度不能超过 512")
+ private String url;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackItemManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackItemManagerController.java
new file mode 100644
index 000000000..4a2543179
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackItemManagerController.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
+import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - IM 表情包项")
+@RestController
+@RequestMapping("/im/manager/face-pack-item")
+@Validated
+public class ImFacePackItemManagerController {
+
+ @Resource
+ private ImFacePackItemService facePackItemService;
+
+ @PostMapping("/create")
+ @Operation(summary = "新增表情")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:create')")
+ public CommonResult createFacePackItem(@Valid @RequestBody ImFacePackItemSaveReqVO reqVO) {
+ return success(facePackItemService.createFacePackItem(reqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改表情")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:update')")
+ public CommonResult updateFacePackItem(@Valid @RequestBody ImFacePackItemSaveReqVO reqVO) {
+ facePackItemService.updateFacePackItem(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除表情")
+ @Parameter(name = "id", description = "编号", required = true, example = "2048")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')")
+ public CommonResult deleteFacePackItem(@RequestParam("id") Long id) {
+ facePackItemService.deleteFacePackItem(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除表情")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')")
+ public CommonResult deleteFacePackItemList(
+ @RequestParam("ids") @Size(max = 100, message = "批量删除最多 100 条") List ids) {
+ facePackItemService.deleteFacePackItemList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得表情分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:query')")
+ public CommonResult> getFacePackItemPage(@Valid ImFacePackItemPageReqVO pageReqVO) {
+ PageResult pageResult = facePackItemService.getFacePackItemPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, ImFacePackItemRespVO.class));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得表情详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "2048")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:query')")
+ public CommonResult getFacePackItem(@RequestParam("id") Long id) {
+ ImFacePackItemDO item = facePackItemService.getFacePackItem(id);
+ return success(BeanUtils.toBean(item, ImFacePackItemRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackManagerController.java
new file mode 100644
index 000000000..ad2f8ff72
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackManagerController.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
+import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - IM 表情包")
+@RestController
+@RequestMapping("/im/manager/face-pack")
+@Validated
+public class ImFacePackManagerController {
+
+ @Resource
+ private ImFacePackService facePackService;
+
+ @PostMapping("/create")
+ @Operation(summary = "新增表情包")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:create')")
+ public CommonResult createFacePack(@Valid @RequestBody ImFacePackSaveReqVO reqVO) {
+ return success(facePackService.createFacePack(reqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改表情包")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:update')")
+ public CommonResult updateFacePack(@Valid @RequestBody ImFacePackSaveReqVO reqVO) {
+ facePackService.updateFacePack(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除表情包")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')")
+ public CommonResult deleteFacePack(@RequestParam("id") Long id) {
+ facePackService.deleteFacePack(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除表情包")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')")
+ public CommonResult deleteFacePackList(@RequestParam("ids")
+ @Size(max = 100, message = "批量删除最多 100 条") List ids) {
+ facePackService.deleteFacePackList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得表情包分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:query')")
+ public CommonResult> getFacePackPage(@Valid ImFacePackPageReqVO pageReqVO) {
+ PageResult pageResult = facePackService.getFacePackPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, ImFacePackRespVO.class));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得表情包详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-pack:query')")
+ public CommonResult getFacePack(@RequestParam("id") Long id) {
+ ImFacePackDO pack = facePackService.getFacePack(id);
+ return success(BeanUtils.toBean(pack, ImFacePackRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFaceUserItemManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFaceUserItemManagerController.java
new file mode 100644
index 000000000..df4a06d6e
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFaceUserItemManagerController.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem.ImFaceUserItemManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem.ImFaceUserItemManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
+import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
+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.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - IM 用户表情")
+@RestController
+@RequestMapping("/im/manager/face-user-item")
+@Validated
+public class ImFaceUserItemManagerController {
+
+ @Resource
+ private ImFaceUserItemService faceUserItemService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得用户表情分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-user-item:query')")
+ public CommonResult> getFaceUserItemPage(
+ @Valid ImFaceUserItemManagerPageReqVO pageReqVO) {
+ PageResult pageResult = faceUserItemService.getFaceUserItemPage(pageReqVO);
+ // 关联回填用户昵称
+ Map userMap = adminUserApi.getUserMap(
+ CollectionUtils.convertSet(pageResult.getList(), ImFaceUserItemDO::getUserId));
+ List voList = CollectionUtils.convertList(pageResult.getList(), item -> {
+ ImFaceUserItemManagerRespVO vo = BeanUtils.toBean(item, ImFaceUserItemManagerRespVO.class);
+ AdminUserRespDTO user = userMap.get(item.getUserId());
+ if (user != null) {
+ vo.setUserNickname(user.getNickname());
+ }
+ return vo;
+ });
+ return success(new PageResult<>(voList, pageResult.getTotal()));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除用户表情")
+ @Parameter(name = "id", description = "编号", required = true, example = "4096")
+ @PreAuthorize("@ss.hasPermission('im:manager:face-user-item:delete')")
+ public CommonResult deleteFaceUserItem(@RequestParam("id") Long id) {
+ faceUserItemService.deleteFaceUserItem(id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemPageReqVO.java
new file mode 100644
index 000000000..eefbbed95
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemPageReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Schema(description = "管理后台 - IM 表情包项分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImFacePackItemPageReqVO extends PageParam {
+
+ @Schema(description = "所属表情包编号", example = "1024")
+ private Long packId;
+
+ @Schema(description = "表情名,模糊匹配", example = "狗")
+ private String name;
+
+ @Schema(description = "状态", example = "0")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemRespVO.java
new file mode 100644
index 000000000..817348f10
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemRespVO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 表情包项 Response VO")
+@Data
+public class ImFacePackItemRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long id;
+
+ @Schema(description = "所属表情包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long packId;
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/pack/cat-001.png")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ private Integer height;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemSaveReqVO.java
new file mode 100644
index 000000000..de6842bea
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemSaveReqVO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 表情包项新增 / 修改 Request VO")
+@Data
+public class ImFacePackItemSaveReqVO {
+
+ @Schema(description = "编号(修改时必填)", example = "2048")
+ private Long id;
+
+ @Schema(description = "所属表情包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "表情包编号不能为空")
+ private Long packId;
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/pack/cat-001.png")
+ @NotBlank(message = "表情图 URL 不能为空")
+ @Size(max = 512, message = "表情图 URL 长度不能超过 512")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ @Size(max = 64, message = "表情名长度不能超过 64")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ @NotNull(message = "渲染宽度不能为空")
+ @Min(value = 1, message = "渲染宽度不能小于 1 像素")
+ @Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+ @NotNull(message = "渲染高度不能为空")
+ @Min(value = 1, message = "渲染高度不能小于 1 像素")
+ @Max(value = 2048, message = "渲染高度不能大于 2048 像素")
+ private Integer height;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "排序不能为空")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "状态不能为空")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackPageReqVO.java
new file mode 100644
index 000000000..e0220afb7
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackPageReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 表情包分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImFacePackPageReqVO extends PageParam {
+
+ @Schema(description = "表情包名称,模糊匹配", example = "猫")
+ private String name;
+
+ @Schema(description = "状态", example = "0")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackRespVO.java
new file mode 100644
index 000000000..bdc53d2d4
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackRespVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 表情包 Response VO")
+@Data
+public class ImFacePackRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
+ private String name;
+
+ @Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
+ private String icon;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackSaveReqVO.java
new file mode 100644
index 000000000..86be43d35
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackSaveReqVO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 表情包新增 / 修改 Request VO")
+@Data
+public class ImFacePackSaveReqVO {
+
+ @Schema(description = "编号(修改时必填)", example = "1024")
+ private Long id;
+
+ @Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
+ @NotBlank(message = "表情包名称不能为空")
+ @Size(max = 64, message = "表情包名称长度不能超过 64")
+ private String name;
+
+ @Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
+ @Size(max = 512, message = "图标长度不能超过 512")
+ private String icon;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "排序不能为空")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "状态不能为空")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerPageReqVO.java
new file mode 100644
index 000000000..dec8f1f39
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerPageReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 用户表情分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImFaceUserItemManagerPageReqVO extends PageParam {
+
+ @Schema(description = "所属用户编号", example = "1024")
+ private Long userId;
+
+ @Schema(description = "表情名,模糊匹配", example = "狗")
+ private String name;
+
+ @Schema(description = "添加时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerRespVO.java
new file mode 100644
index 000000000..723fc48f7
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/useritem/ImFaceUserItemManagerRespVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 用户表情 Response VO")
+@Data
+public class ImFaceUserItemManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
+ private Long id;
+
+ @Schema(description = "所属用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long userId;
+
+ @Schema(description = "所属用户昵称", example = "张三")
+ private String userNickname;
+
+ @Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "https://cdn.example.com/face/user/abc.gif")
+ private String url;
+
+ @Schema(description = "表情名", example = "狗头")
+ private String name;
+
+ @Schema(description = "渲染宽度(像素)", example = "200")
+ private Integer width;
+
+ @Schema(description = "渲染高度(像素)", example = "200")
+ private Integer height;
+
+ @Schema(description = "添加时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendManagerController.java
new file mode 100644
index 000000000..b2f6ae709
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendManagerController.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
+import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
+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.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+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.RestController;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@Tag(name = "管理后台 - IM 好友管理")
+@RestController
+@RequestMapping("/im/manager/friend")
+@Validated
+public class ImFriendManagerController {
+
+ @Resource
+ private ImFriendService friendService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得好友关系分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:friend:query')")
+ public CommonResult> getFriendPage(
+ @Valid ImFriendManagerPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = friendService.getFriendPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 2.1 一次性批量查询用户 + 好友的昵称
+ Set userIds = convertSetByFlatMap(pageResult.getList(),
+ f -> Stream.of(f.getUserId(), f.getFriendUserId()));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ // 2.2 转换为 VO,填充昵称
+ return success(BeanUtils.toBean(pageResult, ImFriendManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getUserId(),
+ user -> vo.setUserNickname(user.getNickname()));
+ MapUtils.findAndThen(userMap, vo.getFriendUserId(),
+ user -> vo.setFriendNickname(user.getNickname()));
+ }));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendRequestManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendRequestManagerController.java
new file mode 100644
index 000000000..eac0c9a9f
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendRequestManagerController.java
@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
+import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
+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.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+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.RestController;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@Tag(name = "管理后台 - IM 好友申请管理")
+@RestController
+@RequestMapping("/im/manager/friend-request")
+@Validated
+public class ImFriendRequestManagerController {
+
+ @Resource
+ private ImFriendRequestService friendRequestService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得好友申请分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:friend-request:query')")
+ public CommonResult> getFriendRequestPage(
+ @Valid ImFriendRequestManagerPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = friendRequestService.getFriendRequestPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+
+ // 2.1 一次性批量查询发起方 + 接收方的昵称
+ Set userIds = convertSetByFlatMap(pageResult.getList(),
+ request -> Stream.of(request.getFromUserId(), request.getToUserId()));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ // 2.2 转换为 VO,填充昵称
+ return success(BeanUtils.toBean(pageResult, ImFriendRequestManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getFromUserId(),
+ user -> vo.setFromNickname(user.getNickname()));
+ MapUtils.findAndThen(userMap, vo.getToUserId(),
+ user -> vo.setToNickname(user.getNickname()));
+ }));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerPageReqVO.java
new file mode 100644
index 000000000..54b5dcce1
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerPageReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 好友关系分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImFriendManagerPageReqVO extends PageParam {
+
+ @Schema(description = "用户编号", example = "1024")
+ private Long userId;
+
+ @Schema(description = "好友用户编号", example = "2048")
+ private Long friendUserId;
+
+ @Schema(description = "好友状态", example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "是否免打扰", example = "false")
+ private Boolean silent;
+
+ @Schema(description = "添加好友时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] addTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerRespVO.java
new file mode 100644
index 000000000..6664f2435
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerRespVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 好友关系 Response VO")
+@Data
+public class ImFriendManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long userId;
+
+ @Schema(description = "用户昵称", example = "张三")
+ private String userNickname;
+
+ @Schema(description = "好友用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long friendUserId;
+
+ @Schema(description = "好友昵称", example = "李四")
+ private String friendNickname;
+
+ @Schema(description = "好友展示备注")
+ private String displayName;
+
+ @Schema(description = "添加来源", example = "1")
+ private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
+
+ @Schema(description = "是否免打扰", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+ private Boolean silent;
+
+ @Schema(description = "是否置顶联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+ private Boolean pinned;
+
+ @Schema(description = "是否拉黑", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+ private Boolean blocked;
+
+ @Schema(description = "好友状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "添加好友时间")
+ private LocalDateTime addTime;
+
+ @Schema(description = "删除好友时间")
+ private LocalDateTime deleteTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerPageReqVO.java
new file mode 100644
index 000000000..a07890928
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerPageReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 好友申请分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImFriendRequestManagerPageReqVO extends PageParam {
+
+ @Schema(description = "发起方用户编号", example = "1024")
+ private Long fromUserId;
+
+ @Schema(description = "接收方用户编号", example = "2048")
+ private Long toUserId;
+
+ @Schema(description = "处理结果", example = "0")
+ private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举类
+
+ @Schema(description = "添加来源", example = "1")
+ private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerRespVO.java
new file mode 100644
index 000000000..9fa9d4d45
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendRequestManagerRespVO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 好友申请 Response VO")
+@Data
+public class ImFriendRequestManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long fromUserId;
+
+ @Schema(description = "发起方昵称", example = "张三")
+ private String fromNickname;
+
+ @Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long toUserId;
+
+ @Schema(description = "接收方昵称", example = "李四")
+ private String toNickname;
+
+ @Schema(description = "申请理由", example = "我是芋艿")
+ private String applyContent;
+
+ @Schema(description = "发起方对接收方的备注")
+ private String displayName;
+
+ @Schema(description = "添加来源", example = "1")
+ private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
+
+ @Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举类
+
+ @Schema(description = "处理理由", example = "暂不通过")
+ private String handleContent;
+
+ @Schema(description = "处理时间")
+ private LocalDateTime handleTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupManagerController.java
new file mode 100644
index 000000000..46fd4eb11
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupManagerController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+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.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - IM 群聊管理")
+@RestController
+@RequestMapping("/im/manager/group")
+@Validated
+public class ImGroupManagerController {
+
+ @Resource
+ private ImGroupService groupService;
+ @Resource
+ private ImGroupMemberService groupMemberService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得群分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
+ public CommonResult> getGroupPage(@Valid ImGroupManagerPageReqVO pageReqVO) {
+ // 1. 分页查询群
+ PageResult pageResult = groupService.getGroupPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 2.1 批量查询相关数据
+ Map userMap = adminUserApi.getUserMap(
+ convertSet(pageResult.getList(), ImGroupDO::getOwnerUserId));
+ Map memberCountMap = groupMemberService.getActiveMemberCountMap(
+ convertSet(pageResult.getList(), ImGroupDO::getId));
+ // 2.2 转换为 VO,填充群主昵称、群成员数量
+ return success(BeanUtils.toBean(pageResult, ImGroupManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getOwnerUserId(),
+ user -> vo.setOwnerNickname(user.getNickname()));
+ vo.setMemberCount(memberCountMap.getOrDefault(vo.getId(), 0L).intValue());
+ }));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得群详情")
+ @Parameter(name = "id", description = "群编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
+ public CommonResult getGroup(@RequestParam("id") Long id) {
+ ImGroupDO group = groupService.getGroup(id);
+ return success(BeanUtils.toBean(group, ImGroupManagerRespVO.class));
+ }
+
+ @PutMapping("/ban")
+ @Operation(summary = "封禁群")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
+ public CommonResult banGroup(@Valid @RequestBody ImGroupManagerBanReqVO reqVO) {
+ groupService.banGroup(getLoginUserId(), reqVO);
+ return success(true);
+ }
+
+ @PutMapping("/unban")
+ @Operation(summary = "解封群")
+ @Parameter(name = "id", description = "群编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
+ public CommonResult unbanGroup(@RequestParam("id") Long id) {
+ groupService.unbanGroup(getLoginUserId(), id);
+ return success(true);
+ }
+
+ @DeleteMapping("/dissolve")
+ @Operation(summary = "解散群")
+ @Parameter(name = "id", description = "群编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:dissolve')")
+ public CommonResult dissolveGroup(@RequestParam("id") Long id) {
+ groupService.dissolveGroupByManager(getLoginUserId(), id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupMemberManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupMemberManagerController.java
new file mode 100644
index 000000000..e1fa1f328
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupMemberManagerController.java
@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
+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 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.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - IM 群成员管理")
+@RestController
+@RequestMapping("/im/manager/group/member")
+@Validated
+public class ImGroupMemberManagerController {
+
+ @Resource
+ private ImGroupMemberService groupMemberService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/list")
+ @Operation(summary = "获得群成员列表(含已退群成员,由前端按需过滤)")
+ @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
+ public CommonResult> getGroupMemberList(@RequestParam("groupId") Long groupId) {
+ // 1. 查询群全部成员(含已退群)
+ List members = groupMemberService.getGroupMemberListByGroupId(groupId);
+ if (CollUtil.isEmpty(members)) {
+ return success(Collections.emptyList());
+ }
+ // 2.1 批量查询用户信息
+ Map userMap = adminUserApi.getUserMap(
+ convertSet(members, ImGroupMemberDO::getUserId));
+ // 2.2 转换为 VO,填充昵称、头像
+ return success(BeanUtils.toBean(members, ImGroupMemberManagerRespVO.class, vo ->
+ MapUtils.findAndThen(userMap, vo.getUserId(), user ->
+ vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()))));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupRequestManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupRequestManagerController.java
new file mode 100644
index 000000000..801031240
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupRequestManagerController.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+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.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+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.RestController;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@Tag(name = "管理后台 - IM 加群申请管理")
+@RestController
+@RequestMapping("/im/manager/group-request")
+@Validated
+public class ImGroupRequestManagerController {
+
+ @Resource
+ private ImGroupRequestService groupRequestService;
+ @Resource
+ private ImGroupService groupService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得加群申请分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:group-request:query')")
+ public CommonResult> getGroupRequestPage(
+ @Valid ImGroupRequestManagerPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = groupRequestService.getGroupRequestPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+
+ // 2.1 批量聚合 user / inviter / handler 用户昵称
+ Set userIds = convertSetByFlatMap(pageResult.getList(),
+ request -> Stream.of(request.getUserId(), request.getInviterUserId(), request.getHandleUserId())
+ .filter(Objects::nonNull));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ // 2.2 批量聚合群信息(取群名)
+ Set groupIds = convertSet(pageResult.getList(), ImGroupRequestDO::getGroupId);
+ Map groupMap = groupService.getGroupMap(groupIds);
+ return success(BeanUtils.toBean(pageResult, ImGroupRequestManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname()));
+ MapUtils.findAndThen(userMap, vo.getInviterUserId(), user -> vo.setInviterNickname(user.getNickname()));
+ MapUtils.findAndThen(userMap, vo.getHandleUserId(), user -> vo.setHandleNickname(user.getNickname()));
+ MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
+ }));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerBanReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerBanReqVO.java
new file mode 100644
index 000000000..a4fb81961
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerBanReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 群聊封禁 Request VO")
+@Data
+public class ImGroupManagerBanReqVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "群编号不能为空")
+ private Long id;
+
+ @Schema(description = "封禁原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "违规内容")
+ @NotBlank(message = "封禁原因不能为空")
+ @Size(max = 200, message = "封禁原因长度不能超过 200")
+ private String reason;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerPageReqVO.java
new file mode 100644
index 000000000..e7da76b1b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerPageReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 群聊分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImGroupManagerPageReqVO extends PageParam {
+
+ @Schema(description = "群名称,模糊匹配", example = "技术交流群")
+ private String name;
+
+ @Schema(description = "群主用户编号", example = "1024")
+ private Long ownerUserId;
+
+ @Schema(description = "群状态", example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 正常 / 1 已解散)
+
+ @Schema(description = "是否封禁", example = "false")
+ private Boolean banned;
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerRespVO.java
new file mode 100644
index 000000000..c1de79232
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerRespVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 群聊 Response VO")
+@Data
+public class ImGroupManagerRespVO {
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "群名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术交流群")
+ private String name;
+
+ @Schema(description = "群头像")
+ private String avatar;
+
+ @Schema(description = "群公告")
+ private String notice;
+
+ @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long ownerUserId;
+
+ @Schema(description = "群主昵称", example = "张三")
+ private String ownerNickname;
+
+ @Schema(description = "群成员数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
+ private Integer memberCount;
+
+ @Schema(description = "群状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "解散时间")
+ private LocalDateTime dissolvedTime;
+
+ @Schema(description = "是否封禁", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+ private Boolean banned;
+
+ @Schema(description = "是否全群禁言")
+ private Boolean mutedAll;
+
+ @Schema(description = "封禁原因")
+ private String bannedReason;
+
+ @Schema(description = "封禁时间")
+ private LocalDateTime bannedTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerPageReqVO.java
new file mode 100644
index 000000000..e51d06685
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerPageReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 加群申请分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImGroupRequestManagerPageReqVO extends PageParam {
+
+ @Schema(description = "群编号", example = "1024")
+ private Long groupId;
+
+ @Schema(description = "申请人 / 被邀请人用户编号", example = "2048")
+ private Long userId;
+
+ @Schema(description = "邀请人用户编号", example = "31460")
+ private Long inviterUserId;
+
+ @Schema(description = "处理结果", example = "0")
+ private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举类
+
+ @Schema(description = "加入来源", example = "1")
+ private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举类
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerRespVO.java
new file mode 100644
index 000000000..6f8684e90
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupRequestManagerRespVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 加群申请 Response VO")
+@Data
+public class ImGroupRequestManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long groupId;
+
+ @Schema(description = "群名称", example = "芋道技术交流群")
+ private String groupName;
+
+ @Schema(description = "申请人 / 被邀请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ private Long userId;
+
+ @Schema(description = "申请人 / 被邀请人昵称", example = "张三")
+ private String userNickname;
+
+ @Schema(description = "邀请人用户编号;NULL 表示用户主动申请", example = "200")
+ private Long inviterUserId;
+
+ @Schema(description = "邀请人昵称", example = "老张")
+ private String inviterNickname;
+
+ @Schema(description = "申请理由", example = "我想加入这个群")
+ private String applyContent;
+
+ @Schema(description = "加入来源", example = "1")
+ private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举类
+
+ @Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举类
+
+ @Schema(description = "处理人用户编号", example = "31460")
+ private Long handleUserId;
+
+ @Schema(description = "处理人昵称", example = "管理员")
+ private String handleNickname;
+
+ @Schema(description = "处理理由", example = "暂不通过")
+ private String handleContent;
+
+ @Schema(description = "处理时间")
+ private LocalDateTime handleTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java
new file mode 100644
index 000000000..b5d1346d9
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 群成员 Response VO")
+@Data
+public class ImGroupMemberManagerRespVO {
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long userId;
+
+ @Schema(description = "用户昵称", example = "张三")
+ private String nickname;
+
+ @Schema(description = "用户头像")
+ private String avatar;
+
+ @Schema(description = "组内显示名", example = "三哥")
+ private String displayUserName;
+
+ @Schema(description = "群备注", example = "技术交流群")
+ private String groupRemark;
+
+ @Schema(description = "是否免打扰", example = "false")
+ private Boolean silent;
+
+ @Schema(description = "成员状态", example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "成员角色;1=群主 2=管理员 3=普通成员", example = "3")
+ private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类
+
+ @Schema(description = "入群时间")
+ private LocalDateTime joinTime;
+
+ @Schema(description = "退群时间")
+ private LocalDateTime quitTime;
+
+ @Schema(description = "禁言到期时间")
+ private LocalDateTime muteEndTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
new file mode 100644
index 000000000..36a3b60bc
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessagePageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessageRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessageSendReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
+import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
+import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - IM 频道消息")
+@RestController
+@RequestMapping("/im/manager/channel-message")
+@Validated
+public class ImChannelMessageManagerController {
+
+ @Resource
+ private ImChannelMessageService channelMessageService;
+ @Resource
+ private ImChannelService channelService;
+ @Resource
+ private ImChannelMaterialService channelMaterialService;
+
+ @PostMapping("/send")
+ @Operation(summary = "立即推送频道消息")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-message:send')")
+ public CommonResult sendMessage(@Valid @RequestBody ImChannelMessageSendReqVO reqVO) {
+ return success(channelMessageService.sendMessage(reqVO));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除频道消息")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-message:delete')")
+ public CommonResult deleteMessage(@RequestParam("id") Long id) {
+ channelMessageService.deleteMessage(id);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得频道消息分页;回填频道名 / 素材标题")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
+ public CommonResult> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) {
+ PageResult pageResult = channelMessageService.getMessagePage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 批量查询频道和素材,并回填频道名 / 素材标题
+ Map channelMap = channelService.getChannelMap(
+ convertSet(pageResult.getList(), ImChannelMessageDO::getChannelId));
+ Map materialMap = channelMaterialService.getMaterialMap(
+ convertSet(pageResult.getList(), ImChannelMessageDO::getMaterialId));
+ return success(BeanUtils.toBean(pageResult, ImChannelMessageRespVO.class, vo -> {
+ MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()));
+ MapUtils.findAndThen(materialMap, vo.getMaterialId(),
+ material -> vo.setMaterialTitle(material.getTitle()).setMaterialCoverUrl(material.getCoverUrl()));
+ }));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImGroupMessageManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImGroupMessageManagerController.java
new file mode 100644
index 000000000..da49a9c16
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImGroupMessageManagerController.java
@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
+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.Valid;
+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.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.AT_USER_ID_ALL;
+
+@Tag(name = "管理后台 - IM 群聊消息")
+@RestController
+@RequestMapping("/im/manager/message/group")
+@Validated
+public class ImGroupMessageManagerController {
+
+ @Resource
+ private ImGroupMessageService groupMessageService;
+ @Resource
+ private ImGroupService groupService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得群聊消息分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
+ public CommonResult> getGroupMessagePage(
+ @Valid ImGroupMessageManagerPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = groupMessageService.getGroupMessagePage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 2.1 批量查询群名称、发送人昵称、@ 用户昵称(-1 表示 @所有人,跳过查询,由前端判断渲染)
+ Map groupMap = groupService.getGroupMap(
+ convertSet(pageResult.getList(), ImGroupMessageDO::getGroupId));
+ Set userIds = convertSetByFlatMap(pageResult.getList(), m -> Stream.concat(
+ Stream.of(m.getSenderId()),
+ CollUtil.emptyIfNull(m.getAtUserIds()).stream()
+ .filter(id -> !Objects.equals(id, AT_USER_ID_ALL))));
+ Map userMap = adminUserApi.getUserMap(userIds);
+ // 2.2 转换为 VO,填充群名 / 发送人昵称 / @ 用户昵称(-1 位置留 null,由前端展示「@所有人」)
+ return success(BeanUtils.toBean(pageResult, ImGroupMessageManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
+ MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
+ if (CollUtil.isNotEmpty(vo.getAtUserIds())) {
+ vo.setAtUserNicknames(convertList(vo.getAtUserIds(), id -> {
+ AdminUserRespDTO user = userMap.get(id);
+ return user != null ? user.getNickname() : null;
+ }));
+ }
+ }));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得群聊消息详情")
+ @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
+ public CommonResult getGroupMessage(@RequestParam("id") Long id) {
+ ImGroupMessageDO message = groupMessageService.getGroupMessage(id);
+ return success(BeanUtils.toBean(message, ImGroupMessageManagerRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImPrivateMessageManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImPrivateMessageManagerController.java
new file mode 100644
index 000000000..8db5b698b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImPrivateMessageManagerController.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
+import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
+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.Valid;
+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.util.Map;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@Tag(name = "管理后台 - IM 私聊消息")
+@RestController
+@RequestMapping("/im/manager/message/private")
+@Validated
+public class ImPrivateMessageManagerController {
+
+ @Resource
+ private ImPrivateMessageService privateMessageService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得私聊消息分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
+ public CommonResult> getPrivateMessagePage(
+ @Valid ImPrivateMessageManagerPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = privateMessageService.getPrivateMessagePage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 2.1 一次性批量查询发送人 + 接收人昵称
+ Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(pageResult.getList(),
+ m -> Stream.of(m.getSenderId(), m.getReceiverId())));
+ // 2.2 转换为 VO,填充昵称
+ return success(BeanUtils.toBean(pageResult, ImPrivateMessageManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
+ MapUtils.findAndThen(userMap, vo.getReceiverId(), user -> vo.setReceiverNickname(user.getNickname()));
+ }));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得私聊消息详情")
+ @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
+ public CommonResult getPrivateMessage(@RequestParam("id") Long id) {
+ ImPrivateMessageDO message = privateMessageService.getPrivateMessage(id);
+ return success(BeanUtils.toBean(message, ImPrivateMessageManagerRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessagePageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessagePageReqVO.java
new file mode 100644
index 000000000..48e1a44c5
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessagePageReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 频道消息分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImChannelMessagePageReqVO extends PageParam {
+
+ @Schema(description = "频道编号", example = "1")
+ private Long channelId;
+
+ @Schema(description = "素材编号", example = "1024")
+ private Long materialId;
+
+ @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] sendTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageRespVO.java
new file mode 100644
index 000000000..9f76ed281
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageRespVO.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - IM 频道消息 Response VO")
+@Data
+public class ImChannelMessageRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long channelId;
+
+ @Schema(description = "频道名称(关联查询填充)")
+ private String channelName;
+
+ @Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long materialId;
+
+ @Schema(description = "素材标题(关联查询填充)")
+ private String materialTitle;
+
+ @Schema(description = "素材封面 URL(关联查询填充)")
+ private String materialCoverUrl;
+
+ @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
+ private Integer type; // 参见 ImMessageTypeEnum 枚举类
+
+ @Schema(description = "消息内容;payload JSON 快照")
+ private String content;
+
+ @Schema(description = "接收人编号列表;为空表示全员")
+ private List receiverUserIds;
+
+ @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime sendTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageSendReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageSendReqVO.java
new file mode 100644
index 000000000..a69ff16c3
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageSendReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - IM 频道消息推送 Request VO")
+@Data
+public class ImChannelMessageSendReqVO {
+
+ @Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "素材编号不能为空")
+ private Long materialId;
+
+ @Schema(description = "接收人编号列表;为空表示全员", example = "[1024, 2048]")
+ private List receiverUserIds;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java
new file mode 100644
index 000000000..86d236d15
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 群聊消息分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImGroupMessageManagerPageReqVO extends PageParam {
+
+ @Schema(description = "群编号", example = "1024")
+ private Long groupId;
+
+ @Schema(description = "发送人编号", example = "1024")
+ private Long senderId;
+
+ @Schema(description = "消息类型", example = "1")
+ private Integer type; // 参见 ImMessageTypeEnum 枚举类
+
+ @Schema(description = "消息内容", example = "你好")
+ private String content;
+
+ @Schema(description = "消息状态", example = "0")
+ private Integer status; // 参见 ImMessageStatusEnum 枚举类
+
+ @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] sendTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java
new file mode 100644
index 000000000..836b3b329
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - IM 群聊消息 Response VO")
+@Data
+public class ImGroupMessageManagerRespVO {
+
+ @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "客户端消息编号", example = "c-uuid-xxx")
+ private String clientMessageId;
+
+ @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long groupId;
+
+ @Schema(description = "群名称", example = "技术交流群")
+ private String groupName;
+
+ @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long senderId;
+
+ @Schema(description = "发送人昵称", example = "张三")
+ private String senderNickname;
+
+ @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer type; // 参见 ImMessageTypeEnum 枚举类
+
+ @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String content;
+
+ @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 ImMessageStatusEnum 枚举类
+
+ @Schema(description = "@ 目标用户编号列表(-1 表示 @所有人)")
+ private List atUserIds;
+ @Schema(description = "@ 目标用户昵称列表(-1 位置为 null,前端根据 atUserIds 自行展示「@所有人」)")
+ private List atUserNicknames;
+
+ @Schema(description = "回执状态", example = "0")
+ private Integer receiptStatus; // 参见 ImGroupMessageReceiptStatusEnum 枚举类
+
+ @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime sendTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java
new file mode 100644
index 000000000..e6a72da76
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 私聊消息分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImPrivateMessageManagerPageReqVO extends PageParam {
+
+ @Schema(description = "发送人编号", example = "1024")
+ private Long senderId;
+
+ @Schema(description = "接收人编号", example = "2048")
+ private Long receiverId;
+
+ @Schema(description = "消息类型", example = "1")
+ private Integer type; // 参见 ImMessageTypeEnum 枚举类
+
+ @Schema(description = "消息内容", example = "你好")
+ private String content;
+
+ @Schema(description = "消息状态", example = "0")
+ private Integer status; // 参见 ImMessageStatusEnum 枚举类
+
+ @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] sendTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java
new file mode 100644
index 000000000..c467a0a87
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 私聊消息 Response VO")
+@Data
+public class ImPrivateMessageManagerRespVO {
+
+ @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "客户端消息编号", example = "c-uuid-xxx")
+ private String clientMessageId;
+
+ @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long senderId;
+
+ @Schema(description = "发送人昵称", example = "张三")
+ private String senderNickname;
+
+ @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long receiverId;
+
+ @Schema(description = "接收人昵称", example = "李四")
+ private String receiverNickname;
+
+ @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer type; // 参见 ImMessageTypeEnum 枚举类
+
+ @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String content;
+
+ @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 ImMessageStatusEnum 枚举类
+
+ @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime sendTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java
new file mode 100644
index 000000000..417e58f0c
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java
@@ -0,0 +1,113 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.rtc;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcParticipantManagerRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
+import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO;
+import cn.iocoder.yudao.module.im.service.group.ImGroupService;
+import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService;
+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.Valid;
+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.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - IM 通话记录")
+@RestController
+@RequestMapping("/im/manager/rtc")
+@Validated
+public class ImRtcCallManagerController {
+
+ @Resource
+ private ImRtcCallService rtcCallService;
+ @Resource
+ private ImGroupService groupService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @GetMapping("/page")
+ @Operation(summary = "获得通话记录分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')")
+ public CommonResult> getCallPage(@Valid ImRtcCallManagerPageReqVO pageReqVO) {
+ PageResult pageResult = rtcCallService.getCallPage(pageReqVO);
+ return success(buildCallRespVOPage(pageResult));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得通话记录详情")
+ @Parameter(name = "id", description = "通话编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')")
+ public CommonResult getCall(@RequestParam("id") Long id) {
+ ImRtcCallDO call = rtcCallService.getCall(id);
+ if (call == null) {
+ return success(null);
+ }
+ return success(CollUtil.getFirst(buildCallRespVOList(Collections.singletonList(call))));
+ }
+
+ @GetMapping("/participant-list")
+ @Operation(summary = "获得通话参与者列表")
+ @Parameter(name = "id", description = "通话编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')")
+ public CommonResult> getCallParticipantList(@RequestParam("id") Long id) {
+ List participants = rtcCallService.getCallParticipantListByCallId(id);
+ if (CollUtil.isEmpty(participants)) {
+ return success(Collections.emptyList());
+ }
+ // 查询用户信息
+ Map userMap = adminUserApi.getUserMap(
+ convertSet(participants, ImRtcParticipantDO::getUserId));
+ // 组装返回
+ return success(BeanUtils.toBean(participants, ImRtcParticipantManagerRespVO.class, vo ->
+ MapUtils.findAndThen(userMap, vo.getUserId(),
+ user -> vo.setUserNickname(user.getNickname()))));
+ }
+
+ // ========== 私有方法:VO 组装 ==========
+
+ private PageResult buildCallRespVOPage(PageResult pageResult) {
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return PageResult.empty(pageResult.getTotal());
+ }
+ return new PageResult<>(buildCallRespVOList(pageResult.getList()), pageResult.getTotal());
+ }
+
+ private List buildCallRespVOList(List calls) {
+ // 查询用户信息
+ Map userMap = adminUserApi.getUserMap(
+ convertSet(calls, ImRtcCallDO::getInviterUserId));
+ Map groupMap = groupService.getGroupMap(
+ convertSet(calls, ImRtcCallDO::getGroupId));
+ // 组装返回
+ return BeanUtils.toBean(calls, ImRtcCallManagerRespVO.class, vo -> {
+ MapUtils.findAndThen(userMap, vo.getInviterUserId(),
+ user -> vo.setInviterNickname(user.getNickname()));
+ MapUtils.findAndThen(groupMap, vo.getGroupId(),
+ group -> vo.setGroupName(group.getName()));
+ });
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java
new file mode 100644
index 000000000..f7878fa02
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 通话记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImRtcCallManagerPageReqVO extends PageParam {
+
+ @Schema(description = "发起人用户编号", example = "1024")
+ private Long inviterUserId;
+
+ @Schema(description = "会话类型", example = "1")
+ private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类
+
+ @Schema(description = "媒体类型", example = "1")
+ private Integer mediaType; // 参见 ImRtcCallMediaTypeEnum 枚举类
+
+ @Schema(description = "通话状态", example = "10")
+ private Integer status; // 参见 ImRtcCallStatusEnum 枚举类
+
+ @Schema(description = "结束原因", example = "1")
+ private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类
+
+ @Schema(description = "发起时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] startTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java
new file mode 100644
index 000000000..0151581de
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 通话记录 Response VO")
+@Data
+public class ImRtcCallManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "业务通话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "uuid-xxx")
+ private String room;
+
+ @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类
+
+ @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer mediaType; // 参见 ImRtcCallMediaTypeEnum 枚举类
+
+ @Schema(description = "发起人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long inviterUserId;
+
+ @Schema(description = "发起人昵称", example = "张三")
+ private String inviterNickname;
+
+ @Schema(description = "群编号;私聊为空", example = "999")
+ private Long groupId;
+
+ @Schema(description = "群名称;私聊为空", example = "测试群")
+ private String groupName;
+
+ @Schema(description = "通话状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ private Integer status; // 参见 ImRtcCallStatusEnum 枚举类
+
+ @Schema(description = "结束原因", example = "1")
+ private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类
+
+ @Schema(description = "发起时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime startTime;
+
+ @Schema(description = "接通时间;未接通为空")
+ private LocalDateTime acceptTime;
+
+ @Schema(description = "结束时间;未结束为空")
+ private LocalDateTime endTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java
new file mode 100644
index 000000000..a990577db
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 通话参与者 Response VO")
+@Data
+public class ImRtcParticipantManagerRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "通话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long callId;
+
+ @Schema(description = "参与者用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long userId;
+
+ @Schema(description = "参与者昵称", example = "张三")
+ private String userNickname;
+
+ @Schema(description = "参与角色", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer role; // 参见 ImRtcParticipantRoleEnum 枚举类
+
+ @Schema(description = "参与状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+ private Integer status; // 参见 ImRtcParticipantStatusEnum 枚举类
+
+ @Schema(description = "被邀请时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime inviteTime;
+
+ @Schema(description = "接听时间;未接听为空")
+ private LocalDateTime acceptTime;
+
+ @Schema(description = "离开时间;未加入为空")
+ private LocalDateTime leaveTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/ImSensitiveWordManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/ImSensitiveWordManagerController.java
new file mode 100644
index 000000000..d69248d34
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/ImSensitiveWordManagerController.java
@@ -0,0 +1,106 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
+import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
+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.Valid;
+import jakarta.validation.constraints.Size;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - IM 敏感词")
+@RestController
+@RequestMapping("/im/manager/sensitive-word")
+@Validated
+public class ImSensitiveWordManagerController {
+
+ @Resource
+ private ImSensitiveWordService sensitiveWordService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "新增敏感词")
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:create')")
+ public CommonResult createSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
+ return success(sensitiveWordService.createSensitiveWord(reqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改敏感词")
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:update')")
+ public CommonResult updateSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
+ sensitiveWordService.updateSensitiveWord(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除敏感词")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
+ public CommonResult deleteSensitiveWord(@RequestParam("id") Long id) {
+ sensitiveWordService.deleteSensitiveWord(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除敏感词")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
+ public CommonResult deleteSensitiveWordList(
+ @RequestParam("ids")
+ @Size(max = 100, message = "批量删除最多 100 条") List ids) {
+ sensitiveWordService.deleteSensitiveWordList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得敏感词分页")
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
+ public CommonResult> getSensitiveWordPage(
+ @Valid ImSensitiveWordPageReqVO pageReqVO) {
+ // 1. 分页查询
+ PageResult pageResult = sensitiveWordService.getSensitiveWordPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty(pageResult.getTotal()));
+ }
+ // 2.1 批量查询创建人昵称
+ Map userMap = adminUserApi.getUserMap(convertSet(pageResult.getList(),
+ word -> NumberUtils.parseLong(word.getCreator())));
+ // 2.2 转换为 VO,填充创建人昵称
+ return success(BeanUtils.toBean(pageResult, ImSensitiveWordRespVO.class, vo ->
+ MapUtils.findAndThen(userMap, NumberUtils.parseLong(vo.getCreator()),
+ user -> vo.setCreatorName(user.getNickname()))));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得敏感词详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
+ public CommonResult getSensitiveWord(@RequestParam("id") Long id) {
+ ImSensitiveWordDO word = sensitiveWordService.getSensitiveWord(id);
+ return success(BeanUtils.toBean(word, ImSensitiveWordRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java
new file mode 100644
index 000000000..4fc21f225
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - IM 敏感词分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ImSensitiveWordPageReqVO extends PageParam {
+
+ @Schema(description = "敏感词,模糊匹配", example = "敏感")
+ private String word;
+
+ @Schema(description = "状态", example = "0")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
+
+ @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordRespVO.java
new file mode 100644
index 000000000..578703fe4
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordRespVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - IM 敏感词 Response VO")
+@Data
+public class ImSensitiveWordRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
+ private String word;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ private Integer status; // 参见 CommonStatusEnum 枚举类
+
+ @Schema(description = "创建人")
+ private String creator;
+ @Schema(description = "创建人昵称", example = "张三")
+ private String creatorName;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java
new file mode 100644
index 000000000..5d421406a
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "管理后台 - IM 敏感词新增 / 修改 Request VO")
+@Data
+public class ImSensitiveWordSaveReqVO {
+
+ @Schema(description = "编号(修改时必填)", example = "1024")
+ private Long id;
+
+ @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
+ @NotBlank(message = "敏感词不能为空")
+ @Size(max = 64, message = "敏感词长度不能超过 64")
+ private String word;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "状态不能为空")
+ @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
+ private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java
new file mode 100644
index 000000000..82403e34b
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerGroupSizeRespVO.java
new file mode 100644
index 000000000..064f0dec0
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java
new file mode 100644
index 000000000..2da082775
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerOverviewRespVO.java
new file mode 100644
index 000000000..dc9e713f3
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTopSenderRespVO.java
new file mode 100644
index 000000000..2f0615870
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerTrendRespVO.java
new file mode 100644
index 000000000..e08d980cd
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/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/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImChannelMessageController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImChannelMessageController.java
new file mode 100644
index 000000000..08e557aaa
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImChannelMessageController.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.im.controller.admin.message;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.channel.ImChannelMessagePullRespVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
+import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
+import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
+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 jakarta.validation.constraints.PositiveOrZero;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
+
+@Tag(name = "用户 APP - IM 频道消息")
+@RestController
+@RequestMapping("/im/channel/message")
+@Validated
+public class ImChannelMessageController {
+
+ @Resource
+ private ImChannelMessageService channelMessageService;
+
+ @GetMapping("/pull")
+ @Operation(summary = "拉取频道消息(离线增量);按 minId 游标分页")
+ public CommonResult> pull(
+ @RequestParam(value = "minId", defaultValue = "0") @PositiveOrZero(message = "minId 不能小于 0") Long minId,
+ @RequestParam(value = "size", defaultValue = "100")
+ @Min(value = 1, message = "size 必须大于 0")
+ @Max(value = 200, message = "size 一次最多 200 条") Integer size) {
+ // 1. 拉取消息列表
+ Long userId = getLoginUserId();
+ List list = channelMessageService.getMessageListForPull(userId, minId, size);
+ if (CollUtil.isEmpty(list)) {
+ return success(Collections.emptyList());
+ }
+ // 2. 按 Redis 已读游标补 status;device A 已读后 device B 拉到这条不再算入未读
+ Map readMaxByChannel = channelMessageService.getChannelReadMaxMessageIdMap(
+ userId, convertSet(list, ImChannelMessageDO::getChannelId));
+ return success(BeanUtils.toBean(list, ImChannelMessagePullRespVO.class, vo -> {
+ Long readMax = readMaxByChannel.get(vo.getChannelId());
+ vo.setStatus(readMax != null && readMax >= vo.getId()
+ ? ImMessageStatusEnum.READ.getStatus()
+ : ImMessageStatusEnum.UNREAD.getStatus());
+ }));
+ }
+
+ @PutMapping("/read")
+ @Operation(summary = "标记频道消息已读")
+ @Parameter(name = "channelId", description = "频道编号", required = true, example = "1")
+ @Parameter(name = "messageId", description = "已读到的消息编号", required = true, example = "100")
+ public CommonResult readChannelMessages(@RequestParam("channelId") Long channelId,
+ @RequestParam("messageId") Long messageId) {
+ channelMessageService.readChannelMessages(getLoginUserId(), channelId, messageId);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
new file mode 100644
index 000000000..4bc719e8a
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.im.controller.admin.message;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
+import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Min;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - IM 群聊消息")
+@RestController
+@RequestMapping("/im/message/group")
+@Validated
+public class ImGroupMessageController {
+
+ @Resource
+ private ImGroupMessageService groupMessageService;
+
+ @PostMapping("/send")
+ @Operation(summary = "发送群聊消息")
+ public CommonResult sendGroupMessage(@Valid @RequestBody ImGroupMessageSendReqVO reqVO) {
+ ImGroupMessageDO message = groupMessageService.sendGroupMessage(getLoginUserId(), reqVO);
+ return success(BeanUtils.toBean(message, ImGroupMessageRespVO.class));
+ }
+
+ @GetMapping("/pull")
+ @Operation(summary = "拉取群聊消息(增量)")
+ @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
+ @Parameter(name = "size", description = "拉取数量", required = true, example = "100")
+ public CommonResult> pullGroupMessageList(
+ @RequestParam("minId") Long minId,
+ @RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
+ List messages = groupMessageService.pullGroupMessageList(getLoginUserId(), minId, size);
+ return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class));
+ }
+
+ @PutMapping("/read")
+ @Operation(summary = "标记群聊消息已读")
+ @Parameter(name = "groupId", description = "群编号", required = true, example = "1")
+ @Parameter(name = "messageId", description = "已读到的消息编号", required = true, example = "100")
+ public CommonResult readGroupMessages(@RequestParam("groupId") Long groupId,
+ @RequestParam("messageId") Long messageId) {
+ groupMessageService.readGroupMessages(getLoginUserId(), groupId, messageId);
+ return success(true);
+ }
+
+ @DeleteMapping("/recall")
+ @Operation(summary = "撤回群聊消息")
+ @Parameter(name = "id", description = "消息编号", required = true, example = "1")
+ public CommonResult recallGroupMessage(@RequestParam("id") Long id) {
+ ImGroupMessageDO message = groupMessageService.recallGroupMessage(getLoginUserId(), id);
+ return success(BeanUtils.toBean(message, ImGroupMessageRespVO.class));
+ }
+
+ @GetMapping("/get-read-user-ids")
+ @Operation(summary = "获取群消息已读用户列表")
+ @Parameter(name = "groupId", description = "群编号", required = true, example = "1")
+ @Parameter(name = "messageId", description = "消息编号", required = true, example = "1")
+ public CommonResult> getGroupReadUserIds(@RequestParam("groupId") Long groupId,
+ @RequestParam("messageId") Long messageId) {
+ return success(groupMessageService.getGroupReadUserIds(getLoginUserId(), groupId, messageId));
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "查询群聊历史消息")
+ public CommonResult> getGroupMessageList(@Valid ImGroupMessageListReqVO reqVO) {
+ List messages = groupMessageService.getGroupMessageList(getLoginUserId(), reqVO);
+ return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class));
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.http b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.http
new file mode 100644
index 000000000..83d177313
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.http
@@ -0,0 +1,18 @@
+### 请求 /send 接口 => 成功
+POST {{baseUrl}}/im/message/send
+Authorization: Bearer {{token}}
+Content-Type: application/json
+tenant-id: {{adminTenentId}}
+
+{
+ "clientMessageId": "123",
+ "receiverId": 1,
+ "conversationType": 1,
+ "contentType": 101,
+ "content": "你好1"
+}
+
+### 请求 /pull 接口 => 成功
+GET {{baseUrl}}/im/message/pull?sequence=0&size=2
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
new file mode 100644
index 000000000..d375ec747
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-server/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.im.controller.admin.message;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
+import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Min;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - IM 私聊消息")
+@RestController
+@RequestMapping("/im/message/private")
+@Validated
+public class ImPrivateMessageController {
+
+ @Resource
+ private ImPrivateMessageService privateMessageService;
+
+ @PostMapping("/send")
+ @Operation(summary = "发送私聊消息")
+ public CommonResult sendPrivateMessage(
+ @Valid @RequestBody ImPrivateMessageSendReqVO reqVO) {
+ ImPrivateMessageDO message = privateMessageService.sendPrivateMessage(getLoginUserId(), reqVO);
+ return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class));
+ }
+
+ @GetMapping("/pull")
+ @Operation(summary = "拉取私聊消息(增量)")
+ @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
+ @Parameter(name = "size", description = "拉取数量", required = true, example = "100")
+ public CommonResult> pullPrivateMessageList(
+ @RequestParam("minId") Long minId,
+ @RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
+ List messages = privateMessageService.pullPrivateMessageList(getLoginUserId(), minId, size);
+ return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
+ }
+
+ @PutMapping("/read")
+ @Operation(summary = "标记私聊消息已读")
+ @Parameter(name = "receiverId", description = "接收方用户编号(对方)", required = true, example = "2")
+ @Parameter(name = "messageId", description = "已读位置(含),通常是会话内最大消息编号", required = true, example = "100")
+ public CommonResult readPrivateMessages(@RequestParam("receiverId") Long receiverId,
+ @RequestParam("messageId") Long messageId) {
+ privateMessageService.readPrivateMessages(getLoginUserId(), receiverId, messageId);
+ return success(true);
+ }
+
+ @GetMapping("/max-read-message-id")
+ @Operation(summary = "查询对方已读到我发的最大消息 id",
+ description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,结果用于翻转本地自发消息状态")
+ @Parameter(name = "peerId", description = "对方用户编号", required = true, example = "2")
+ public CommonResult getMaxReadMessageId(@RequestParam("peerId") Long peerId) {
+ return success(privateMessageService.getMaxReadMessageId(getLoginUserId(), peerId));
+ }
+
+ @DeleteMapping("/recall")
+ @Operation(summary = "撤回私聊消息")
+ @Parameter(name = "id", description = "消息编号", required = true, example = "1")
+ public CommonResult