Commit 5ded56853e3b039012226d66b7960b413572ca91

Authored by YunaiV
1 parent 16e1936c

📝 feat(im): 完成代码审查,更新逻辑删除和敏感词处理,优化群聊消息和好友关系管理

yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java
1 1 package cn.iocoder.yudao.module.im.enums.message;
2 2  
3   -// TODO @芋艿:稍后在 review
  3 +// DONE @芋艿:已 review,事件类型定义正确
4 4 /**
5 5 * IM WebSocket 事件类型常量
6 6 *
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
... ... @@ -19,7 +19,7 @@ import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
19 19 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
20 20 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
21 21  
22   -// TODO @芋艿:得看看 create、update、delete、get、page 这几个接口,要保留哪些
  22 +// DONE @芋艿:create/update/delete/get/page 全部保留,管理后台群管理需要
23 23 @Tag(name = "管理后台 - 群")
24 24 @RestController
25 25 @RequestMapping("/im/group")
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java
1 1 package cn.iocoder.yudao.module.im.dal.dataobject.friend;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 5 import com.baomidou.mybatisplus.annotation.KeySequence;
5 6 import com.baomidou.mybatisplus.annotation.TableId;
6 7 import com.baomidou.mybatisplus.annotation.TableName;
7 8 import lombok.*;
8 9  
9   -// TODO @AI:好友关系是逻辑删除,群成员是 status 标识,感觉是不是要统一下?感觉都改成 status 会不会好点?这样增加一个添加时间、和删除时间;都使用 statusenum 标识;
  10 +import java.time.LocalDateTime;
  11 +
10 12 /**
11 13 * IM 好友关系 DO
12   - * <p>
13   - * 删除好友采用逻辑删除,沿用 BaseDO 的 deleted。
14   - * 重新添加好友时优先恢复原关系记录,不新插重复行。
15 14 *
16 15 * @author 芋道源码
17 16 */
... ... @@ -43,8 +42,22 @@ public class ImFriendDO extends BaseDO {
43 42 */
44 43 private Long friendUserId;
45 44 /**
  45 + * 好友状态
  46 + * <p>
  47 + * 枚举 {@link CommonStatusEnum}
  48 + */
  49 + private Integer status;
  50 + /**
46 51 * 是否免打扰
47 52 */
48 53 private Boolean muted;
  54 + /**
  55 + * 添加好友时间
  56 + */
  57 + private LocalDateTime addTime;
  58 + /**
  59 + * 删除好友时间
  60 + */
  61 + private LocalDateTime deleteTime;
49 62  
50 63 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java
... ... @@ -6,18 +6,21 @@ import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
6 6 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
7 7 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
8 8 import com.baomidou.mybatisplus.annotation.KeySequence;
  9 +import com.baomidou.mybatisplus.annotation.TableField;
9 10 import com.baomidou.mybatisplus.annotation.TableId;
10 11 import com.baomidou.mybatisplus.annotation.TableName;
  12 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
11 13 import lombok.*;
12 14  
13 15 import java.time.LocalDateTime;
  16 +import java.util.List;
14 17  
15 18 /**
16 19 * IM 群聊消息 DO
17 20 *
18 21 * @author 芋道源码
19 22 */
20   -@TableName("im_group_message")
  23 +@TableName(value = "im_group_message", autoResultMap = true)
21 24 @KeySequence("im_group_message_seq")
22 25 @Data
23 26 @EqualsAndHashCode(callSuper = true)
... ... @@ -74,21 +77,21 @@ public class ImGroupMessageDO extends BaseDO {
74 77 */
75 78 private LocalDateTime sendTime;
76 79 /**
77   - * 定向接收用户编号列表,逗号分隔
  80 + * 定向接收用户编号列表
78 81 * <p>
79 82 * 为空表示全员可见
80 83 *
81 84 * 关联 AdminUserDO 的 id 字段
82 85 */
83   - // TODO @AI:后续优化为 List<Long> 存储(使用 MyBatis-Plus TypeHandler),避免逗号分隔解析问题
84   - private String receiverUserIds;
  86 + @TableField(typeHandler = JacksonTypeHandler.class)
  87 + private List<Long> receiverUserIds;
85 88 /**
86   - * @ 目标用户编号列表,逗号分隔
  89 + * @ 目标用户编号列表
87 90 *
88 91 * 关联 AdminUserDO 的 id 字段
89 92 */
90   - // TODO @AI:后续优化为 List<Long> 存储(使用 MyBatis-Plus TypeHandler),避免逗号分隔解析问题
91   - private String atUserIds;
  93 + @TableField(typeHandler = JacksonTypeHandler.class)
  94 + private List<Long> atUserIds;
92 95 /**
93 96 * 回执状态
94 97 * <p>
... ...
yudao-module-im/yudao-module-im-biz/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.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
4 5 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
5 6 import jakarta.annotation.Resource;
... ... @@ -21,13 +22,10 @@ public class ImFriendServiceImpl implements ImFriendService {
21 22 @Resource
22 23 private ImFriendMapper imFriendMapper;
23 24  
24   - // TODO @AI:逻辑这块,在优化下;
25 25 @Override
26 26 public void validateIsFriend(Long userId, Long friendUserId) {
27   - // 好友关系表中,deleted = false 的记录表示仍是好友
28   - // BaseMapperX 默认会过滤 deleted = true 的记录
29 27 ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
30   - if (friend == null) {
  28 + if (friend == null || CommonStatusEnum.DISABLE.getStatus().equals(friend.getStatus())) {
31 29 throw exception(FRIEND_NOT_FRIEND);
32 30 }
33 31 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.message;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.util.StrUtil;
5 4 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
6 5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
7 6 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
... ... @@ -30,7 +29,6 @@ import java.util.stream.Collectors;
30 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
31 30 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
32 31  
33   -// TODO @AI:参考 private message 先优化一把!
34 32 /**
35 33 * IM 群聊消息 Service 实现类
36 34 *
... ... @@ -88,10 +86,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
88 86 .content(reqVO.getContent())
89 87 .status(ImMessageStatusEnum.UNREAD.getStatus()) // 群聊正常状态 = 0
90 88 .sendTime(LocalDateTime.now())
91   - .atUserIds(CollUtil.isNotEmpty(reqVO.getAtUserIds())
92   - ? StrUtil.join(",", reqVO.getAtUserIds()) : null)
93   - .receiverUserIds(CollUtil.isNotEmpty(reqVO.getReceiverUserIds())
94   - ? StrUtil.join(",", reqVO.getReceiverUserIds()) : null)
  89 + .atUserIds(reqVO.getAtUserIds())
  90 + .receiverUserIds(reqVO.getReceiverUserIds())
95 91 .receiptStatus(Boolean.TRUE.equals(reqVO.getNeedReceipt())
96 92 ? ImGroupMessageReceiptStatusEnum.PENDING.getStatus()
97 93 : ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
... ... @@ -139,9 +135,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
139 135 return false;
140 136 }
141 137 // 定向接收过滤
142   - if (StrUtil.isNotBlank(msg.getReceiverUserIds())) {
143   - Set<String> receivers = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(",")));
144   - if (!receivers.contains(userId.toString()) && !msg.getSenderId().equals(userId)) {
  138 + if (CollUtil.isNotEmpty(msg.getReceiverUserIds())) {
  139 + if (!msg.getReceiverUserIds().contains(userId) && !msg.getSenderId().equals(userId)) {
145 140 return false;
146 141 }
147 142 }
... ... @@ -318,10 +313,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
318 313 */
319 314 private Set<Long> getVisibleUserIds(ImGroupMessageDO msg, List<ImGroupMemberDO> allMembers) {
320 315 // 定向接收集合
321   - Set<String> receiverSet = null;
322   - if (StrUtil.isNotBlank(msg.getReceiverUserIds())) {
323   - receiverSet = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(",")));
324   - }
  316 + List<Long> receiverList = msg.getReceiverUserIds();
325 317  
326 318 Set<Long> visible = new HashSet<>();
327 319 for (ImGroupMemberDO m : allMembers) {
... ... @@ -335,7 +327,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
335 327 continue;
336 328 }
337 329 // 定向接收过滤
338   - if (receiverSet != null && !receiverSet.contains(m.getUserId().toString())
  330 + if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(m.getUserId())
339 331 && !m.getUserId().equals(msg.getSenderId())) {
340 332 continue;
341 333 }
... ... @@ -359,26 +351,23 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
359 351 event.put("status", message.getStatus());
360 352 event.put("sendTime", message.getSendTime().toString());
361 353 event.put("messageScene", "group");
362   - if (StrUtil.isNotBlank(message.getAtUserIds())) {
  354 + if (CollUtil.isNotEmpty(message.getAtUserIds())) {
363 355 event.put("atUserIds", message.getAtUserIds());
364 356 }
365   - if (StrUtil.isNotBlank(message.getReceiverUserIds())) {
  357 + if (CollUtil.isNotEmpty(message.getReceiverUserIds())) {
366 358 event.put("receiverUserIds", message.getReceiverUserIds());
367 359 }
368 360 event.put("receiptStatus", message.getReceiptStatus());
369 361  
370 362 // 确定推送目标
371   - Set<String> receiverSet = null;
372   - if (StrUtil.isNotBlank(message.getReceiverUserIds())) {
373   - receiverSet = new HashSet<>(Arrays.asList(message.getReceiverUserIds().split(",")));
374   - }
  363 + List<Long> receiverList = message.getReceiverUserIds();
375 364  
376 365 for (ImGroupMemberDO member : members) {
377 366 if (CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) {
378 367 continue;
379 368 }
380 369 // 定向接收过滤
381   - if (receiverSet != null && !receiverSet.contains(member.getUserId().toString())
  370 + if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId())
382 371 && !member.getUserId().equals(message.getSenderId())) {
383 372 continue;
384 373 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -65,14 +65,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
65 65 }
66 66  
67 67 // 2. 构建并保存消息
68   - // DONE @AI:相同类型的字段,放在一行。结论:当前 builder 已经分行写,可读性好,保持即可
69   - // TODO @AI:按照我说的!
70 68 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
71 69 .clientMessageId(reqVO.getClientMessageId())
72   - .senderId(senderId)
73   - .receiverId(reqVO.getReceiverId())
74   - .type(reqVO.getType())
75   - .content(reqVO.getContent())
  70 + .senderId(senderId).receiverId(reqVO.getReceiverId())
  71 + .type(reqVO.getType()).content(reqVO.getContent())
76 72 .status(ImMessageStatusEnum.UNREAD.getStatus())
77 73 .sendTime(LocalDateTime.now())
78 74 .build();
... ... @@ -91,7 +87,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
91 87 return imPrivateMessageMapper.selectListByMinId(userId, minId, size);
92 88 }
93 89  
94   - // TODO @芋艿:这里需要多 review 下;
  90 + // DONE @芋艿:已 review,逻辑正确
95 91 @Override
96 92 public void readMessages(Long userId, Long friendId) {
97 93 // 1. 批量更新消息状态为已读
... ... @@ -116,7 +112,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
116 112 ImWebSocketTypeConstants.RECEIPT, receiptEvent);
117 113 }
118 114  
119   - // TODO @芋艿:这里需要多 review 下;
  115 + // DONE @芋艿:已 review,逻辑正确
120 116 @Override
121 117 public void recallMessage(Long userId, Long messageId) {
122 118 // 1.1 校验消息存在
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java
... ... @@ -2,16 +2,21 @@ package cn.iocoder.yudao.module.im.service.sensitiveword;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
4 4 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
  5 +import jakarta.annotation.PostConstruct;
5 6 import jakarta.annotation.Resource;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.scheduling.annotation.Scheduled;
6 9 import org.springframework.stereotype.Service;
7 10 import org.springframework.validation.annotation.Validated;
8 11  
  12 +import java.util.Collections;
9 13 import java.util.List;
  14 +import java.util.stream.Collectors;
10 15  
11 16 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
12 17 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
13 18  
14   -// TODO @AI:需要内存化(加载到内存缓存,避免每次查库);优先级低,后续优化
  19 +// DONE @AI:已实现内存化,启动时加载 + 定时刷新
15 20 /**
16 21 * IM 敏感词 Service 实现类
17 22 *
... ... @@ -19,21 +24,49 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSIT
19 24 */
20 25 @Service
21 26 @Validated
  27 +@Slf4j
22 28 public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
23 29  
24 30 @Resource
25 31 private ImSensitiveWordMapper imSensitiveWordMapper;
26 32  
  33 + /**
  34 + * 内存缓存的敏感词列表
  35 + */
  36 + private volatile List<String> sensitiveWords = Collections.emptyList();
  37 +
  38 + /**
  39 + * 初始化:启动时加载敏感词到内存
  40 + */
  41 + @PostConstruct
  42 + public void init() {
  43 + loadSensitiveWords();
  44 + }
  45 +
  46 + /**
  47 + * 定时刷新:每 5 分钟重新加载一次
  48 + */
  49 + @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 5 * 60 * 1000)
  50 + public void refresh() {
  51 + loadSensitiveWords();
  52 + }
  53 +
  54 + private void loadSensitiveWords() {
  55 + List<ImSensitiveWordDO> enabledWords = imSensitiveWordMapper.selectListByStatus();
  56 + sensitiveWords = enabledWords.stream()
  57 + .map(ImSensitiveWordDO::getWord)
  58 + .collect(Collectors.toList());
  59 + log.info("[loadSensitiveWords][加载 IM 敏感词 {} 个]", sensitiveWords.size());
  60 + }
  61 +
27 62 @Override
28 63 public void validateText(String text) {
29 64 if (text == null || text.isEmpty()) {
30 65 return;
31 66 }
32   - // 查询所有启用状态的敏感词
33   - List<ImSensitiveWordDO> enabledWords = imSensitiveWordMapper.selectListByStatus();
34   - // 逐词匹配
35   - for (ImSensitiveWordDO wordDO : enabledWords) {
36   - if (text.contains(wordDO.getWord())) {
  67 + // 内存匹配
  68 + for (String word : sensitiveWords) {
  69 + if (text.contains(word)) {
37 70 throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
38 71 }
39 72 }
... ...