From 2ce3a99318ebf726c593d32d6d33d5b8562b479e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 20 Apr 2026 10:03:35 +0800 Subject: [PATCH] ✨ feat(im): 优化消息撤回的逻辑 --- yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java | 1 + yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java | 20 ++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java | 18 ++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java | 3 ++- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java | 49 +++++++++++++++++++++++++++---------------------- 5 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java index 08c2806..e6faccd 100644 --- a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java +++ b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java @@ -20,6 +20,7 @@ public interface ErrorCodeConstants { ErrorCode MESSAGE_RECEIVER_NOT_EXISTS = new ErrorCode(1_040_300_001, "接收人不存在"); ErrorCode MESSAGE_RECALL_DENIED = new ErrorCode(1_040_300_002, "只能撤回自己发送的消息"); ErrorCode MESSAGE_ALREADY_RECALLED = new ErrorCode(1_040_300_003, "消息已撤回"); + ErrorCode MESSAGE_RECALL_TIMEOUT = new ErrorCode(1_040_300_007, "超过 {} 分钟的消息无法撤回"); ErrorCode MESSAGE_SENSITIVE_WORD_BLOCKED = new ErrorCode(1_040_300_004, "消息包含敏感词,无法发送"); ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量超出限制"); ErrorCode MESSAGE_CLIENT_ID_DUPLICATE = new ErrorCode(1_040_300_006, "消息已发送,请勿重复提交"); diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java new file mode 100644 index 0000000..ae4214d --- /dev/null +++ b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.im.enums; + +/** + * IM 通用常量 + * + * @author 芋道源码 + */ +public interface ImCommonConstants { + + /** + * pull 最大拉取数量 + */ + int MESSAGE_MAX_PULL_SIZE = 1000; + + /** + * 消息撤回时间限制(分钟) + */ + int MESSAGE_RECALL_TIMEOUT_MINUTES = 5; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java new file mode 100644 index 0000000..a9ba575 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.im.dal.dataobject.message.content; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 撤回提示消息内容 + */ +@Data +@Accessors(chain = true) +public class RecallMessage { + + /** + * 被撤回的原消息编号 + */ + private Long messageId; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java index eedbf0f..5e3ece9 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java @@ -44,7 +44,8 @@ public interface ImPrivateMessageService { * * @param userId 当前用户编号 * @param messageId 消息编号 + * @return 撤回后的消息 */ - void recallPrivateMessage(Long userId, Long messageId); + ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId); } 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 c87c664..de9c2b6 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 @@ -1,20 +1,23 @@ package cn.iocoder.yudao.module.im.service.message; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage; import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageSceneEnum; +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage; -import cn.iocoder.yudao.module.im.websocket.ImMessageRecallMessage; import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage; import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage; import jakarta.annotation.Resource; @@ -29,6 +32,8 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; /** * IM 私聊消息 Service 实现类 @@ -40,11 +45,6 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; @Slf4j public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { - /** - * pull 最大拉取数量 - */ - private static final int MAX_PULL_SIZE = 1000; - @Resource private ImPrivateMessageMapper privateMessageMapper; @@ -85,7 +85,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @Override public List pullPrivateMessageList(Long userId, Long minId, Integer size) { - if (size > MAX_PULL_SIZE) { + if (size > MESSAGE_MAX_PULL_SIZE) { throw exception(MESSAGE_PULL_SIZE_EXCEEDED); } List messages = privateMessageMapper.selectListByMinId(userId, minId, size); @@ -111,37 +111,42 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { getSelf().readPrivateMessageEvent(userId, receiverId); } - // DONE @芋艿:已 review,逻辑正确 @Override - public void recallPrivateMessage(Long userId, Long messageId) { + public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) { // 1.1 校验消息存在 ImPrivateMessageDO message = privateMessageMapper.selectById(messageId); if (message == null) { throw exception(MESSAGE_NOT_EXISTS); } // 1.2 只能撤回自己发送的消息 - // DONE @AI:Notequals。结论:这里使用 !equals() 是正确的,ObjUtil.notEqual 也可以但无必要引入 - if (!message.getSenderId().equals(userId)) { + if (ObjUtil.notEqual(message.getSenderId(), userId)) { throw exception(MESSAGE_RECALL_DENIED); } // 1.3 不能重复撤回 if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { throw exception(MESSAGE_ALREADY_RECALLED); } + // 1.4 只允许撤回限定时间内的消息 + if (message.getSendTime().plusMinutes(MESSAGE_RECALL_TIMEOUT_MINUTES).isBefore(LocalDateTime.now())) { + throw exception(MESSAGE_RECALL_TIMEOUT, MESSAGE_RECALL_TIMEOUT_MINUTES); + } - // 2. 更新消息状态为撤回 + // 2. 更新原消息状态为撤回 privateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId) .setStatus(ImMessageStatusEnum.RECALL.getStatus())); - // 3. 发送 RECALL 事件 - ImMessageRecallMessage recallMessage = new ImMessageRecallMessage() - .setMessageId(messageId).setSenderId(userId).setScene(ImMessageSceneEnum.PRIVATE.getScene()); - // 3.1 通知接收方 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), - ImMessageRecallMessage.TYPE, recallMessage); - // 3.2 通知发送方自己的其他终端 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, - ImMessageRecallMessage.TYPE, recallMessage); + // 3. 插入一条 TIP_TEXT 消息作为撤回提示 + RecallMessage recallContent = new RecallMessage() + .setMessageId(messageId); + ImPrivateMessageDO tipMessage = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID()) + .setSenderId(userId).setReceiverId(message.getReceiverId()) + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(JsonUtils.toJsonString(recallContent)) + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()); + privateMessageMapper.insert(tipMessage); + + // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示) + getSelf().sendPrivateMessageEvent(tipMessage); + return tipMessage; } /** -- libgit2 0.21.4