Commit 2ce3a99318ebf726c593d32d6d33d5b8562b479e

Authored by YunaiV
1 parent 04ea1c9d

✨ feat(im): 优化消息撤回的逻辑

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 { @@ -20,6 +20,7 @@ public interface ErrorCodeConstants {
20 ErrorCode MESSAGE_RECEIVER_NOT_EXISTS = new ErrorCode(1_040_300_001, "接收人不存在"); 20 ErrorCode MESSAGE_RECEIVER_NOT_EXISTS = new ErrorCode(1_040_300_001, "接收人不存在");
21 ErrorCode MESSAGE_RECALL_DENIED = new ErrorCode(1_040_300_002, "只能撤回自己发送的消息"); 21 ErrorCode MESSAGE_RECALL_DENIED = new ErrorCode(1_040_300_002, "只能撤回自己发送的消息");
22 ErrorCode MESSAGE_ALREADY_RECALLED = new ErrorCode(1_040_300_003, "消息已撤回"); 22 ErrorCode MESSAGE_ALREADY_RECALLED = new ErrorCode(1_040_300_003, "消息已撤回");
  23 + ErrorCode MESSAGE_RECALL_TIMEOUT = new ErrorCode(1_040_300_007, "超过 {} 分钟的消息无法撤回");
23 ErrorCode MESSAGE_SENSITIVE_WORD_BLOCKED = new ErrorCode(1_040_300_004, "消息包含敏感词,无法发送"); 24 ErrorCode MESSAGE_SENSITIVE_WORD_BLOCKED = new ErrorCode(1_040_300_004, "消息包含敏感词,无法发送");
24 ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量超出限制"); 25 ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量超出限制");
25 ErrorCode MESSAGE_CLIENT_ID_DUPLICATE = new ErrorCode(1_040_300_006, "消息已发送,请勿重复提交"); 26 ErrorCode MESSAGE_CLIENT_ID_DUPLICATE = new ErrorCode(1_040_300_006, "消息已发送,请勿重复提交");
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.enums;
  2 +
  3 +/**
  4 + * IM 通用常量
  5 + *
  6 + * @author 芋道源码
  7 + */
  8 +public interface ImCommonConstants {
  9 +
  10 + /**
  11 + * pull 最大拉取数量
  12 + */
  13 + int MESSAGE_MAX_PULL_SIZE = 1000;
  14 +
  15 + /**
  16 + * 消息撤回时间限制(分钟)
  17 + */
  18 + int MESSAGE_RECALL_TIMEOUT_MINUTES = 5;
  19 +
  20 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * 撤回提示消息内容
  8 + */
  9 +@Data
  10 +@Accessors(chain = true)
  11 +public class RecallMessage {
  12 +
  13 + /**
  14 + * 被撤回的原消息编号
  15 + */
  16 + private Long messageId;
  17 +
  18 +}
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 { @@ -44,7 +44,8 @@ public interface ImPrivateMessageService {
44 * 44 *
45 * @param userId 当前用户编号 45 * @param userId 当前用户编号
46 * @param messageId 消息编号 46 * @param messageId 消息编号
  47 + * @return 撤回后的消息
47 */ 48 */
48 - void recallPrivateMessage(Long userId, Long messageId); 49 + ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId);
49 50
50 } 51 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.ObjUtil;
4 import cn.hutool.extra.spring.SpringUtil; 6 import cn.hutool.extra.spring.SpringUtil;
5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 7 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  8 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; 10 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
8 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 11 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
9 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 12 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  13 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
10 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 14 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
11 -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;  
12 import cn.iocoder.yudao.module.im.enums.message.ImMessageSceneEnum; 15 import cn.iocoder.yudao.module.im.enums.message.ImMessageSceneEnum;
  16 +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
13 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
14 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 18 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
15 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 19 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
16 import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage; 20 import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage;
17 -import cn.iocoder.yudao.module.im.websocket.ImMessageRecallMessage;  
18 import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage; 21 import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage;
19 import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage; 22 import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage;
20 import jakarta.annotation.Resource; 23 import jakarta.annotation.Resource;
@@ -29,6 +32,8 @@ import java.util.List; @@ -29,6 +32,8 @@ import java.util.List;
29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
30 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 33 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
31 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
  35 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE;
  36 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES;
32 37
33 /** 38 /**
34 * IM 私聊消息 Service 实现类 39 * IM 私聊消息 Service 实现类
@@ -40,11 +45,6 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; @@ -40,11 +45,6 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
40 @Slf4j 45 @Slf4j
41 public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { 46 public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
42 47
43 - /**  
44 - * pull 最大拉取数量  
45 - */  
46 - private static final int MAX_PULL_SIZE = 1000;  
47 -  
48 @Resource 48 @Resource
49 private ImPrivateMessageMapper privateMessageMapper; 49 private ImPrivateMessageMapper privateMessageMapper;
50 50
@@ -85,7 +85,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -85,7 +85,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
85 85
86 @Override 86 @Override
87 public List<ImPrivateMessageDO> pullPrivateMessageList(Long userId, Long minId, Integer size) { 87 public List<ImPrivateMessageDO> pullPrivateMessageList(Long userId, Long minId, Integer size) {
88 - if (size > MAX_PULL_SIZE) { 88 + if (size > MESSAGE_MAX_PULL_SIZE) {
89 throw exception(MESSAGE_PULL_SIZE_EXCEEDED); 89 throw exception(MESSAGE_PULL_SIZE_EXCEEDED);
90 } 90 }
91 List<ImPrivateMessageDO> messages = privateMessageMapper.selectListByMinId(userId, minId, size); 91 List<ImPrivateMessageDO> messages = privateMessageMapper.selectListByMinId(userId, minId, size);
@@ -111,37 +111,42 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -111,37 +111,42 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
111 getSelf().readPrivateMessageEvent(userId, receiverId); 111 getSelf().readPrivateMessageEvent(userId, receiverId);
112 } 112 }
113 113
114 - // DONE @芋艿:已 review,逻辑正确  
115 @Override 114 @Override
116 - public void recallPrivateMessage(Long userId, Long messageId) { 115 + public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) {
117 // 1.1 校验消息存在 116 // 1.1 校验消息存在
118 ImPrivateMessageDO message = privateMessageMapper.selectById(messageId); 117 ImPrivateMessageDO message = privateMessageMapper.selectById(messageId);
119 if (message == null) { 118 if (message == null) {
120 throw exception(MESSAGE_NOT_EXISTS); 119 throw exception(MESSAGE_NOT_EXISTS);
121 } 120 }
122 // 1.2 只能撤回自己发送的消息 121 // 1.2 只能撤回自己发送的消息
123 - // DONE @AI:Notequals。结论:这里使用 !equals() 是正确的,ObjUtil.notEqual 也可以但无必要引入  
124 - if (!message.getSenderId().equals(userId)) { 122 + if (ObjUtil.notEqual(message.getSenderId(), userId)) {
125 throw exception(MESSAGE_RECALL_DENIED); 123 throw exception(MESSAGE_RECALL_DENIED);
126 } 124 }
127 // 1.3 不能重复撤回 125 // 1.3 不能重复撤回
128 if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { 126 if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) {
129 throw exception(MESSAGE_ALREADY_RECALLED); 127 throw exception(MESSAGE_ALREADY_RECALLED);
130 } 128 }
  129 + // 1.4 只允许撤回限定时间内的消息
  130 + if (message.getSendTime().plusMinutes(MESSAGE_RECALL_TIMEOUT_MINUTES).isBefore(LocalDateTime.now())) {
  131 + throw exception(MESSAGE_RECALL_TIMEOUT, MESSAGE_RECALL_TIMEOUT_MINUTES);
  132 + }
131 133
132 - // 2. 更新消息状态为撤回 134 + // 2. 更新原消息状态为撤回
133 privateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId) 135 privateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId)
134 .setStatus(ImMessageStatusEnum.RECALL.getStatus())); 136 .setStatus(ImMessageStatusEnum.RECALL.getStatus()));
135 137
136 - // 3. 发送 RECALL 事件  
137 - ImMessageRecallMessage recallMessage = new ImMessageRecallMessage()  
138 - .setMessageId(messageId).setSenderId(userId).setScene(ImMessageSceneEnum.PRIVATE.getScene());  
139 - // 3.1 通知接收方  
140 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(),  
141 - ImMessageRecallMessage.TYPE, recallMessage);  
142 - // 3.2 通知发送方自己的其他终端  
143 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,  
144 - ImMessageRecallMessage.TYPE, recallMessage); 138 + // 3. 插入一条 TIP_TEXT 消息作为撤回提示
  139 + RecallMessage recallContent = new RecallMessage()
  140 + .setMessageId(messageId);
  141 + ImPrivateMessageDO tipMessage = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
  142 + .setSenderId(userId).setReceiverId(message.getReceiverId())
  143 + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(JsonUtils.toJsonString(recallContent))
  144 + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now());
  145 + privateMessageMapper.insert(tipMessage);
  146 +
  147 + // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示)
  148 + getSelf().sendPrivateMessageEvent(tipMessage);
  149 + return tipMessage;
145 } 150 }
146 151
147 /** 152 /**