Commit 28cf87ac8afff15d1e362152e8aa65dfca1f7a04

Authored by YunaiV
1 parent 0d7cb763

feat(im): 重构普通消息类型,和 openim 的消息编号对齐

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
... ... @@ -54,15 +54,4 @@ public interface ImCommonConstants {
54 54 */
55 55 int MESSAGE_GROUP_PULL_MAX_DAYS = 30;
56 56  
57   - // ==================== 好友相关提示消息 ====================
58   -
59   - /**
60   - * 好友添加系统提示
61   - */
62   - String FRIEND_ADD_TIP_MESSAGE = "你们已成为好友,现在可以开始聊天了";
63   - /**
64   - * 好友删除系统提示(仅自己可见,单边推送)
65   - */
66   - String FRIEND_DELETE_TIP_MESSAGE = "你已删除好友";
67   -
68 57 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
... ... @@ -17,20 +17,49 @@ import java.util.Arrays;
17 17 @RequiredArgsConstructor
18 18 public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
19 19  
20   - // ========== 用户聊天消息 ==========
21   - TEXT(0, "文本", true, true), // 对应 TextMessage 类
22   - IMAGE(1, "图片", true, true), // 对应 ImageMessage 类
23   - FILE(2, "文件", true, true), // 对应 FileMessage 类
24   - VOICE(3, "语音", true, true), // 对应 AudioMessage 类
25   - VIDEO(4, "视频", true, true), // 对应 VideoMessage 类
26   -
27   - // ========== 信号类 ==========
28   - RECALL(10, "撤回", true, false), // 对应 RecallMessage 类
29   - READ(11, "已读", false, false), // 暂无
30   - RECEIPT(12, "回执", false, false), // 暂无
  20 + // ========== 用户聊天消息(101-105 直接复用 OpenIM 段位编号) ==========
  21 + /**
  22 + * 对应 OpenIM:Text 101
  23 + * 对应自己的类:TextMessage
  24 + */
  25 + TEXT(101, "文本", true, true),
  26 + /**
  27 + * 对应 OpenIM:Picture 102
  28 + * 对应自己的类:ImageMessage
  29 + */
  30 + IMAGE(102, "图片", true, true),
  31 + /**
  32 + * 对应 OpenIM:Sound 103
  33 + * 对应自己的类:AudioMessage
  34 + */
  35 + VOICE(103, "语音", true, true),
  36 + /**
  37 + * 对应 OpenIM:Video 104
  38 + * 对应自己的类:VideoMessage
  39 + */
  40 + VIDEO(104, "视频", true, true),
  41 + /**
  42 + * 对应 OpenIM:File 105
  43 + * 对应自己的类:FileMessage
  44 + */
  45 + FILE(105, "文件", true, true),
31 46  
32   - // ========== 系统提示 ==========
33   - TIP_TEXT(21, "系统提示", true, false), // 对应 TextMessage 类
  47 + // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ==========
  48 + /**
  49 + * 对应 OpenIM:RevokeNotification 2101
  50 + * 对应自己的类:RecallMessage
  51 + */
  52 + RECALL(2101, "撤回", true, false),
  53 + /**
  54 + * 对应 OpenIM:HasReadReceipt 2200
  55 + * 对应自己的类:无(payload 走 ImXxxMessageDTO 顶层字段)
  56 + */
  57 + RECEIPT(2200, "回执", false, false),
  58 + /**
  59 + * 对应 OpenIM:无(自有扩展,OpenIM 走 ConversationChangeNotification 1300 路径)
  60 + * 对应自己的类:无(payload 走 ImXxxMessageDTO 顶层字段)
  61 + */
  62 + READ(2201, "已读", false, false),
34 63  
35 64 // ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
36 65 /**
... ... @@ -52,11 +81,12 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
52 81 */
53 82 FRIEND_REQUEST_RECEIVED(1203, "收到新的好友申请", false, false),
54 83 /**
55   - * 对应 OpenIM:FriendAddedNotification 1204
  84 + * 对应 OpenIM:FriendAddedNotification 1204(OpenIM friendAdded.isSendMsg=false 默认不入消息流;本系统改为入库当会话气泡)
56 85 * 对应自己的类:FriendAddNotification
57   - * 场景:双方建立好友关系(同意申请 / 管理员导入),推给 A、B 双方多端
  86 + * 场景:双方建立好友关系,单条入库(sender=fromUserId, receiver=toUserId);双向 WebSocket 自动覆盖双方多端
  87 + * 注意:silentReAddFriend 单边语义场景,发送时显式 setPersistent(false) 覆盖默认值
58 88 */
59   - FRIEND_ADD(1204, "新增好友", false, false),
  89 + FRIEND_ADD(1204, "新增好友", true, false),
60 90 /**
61 91 * 对应 OpenIM:FriendDeletedNotification 1205
62 92 * 对应自己的类:FriendDeleteNotification
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
... ... @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
11 11 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
12 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
13 13 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  14 +import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
14 15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
15 16 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
16 17 import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.*;
... ... @@ -32,8 +33,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
32 33 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND_STATE;
33 34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED;
34 35 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
35   -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE;
36   -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE;
37 36  
38 37 /**
39 38 * IM 好友关系 Service 实现类
... ... @@ -130,19 +129,12 @@ public class ImFriendServiceImpl implements ImFriendService {
130 129 addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
131 130 addFriend0(toUserId, fromUserId, null, request.getAddSource());
132 131  
133   - // 2.1 推 TIP 系统消息(双方私聊会话里看到「你们已成为好友」);TIP 走会话入库,FRIEND_ADD 走事件通知
134   - privateMessageService.sendTipPrivateMessage(fromUserId, toUserId, FRIEND_ADD_TIP_MESSAGE);
135   -
136   - // 2.2.1 推 FRIEND_ADD 给 fromUser 多端
137   - FriendAddNotification toFrom = (FriendAddNotification) new FriendAddNotification()
138   - .setOperatorUserId(toUserId).setFriendUserId(toUserId);
139   - websocketService.sendPrivateMessageAsync(fromUserId, ImPrivateMessageDTO.ofFriendNotification(
140   - ImMessageTypeEnum.FRIEND_ADD.getType(), toUserId, fromUserId, toFrom));
141   - // 2.2.2 推 FRIEND_ADD 给 toUser 多端
142   - FriendAddNotification toTo = (FriendAddNotification) new FriendAddNotification()
143   - .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
144   - websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
145   - ImMessageTypeEnum.FRIEND_ADD.getType(), fromUserId, toUserId, toTo));
  132 + // 2. 发送 FRIEND_ADD 入库(双方拉历史都能看到「你们已成为好友」会话气泡)+ 双向 WebSocket 自动覆盖双方多端
  133 + // operatorUserId=fromUserId 标记申请发起方;前端按 (currentUserId === operatorUserId) 区分视角,文案固定不依赖此字段
  134 + FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
  135 + .setOperatorUserId(fromUserId).setFriendUserId(toUserId);
  136 + privateMessageService.sendPrivateMessage(fromUserId, new ImPrivateMessageSendDTO()
  137 + .setReceiverId(toUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType()).setContent(payload));
146 138 }
147 139  
148 140 @Override
... ... @@ -155,15 +147,14 @@ public class ImFriendServiceImpl implements ImFriendService {
155 147 // 1. 单边重新启用我侧好友关系
156 148 addFriend0(userId, friendUserId, displayName, addSource);
157 149  
158   - // 2. 推 TIP「你们已成为好友」走单边语义(persistent=false):不入库 + 仅推 userId 多端,对方完全不感知
159   - privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_ADD_TIP_MESSAGE, false);
160   -
161   - // 3. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉)
162   - // operatorUserId 填 friendUserId(对方):让 userId 多端 UI 呈现「对方加了我」的视觉效果,与 silent 语义对齐
  150 + // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(对方完全不感知,保持「对方一直把我当好友」错觉)
  151 + // operatorUserId 填 friendUserId(对方):让 userId 多端 UI 呈现「对方加了我」视角,与 silent 语义对齐
  152 + // 前端按 type=FRIEND_ADD 渲染会话气泡(瞬时,不入库刷新即消失)
163 153 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
164 154 .setOperatorUserId(friendUserId).setFriendUserId(friendUserId);
165   - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
166   - ImMessageTypeEnum.FRIEND_ADD.getType(), friendUserId, userId, payload));
  155 + privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
  156 + .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType())
  157 + .setContent(payload).setPersistent(false));
167 158 }
168 159  
169 160 @Override
... ... @@ -176,17 +167,14 @@ public class ImFriendServiceImpl implements ImFriendService {
176 167 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
177 168 deleteFriend0(userId, friendUserId);
178 169  
179   - // 2. 仅 clear=false 时推 TIP「你已删除好友」(单边 persistent=false);
180   - // clear=true / null 时跳过 —— 避免 TIP 晚于 FRIEND_DELETE 到达把刚清的会话复活
181   - if (Boolean.FALSE.equals(clear)) {
182   - privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE, false);
183   - }
184   -
185   - // 3. 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知);clear 透传让多端清理动作一致
  170 + // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(friendUserId 不感知);clear 透传让多端清理动作一致
  171 + // clear=false 时前端按 type=FRIEND_DELETE 渲染「你已删除好友」会话气泡(瞬时);
  172 + // clear=true 时前端按 clear 字段直接清会话,跳过气泡渲染
186 173 FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification()
187 174 .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear);
188   - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
189   - ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload));
  175 + privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
  176 + .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_DELETE.getType())
  177 + .setContent(payload).setPersistent(false));
190 178 }
191 179  
192 180 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
... ... @@ -38,25 +38,6 @@ public interface ImPrivateMessageService {
38 38 ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendDTO dto);
39 39  
40 40 /**
41   - * 【系统调用】发送系统提示消息(TIP_TEXT)
42   - *
43   - * @param senderId 发送人编号
44   - * @param receiverId 接收人编号
45   - * @param content 提示文本
46   - */
47   - void sendTipPrivateMessage(Long senderId, Long receiverId, String content);
48   -
49   - /**
50   - * 【系统调用】发送系统提示消息(TIP_TEXT),可覆盖 persistent
51   - *
52   - * @param senderId 发送人编号
53   - * @param receiverId 接收人编号
54   - * @param content 提示文本
55   - * @param persistent null 走默认;false 单边(不入库 + 仅推 sender 多端,对方不感知);true 强制双向 + 入库
56   - */
57   - void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent);
58   -
59   - /**
60 41 * 【用户调用】撤回私聊消息
61 42 *
62 43 * @param userId 当前用户编号
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -123,17 +123,6 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
123 123 }
124 124  
125 125 @Override
126   - public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) {
127   - sendTipPrivateMessage(senderId, receiverId, content, null);
128   - }
129   -
130   - @Override
131   - public void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent) {
132   - sendPrivateMessage(senderId, new ImPrivateMessageSendDTO().setReceiverId(receiverId)
133   - .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content).setPersistent(persistent));
134   - }
135   -
136   - @Override
137 126 @Transactional(rollbackFor = Exception.class)
138 127 public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) {
139 128 // 1.1 校验消息存在
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
... ... @@ -178,10 +178,8 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
178 178 verify(adminUserApi).validateUser(2L);
179 179 // 断言:双向插入 2 条好友关系
180 180 verify(imFriendMapper, times(2)).insert(any(ImFriendDO.class));
181   - // 断言:发送提示消息 + 给双方各推送一次好友添加事件
182   - verify(privateMessageService).sendTipPrivateMessage(eq(1L), eq(2L), anyString());
183   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
184   - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
  181 + // 断言:FRIEND_ADD 单条入库(双向 WebSocket 推送由 sendPrivateMessage 内部完成,mock 下不可见)
  182 + verify(privateMessageService).sendPrivateMessage(eq(1L), any());
185 183 }
186 184 }
187 185  
... ... @@ -250,10 +248,8 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
250 248  
251 249 // 断言:双向更新为 DISABLE
252 250 verify(imFriendMapper, times(2)).updateById(any(ImFriendDO.class));
253   - // 断言:推送提示消息 + 双方各推送一次好友删除事件
254   - verify(privateMessageService).sendTipPrivateMessage(eq(1L), eq(2L), anyString());
  251 + // 断言:仅给 userId 多端推送 FRIEND_DELETE 事件(friendUserId 不感知)
255 252 verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
256   - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
257 253 }
258 254 }
259 255  
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
... ... @@ -562,9 +562,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
562 562 // 调用
563 563 ImGroupMessageDO result = groupMessageService.recallGroupMessage(1L, 50L);
564 564  
565   - // 断言:返回 TIP_TEXT 消息
  565 + // 断言:返回撤回消息
566 566 assertNotNull(result);
567   - // 验证:更新原消息状态 + 插入 tipMessage
  567 + // 验证:更新原消息状态 + 插入 RecallMessage
568 568 verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class));
569 569 verify(groupMessageMapper).insert(any(ImGroupMessageDO.class));
570 570 // 验证:给 2 个活跃成员推送撤回提示
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
... ... @@ -206,9 +206,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
206 206 // 调用
207 207 ImPrivateMessageDO result = privateMessageService.recallPrivateMessage(1L, 10L);
208 208  
209   - // 断言:返回 TIP_TEXT 消息
  209 + // 断言:返回撤回消息
210 210 assertNotNull(result);
211   - // 验证:更新原消息状态 + 插入 tipMessage
  211 + // 验证:更新原消息状态 + 插入 RecallMessage
212 212 verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class));
213 213 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class));
214 214 // 验证推送了消息(给接收方和发送方)
... ... @@ -375,29 +375,6 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
375 375 verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
376 376 }
377 377  
378   - // ========== sendTipPrivateMessage ==========
379   -
380   - @Test
381   - public void testSendTipPrivateMessage_success() {
382   - // 调用
383   - privateMessageService.sendTipPrivateMessage(1L, 2L, "好友添加成功");
384   -
385   - // 断言:插入一条 TIP_TEXT 提示消息
386   - ArgumentCaptor<ImPrivateMessageDO> captor = ArgumentCaptor.forClass(ImPrivateMessageDO.class);
387   - verify(privateMessageMapper).insert(captor.capture());
388   - ImPrivateMessageDO tip = captor.getValue();
389   - assertEquals(1L, tip.getSenderId());
390   - assertEquals(2L, tip.getReceiverId());
391   - assertEquals(ImMessageTypeEnum.TIP_TEXT.getType(), tip.getType());
392   - assertEquals("好友添加成功", tip.getContent());
393   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), tip.getStatus());
394   - assertNotNull(tip.getClientMessageId());
395   - assertNotNull(tip.getSendTime());
396   - // 断言:给双方各推送一次
397   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
398   - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
399   - }
400   -
401 378 // ========== getPrivateMessageList ==========
402 379  
403 380 @Test
... ...