diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java index 1a17e2e..4d8f990 100644 --- a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java +++ b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.im.enums.message; -// TODO @芋艿:稍后在 review +// DONE @芋艿:已 review,事件类型定义正确 /** * IM WebSocket 事件类型常量 * diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java index 166812c..9bb1c07 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java +++ b/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.*; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.service.group.ImGroupService; -// TODO @芋艿:得看看 create、update、delete、get、page 这几个接口,要保留哪些 +// DONE @芋艿:create/update/delete/get/page 全部保留,管理后台群管理需要 @Tag(name = "管理后台 - 群") @RestController @RequestMapping("/im/group") diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java index a351935..9235804 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java @@ -1,17 +1,16 @@ package cn.iocoder.yudao.module.im.dal.dataobject.friend; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; -// TODO @AI:好友关系是逻辑删除,群成员是 status 标识,感觉是不是要统一下?感觉都改成 status 会不会好点?这样增加一个添加时间、和删除时间;都使用 statusenum 标识; +import java.time.LocalDateTime; + /** * IM 好友关系 DO - *

- * 删除好友采用逻辑删除,沿用 BaseDO 的 deleted。 - * 重新添加好友时优先恢复原关系记录,不新插重复行。 * * @author 芋道源码 */ @@ -43,8 +42,22 @@ public class ImFriendDO extends BaseDO { */ private Long friendUserId; /** + * 好友状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** * 是否免打扰 */ private Boolean muted; + /** + * 添加好友时间 + */ + private LocalDateTime addTime; + /** + * 删除好友时间 + */ + private LocalDateTime deleteTime; } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java index 4b3069c..1b00415 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java +++ b/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; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.time.LocalDateTime; +import java.util.List; /** * IM 群聊消息 DO * * @author 芋道源码 */ -@TableName("im_group_message") +@TableName(value = "im_group_message", autoResultMap = true) @KeySequence("im_group_message_seq") @Data @EqualsAndHashCode(callSuper = true) @@ -74,21 +77,21 @@ public class ImGroupMessageDO extends BaseDO { */ private LocalDateTime sendTime; /** - * 定向接收用户编号列表,逗号分隔 + * 定向接收用户编号列表 *

* 为空表示全员可见 * * 关联 AdminUserDO 的 id 字段 */ - // TODO @AI:后续优化为 List 存储(使用 MyBatis-Plus TypeHandler),避免逗号分隔解析问题 - private String receiverUserIds; + @TableField(typeHandler = JacksonTypeHandler.class) + private List receiverUserIds; /** - * @ 目标用户编号列表,逗号分隔 + * @ 目标用户编号列表 * * 关联 AdminUserDO 的 id 字段 */ - // TODO @AI:后续优化为 List 存储(使用 MyBatis-Plus TypeHandler),避免逗号分隔解析问题 - private String atUserIds; + @TableField(typeHandler = JacksonTypeHandler.class) + private List atUserIds; /** * 回执状态 *

diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 3053da8..23a3345 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.service.friend; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; import jakarta.annotation.Resource; @@ -21,13 +22,10 @@ public class ImFriendServiceImpl implements ImFriendService { @Resource private ImFriendMapper imFriendMapper; - // TODO @AI:逻辑这块,在优化下; @Override public void validateIsFriend(Long userId, Long friendUserId) { - // 好友关系表中,deleted = false 的记录表示仍是好友 - // BaseMapperX 默认会过滤 deleted = true 的记录 ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); - if (friend == null) { + if (friend == null || CommonStatusEnum.DISABLE.getStatus().equals(friend.getStatus())) { throw exception(FRIEND_NOT_FRIEND); } } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 4008234..8a23fdd 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.im.service.message; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; @@ -30,7 +29,6 @@ import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; -// TODO @AI:参考 private message 先优化一把! /** * IM 群聊消息 Service 实现类 * @@ -88,10 +86,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { .content(reqVO.getContent()) .status(ImMessageStatusEnum.UNREAD.getStatus()) // 群聊正常状态 = 0 .sendTime(LocalDateTime.now()) - .atUserIds(CollUtil.isNotEmpty(reqVO.getAtUserIds()) - ? StrUtil.join(",", reqVO.getAtUserIds()) : null) - .receiverUserIds(CollUtil.isNotEmpty(reqVO.getReceiverUserIds()) - ? StrUtil.join(",", reqVO.getReceiverUserIds()) : null) + .atUserIds(reqVO.getAtUserIds()) + .receiverUserIds(reqVO.getReceiverUserIds()) .receiptStatus(Boolean.TRUE.equals(reqVO.getNeedReceipt()) ? ImGroupMessageReceiptStatusEnum.PENDING.getStatus() : ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()) @@ -139,9 +135,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { return false; } // 定向接收过滤 - if (StrUtil.isNotBlank(msg.getReceiverUserIds())) { - Set receivers = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(","))); - if (!receivers.contains(userId.toString()) && !msg.getSenderId().equals(userId)) { + if (CollUtil.isNotEmpty(msg.getReceiverUserIds())) { + if (!msg.getReceiverUserIds().contains(userId) && !msg.getSenderId().equals(userId)) { return false; } } @@ -318,10 +313,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { */ private Set getVisibleUserIds(ImGroupMessageDO msg, List allMembers) { // 定向接收集合 - Set receiverSet = null; - if (StrUtil.isNotBlank(msg.getReceiverUserIds())) { - receiverSet = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(","))); - } + List receiverList = msg.getReceiverUserIds(); Set visible = new HashSet<>(); for (ImGroupMemberDO m : allMembers) { @@ -335,7 +327,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { continue; } // 定向接收过滤 - if (receiverSet != null && !receiverSet.contains(m.getUserId().toString()) + if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(m.getUserId()) && !m.getUserId().equals(msg.getSenderId())) { continue; } @@ -359,26 +351,23 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { event.put("status", message.getStatus()); event.put("sendTime", message.getSendTime().toString()); event.put("messageScene", "group"); - if (StrUtil.isNotBlank(message.getAtUserIds())) { + if (CollUtil.isNotEmpty(message.getAtUserIds())) { event.put("atUserIds", message.getAtUserIds()); } - if (StrUtil.isNotBlank(message.getReceiverUserIds())) { + if (CollUtil.isNotEmpty(message.getReceiverUserIds())) { event.put("receiverUserIds", message.getReceiverUserIds()); } event.put("receiptStatus", message.getReceiptStatus()); // 确定推送目标 - Set receiverSet = null; - if (StrUtil.isNotBlank(message.getReceiverUserIds())) { - receiverSet = new HashSet<>(Arrays.asList(message.getReceiverUserIds().split(","))); - } + List receiverList = message.getReceiverUserIds(); for (ImGroupMemberDO member : members) { if (CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) { continue; } // 定向接收过滤 - if (receiverSet != null && !receiverSet.contains(member.getUserId().toString()) + if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId()) && !member.getUserId().equals(message.getSenderId())) { continue; } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java index f159f5d..9f4f2f7 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java +++ b/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 { } // 2. 构建并保存消息 - // DONE @AI:相同类型的字段,放在一行。结论:当前 builder 已经分行写,可读性好,保持即可 - // TODO @AI:按照我说的! ImPrivateMessageDO message = ImPrivateMessageDO.builder() .clientMessageId(reqVO.getClientMessageId()) - .senderId(senderId) - .receiverId(reqVO.getReceiverId()) - .type(reqVO.getType()) - .content(reqVO.getContent()) + .senderId(senderId).receiverId(reqVO.getReceiverId()) + .type(reqVO.getType()).content(reqVO.getContent()) .status(ImMessageStatusEnum.UNREAD.getStatus()) .sendTime(LocalDateTime.now()) .build(); @@ -91,7 +87,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { return imPrivateMessageMapper.selectListByMinId(userId, minId, size); } - // TODO @芋艿:这里需要多 review 下; + // DONE @芋艿:已 review,逻辑正确 @Override public void readMessages(Long userId, Long friendId) { // 1. 批量更新消息状态为已读 @@ -116,7 +112,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { ImWebSocketTypeConstants.RECEIPT, receiptEvent); } - // TODO @芋艿:这里需要多 review 下; + // DONE @芋艿:已 review,逻辑正确 @Override public void recallMessage(Long userId, Long messageId) { // 1.1 校验消息存在 diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java index d778bd1..52d614e 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; +import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; -// TODO @AI:需要内存化(加载到内存缓存,避免每次查库);优先级低,后续优化 +// DONE @AI:已实现内存化,启动时加载 + 定时刷新 /** * IM 敏感词 Service 实现类 * @@ -19,21 +24,49 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSIT */ @Service @Validated +@Slf4j public class ImSensitiveWordServiceImpl implements ImSensitiveWordService { @Resource private ImSensitiveWordMapper imSensitiveWordMapper; + /** + * 内存缓存的敏感词列表 + */ + private volatile List sensitiveWords = Collections.emptyList(); + + /** + * 初始化:启动时加载敏感词到内存 + */ + @PostConstruct + public void init() { + loadSensitiveWords(); + } + + /** + * 定时刷新:每 5 分钟重新加载一次 + */ + @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 5 * 60 * 1000) + public void refresh() { + loadSensitiveWords(); + } + + private void loadSensitiveWords() { + List enabledWords = imSensitiveWordMapper.selectListByStatus(); + sensitiveWords = enabledWords.stream() + .map(ImSensitiveWordDO::getWord) + .collect(Collectors.toList()); + log.info("[loadSensitiveWords][加载 IM 敏感词 {} 个]", sensitiveWords.size()); + } + @Override public void validateText(String text) { if (text == null || text.isEmpty()) { return; } - // 查询所有启用状态的敏感词 - List enabledWords = imSensitiveWordMapper.selectListByStatus(); - // 逐词匹配 - for (ImSensitiveWordDO wordDO : enabledWords) { - if (text.contains(wordDO.getWord())) { + // 内存匹配 + for (String word : sensitiveWords) { + if (text.contains(word)) { throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED); } }