Commit fc90635abbf739518f18204ceeae90f3f00e748e

Authored by YunaiV
1 parent 37f0a8a0

✨ feat(im): 新增 friend、group 等管理后台接口

Showing 27 changed files with 282 additions and 278 deletions
yudao-dependencies/pom.xml
... ... @@ -50,6 +50,7 @@
50 50 <!-- 工具类相关 -->
51 51 <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
52 52 <jsoup.version>1.21.2</jsoup.version>
  53 + <sensitive-word.version>0.29.5</sensitive-word.version>
53 54 <lombok.version>1.18.42</lombok.version>
54 55 <mapstruct.version>1.6.3</mapstruct.version>
55 56 <hutool-5.version>5.8.42</hutool-5.version>
... ... @@ -558,6 +559,12 @@
558 559 <version>${jsoup.version}</version>
559 560 </dependency>
560 561  
  562 + <dependency>
  563 + <groupId>com.github.houbb</groupId>
  564 + <artifactId>sensitive-word</artifactId> <!-- 敏感词检测:trie 树高效匹配 -->
  565 + <version>${sensitive-word.version}</version>
  566 + </dependency>
  567 +
561 568 <!-- Vert.x -->
562 569 <dependency>
563 570 <groupId>io.vertx</groupId>
... ...
yudao-module-im/pom.xml
... ... @@ -62,12 +62,10 @@
62 62 <artifactId>yudao-spring-boot-starter-excel</artifactId>
63 63 </dependency>
64 64  
65   - <!-- 敏感词检测:houbb sensitive-word(trie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格) -->
66   - <!-- TODO @AI:放到基础 pom 里管理。 -->
  65 + <!-- 敏感词检测:sensitive-word(trie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格),版本由 yudao-dependencies 统一管理 -->
67 66 <dependency>
68 67 <groupId>com.github.houbb</groupId>
69 68 <artifactId>sensitive-word</artifactId>
70   - <version>0.29.5</version>
71 69 </dependency>
72 70  
73 71 <!-- Test 测试相关 -->
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendManagerController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 8 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
6 9 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
  10 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
7 11 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
  12 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  13 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
8 14 import io.swagger.v3.oas.annotations.Operation;
9 15 import io.swagger.v3.oas.annotations.tags.Tag;
10 16 import jakarta.annotation.Resource;
... ... @@ -15,7 +21,12 @@ import org.springframework.web.bind.annotation.GetMapping;
15 21 import org.springframework.web.bind.annotation.RequestMapping;
16 22 import org.springframework.web.bind.annotation.RestController;
17 23  
  24 +import java.util.Map;
  25 +import java.util.Set;
  26 +import java.util.stream.Stream;
  27 +
18 28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
19 30  
20 31 @Tag(name = "管理后台 - IM 好友管理")
21 32 @RestController
... ... @@ -25,13 +36,30 @@ public class ImFriendManagerController {
25 36  
26 37 @Resource
27 38 private ImFriendService friendService;
  39 + @Resource
  40 + private AdminUserApi adminUserApi;
28 41  
29 42 @GetMapping("/page")
30 43 @Operation(summary = "获得好友关系分页")
31 44 @PreAuthorize("@ss.hasPermission('im:manager:friend:query')")
32 45 public CommonResult<PageResult<ImFriendManagerRespVO>> getFriendPage(
33 46 @Valid ImFriendManagerPageReqVO pageReqVO) {
34   - return success(friendService.getFriendPage(pageReqVO));
  47 + // 1. 分页查询
  48 + PageResult<ImFriendDO> pageResult = friendService.getFriendPage(pageReqVO);
  49 + if (CollUtil.isEmpty(pageResult.getList())) {
  50 + return success(PageResult.empty(pageResult.getTotal()));
  51 + }
  52 + // 2.1 一次性批量查询用户 + 好友的昵称
  53 + Set<Long> userIds = convertSetByFlatMap(pageResult.getList(),
  54 + f -> Stream.of(f.getUserId(), f.getFriendUserId()));
  55 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
  56 + // 2.2 转换为 VO,填充昵称
  57 + return success(BeanUtils.toBean(pageResult, ImFriendManagerRespVO.class, vo -> {
  58 + MapUtils.findAndThen(userMap, vo.getUserId(),
  59 + user -> vo.setUserNickname(user.getNickname()));
  60 + MapUtils.findAndThen(userMap, vo.getFriendUserId(),
  61 + user -> vo.setFriendNickname(user.getNickname()));
  62 + }));
35 63 }
36 64  
37 65 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupManagerController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.group;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
5 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 8 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
7 9 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
8 10 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
9 11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  12 +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
10 13 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
  14 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  15 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
11 16 import io.swagger.v3.oas.annotations.Operation;
12 17 import io.swagger.v3.oas.annotations.Parameter;
13 18 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -17,7 +22,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
17 22 import org.springframework.validation.annotation.Validated;
18 23 import org.springframework.web.bind.annotation.*;
19 24  
  25 +import java.util.Map;
  26 +
20 27 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
21 29  
22 30 @Tag(name = "管理后台 - IM 群聊管理")
23 31 @RestController
... ... @@ -27,12 +35,32 @@ public class ImGroupManagerController {
27 35  
28 36 @Resource
29 37 private ImGroupService groupService;
  38 + @Resource
  39 + private ImGroupMemberService groupMemberService;
  40 +
  41 + @Resource
  42 + private AdminUserApi adminUserApi;
30 43  
31 44 @GetMapping("/page")
32 45 @Operation(summary = "获得群分页")
33 46 @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
34 47 public CommonResult<PageResult<ImGroupManagerRespVO>> getGroupPage(@Valid ImGroupManagerPageReqVO pageReqVO) {
35   - return success(groupService.getGroupPage(pageReqVO));
  48 + // 1. 分页查询群
  49 + PageResult<ImGroupDO> pageResult = groupService.getGroupPage(pageReqVO);
  50 + if (CollUtil.isEmpty(pageResult.getList())) {
  51 + return success(PageResult.empty(pageResult.getTotal()));
  52 + }
  53 + // 2.1 批量查询相关数据
  54 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  55 + convertSet(pageResult.getList(), ImGroupDO::getOwnerUserId));
  56 + Map<Long, Long> memberCountMap = groupMemberService.getActiveMemberCountMap(
  57 + convertSet(pageResult.getList(), ImGroupDO::getId));
  58 + // 2.2 转换为 VO,填充群主昵称、群成员数量
  59 + return success(BeanUtils.toBean(pageResult, ImGroupManagerRespVO.class, vo -> {
  60 + MapUtils.findAndThen(userMap, vo.getOwnerUserId(),
  61 + user -> vo.setOwnerNickname(user.getNickname()));
  62 + vo.setMemberCount(memberCountMap.getOrDefault(vo.getId(), 0L).intValue());
  63 + }));
36 64 }
37 65  
38 66 @GetMapping("/get")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupMemberManagerController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.group;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 7 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
5 9 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
  10 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  11 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
6 12 import io.swagger.v3.oas.annotations.Operation;
7 13 import io.swagger.v3.oas.annotations.Parameter;
8 14 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -14,9 +20,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
14 20 import org.springframework.web.bind.annotation.RequestParam;
15 21 import org.springframework.web.bind.annotation.RestController;
16 22  
  23 +import java.util.Collections;
17 24 import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.Set;
18 27  
19 28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 30  
21 31 @Tag(name = "管理后台 - IM 群成员管理")
22 32 @RestController
... ... @@ -26,13 +36,26 @@ public class ImGroupMemberManagerController {
26 36  
27 37 @Resource
28 38 private ImGroupMemberService groupMemberService;
  39 + @Resource
  40 + private AdminUserApi adminUserApi;
29 41  
30 42 @GetMapping("/list")
31 43 @Operation(summary = "获得群成员列表")
32 44 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
33 45 @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
34 46 public CommonResult<List<ImGroupMemberManagerRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
35   - return success(groupMemberService.getGroupMemberList(groupId));
  47 + // 1. 查询有效群成员
  48 + List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
  49 + if (CollUtil.isEmpty(members)) {
  50 + return success(Collections.emptyList());
  51 + }
  52 + // 2.1 批量查询用户信息
  53 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  54 + convertSet(members, ImGroupMemberDO::getUserId));
  55 + // 2.2 转换为 VO,填充昵称、头像
  56 + return success(BeanUtils.toBean(members, ImGroupMemberManagerRespVO.class, vo ->
  57 + MapUtils.findAndThen(userMap, vo.getUserId(), user ->
  58 + vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()))));
36 59 }
37 60  
38 61 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/ImGroupManagerPageReqVO.java
... ... @@ -21,8 +21,8 @@ public class ImGroupManagerPageReqVO extends PageParam {
21 21 @Schema(description = "群主用户编号", example = "1024")
22 22 private Long ownerUserId;
23 23  
24   - @Schema(description = "群状态,参见 CommonStatusEnum 枚举类(0 正常 / 1 已解散)", example = "0")
25   - private Integer status;
  24 + @Schema(description = "群状态", example = "0")
  25 + private Integer status; // 参见 CommonStatusEnum 枚举类(0 正常 / 1 已解散)
26 26  
27 27 @Schema(description = "是否封禁", example = "false")
28 28 private Boolean banned;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImGroupMessageManagerController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.message;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
5 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
7 9 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
  10 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
8 11 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
  12 +import cn.iocoder.yudao.module.im.service.group.ImGroupService;
9 13 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
  14 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  15 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
10 16 import io.swagger.v3.oas.annotations.Operation;
11 17 import io.swagger.v3.oas.annotations.Parameter;
12 18 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -14,9 +20,15 @@ import jakarta.annotation.Resource;
14 20 import jakarta.validation.Valid;
15 21 import org.springframework.security.access.prepost.PreAuthorize;
16 22 import org.springframework.validation.annotation.Validated;
17   -import org.springframework.web.bind.annotation.*;
  23 +import org.springframework.web.bind.annotation.GetMapping;
  24 +import org.springframework.web.bind.annotation.RequestMapping;
  25 +import org.springframework.web.bind.annotation.RequestParam;
  26 +import org.springframework.web.bind.annotation.RestController;
  27 +
  28 +import java.util.Map;
18 29  
19 30 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  31 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 32  
21 33 @Tag(name = "管理后台 - IM 群聊消息")
22 34 @RestController
... ... @@ -26,13 +38,31 @@ public class ImGroupMessageManagerController {
26 38  
27 39 @Resource
28 40 private ImGroupMessageService groupMessageService;
  41 + @Resource
  42 + private ImGroupService groupService;
  43 + @Resource
  44 + private AdminUserApi adminUserApi;
29 45  
30 46 @GetMapping("/page")
31 47 @Operation(summary = "获得群聊消息分页")
32 48 @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
33 49 public CommonResult<PageResult<ImGroupMessageManagerRespVO>> getGroupMessagePage(
34 50 @Valid ImGroupMessageManagerPageReqVO pageReqVO) {
35   - return success(groupMessageService.getGroupMessagePage(pageReqVO));
  51 + // 1. 分页查询
  52 + PageResult<ImGroupMessageDO> pageResult = groupMessageService.getGroupMessagePage(pageReqVO);
  53 + if (CollUtil.isEmpty(pageResult.getList())) {
  54 + return success(PageResult.empty(pageResult.getTotal()));
  55 + }
  56 + // 2.1 批量查询群名称、发送人昵称
  57 + Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(
  58 + convertSet(pageResult.getList(), ImGroupMessageDO::getGroupId));
  59 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  60 + convertSet(pageResult.getList(), ImGroupMessageDO::getSenderId));
  61 + // 2.2 转换为 VO,填充群名 / 发送人昵称
  62 + return success(BeanUtils.toBean(pageResult, ImGroupMessageManagerRespVO.class, vo -> {
  63 + MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
  64 + MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
  65 + }));
36 66 }
37 67  
38 68 @GetMapping("/get")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImPrivateMessageManagerController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.message;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
5 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
7 9 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
8 10 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 11 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  12 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  13 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
10 14 import io.swagger.v3.oas.annotations.Operation;
11 15 import io.swagger.v3.oas.annotations.Parameter;
12 16 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -14,9 +18,16 @@ import jakarta.annotation.Resource;
14 18 import jakarta.validation.Valid;
15 19 import org.springframework.security.access.prepost.PreAuthorize;
16 20 import org.springframework.validation.annotation.Validated;
17   -import org.springframework.web.bind.annotation.*;
  21 +import org.springframework.web.bind.annotation.GetMapping;
  22 +import org.springframework.web.bind.annotation.RequestMapping;
  23 +import org.springframework.web.bind.annotation.RequestParam;
  24 +import org.springframework.web.bind.annotation.RestController;
  25 +
  26 +import java.util.Map;
  27 +import java.util.stream.Stream;
18 28  
19 29 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  30 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
20 31  
21 32 @Tag(name = "管理后台 - IM 私聊消息")
22 33 @RestController
... ... @@ -26,13 +37,27 @@ public class ImPrivateMessageManagerController {
26 37  
27 38 @Resource
28 39 private ImPrivateMessageService privateMessageService;
  40 + @Resource
  41 + private AdminUserApi adminUserApi;
29 42  
30 43 @GetMapping("/page")
31 44 @Operation(summary = "获得私聊消息分页")
32 45 @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
33 46 public CommonResult<PageResult<ImPrivateMessageManagerRespVO>> getPrivateMessagePage(
34 47 @Valid ImPrivateMessageManagerPageReqVO pageReqVO) {
35   - return success(privateMessageService.getPrivateMessagePage(pageReqVO));
  48 + // 1. 分页查询
  49 + PageResult<ImPrivateMessageDO> pageResult = privateMessageService.getPrivateMessagePage(pageReqVO);
  50 + if (CollUtil.isEmpty(pageResult.getList())) {
  51 + return success(PageResult.empty(pageResult.getTotal()));
  52 + }
  53 + // 2.1 一次性批量查询发送人 + 接收人昵称
  54 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSetByFlatMap(pageResult.getList(),
  55 + m -> Stream.of(m.getSenderId(), m.getReceiverId())));
  56 + // 2.2 转换为 VO,填充昵称
  57 + return success(BeanUtils.toBean(pageResult, ImPrivateMessageManagerRespVO.class, vo -> {
  58 + MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
  59 + MapUtils.findAndThen(userMap, vo.getReceiverId(), user -> vo.setReceiverNickname(user.getNickname()));
  60 + }));
36 61 }
37 62  
38 63 @GetMapping("/get")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java
... ... @@ -21,12 +21,11 @@ public class ImGroupMessageManagerPageReqVO extends PageParam {
21 21 @Schema(description = "发送人编号", example = "1024")
22 22 private Long senderId;
23 23  
24   - // TODO @AI:参见 ImMessageTypeEnum 枚举类,都放到 // 注释后面。注意加到我的 memory 里。其它类似也处理下。im 的。
25   - @Schema(description = "消息类型,参见 ImMessageTypeEnum 枚举类", example = "1")
26   - private Integer type;
  24 + @Schema(description = "消息类型", example = "1")
  25 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
27 26  
28   - @Schema(description = "消息状态,参见 ImMessageStatusEnum 枚举类", example = "0")
29   - private Integer status;
  27 + @Schema(description = "消息状态", example = "0")
  28 + private Integer status; // 参见 ImMessageStatusEnum 枚举类
30 29  
31 30 @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
32 31 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java
... ... @@ -21,11 +21,11 @@ public class ImPrivateMessageManagerPageReqVO extends PageParam {
21 21 @Schema(description = "接收人编号", example = "2048")
22 22 private Long receiverId;
23 23  
24   - @Schema(description = "消息类型,参见 ImMessageTypeEnum 枚举类", example = "1")
25   - private Integer type;
  24 + @Schema(description = "消息类型", example = "1")
  25 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
26 26  
27   - @Schema(description = "消息状态,参见 ImMessageStatusEnum 枚举类", example = "0")
28   - private Integer status;
  27 + @Schema(description = "消息状态", example = "0")
  28 + private Integer status; // 参见 ImMessageStatusEnum 枚举类
29 29  
30 30 @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
31 31 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java
... ... @@ -18,8 +18,8 @@ public class ImSensitiveWordPageReqVO extends PageParam {
18 18 @Schema(description = "敏感词,模糊匹配", example = "敏感")
19 19 private String word;
20 20  
21   - @Schema(description = "状态,参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)", example = "0")
22   - private Integer status;
  21 + @Schema(description = "状态", example = "0")
  22 + private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
23 23  
24 24 @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
25 25 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java
... ... @@ -18,9 +18,8 @@ public class ImSensitiveWordSaveReqVO {
18 18 @Size(max = 64, message = "敏感词长度不能超过 64")
19 19 private String word;
20 20  
21   - @Schema(description = "状态,参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)",
22   - requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  21 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
23 22 @NotNull(message = "状态不能为空")
24   - private Integer status;
  23 + private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
25 24  
26 25 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
... ... @@ -90,7 +90,6 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
90 90 if (CollUtil.isEmpty(groupIds)) {
91 91 return Collections.emptyMap();
92 92 }
93   - // TODO @AI:使用 mybatis plus join,可以简化这个操作么?
94 93 List<Map<String, Object>> rows = selectMaps(Wrappers.<ImGroupMemberDO>query()
95 94 .select("group_id AS groupId", "COUNT(*) AS cnt")
96 95 .in("group_id", groupIds)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/sensitiveword/ImSensitiveWordMapper.java
... ... @@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
7 7 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
8 8 import org.apache.ibatis.annotations.Mapper;
  9 +import org.apache.ibatis.annotations.Param;
  10 +import org.apache.ibatis.annotations.Select;
9 11  
  12 +import java.time.LocalDateTime;
10 13 import java.util.List;
11 14  
12 15 /**
... ... @@ -35,4 +38,7 @@ public interface ImSensitiveWordMapper extends BaseMapperX&lt;ImSensitiveWordDO&gt; {
35 38 .orderByDesc(ImSensitiveWordDO::getId));
36 39 }
37 40  
  41 + @Select("SELECT MAX(update_time) FROM im_sensitive_word")
  42 + LocalDateTime selectMaxUpdateTime(@Param("tenantId") Long tenantId);
  43 +
38 44 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/sensitiveword/ImSensitiveWordConfiguration.java deleted
1   -package cn.iocoder.yudao.module.im.framework.sensitiveword;
2   -
3   -import com.github.houbb.sensitive.word.bs.SensitiveWordBs;
4   -import org.springframework.context.annotation.Bean;
5   -import org.springframework.context.annotation.Configuration;
6   -
7   -/**
8   - * IM 敏感词检测配置
9   - *
10   - * @author 芋道源码
11   - */
12   -@Configuration
13   -public class ImSensitiveWordConfiguration {
14   -
15   - // TODO @AI:可能需要按需创建;因为多租户。
16   - /**
17   - * 装配 {@link SensitiveWordBs}:
18   - * 词库来源 = {@link ImSensitiveWordDeny}(DB 里启用的敏感词),并开启常用规范化能力。
19   - */
20   - @Bean
21   - public SensitiveWordBs sensitiveWordBs(ImSensitiveWordDeny imSensitiveWordDeny) {
22   - return SensitiveWordBs.newInstance()
23   - .wordDeny(imSensitiveWordDeny)
24   - .ignoreCase(true)
25   - .ignoreWidth(true) // 忽略全/半角
26   - .ignoreNumStyle(true) // 忽略数字风格(中文/阿拉伯)
27   - .ignoreChineseStyle(true) // 忽略繁简体
28   - .enableWordCheck(true)
29   - .init();
30   - }
31   -
32   -}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/sensitiveword/ImSensitiveWordDeny.java deleted
1   -package cn.iocoder.yudao.module.im.framework.sensitiveword;
2   -
3   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4   -import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
5   -import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
6   -import com.github.houbb.sensitive.word.api.IWordDeny;
7   -import jakarta.annotation.Resource;
8   -import org.springframework.stereotype.Component;
9   -
10   -import java.util.List;
11   -
12   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
13   -
14   -// TODO @AI:是不是可以,直接塞入一个数组。而不是这里查询???
15   -/**
16   - * 自定义敏感词数据来源:从 im_sensitive_word 表加载启用状态的敏感词
17   - * <p>
18   - * 由 {@link com.github.houbb.sensitive.word.bs.SensitiveWordBs} 在 init() 时回调,
19   - * 因此 CRUD 完成后调用 sensitiveWordBs.init() 即可实现热更新。
20   - *
21   - * @author 芋道源码
22   - */
23   -@Component
24   -public class ImSensitiveWordDeny implements IWordDeny {
25   -
26   - @Resource
27   - private ImSensitiveWordMapper sensitiveWordMapper;
28   -
29   - @Override
30   - public List<String> deny() {
31   - return convertList(
32   - sensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()),
33   - ImSensitiveWordDO::getWord);
34   - }
35   -
36   -}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.service.friend;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
5 5 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
6   -import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
7 6 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
8 7  
9 8 import java.util.Collection;
... ... @@ -90,8 +89,8 @@ public interface ImFriendService {
90 89 // ==================== 管理后台 ====================
91 90  
92 91 /**
93   - * 【管理后台】分页查询好友关系(已填充用户 / 好友昵称)
  92 + * 【管理后台】分页查询好友关系
94 93 */
95   - PageResult<ImFriendManagerRespVO> getFriendPage(ImFriendManagerPageReqVO reqVO);
  94 + PageResult<ImFriendDO> getFriendPage(ImFriendManagerPageReqVO reqVO);
96 95  
97 96 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4 3 import cn.hutool.extra.spring.SpringUtil;
5 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
8   -import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 6 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
10 7 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
11   -import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
12 8 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
13 9 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
14 10 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
15 11 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
16 12 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
17 13 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
18   -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
19 14 import jakarta.annotation.Resource;
20 15 import lombok.extern.slf4j.Slf4j;
21 16 import org.springframework.cache.annotation.CacheEvict;
... ... @@ -27,10 +22,11 @@ import org.springframework.transaction.annotation.Transactional;
27 22 import org.springframework.validation.annotation.Validated;
28 23  
29 24 import java.time.LocalDateTime;
30   -import java.util.*;
  25 +import java.util.Collection;
  26 +import java.util.List;
  27 +import java.util.Objects;
31 28  
32 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
33   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
34 30 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND;
35 31 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
36 32 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
... ... @@ -189,26 +185,9 @@ public class ImFriendServiceImpl implements ImFriendService {
189 185  
190 186 // ==================== 管理后台 ====================
191 187  
192   - // TODO @AI:拿到 controller 拼接数据,简化 service;
193 188 @Override
194   - public PageResult<ImFriendManagerRespVO> getFriendPage(ImFriendManagerPageReqVO reqVO) {
195   - // 1. 分页查询
196   - PageResult<ImFriendDO> pageResult = imFriendMapper.selectPage(reqVO);
197   - if (CollUtil.isEmpty(pageResult.getList())) {
198   - return PageResult.empty(pageResult.getTotal());
199   - }
200   -
201   - // 2. 一次性批量查询用户 + 好友的昵称
202   - Set<Long> userIds = new HashSet<>();
203   - userIds.addAll(convertSet(pageResult.getList(), ImFriendDO::getUserId));
204   - userIds.addAll(convertSet(pageResult.getList(), ImFriendDO::getFriendUserId));
205   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
206   -
207   - // 3. 转换为 VO,填充昵称
208   - return BeanUtils.toBean(pageResult, ImFriendManagerRespVO.class, vo -> {
209   - MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname()));
210   - MapUtils.findAndThen(userMap, vo.getFriendUserId(), user -> vo.setFriendNickname(user.getNickname()));
211   - });
  189 + public PageResult<ImFriendDO> getFriendPage(ImFriendManagerPageReqVO reqVO) {
  190 + return imFriendMapper.selectPage(reqVO);
212 191 }
213 192  
214 193 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
1 1 package cn.iocoder.yudao.module.im.service.group;
2 2  
3 3 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
4   -import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
5 4 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
6 5 import jakarta.validation.Valid;
7 6  
... ... @@ -156,14 +155,4 @@ public interface ImGroupMemberService {
156 155 */
157 156 Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds);
158 157  
159   - // ==================== 管理后台 ====================
160   -
161   - /**
162   - * 【管理后台】获取群成员列表
163   - *
164   - * @param groupId 群编号
165   - * @return 群成员列表
166   - */
167   - List<ImGroupMemberManagerRespVO> getGroupMemberList(Long groupId);
168   -
169 158 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
... ... @@ -2,16 +2,12 @@ package cn.iocoder.yudao.module.im.service.group;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
6 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
8   -import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
9 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
10 8 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
11 9 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
12 10 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
13   -import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
14   -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
15 11 import jakarta.annotation.Resource;
16 12 import lombok.extern.slf4j.Slf4j;
17 13 import org.springframework.cache.annotation.CacheEvict;
... ... @@ -44,9 +40,6 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
44 40 @Resource
45 41 private ImWebSocketService webSocketService;
46 42  
47   - @Resource
48   - private AdminUserApi adminUserApi;
49   -
50 43 @Override
51 44 public ImGroupMemberDO getGroupMember(Long id) {
52 45 return groupMemberMapper.selectById(id);
... ... @@ -222,25 +215,4 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
222 215 return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus());
223 216 }
224 217  
225   - // ==================== 管理后台 ====================
226   -
227   - // TODO @AI:这个拼接的逻辑,拿到 controller,这里只返回 list do;
228   - @Override
229   - public List<ImGroupMemberManagerRespVO> getGroupMemberList(Long groupId) {
230   - // 1. 查询有效群成员
231   - List<ImGroupMemberDO> members = getActiveGroupMemberListByGroupId(groupId);
232   - if (CollUtil.isEmpty(members)) {
233   - return Collections.emptyList();
234   - }
235   -
236   - // 2. 批量查询用户信息
237   - Set<Long> userIds = convertSet(members, ImGroupMemberDO::getUserId);
238   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
239   -
240   - // 3. 转换为 VO,填充昵称、头像
241   - return BeanUtils.toBean(members, ImGroupMemberManagerRespVO.class, vo ->
242   - MapUtils.findAndThen(userMap, vo.getUserId(), user ->
243   - vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())));
244   - }
245   -
246 218 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
... ... @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMember
7 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
8 8 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
9 9 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
10   -import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
11 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
12 11 import jakarta.validation.Valid;
13 12  
... ... @@ -138,15 +137,13 @@ public interface ImGroupService {
138 137  
139 138 // ==================== 管理后台 ====================
140 139  
141   - // TODO DONE @AI:是不是 controller 拆分哈;(getGroupPage 留在 ImGroupService;getGroupMemberList 已拆到 ImGroupMemberService + ImGroupMemberManagerController)
142   - // TODO @AI:不对;应该 VO 放在 controller 拼接
143 140 /**
144 141 * 【管理后台】分页查询群列表
145 142 *
146 143 * @param pageReqVO 分页查询条件
147 144 * @return 群分页列表
148 145 */
149   - PageResult<ImGroupManagerRespVO> getGroupPage(ImGroupManagerPageReqVO pageReqVO);
  146 + PageResult<ImGroupDO> getGroupPage(ImGroupManagerPageReqVO pageReqVO);
150 147  
151 148 /**
152 149 * 【管理后台】封禁群
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
... ... @@ -8,7 +8,6 @@ import cn.hutool.core.util.StrUtil;
8 8 import cn.hutool.extra.spring.SpringUtil;
9 9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
10 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
11   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
12 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
13 12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
14 13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
... ... @@ -16,7 +15,6 @@ import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMember
16 15 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
17 16 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
18 17 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
19   -import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
20 18 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
21 19 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
22 20 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
... ... @@ -288,24 +286,8 @@ public class ImGroupServiceImpl implements ImGroupService {
288 286 // ==================== 管理后台 ====================
289 287  
290 288 @Override
291   - public PageResult<ImGroupManagerRespVO> getGroupPage(ImGroupManagerPageReqVO pageReqVO) {
292   - // 1. 分页查询群
293   - PageResult<ImGroupDO> pageResult = groupMapper.selectPage(pageReqVO);
294   - if (CollUtil.isEmpty(pageResult.getList())) {
295   - return PageResult.empty(pageResult.getTotal());
296   - }
297   -
298   - // 2.1 批量查询群主昵称
299   - Set<Long> ownerUserIds = convertSet(pageResult.getList(), ImGroupDO::getOwnerUserId);
300   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(ownerUserIds);
301   - // 2.2 一次 GROUP BY 查出本页所有群的活跃成员数,避免按行 N+1
302   - Set<Long> groupIds = convertSet(pageResult.getList(), ImGroupDO::getId);
303   - Map<Long, Long> memberCountMap = groupMemberService.getActiveMemberCountMap(groupIds);
304   - // 2.3 转换为 VO,填充群主昵称、群成员数量
305   - return BeanUtils.toBean(pageResult, ImGroupManagerRespVO.class, vo -> {
306   - MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerNickname(user.getNickname()));
307   - vo.setMemberCount(memberCountMap.getOrDefault(vo.getId(), 0L).intValue());
308   - });
  289 + public PageResult<ImGroupDO> getGroupPage(ImGroupManagerPageReqVO pageReqVO) {
  290 + return groupMapper.selectPage(pageReqVO);
309 291 }
310 292  
311 293 @Override
... ... @@ -328,7 +310,6 @@ public class ImGroupServiceImpl implements ImGroupService {
328 310 throw exception(GROUP_NOT_EXISTS);
329 311 }
330 312 // 2. 解封(保留 bannedReason / bannedTime 作为历史记录)
331   - // TODO DONE @AI:不要使用 mybatis plus 的方法,直接操作。另外,感觉封禁时间、封禁原因,没必要置空,留着问题不大。
332 313 groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false));
333 314 }
334 315  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.im.service.message;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
5   -import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
6 5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
7 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
8 7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
... ... @@ -118,9 +117,9 @@ public interface ImGroupMessageService {
118 117 // ==================== 管理后台 ====================
119 118  
120 119 /**
121   - * 【管理后台】分页查询群聊消息(已填充群名称、发送人昵称)
  120 + * 【管理后台】分页查询群聊消息
122 121 */
123   - PageResult<ImGroupMessageManagerRespVO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO);
  122 + PageResult<ImGroupMessageDO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO);
124 123  
125 124 /**
126 125 * 【管理后台】获取群聊消息详情
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
... ... @@ -7,14 +7,11 @@ import cn.hutool.core.util.ObjUtil;
7 7 import cn.hutool.extra.spring.SpringUtil;
8 8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
9 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
11 10 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
13 12 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
14   -import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
15 13 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
16 14 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
17   -import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
18 15 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
19 16 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
20 17 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
... ... @@ -28,8 +25,6 @@ import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
28 25 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
29 26 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
30 27 import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
31   -import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
32   -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
33 28 import jakarta.annotation.Resource;
34 29 import lombok.extern.slf4j.Slf4j;
35 30 import org.springframework.scheduling.annotation.Async;
... ... @@ -77,9 +72,6 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
77 72 @Resource
78 73 private ImWebSocketService imWebSocketService;
79 74  
80   - @Resource
81   - private AdminUserApi adminUserApi;
82   -
83 75 @Override
84 76 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) {
85 77 // 1.1 幂等校验:根据 senderId + clientMessageId 查重
... ... @@ -554,26 +546,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
554 546  
555 547 // ==================== 管理后台 ====================
556 548  
557   - // TODO @AI:这些挪到 controller 拼接 VO
558 549 @Override
559   - public PageResult<ImGroupMessageManagerRespVO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO) {
560   - // 1. 分页查询
561   - PageResult<ImGroupMessageDO> pageResult = groupMessageMapper.selectPage(reqVO);
562   - if (CollUtil.isEmpty(pageResult.getList())) {
563   - return PageResult.empty(pageResult.getTotal());
564   - }
565   -
566   - // 2. 批量查询群名称、发送人昵称
567   - Set<Long> groupIds = convertSet(pageResult.getList(), ImGroupMessageDO::getGroupId);
568   - Set<Long> senderIds = convertSet(pageResult.getList(), ImGroupMessageDO::getSenderId);
569   - Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(groupIds);
570   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(senderIds);
571   -
572   - // 3. 转换为 VO,填充群名 / 发送人昵称
573   - return BeanUtils.toBean(pageResult, ImGroupMessageManagerRespVO.class, vo -> {
574   - MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
575   - MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
576   - });
  550 + public PageResult<ImGroupMessageDO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO) {
  551 + return groupMessageMapper.selectPage(reqVO);
577 552 }
578 553  
579 554 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.im.service.message;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
5   -import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
6 5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
7 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
8 7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
... ... @@ -91,9 +90,9 @@ public interface ImPrivateMessageService {
91 90 // ==================== 管理后台 ====================
92 91  
93 92 /**
94   - * 【管理后台】分页查询私聊消息(已填充发送人 / 接收人昵称)
  93 + * 【管理后台】分页查询私聊消息
95 94 */
96   - PageResult<ImPrivateMessageManagerRespVO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO);
  95 + PageResult<ImPrivateMessageDO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO);
97 96  
98 97 /**
99 98 * 【管理后台】获取私聊消息详情
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.message;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4 3 import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.util.IdUtil;
6 5 import cn.hutool.core.util.ObjUtil;
7 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
9 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
11 9 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
12   -import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
13 10 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
14 11 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
15 12 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
... ... @@ -21,8 +18,6 @@ import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
21 18 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
22 19 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
23 20 import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
24   -import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
25   -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
26 21 import jakarta.annotation.Resource;
27 22 import lombok.extern.slf4j.Slf4j;
28 23 import org.springframework.stereotype.Service;
... ... @@ -30,13 +25,9 @@ import org.springframework.transaction.annotation.Transactional;
30 25 import org.springframework.validation.annotation.Validated;
31 26  
32 27 import java.time.LocalDateTime;
33   -import java.util.HashSet;
34 28 import java.util.List;
35   -import java.util.Map;
36   -import java.util.Set;
37 29  
38 30 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
39   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
40 31 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
41 32 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*;
42 33  
... ... @@ -61,9 +52,6 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
61 52 @Resource
62 53 private ImWebSocketService imWebSocketService;
63 54  
64   - @Resource
65   - private AdminUserApi adminUserApi;
66   -
67 55 @Override
68 56 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) {
69 57 // 1.1 幂等校验:根据 senderId + clientMessageId 查重
... ... @@ -183,26 +171,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
183 171  
184 172 // ==================== 管理后台 ====================
185 173  
186   - // TODO @AI:这些挪到 controller 拼接 VO
  174 + // DONE @AI:这些挪到 controller 拼接 VO
187 175 @Override
188   - public PageResult<ImPrivateMessageManagerRespVO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO) {
189   - // 1. 分页查询
190   - PageResult<ImPrivateMessageDO> pageResult = privateMessageMapper.selectPage(reqVO);
191   - if (CollUtil.isEmpty(pageResult.getList())) {
192   - return PageResult.empty(pageResult.getTotal());
193   - }
194   -
195   - // 2. 一次性批量查询发送人 + 接收人昵称
196   - Set<Long> userIds = new HashSet<>();
197   - userIds.addAll(convertSet(pageResult.getList(), ImPrivateMessageDO::getSenderId));
198   - userIds.addAll(convertSet(pageResult.getList(), ImPrivateMessageDO::getReceiverId));
199   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
200   -
201   - // 3. 转换为 VO,填充昵称
202   - return BeanUtils.toBean(pageResult, ImPrivateMessageManagerRespVO.class, vo -> {
203   - MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
204   - MapUtils.findAndThen(userMap, vo.getReceiverId(), user -> vo.setReceiverNickname(user.getNickname()));
205   - });
  176 + public PageResult<ImPrivateMessageDO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO) {
  177 + return privateMessageMapper.selectPage(reqVO);
206 178 }
207 179  
208 180 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java
... ... @@ -3,21 +3,35 @@ package cn.iocoder.yudao.module.im.service.sensitiveword;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
7 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  10 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  11 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
8 12 import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
9 13 import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
10 14 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
11 15 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
12 16 import com.github.houbb.sensitive.word.bs.SensitiveWordBs;
  17 +import com.google.common.cache.CacheLoader;
  18 +import com.google.common.cache.LoadingCache;
  19 +import com.google.common.util.concurrent.Futures;
  20 +import com.google.common.util.concurrent.ListenableFuture;
13 21 import jakarta.annotation.Resource;
  22 +import lombok.AllArgsConstructor;
  23 +import lombok.Data;
14 24 import lombok.extern.slf4j.Slf4j;
15 25 import org.springframework.stereotype.Service;
16 26 import org.springframework.validation.annotation.Validated;
17 27  
  28 +import java.time.Duration;
  29 +import java.time.LocalDateTime;
18 30 import java.util.List;
  31 +import java.util.Objects;
19 32  
20 33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
21 35 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
22 36 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED;
23 37 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
... ... @@ -37,34 +51,92 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
37 51 @Resource
38 52 private ImSensitiveWordMapper sensitiveWordMapper;
39 53  
40   - // TODO @AI:当前节点,立即刷新。其他节点,通过 job 扫描最后更新时间(只要有不同于目前最后一条记录的最大更新时间,就重新构建对应的树)。
41 54 /**
42   - * houbb 敏感词检测器,词库由 ImSensitiveWordDeny 在 init 时回调加载
  55 + * 缓存条目
43 56 */
44   - @Resource
45   - private SensitiveWordBs sensitiveWordBs;
  57 + @Data
  58 + @AllArgsConstructor
  59 + private static class SensitiveWordBsCache {
  60 +
  61 + /**
  62 + * 敏感词检测器
  63 + */
  64 + private SensitiveWordBs bs;
  65 + /**
  66 + * 构建本实例时数据库里的 max(update_time),作为下次刷新比对的基线
  67 + */
  68 + private LocalDateTime maxUpdateTime;
  69 +
  70 + }
46 71  
47   - // TODO @AI:你看看现有仓库里,是否有类似这种的。按照最后更新判断,是否需要刷新本地缓存。
48 72 /**
49   - * 触发 houbb 词库重新加载(重建 trie)
  73 + * 租户 → SensitiveWordBs 实例的本地缓存
50 74 * <p>
51   - * 加 synchronized 是因为 SensitiveWordBs.init() 内部对 wordData 做就地写入,
52   - * 若与并发的 contains() 重叠会读到半构建的 trie。互斥后短暂阻塞 contains 即可保证可见性。
  75 + * 每分钟触发一次异步 reload,先读 max(update_time),没变就复用旧实例(避免 trie 重建),变了才重新读词库 + 重建。
  76 + * 单实例 CRUD 后另外通过 {@link #invalidateSensitiveWordBsCaches()} 立即让本机失效,多实例靠定时刷新最长 1 分钟内收敛。
  77 + */
  78 + @SuppressWarnings({"Convert2Diamond", "NullableProblems"})
  79 + private final LoadingCache<Long, SensitiveWordBsCache> sensitiveWordBsCaches = CacheUtils.buildAsyncReloadingCache(
  80 + Duration.ofMinutes(1L), // 1 分钟过期
  81 + new CacheLoader<Long, SensitiveWordBsCache>() {
  82 +
  83 + @Override
  84 + public SensitiveWordBsCache load(Long tenantId) {
  85 + return loadFresh(tenantId);
  86 + }
  87 +
  88 + @Override
  89 + public ListenableFuture<SensitiveWordBsCache> reload(Long tenantId, SensitiveWordBsCache oldValue) {
  90 + // 先比对 max(update_time);没变 → 复用旧实例,避免无谓地重建 trie
  91 + LocalDateTime currentMax = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
  92 + if (Objects.equals(oldValue.getMaxUpdateTime(), currentMax)) {
  93 + return Futures.immediateFuture(oldValue);
  94 + }
  95 + // 变了 → 重新读词库并重建 trie
  96 + return Futures.immediateFuture(loadFresh(tenantId));
  97 + }
  98 +
  99 + });
  100 +
  101 + private SensitiveWordBsCache loadFresh(Long tenantId) {
  102 + return TenantUtils.execute(tenantId, () -> {
  103 + // 先取基线时间再读词库:反过来在两次查询之间出现的新插入会被漏感知
  104 + LocalDateTime maxUpdateTime = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
  105 + List<ImSensitiveWordDO> words = sensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  106 + // 构建敏感词检测器
  107 + SensitiveWordBs bs = SensitiveWordBs.newInstance()
  108 + .wordDeny(() -> convertList(words, ImSensitiveWordDO::getWord))
  109 + .ignoreCase(true)
  110 + .ignoreWidth(true) // 忽略全/半角
  111 + .ignoreNumStyle(true) // 忽略数字风格(中文/阿拉伯)
  112 + .ignoreChineseStyle(true) // 忽略繁简体
  113 + .enableWordCheck(true)
  114 + .init();
  115 + return new SensitiveWordBsCache(bs, maxUpdateTime);
  116 + });
  117 + }
  118 +
  119 + /**
  120 + * 强制让敏感词缓存失效,下次访问按最新 DB 重建
53 121 * <p>
54   - * init() 本身幂等,单实例可以直接调;多实例部署时需配合 Redis pub/sub 广播让各实例 init。
  122 + * 有租户上下文:仅失效该租户。无租户上下文(如系统级 / 跨租户清理):兜底失效所有租户。
55 123 */
56   - private synchronized void reloadSensitiveWords() {
57   - sensitiveWordBs.init();
58   - log.info("[reloadSensitiveWords][敏感词库已 reload]");
  124 + private void invalidateSensitiveWordBsCaches() {
  125 + Long tenantId = TenantContextHolder.getTenantId();
  126 + if (tenantId != null) {
  127 + sensitiveWordBsCaches.invalidate(tenantId);
  128 + return;
  129 + }
  130 + sensitiveWordBsCaches.invalidateAll();
59 131 }
60 132  
61   - // TODO @AI:需要按照租户来。
62 133 @Override
63 134 public void validateText(String text) {
64 135 if (StrUtil.isBlank(text)) {
65 136 return;
66 137 }
67   - if (sensitiveWordBs.contains(text)) {
  138 + SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(TenantContextHolder.getRequiredTenantId()).getBs();
  139 + if (bs.contains(text)) {
68 140 throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
69 141 }
70 142 }
... ... @@ -89,9 +161,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
89 161 // 2.1 入库
90 162 ImSensitiveWordDO word = BeanUtils.toBean(reqVO, ImSensitiveWordDO.class);
91 163 sensitiveWordMapper.insert(word);
92   - // TODO @AI:houbb 这个注释有点奇怪!
93   - // 2.2 触发 houbb 词库 reload
94   - reloadSensitiveWords();
  164 + // 2.2 强制失效本机缓存(多实例靠定时刷新收敛)
  165 + invalidateSensitiveWordBsCaches();
95 166 return word.getId();
96 167 }
97 168  
... ... @@ -105,9 +176,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
105 176 // 2.1 更新
106 177 ImSensitiveWordDO updateObj = BeanUtils.toBean(reqVO, ImSensitiveWordDO.class);
107 178 sensitiveWordMapper.updateById(updateObj);
108   - // 2.2 TODO @AI:houbb 这个注释有点奇怪!
109   - // 2.2 触发 houbb 词库 reload
110   - reloadSensitiveWords();
  179 + // 2.2 强制失效本机缓存
  180 + invalidateSensitiveWordBsCaches();
111 181 }
112 182  
113 183 @Override
... ... @@ -117,8 +187,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
117 187  
118 188 // 2.1 删除
119 189 sensitiveWordMapper.deleteById(id);
120   - // 2.2 触发 houbb 词库 reload
121   - reloadSensitiveWords();
  190 + // 2.2 强制失效本机缓存
  191 + invalidateSensitiveWordBsCaches();
122 192 }
123 193  
124 194  
... ... @@ -127,11 +197,10 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
127 197 if (CollUtil.isEmpty(ids)) {
128 198 return;
129 199 }
130   -
131 200 // 1. 删除
132 201 sensitiveWordMapper.deleteByIds(ids);
133   - // 触发 houbb 词库 reload
134   - reloadSensitiveWords();
  202 + // 2. 强制失效本机缓存
  203 + invalidateSensitiveWordBsCaches();
135 204 }
136 205  
137 206 private void validateSensitiveWordExists(Long id) {
... ... @@ -148,7 +217,6 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
148 217 if (exist == null) {
149 218 return;
150 219 }
151   - // TODO @AI:是不是 id 不用判断
152 220 if (id == null || ObjUtil.notEqual(exist.getId(), id)) {
153 221 throw exception(SENSITIVE_WORD_DUPLICATED, word);
154 222 }
... ...