Commit 5ded56853e3b039012226d66b7960b413572ca91
1 parent
16e1936c
📝 feat(im): 完成代码审查,更新逻辑删除和敏感词处理,优化群聊消息和好友关系管理
Showing
8 changed files
with
84 additions
and
52 deletions
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java
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 | } | ... | ... |