Commit 3e38e77ee6f68da490e27f86111ecbbc865bc809

Authored by YunaiV
1 parent 3dd71793

refactor: 扁平化 IM WebSocket 通知推送 API

- 将 WebSocket 推送入口统一为 userId/userIds + conversationType + contentType + payload
- 移除业务侧 ImNotificationWebSocketDTO 构造和无会话专用发送入口
- 收敛私聊、群聊、频道、好友、加群申请、RTC 通知调用路径
- 精简 ImNotificationWebSocketDTO,仅保留统一外壳字段
- 保留群消息 payload 的 receiptStatus、readCount、receiverUserIds
- 更新相关单元测试,覆盖群消息通知 payload 字段
Showing 18 changed files with 221 additions and 185 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java
@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.im.mq.consumer.friend; @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.im.mq.consumer.friend;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
5 -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 5 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  6 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
6 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 7 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
7 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 8 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
8 -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;  
9 -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendInfoUpdatedNotification; 9 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendInfoUpdatedNotification;
10 import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage; 10 import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage;
11 import jakarta.annotation.Resource; 11 import jakarta.annotation.Resource;
12 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
@@ -52,8 +52,9 @@ public class AdminUserProfileUpdateConsumer { @@ -52,8 +52,9 @@ public class AdminUserProfileUpdateConsumer {
52 try { 52 try {
53 FriendInfoUpdatedNotification payload = (FriendInfoUpdatedNotification) new FriendInfoUpdatedNotification() 53 FriendInfoUpdatedNotification payload = (FriendInfoUpdatedNotification) new FriendInfoUpdatedNotification()
54 .setOperatorUserId(userId).setFriendUserId(userId); 54 .setOperatorUserId(userId).setFriendUserId(userId);
55 - websocketService.sendPrivateMessageAsync(friend.getFriendUserId(), ImPrivateMessageDTO.ofFriendNotification(  
56 - ImMessageTypeEnum.FRIEND_INFO_UPDATED.getType(), userId, friend.getFriendUserId(), payload)); 55 + websocketService.sendNotificationAsync(friend.getFriendUserId(),
  56 + ImConversationTypeEnum.NONE.getType(),
  57 + ImContentTypeEnum.FRIEND_INFO_UPDATED.getType(), payload);
57 successCount++; 58 successCount++;
58 } catch (Exception e) { 59 } catch (Exception e) {
59 log.warn("[onMessage][userId({}) friendUserId({}) 推送失败]", 60 log.warn("[onMessage][userId({}) friendUserId({}) 推送失败]",
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
@@ -14,7 +14,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; @@ -14,7 +14,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
14 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper; 14 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
15 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum; 15 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
16 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum; 16 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
17 -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 17 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  18 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
18 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
20 import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestApprovedNotification; 21 import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestApprovedNotification;
@@ -104,8 +105,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -104,8 +105,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
104 if (fromUser != null) { 105 if (fromUser != null) {
105 payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar()); 106 payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar());
106 } 107 }
107 - websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(  
108 - ImMessageTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), fromUserId, toUserId, payload)); 108 + websocketService.sendNotificationAsync(toUserId, ImConversationTypeEnum.NONE.getType(),
  109 + ImContentTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), payload);
109 110
110 // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程 111 // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程
111 // 回调内 try/catch 兜底 —— afterCommit 异常会被 Spring 静默吞掉,否则同意失败时申请方永远等不到 APPROVED 112 // 回调内 try/catch 兜底 —— afterCommit 异常会被 Spring 静默吞掉,否则同意失败时申请方永远等不到 APPROVED
@@ -191,8 +192,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -191,8 +192,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
191 FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification) 192 FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification)
192 new FriendRequestApprovedNotification().setRequestId(request.getId()) 193 new FriendRequestApprovedNotification().setRequestId(request.getId())
193 .setOperatorUserId(userId).setFriendUserId(userId); 194 .setOperatorUserId(userId).setFriendUserId(userId);
194 - websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(  
195 - ImMessageTypeEnum.FRIEND_REQUEST_APPROVED.getType(), userId, request.getFromUserId(), payload)); 195 + websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
  196 + ImContentTypeEnum.FRIEND_REQUEST_APPROVED.getType(), payload);
196 } 197 }
197 198
198 @Override 199 @Override
@@ -216,8 +217,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -216,8 +217,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
216 new FriendRequestRejectedNotification().setRequestId(request.getId()) 217 new FriendRequestRejectedNotification().setRequestId(request.getId())
217 .setHandleContent(handleContent) 218 .setHandleContent(handleContent)
218 .setOperatorUserId(userId).setFriendUserId(userId); 219 .setOperatorUserId(userId).setFriendUserId(userId);
219 - websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(  
220 - ImMessageTypeEnum.FRIEND_REQUEST_REJECTED.getType(), userId, request.getFromUserId(), payload)); 220 + websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
  221 + ImContentTypeEnum.FRIEND_REQUEST_REJECTED.getType(), payload);
221 } 222 }
222 223
223 @Override 224 @Override
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
@@ -11,12 +11,12 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; @@ -11,12 +11,12 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
11 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; 11 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
12 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; 12 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
13 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum; 13 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
14 -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 14 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  15 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
15 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; 16 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
16 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO; 17 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
17 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 18 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
18 -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;  
19 -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.*; 19 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.*;
20 import jakarta.annotation.Resource; 20 import jakarta.annotation.Resource;
21 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
22 import org.springframework.cache.annotation.CacheEvict; 22 import org.springframework.cache.annotation.CacheEvict;
@@ -160,8 +160,8 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -160,8 +160,8 @@ public class ImFriendServiceImpl implements ImFriendService {
160 FriendUpdateNotification payload = (FriendUpdateNotification) new FriendUpdateNotification() 160 FriendUpdateNotification payload = (FriendUpdateNotification) new FriendUpdateNotification()
161 .setDisplayName(reqVO.getDisplayName()).setSilent(reqVO.getSilent()).setPinned(reqVO.getPinned()) 161 .setDisplayName(reqVO.getDisplayName()).setSilent(reqVO.getSilent()).setPinned(reqVO.getPinned())
162 .setOperatorUserId(userId).setFriendUserId(reqVO.getFriendUserId()); 162 .setOperatorUserId(userId).setFriendUserId(reqVO.getFriendUserId());
163 - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(  
164 - ImMessageTypeEnum.FRIEND_UPDATE.getType(), userId, userId, payload)); 163 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  164 + ImContentTypeEnum.FRIEND_UPDATE.getType(), payload);
165 } 165 }
166 166
167 @Override 167 @Override
@@ -183,7 +183,7 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -183,7 +183,7 @@ public class ImFriendServiceImpl implements ImFriendService {
183 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification() 183 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
184 .setOperatorUserId(fromUserId).setFriendUserId(toUserId); 184 .setOperatorUserId(fromUserId).setFriendUserId(toUserId);
185 privateMessageService.sendPrivateMessage(fromUserId, new ImPrivateMessageSendDTO() 185 privateMessageService.sendPrivateMessage(fromUserId, new ImPrivateMessageSendDTO()
186 - .setReceiverId(toUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType()).setContent(payload)); 186 + .setReceiverId(toUserId).setType(ImContentTypeEnum.FRIEND_ADD.getType()).setContent(payload));
187 } 187 }
188 188
189 @Override 189 @Override
@@ -202,7 +202,7 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -202,7 +202,7 @@ public class ImFriendServiceImpl implements ImFriendService {
202 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification() 202 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
203 .setOperatorUserId(friendUserId).setFriendUserId(friendUserId); 203 .setOperatorUserId(friendUserId).setFriendUserId(friendUserId);
204 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO() 204 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
205 - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType()) 205 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_ADD.getType())
206 .setContent(payload).setPersistent(false)); 206 .setContent(payload).setPersistent(false));
207 } 207 }
208 208
@@ -224,7 +224,7 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -224,7 +224,7 @@ public class ImFriendServiceImpl implements ImFriendService {
224 FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification() 224 FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification()
225 .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear); 225 .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear);
226 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO() 226 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
227 - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_DELETE.getType()) 227 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_DELETE.getType())
228 .setContent(payload).setPersistent(false)); 228 .setContent(payload).setPersistent(false));
229 } 229 }
230 230
@@ -250,8 +250,8 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -250,8 +250,8 @@ public class ImFriendServiceImpl implements ImFriendService {
250 // 3. 推 FRIEND_BLOCK 给 A 多端 250 // 3. 推 FRIEND_BLOCK 给 A 多端
251 FriendBlockNotification payload = (FriendBlockNotification) new FriendBlockNotification() 251 FriendBlockNotification payload = (FriendBlockNotification) new FriendBlockNotification()
252 .setOperatorUserId(userId).setFriendUserId(friendUserId); 252 .setOperatorUserId(userId).setFriendUserId(friendUserId);
253 - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(  
254 - ImMessageTypeEnum.FRIEND_BLOCK.getType(), userId, userId, payload)); 253 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  254 + ImContentTypeEnum.FRIEND_BLOCK.getType(), payload);
255 } 255 }
256 256
257 @Override 257 @Override
@@ -276,8 +276,8 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -276,8 +276,8 @@ public class ImFriendServiceImpl implements ImFriendService {
276 // 3. 推 FRIEND_UNBLOCK 给 A 多端 276 // 3. 推 FRIEND_UNBLOCK 给 A 多端
277 FriendUnblockNotification payload = (FriendUnblockNotification) new FriendUnblockNotification() 277 FriendUnblockNotification payload = (FriendUnblockNotification) new FriendUnblockNotification()
278 .setOperatorUserId(userId).setFriendUserId(friendUserId); 278 .setOperatorUserId(userId).setFriendUserId(friendUserId);
279 - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(  
280 - ImMessageTypeEnum.FRIEND_UNBLOCK.getType(), userId, userId, payload)); 279 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  280 + ImContentTypeEnum.FRIEND_UNBLOCK.getType(), payload);
281 } 281 }
282 282
283 /** 283 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
@@ -13,7 +13,8 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; @@ -13,7 +13,8 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
13 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
15 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; 15 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
16 -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 16 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  17 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
17 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 18 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; 19 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
@@ -100,8 +101,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -100,8 +101,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
100 AdminUserRespDTO applyUser = adminUserApi.getUser(userId); 101 AdminUserRespDTO applyUser = adminUserApi.getUser(userId);
101 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); 102 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
102 for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) { 103 for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) {
103 - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(  
104 - ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), userId, receiverUserId, payload)); 104 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  105 + ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload);
105 } 106 }
106 return request; 107 return request;
107 } 108 }
@@ -200,8 +201,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -200,8 +201,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
200 AdminUserRespDTO applyUser = userMap.get(request.getUserId()); 201 AdminUserRespDTO applyUser = userMap.get(request.getUserId());
201 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); 202 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
202 for (Long receiverUserId : ownerAndAdmins) { 203 for (Long receiverUserId : ownerAndAdmins) {
203 - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(  
204 - ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), inviterUserId, receiverUserId, payload)); 204 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  205 + ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload);
205 } 206 }
206 } 207 }
207 } 208 }
@@ -405,8 +406,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -405,8 +406,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
405 Set<Long> receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group)); 406 Set<Long> receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group));
406 receivers.add(applicantUserId); 407 receivers.add(applicantUserId);
407 for (Long receiverUserId : receivers) { 408 for (Long receiverUserId : receivers) {
408 - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(  
409 - messageType, operatorUserId, receiverUserId, payload)); 409 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  410 + messageType, payload);
410 } 411 }
411 } 412 }
412 413
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
@@ -97,7 +97,8 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService { @@ -97,7 +97,8 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
97 */ 97 */
98 @Async 98 @Async
99 public void readChannelMessageEvent(Long userId, Long channelId, Long readId) { 99 public void readChannelMessageEvent(Long userId, Long channelId, Long readId) {
100 - webSocketService.sendChannelMessageAsync(userId, ImChannelMessageDTO.ofRead(channelId, readId)); 100 + webSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.CHANNEL.getType(),
  101 + ImContentTypeEnum.READ.getType(), ImMessageReadNotification.ofChannel(channelId, readId));
101 } 102 }
102 103
103 private ImChannelMessageServiceImpl getSelf() { 104 private ImChannelMessageServiceImpl getSelf() {
@@ -121,11 +122,12 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService { @@ -121,11 +122,12 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
121 channelMessageMapper.insert(message); 122 channelMessageMapper.insert(message);
122 123
123 // 3. 异步推 WebSocket:指定用户走点对点;全员(receiverUserIds 为空)走广播 124 // 3. 异步推 WebSocket:指定用户走点对点;全员(receiverUserIds 为空)走广播
124 - ImChannelMessageDTO dto = ImChannelMessageDTO.ofSend(message); 125 + ImChannelMessageNotification dto = ImChannelMessageNotification.ofSend(message);
125 if (CollUtil.isNotEmpty(reqVO.getReceiverUserIds())) { 126 if (CollUtil.isNotEmpty(reqVO.getReceiverUserIds())) {
126 - webSocketService.sendChannelMessageAsync(reqVO.getReceiverUserIds(), dto); 127 + webSocketService.sendNotificationAsync(reqVO.getReceiverUserIds(), ImConversationTypeEnum.CHANNEL.getType(),
  128 + dto.getType(), dto);
127 } else { 129 } else {
128 - webSocketService.broadcastChannelMessageAsync(dto); 130 + webSocketService.broadcastNotificationAsync(ImConversationTypeEnum.CHANNEL.getType(), dto.getType(), dto);
129 } 131 }
130 return message.getId(); 132 return message.getId();
131 } 133 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -118,7 +118,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -118,7 +118,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
118 } 118 }
119 119
120 // 3. WebSocket 异步推送(快照内可见成员 + 发送方多端同步) 120 // 3. WebSocket 异步推送(快照内可见成员 + 发送方多端同步)
121 - imWebSocketService.sendGroupMessageAsync(receiverUserIds, ImGroupMessageDTO.ofSend(message)); 121 + ImGroupMessageNotification notification = ImGroupMessageNotification.ofSend(message);
  122 + imWebSocketService.sendNotificationAsync(receiverUserIds, ImConversationTypeEnum.GROUP.getType(),
  123 + notification.getType(), notification);
122 return message; 124 return message;
123 } 125 }
124 126
@@ -149,7 +151,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -149,7 +151,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
149 } 151 }
150 152
151 // 2. WebSocket 异步推送 153 // 2. WebSocket 异步推送
152 - imWebSocketService.sendGroupMessageAsync(receiverUserIds, ImGroupMessageDTO.ofSend(message)); 154 + ImGroupMessageNotification notification = ImGroupMessageNotification.ofSend(message);
  155 + imWebSocketService.sendNotificationAsync(receiverUserIds, ImConversationTypeEnum.GROUP.getType(),
  156 + notification.getType(), notification);
153 return message; 157 return message;
154 } 158 }
155 159
@@ -347,8 +351,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -347,8 +351,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
347 @SuppressWarnings("DataFlowIssue") 351 @SuppressWarnings("DataFlowIssue")
348 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) { 352 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) {
349 // 1. 发送 READ 事件给自己的其他终端(多端同步) 353 // 1. 发送 READ 事件给自己的其他终端(多端同步)
350 - imWebSocketService.sendGroupMessageAsync(userId,  
351 - ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId)); 354 + imWebSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.GROUP.getType(),
  355 + ImContentTypeEnum.READ.getType(),
  356 + ImMessageReadNotification.ofGroup(userId, groupId, newMaxMessageId));
352 357
353 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息 358 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息
354 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt( 359 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt(
@@ -378,8 +383,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -378,8 +383,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
378 } 383 }
379 384
380 // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度) 385 // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度)
381 - imWebSocketService.sendGroupMessageAsync(message.getSenderId(),  
382 - ImGroupMessageDTO.ofReceipt(message.getId(), groupId, readCount, newReceiptStatus)); 386 + imWebSocketService.sendNotificationAsync(message.getSenderId(), ImConversationTypeEnum.GROUP.getType(),
  387 + ImContentTypeEnum.RECEIPT.getType(),
  388 + ImMessageReceiptNotification.ofGroup(message.getId(), groupId, readCount, newReceiptStatus));
383 } 389 }
384 } 390 }
385 391
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
@@ -103,9 +103,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -103,9 +103,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
103 } 103 }
104 104
105 // 3. WebSocket 异步推送:接收方 + 发送方多端同步 105 // 3. WebSocket 异步推送:接收方 + 发送方多端同步
106 - ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);  
107 - imWebSocketService.sendPrivateMessageAsync(message.getReceiverId(), websocketMessage);  
108 - imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage); 106 + ImPrivateMessageNotification notification = ImPrivateMessageNotification.ofSend(message);
  107 + imWebSocketService.sendNotificationAsync(message.getReceiverId(), ImConversationTypeEnum.PRIVATE.getType(),
  108 + notification.getType(), notification);
  109 + imWebSocketService.sendNotificationAsync(senderId, ImConversationTypeEnum.PRIVATE.getType(),
  110 + notification.getType(), notification);
109 return message; 111 return message;
110 } 112 }
111 113
@@ -131,11 +133,13 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -131,11 +133,13 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
131 } 133 }
132 134
133 // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知 135 // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知
134 - ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message); 136 + ImPrivateMessageNotification notification = ImPrivateMessageNotification.ofSend(message);
135 if (persistent) { 137 if (persistent) {
136 - imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage); 138 + imWebSocketService.sendNotificationAsync(dto.getReceiverId(), ImConversationTypeEnum.PRIVATE.getType(),
  139 + notification.getType(), notification);
137 } 140 }
138 - imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage); 141 + imWebSocketService.sendNotificationAsync(senderId, ImConversationTypeEnum.PRIVATE.getType(),
  142 + notification.getType(), notification);
139 return message; 143 return message;
140 } 144 }
141 145
@@ -256,10 +260,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -256,10 +260,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
256 } 260 }
257 261
258 // 4. 异步发送 READ + RECEIPT 事件(已读位置以前端上报为准,与多端 / 对方 UI 显示一致) 262 // 4. 异步发送 READ + RECEIPT 事件(已读位置以前端上报为准,与多端 / 对方 UI 显示一致)
259 - imWebSocketService.sendPrivateMessageAsync(userId,  
260 - ImPrivateMessageDTO.ofRead(userId, receiverId, messageId));  
261 - imWebSocketService.sendPrivateMessageAsync(receiverId,  
262 - ImPrivateMessageDTO.ofReceipt(userId, receiverId, messageId)); 263 + imWebSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.PRIVATE.getType(),
  264 + ImContentTypeEnum.READ.getType(), ImMessageReadNotification.ofPrivate(userId, receiverId, messageId));
  265 + imWebSocketService.sendNotificationAsync(receiverId, ImConversationTypeEnum.PRIVATE.getType(),
  266 + ImContentTypeEnum.RECEIPT.getType(),
  267 + ImMessageReceiptNotification.ofPrivate(userId, receiverId, messageId));
263 } 268 }
264 269
265 @Override 270 @Override
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
@@ -56,7 +56,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; @@ -56,7 +56,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
56 * 并发幂等:同好友对 / 同群活跃唯一性走 {@link ImRtcCallLockRedisDAO} 分布式锁 + 锁内 SELECT 兜底;webhook 兜底走条件 UPDATE; 56 * 并发幂等:同好友对 / 同群活跃唯一性走 {@link ImRtcCallLockRedisDAO} 分布式锁 + 锁内 SELECT 兜底;webhook 兜底走条件 UPDATE;
57 * <p> 57 * <p>
58 * 推送通道分流: 58 * 推送通道分流:
59 - * 1601 RTC_CALL(INVITING / JOINED / REJECTED / NO_ANSWER / LEFT 子类型)→ {@link ImWebSocketService#sendPrivateMessageAsync} 仅推参与方; 59 + * 1601 RTC_CALL(INVITING / JOINED / REJECTED / NO_ANSWER / LEFT 子类型)→ {@link ImWebSocketService#sendNotificationAsync} 仅推参与方;
60 * 1602 / 1603 PARTICIPANT_CONNECTED / DISCONNECTED → {@link ImWebSocketService} 推参与方 + 群通话场景广播全群; 60 * 1602 / 1603 PARTICIPANT_CONNECTED / DISCONNECTED → {@link ImWebSocketService} 推参与方 + 群通话场景广播全群;
61 * 1610 RTC_CALL_START + 1611 RTC_CALL_END → {@link ImPrivateMessageService} / {@link ImGroupMessageService} 入消息流当聊天 tip 61 * 1610 RTC_CALL_START + 1611 RTC_CALL_END → {@link ImPrivateMessageService} / {@link ImGroupMessageService} 入消息流当聊天 tip
62 * (START 仅群通话;两者分别在 invite / cancel(leave) 事务里 INSERT,自增 id 自然保证顺序) 62 * (START 仅群通话;两者分别在 invite / cancel(leave) 事务里 INSERT,自增 id 自然保证顺序)
@@ -1001,8 +1001,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -1001,8 +1001,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1001 String token = signToken(inviteeId, resolveDisplayName(invitee, inviteeId), call.getRoom()); 1001 String token = signToken(inviteeId, resolveDisplayName(invitee, inviteeId), call.getRoom());
1002 ImRtcCallNotification payload = ImRtcCallNotification.ofInvite( 1002 ImRtcCallNotification payload = ImRtcCallNotification.ofInvite(
1003 call, inviter, imProperties.getRtc().getLivekitUrl(), token, inviteeIds); 1003 call, inviter, imProperties.getRtc().getLivekitUrl(), token, inviteeIds);
1004 - webSocketService.sendPrivateMessageAsync(inviteeId, ImPrivateMessageDTO.ofRtcNotification(  
1005 - ImMessageTypeEnum.RTC_CALL.getType(), call.getInviterUserId(), inviteeId, payload)); 1004 + webSocketService.sendNotificationAsync(inviteeId, ImConversationTypeEnum.NONE.getType(),
  1005 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1006 } 1006 }
1007 1007
1008 /** 1008 /**
@@ -1017,8 +1017,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -1017,8 +1017,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1017 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; 1017 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
1018 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator); 1018 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
1019 for (Long receiverUserId : getCallAudienceUserIdList(call)) { 1019 for (Long receiverUserId : getCallAudienceUserIdList(call)) {
1020 - webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification(  
1021 - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload)); 1020 + webSocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  1021 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1022 } 1022 }
1023 } 1023 }
1024 1024
@@ -1034,8 +1034,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -1034,8 +1034,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1034 private void pushCallNoAnswerNotification(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) { 1034 private void pushCallNoAnswerNotification(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
1035 ImRtcCallNotification payload = ImRtcCallNotification.ofNoAnswer(call, operatorUserId, operator); 1035 ImRtcCallNotification payload = ImRtcCallNotification.ofNoAnswer(call, operatorUserId, operator);
1036 for (Long receiverUserId : getCallAudienceUserIdList(call)) { 1036 for (Long receiverUserId : getCallAudienceUserIdList(call)) {
1037 - webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification(  
1038 - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload)); 1037 + webSocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  1038 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1039 } 1039 }
1040 } 1040 }
1041 1041
@@ -1074,8 +1074,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -1074,8 +1074,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1074 if (CollUtil.isEmpty(receivers)) { 1074 if (CollUtil.isEmpty(receivers)) {
1075 return; 1075 return;
1076 } 1076 }
1077 - ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofRtcNotification(type, actorUserId, null, payload);  
1078 - webSocketService.sendPrivateMessageAsync(receivers, dto); 1077 + webSocketService.sendNotificationAsync(receivers, ImConversationTypeEnum.NONE.getType(), type, payload);
1079 } 1078 }
1080 1079
1081 /** 1080 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java
@@ -34,55 +34,24 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { @@ -34,55 +34,24 @@ public class ImWebSocketServiceImpl implements ImWebSocketService {
34 private WebSocketSenderApi webSocketSenderApi; 34 private WebSocketSenderApi webSocketSenderApi;
35 35
36 @Override 36 @Override
37 - public void sendPrivateMessageAsync(Collection<Long> userIds, ImPrivateMessageDTO dto) {  
38 - // 说明:通过 executeAfterTransaction 保证事务提交后再推送,避免客户端收到消息后查询数据库时事务尚未提交  
39 - // 通过 getSelf() 获取 Spring 代理对象调用 @Async 方法,确保异步 AOP 生效(直接 this 调用会绕过代理)  
40 - executeAfterTransaction(() -> getSelf().doSendPrivateMessage(userIds, dto)); 37 + public void sendNotificationAsync(Collection<Long> userIds, Integer conversationType, Integer contentType,
  38 + Object payload) {
  39 + ImNotificationWebSocketDTO notification = buildNotification(conversationType, contentType, payload);
  40 + executeAfterTransaction(() -> getSelf().doSendNotification(userIds, notification));
41 } 41 }
42 42
43 @Override 43 @Override
44 - public void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto) {  
45 - executeAfterTransaction(() -> getSelf().doSendGroupMessage(userIds, dto)); 44 + public void broadcastNotificationAsync(Integer conversationType, Integer contentType, Object payload) {
  45 + ImNotificationWebSocketDTO notification = buildNotification(conversationType, contentType, payload);
  46 + executeAfterTransaction(() -> getSelf().doBroadcastNotification(notification));
46 } 47 }
47 48
48 - @Override  
49 - public void sendChannelMessageAsync(Collection<Long> userIds, ImChannelMessageDTO dto) {  
50 - executeAfterTransaction(() -> getSelf().doSendChannelMessage(userIds, dto));  
51 - }  
52 -  
53 - @Override  
54 - public void broadcastChannelMessageAsync(ImChannelMessageDTO dto) {  
55 - executeAfterTransaction(() -> getSelf().doBroadcastChannelMessage(dto));  
56 - }  
57 -  
58 - /**  
59 - * 异步发送私聊 WebSocket 消息;多收件人共享同一 dto,避免按收件人重复注册 afterCommit 回调  
60 - */  
61 - @Async  
62 - public void doSendPrivateMessage(Collection<Long> userIds, ImPrivateMessageDTO dto) {  
63 - for (Long userId : getDistinctUserIds(userIds)) {  
64 - try {  
65 - webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId,  
66 - ImPrivateMessageDTO.TYPE, dto);  
67 - } catch (Exception e) {  
68 - log.error("[doSendPrivateMessage][userId({}) dto({}) 发送失败]", userId, dto, e);  
69 - }  
70 - }  
71 - }  
72 -  
73 - /**  
74 - * 异步发送群聊 WebSocket 消息  
75 - */  
76 - @Async  
77 - public void doSendGroupMessage(Collection<Long> userIds, ImGroupMessageDTO dto) {  
78 - for (Long userId : getDistinctUserIds(userIds)) {  
79 - try {  
80 - webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId,  
81 - ImGroupMessageDTO.TYPE, dto);  
82 - } catch (Exception e) {  
83 - log.error("[doSendGroupMessage][userId({}) dto({}) 发送失败]", userId, dto, e);  
84 - }  
85 - } 49 + private static ImNotificationWebSocketDTO buildNotification(Integer conversationType, Integer contentType,
  50 + Object payload) {
  51 + return new ImNotificationWebSocketDTO()
  52 + .setConversationType(conversationType)
  53 + .setContentType(contentType)
  54 + .setPayload(payload);
86 } 55 }
87 56
88 /** 57 /**
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java
@@ -112,7 +112,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -112,7 +112,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
112 assertNull(result); 112 assertNull(result);
113 verify(friendService).silentReAddFriend(eq(1L), eq(2L), eq("老张"), eq(1)); 113 verify(friendService).silentReAddFriend(eq(1L), eq(2L), eq("老张"), eq(1));
114 verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class)); 114 verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class));
115 - verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 115 + verify(websocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
116 } 116 }
117 117
118 @Test 118 @Test
@@ -157,7 +157,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -157,7 +157,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
157 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), saved.getHandleResult()); 157 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), saved.getHandleResult());
158 assertSame(saved, result); 158 assertSame(saved, result);
159 // 断言:推送给接收方 159 // 断言:推送给接收方
160 - verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); 160 + verify(websocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
161 } 161 }
162 162
163 @Test 163 @Test
@@ -210,7 +210,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -210,7 +210,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
210 any(LocalDateTime.class)); 210 any(LocalDateTime.class));
211 assertEquals(100L, result.getId()); 211 assertEquals(100L, result.getId());
212 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); 212 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult());
213 - verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); 213 + verify(websocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
214 } 214 }
215 215
216 // ========== agreeFriendRequest ========== 216 // ========== agreeFriendRequest ==========
@@ -230,7 +230,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -230,7 +230,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
230 // 断言:双向建立好友 + 推 APPROVED 给发起方 230 // 断言:双向建立好友 + 推 APPROVED 给发起方
231 verify(adminUserApi).validateUserList(ListUtil.of(1L, 2L)); 231 verify(adminUserApi).validateUserList(ListUtil.of(1L, 2L));
232 verify(friendService).becomeFriends(request); 232 verify(friendService).becomeFriends(request);
233 - verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); 233 + verify(websocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
234 } 234 }
235 235
236 @Test 236 @Test
@@ -305,7 +305,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -305,7 +305,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
305 eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), captor.capture()); 305 eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), captor.capture());
306 assertEquals(ImFriendRequestHandleResultEnum.REFUSED.getResult(), captor.getValue().getHandleResult()); 306 assertEquals(ImFriendRequestHandleResultEnum.REFUSED.getResult(), captor.getValue().getHandleResult());
307 assertEquals("不认识", captor.getValue().getHandleContent()); 307 assertEquals("不认识", captor.getValue().getHandleContent());
308 - verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); 308 + verify(websocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
309 } 309 }
310 310
311 @Test 311 @Test
@@ -320,7 +320,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { @@ -320,7 +320,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
320 ServiceException exception = assertThrows(ServiceException.class, 320 ServiceException exception = assertThrows(ServiceException.class,
321 () -> friendRequestService.refuseFriendRequest(2L, 100L, "x")); 321 () -> friendRequestService.refuseFriendRequest(2L, 100L, "x"));
322 assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode()); 322 assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode());
323 - verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 323 + verify(websocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
324 } 324 }
325 325
326 // ========== getMyFriendRequestList ========== 326 // ========== getMyFriendRequestList ==========
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
@@ -68,7 +68,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -68,7 +68,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
68 assertEquals(100L, captor.getValue().getId()); 68 assertEquals(100L, captor.getValue().getId());
69 assertTrue(captor.getValue().getSilent()); 69 assertTrue(captor.getValue().getSilent());
70 // 断言:推送了好友更新通知 70 // 断言:推送了好友更新通知
71 - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); 71 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
72 } 72 }
73 73
74 @Test 74 @Test
@@ -100,7 +100,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -100,7 +100,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
100 () -> friendService.updateFriend(1L, reqVO)); 100 () -> friendService.updateFriend(1L, reqVO));
101 assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode()); 101 assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode());
102 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); 102 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
103 - verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class)); 103 + verify(imWebSocketService, never()).sendNotificationAsync(any(Long.class), anyInt(), anyInt(), any());
104 } 104 }
105 105
106 @Test 106 @Test
@@ -123,7 +123,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -123,7 +123,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
123 assertEquals("老张", captor.getValue().getDisplayName()); 123 assertEquals("老张", captor.getValue().getDisplayName());
124 assertNull(captor.getValue().getSilent()); 124 assertNull(captor.getValue().getSilent());
125 // 断言:推送好友更新通知 125 // 断言:推送好友更新通知
126 - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); 126 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
127 } 127 }
128 128
129 @Test 129 @Test
@@ -138,7 +138,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -138,7 +138,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
138 // 断言:没查记录、没触发 SQL 更新 / 没发 WebSocket 推送 138 // 断言:没查记录、没触发 SQL 更新 / 没发 WebSocket 推送
139 verify(imFriendMapper, never()).selectByUserIdAndFriendUserId(anyLong(), anyLong()); 139 verify(imFriendMapper, never()).selectByUserIdAndFriendUserId(anyLong(), anyLong());
140 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); 140 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
141 - verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class)); 141 + verify(imWebSocketService, never()).sendNotificationAsync(any(Long.class), anyInt(), anyInt(), any());
142 } 142 }
143 143
144 // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== 144 // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ==========
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java
@@ -116,7 +116,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -116,7 +116,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
116 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), anyInt(), anyLong()); 116 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), anyInt(), anyLong());
117 verify(groupRequestMapper).insert(any(ImGroupRequestDO.class)); 117 verify(groupRequestMapper).insert(any(ImGroupRequestDO.class));
118 // 1503 推送给 owner(99) + admin(98),去重后两条 118 // 1503 推送给 owner(99) + admin(98),去重后两条
119 - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 119 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
120 } 120 }
121 121
122 @Test 122 @Test
@@ -149,7 +149,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -149,7 +149,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
149 eq(ImGroupAddSourceEnum.SEARCH.getSource()), any()); 149 eq(ImGroupAddSourceEnum.SEARCH.getSource()), any());
150 assertEquals(50L, result.getId()); 150 assertEquals(50L, result.getId());
151 assertEquals(ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); 151 assertEquals(ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult());
152 - verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class)); 152 + verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any());
153 } 153 }
154 154
155 @Test 155 @Test
@@ -208,7 +208,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -208,7 +208,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
208 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); 208 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
209 assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType()); 209 assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
210 // 1505 推送给申请人 + owner,去重后两条 210 // 1505 推送给申请人 + owner,去重后两条
211 - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 211 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
212 } 212 }
213 213
214 @Test 214 @Test
@@ -278,7 +278,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -278,7 +278,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
278 278
279 // 不写群成员;推 1506 给申请人 + 群主(同一人 1L 时去重为 1 + 申请人 2L = 2 条) 279 // 不写群成员;推 1506 给申请人 + 群主(同一人 1L 时去重为 1 + 申请人 2L = 2 条)
280 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), any(), any()); 280 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), any(), any());
281 - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 281 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
282 } 282 }
283 283
284 // ==================== createInviteRequestList ==================== 284 // ==================== createInviteRequestList ====================
@@ -305,7 +305,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -305,7 +305,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
305 // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧 305 // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧
306 ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); 306 ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class);
307 verify(groupRequestMapper, times(2)).insert(captor.capture()); 307 verify(groupRequestMapper, times(2)).insert(captor.capture());
308 - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 308 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
309 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 309 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值
310 Collection<ImGroupRequestDO> inserted = captor.getAllValues(); 310 Collection<ImGroupRequestDO> inserted = captor.getAllValues();
311 assertEquals(2, inserted.size()); 311 assertEquals(2, inserted.size());
@@ -337,7 +337,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -337,7 +337,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
337 // 断言:复用并重置旧邀请申请 337 // 断言:复用并重置旧邀请申请
338 verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L), 338 verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L),
339 eq(ImGroupAddSourceEnum.INVITE.getSource()), any()); 339 eq(ImGroupAddSourceEnum.INVITE.getSource()), any());
340 - verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class)); 340 + verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any());
341 } 341 }
342 342
343 private AdminUserRespDTO buildUser(Long id, String nickname) { 343 private AdminUserRespDTO buildUser(Long id, String nickname) {
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
@@ -342,7 +342,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -342,7 +342,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
342 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(), 342 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
343 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L)); 343 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
344 verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection()); 344 verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection());
345 - verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 345 + verify(webSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
346 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); 346 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
347 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture()); 347 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
348 assertEquals(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType()); 348 assertEquals(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
@@ -526,7 +526,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -526,7 +526,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
526 526
527 // 断言:不会触发添加、不会推送 527 // 断言:不会触发添加、不会推送
528 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection()); 528 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection());
529 - verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 529 + verify(webSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
530 } 530 }
531 } 531 }
532 532
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImplTest.java
@@ -88,7 +88,7 @@ public class ImChannelMessageServiceImplTest extends BaseMockitoUnitTest { @@ -88,7 +88,7 @@ public class ImChannelMessageServiceImplTest extends BaseMockitoUnitTest {
88 channelMessageService.readChannelMessages(1L, 10L, 100L); 88 channelMessageService.readChannelMessages(1L, 10L, 100L);
89 89
90 // 断言:读位置未前进 → 不推 READ 事件 90 // 断言:读位置未前进 → 不推 READ 事件
91 - verify(webSocketService, never()).sendChannelMessageAsync(anyLong(), any()); 91 + verify(webSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
92 } 92 }
93 93
94 } 94 }
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
@@ -116,9 +116,16 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -116,9 +116,16 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
116 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus()); 116 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
117 117
118 // 验证推送给 3 个群成员(含发送者自己,用于多端同步) 118 // 验证推送给 3 个群成员(含发送者自己,用于多端同步)
119 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 119 + ArgumentCaptor<ImGroupMessageNotification> payloadCaptor =
  120 + ArgumentCaptor.forClass(ImGroupMessageNotification.class);
  121 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
120 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)), 122 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)),
121 - any(ImGroupMessageDTO.class)); 123 + eq(ImConversationTypeEnum.GROUP.getType()), eq(ImContentTypeEnum.TEXT.getType()),
  124 + payloadCaptor.capture());
  125 + ImGroupMessageNotification payload = payloadCaptor.getValue();
  126 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), payload.getReceiptStatus());
  127 + assertEquals(0, payload.getReadCount());
  128 + assertEquals(List.of(1L, 2L, 3L), payload.getReceiverUserIds());
122 } 129 }
123 } 130 }
124 131
@@ -152,9 +159,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -152,9 +159,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
152 159
153 // 断言:固化快照必含发送者,推送目标补回发送者 = {1,2,3} 160 // 断言:固化快照必含发送者,推送目标补回发送者 = {1,2,3}
154 assertTrue(result.getReceiverUserIds().contains(1L), "发送者必须在 receiver_user_ids 快照内"); 161 assertTrue(result.getReceiverUserIds().contains(1L), "发送者必须在 receiver_user_ids 快照内");
155 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 162 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
156 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)), 163 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)),
157 - any(ImGroupMessageDTO.class)); 164 + anyInt(), anyInt(), any());
158 } 165 }
159 } 166 }
160 167
@@ -211,7 +218,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -211,7 +218,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
211 // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3}) 218 // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3})
212 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L) 219 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
213 .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L)) 220 .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L))
214 - .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"密\"}") 221 + .type(ImContentTypeEnum.TEXT.getType()).content("{\"content\":\"密\"}")
215 .sendTime(LocalDateTime.now()).build(); 222 .sendTime(LocalDateTime.now()).build();
216 when(groupMessageMapper.selectById(500L)).thenReturn(original); 223 when(groupMessageMapper.selectById(500L)).thenReturn(original);
217 224
@@ -237,7 +244,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -237,7 +244,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
237 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L)); 244 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
238 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L) 245 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
239 .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L, 3L)) 246 .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L, 3L))
240 - .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"公开\"}") 247 + .type(ImContentTypeEnum.TEXT.getType()).content("{\"content\":\"公开\"}")
241 .sendTime(LocalDateTime.now()).build(); 248 .sendTime(LocalDateTime.now()).build();
242 when(groupMessageMapper.selectById(500L)).thenReturn(original); 249 when(groupMessageMapper.selectById(500L)).thenReturn(original);
243 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> { 250 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> {
@@ -390,9 +397,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -390,9 +397,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
390 verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class)); 397 verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class));
391 verify(groupMessageMapper).insert(any(ImGroupMessageDO.class)); 398 verify(groupMessageMapper).insert(any(ImGroupMessageDO.class));
392 // 验证:给 2 个活跃成员推送撤回提示 399 // 验证:给 2 个活跃成员推送撤回提示
393 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 400 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
394 ids.size() == 2 && ids.contains(1L) && ids.contains(2L)), 401 ids.size() == 2 && ids.contains(1L) && ids.contains(2L)),
395 - any(ImGroupMessageDTO.class)); 402 + anyInt(), anyInt(), any());
396 } 403 }
397 } 404 }
398 405
@@ -588,7 +595,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -588,7 +595,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
588 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); 595 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
589 // 断言:不入库、不推送 596 // 断言:不入库、不推送
590 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class)); 597 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
591 - verify(imWebSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 598 + verify(imWebSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
592 } 599 }
593 600
594 // ========== 撤回:补充边界 ========== 601 // ========== 撤回:补充边界 ==========
@@ -649,7 +656,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -649,7 +656,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
649 656
650 // 断言:已读位置更新 + READ 事件 657 // 断言:已读位置更新 + READ 事件
651 verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L); 658 verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L);
652 - verify(imWebSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); 659 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
653 } 660 }
654 } 661 }
655 662
@@ -664,7 +671,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -664,7 +671,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
664 assertEquals(MESSAGE_GROUP_READ_DISABLED.getCode(), exception.getCode()); 671 assertEquals(MESSAGE_GROUP_READ_DISABLED.getCode(), exception.getCode());
665 // 断言:已读位置不写、不推送 672 // 断言:已读位置不写、不推送
666 verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong()); 673 verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
667 - verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class)); 674 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
668 } 675 }
669 676
670 @Test 677 @Test
@@ -726,7 +733,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -726,7 +733,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
726 733
727 // 断言:调用了 CAS 更新但读位置未前进 → 不推送 734 // 断言:调用了 CAS 更新但读位置未前进 → 不推送
728 verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L); 735 verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L);
729 - verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class)); 736 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
730 } 737 }
731 738
732 @Test 739 @Test
@@ -787,7 +794,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -787,7 +794,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
787 assertEquals(100L, captor.getValue().getId()); 794 assertEquals(100L, captor.getValue().getId());
788 assertEquals(ImMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus()); 795 assertEquals(ImMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus());
789 // 断言:给消息发送方(5)推送 RECEIPT 事件 796 // 断言:给消息发送方(5)推送 RECEIPT 事件
790 - verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class)); 797 + verify(imWebSocketService).sendNotificationAsync(eq(5L), anyInt(), anyInt(), any());
791 } 798 }
792 799
793 @Test 800 @Test
@@ -824,7 +831,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -824,7 +831,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
824 // 断言:回执状态保持 PENDING,不更新 DB 831 // 断言:回执状态保持 PENDING,不更新 DB
825 verify(groupMessageMapper, never()).updateById(any(ImGroupMessageDO.class)); 832 verify(groupMessageMapper, never()).updateById(any(ImGroupMessageDO.class));
826 // 仍然推送 RECEIPT 事件给发送方(携带已读人数) 833 // 仍然推送 RECEIPT 事件给发送方(携带已读人数)
827 - verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class)); 834 + verify(imWebSocketService).sendNotificationAsync(eq(5L), anyInt(), anyInt(), any());
828 } 835 }
829 836
830 // ========== sendGroupMessage(senderId, dto):receiverUserIds 定向过滤 ========== 837 // ========== sendGroupMessage(senderId, dto):receiverUserIds 定向过滤 ==========
@@ -833,17 +840,17 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -833,17 +840,17 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
833 public void testSendGroupMessage_noReceiverUserIds_broadcastsToAllMembers() { 840 public void testSendGroupMessage_noReceiverUserIds_broadcastsToAllMembers() {
834 // 准备:无定向 → 应发给所有 ENABLE 成员(含发送者自己,多端同步) 841 // 准备:无定向 → 应发给所有 ENABLE 成员(含发送者自己,多端同步)
835 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() 842 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
836 - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType()) 843 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
837 .setContent("{\"messageId\":1}"); 844 .setContent("{\"messageId\":1}");
838 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)) 845 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
839 .thenReturn(List.of(1L, 2L, 3L, 4L)); 846 .thenReturn(List.of(1L, 2L, 3L, 4L));
840 847
841 groupMessageService.sendGroupMessage(1L, dto); 848 groupMessageService.sendGroupMessage(1L, dto);
842 849
843 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 850 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
844 ids.size() == 4 && ids.contains(1L) && ids.contains(2L) 851 ids.size() == 4 && ids.contains(1L) && ids.contains(2L)
845 && ids.contains(3L) && ids.contains(4L)), 852 && ids.contains(3L) && ids.contains(4L)),
846 - any(ImGroupMessageDTO.class)); 853 + anyInt(), anyInt(), any());
847 } 854 }
848 855
849 @Test 856 @Test
@@ -851,7 +858,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -851,7 +858,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
851 // 准备:定向给 {2,3},发送者 1;群成员 {1,2,3,4} 858 // 准备:定向给 {2,3},发送者 1;群成员 {1,2,3,4}
852 // 预期推送目标:{1,2,3}(发送者自己多端同步 + 定向列表);4 被过滤 859 // 预期推送目标:{1,2,3}(发送者自己多端同步 + 定向列表);4 被过滤
853 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() 860 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
854 - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType()) 861 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
855 .setContent("{\"messageId\":1}") 862 .setContent("{\"messageId\":1}")
856 .setReceiverUserIds(List.of(2L, 3L)); 863 .setReceiverUserIds(List.of(2L, 3L));
857 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)) 864 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
@@ -859,10 +866,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -859,10 +866,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
859 866
860 groupMessageService.sendGroupMessage(1L, dto); 867 groupMessageService.sendGroupMessage(1L, dto);
861 868
862 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 869 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
863 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) 870 ids.size() == 3 && ids.contains(1L) && ids.contains(2L)
864 && ids.contains(3L) && !ids.contains(4L)), 871 && ids.contains(3L) && !ids.contains(4L)),
865 - any(ImGroupMessageDTO.class)); 872 + anyInt(), anyInt(), any());
866 } 873 }
867 874
868 @Test 875 @Test
@@ -870,7 +877,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -870,7 +877,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
870 // 边界:发送者不在群成员缓存里(理论上应先被 validateMemberInGroup 挡住,这里纯防御) 877 // 边界:发送者不在群成员缓存里(理论上应先被 validateMemberInGroup 挡住,这里纯防御)
871 // 预期:定向用户 + 发送者自己(始终纳入快照保证多端同步);非定向成员 4 被过滤 878 // 预期:定向用户 + 发送者自己(始终纳入快照保证多端同步);非定向成员 4 被过滤
872 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() 879 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
873 - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType()) 880 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
874 .setContent("{\"messageId\":1}") 881 .setContent("{\"messageId\":1}")
875 .setReceiverUserIds(List.of(2L, 3L)); 882 .setReceiverUserIds(List.of(2L, 3L));
876 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)) 883 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
@@ -878,10 +885,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -878,10 +885,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
878 885
879 groupMessageService.sendGroupMessage(99L, dto); 886 groupMessageService.sendGroupMessage(99L, dto);
880 887
881 - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 888 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
882 ids.size() == 3 && ids.contains(2L) && ids.contains(3L) 889 ids.size() == 3 && ids.contains(2L) && ids.contains(3L)
883 && ids.contains(99L) && !ids.contains(4L)), 890 && ids.contains(99L) && !ids.contains(4L)),
884 - any(ImGroupMessageDTO.class)); 891 + anyInt(), anyInt(), any());
885 } 892 }
886 893
887 // ========== sendGroupMessage(senderId, dto):helper 行为 ========== 894 // ========== sendGroupMessage(senderId, dto):helper 行为 ==========
@@ -935,7 +942,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -935,7 +942,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
935 groupMessageService.sendGroupMessage(1L, dto); 942 groupMessageService.sendGroupMessage(1L, dto);
936 943
937 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class)); 944 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
938 - verify(imWebSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 945 + verify(imWebSocketService).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
939 } 946 }
940 947
941 @Test 948 @Test
@@ -949,7 +956,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -949,7 +956,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
949 956
950 // 断言:不读取 active members,按调用方 targets 推送 957 // 断言:不读取 active members,按调用方 targets 推送
951 verify(groupMemberService, never()).getActiveGroupMemberUserIdsByGroupId(anyLong()); 958 verify(groupMemberService, never()).getActiveGroupMemberUserIdsByGroupId(anyLong());
952 - verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class)); 959 + verify(imWebSocketService).sendNotificationAsync(eq(targets), anyInt(), anyInt(), any());
953 } 960 }
954 961
955 // ========== getGroupMessageList ========== 962 // ========== getGroupMessageList ==========
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate
7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
  10 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
10 import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum; 11 import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
11 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
12 import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum; 13 import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
@@ -16,8 +17,10 @@ import cn.iocoder.yudao.module.im.service.friend.ImFriendService; @@ -16,8 +17,10 @@ import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
16 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 17 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
17 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO; 18 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
18 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
19 -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;  
20 -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; 20 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
  21 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReceiptNotification;
  22 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImPrivateMessageNotification;
  23 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
21 import org.junit.jupiter.api.Test; 24 import org.junit.jupiter.api.Test;
22 import org.mockito.ArgumentCaptor; 25 import org.mockito.ArgumentCaptor;
23 import org.mockito.InjectMocks; 26 import org.mockito.InjectMocks;
@@ -61,7 +64,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -61,7 +64,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
61 ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); 64 ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO();
62 reqVO.setClientMessageId("test-uuid-001"); 65 reqVO.setClientMessageId("test-uuid-001");
63 reqVO.setReceiverId(2L); 66 reqVO.setReceiverId(2L);
64 - reqVO.setType(ImMessageTypeEnum.TEXT.getType()); 67 + reqVO.setType(ImContentTypeEnum.TEXT.getType());
65 reqVO.setContent("{\"content\":\"你好\"}"); 68 reqVO.setContent("{\"content\":\"你好\"}");
66 return reqVO; 69 return reqVO;
67 } 70 }
@@ -87,7 +90,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -87,7 +90,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
87 assertNotNull(result); 90 assertNotNull(result);
88 assertEquals(1L, result.getSenderId()); 91 assertEquals(1L, result.getSenderId());
89 assertEquals(2L, result.getReceiverId()); 92 assertEquals(2L, result.getReceiverId());
90 - assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType()); 93 + assertEquals(ImContentTypeEnum.TEXT.getType(), result.getType());
91 assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus()); 94 assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
92 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus(), 95 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus(),
93 "用户私聊消息默认需要回执(PENDING)"); 96 "用户私聊消息默认需要回执(PENDING)");
@@ -98,8 +101,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -98,8 +101,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
98 verify(sensitiveWordService).validateText(reqVO.getContent()); 101 verify(sensitiveWordService).validateText(reqVO.getContent());
99 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class)); 102 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class));
100 // 验证推送给接收方和发送方 103 // 验证推送给接收方和发送方
101 - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));  
102 - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); 104 + verify(imWebSocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
  105 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
103 } 106 }
104 107
105 @Test 108 @Test
@@ -187,22 +190,25 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -187,22 +190,25 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
187 190
188 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确 191 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确
189 ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); 192 ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class);
190 - ArgumentCaptor<ImPrivateMessageDTO> contentCaptor = ArgumentCaptor.forClass(ImPrivateMessageDTO.class);  
191 - verify(imWebSocketService, times(2)).sendPrivateMessageAsync(  
192 - userCaptor.capture(), contentCaptor.capture()); 193 + ArgumentCaptor<Integer> contentTypeCaptor = ArgumentCaptor.forClass(Integer.class);
  194 + ArgumentCaptor<Object> payloadCaptor = ArgumentCaptor.forClass(Object.class);
  195 + verify(imWebSocketService, times(2)).sendNotificationAsync(
  196 + userCaptor.capture(), eq(ImConversationTypeEnum.PRIVATE.getType()),
  197 + contentTypeCaptor.capture(), payloadCaptor.capture());
193 198
194 // 第一次:发给自己的 READ 事件 199 // 第一次:发给自己的 READ 事件
195 assertEquals(1L, userCaptor.getAllValues().get(0)); 200 assertEquals(1L, userCaptor.getAllValues().get(0));
196 - ImPrivateMessageDTO readPayload = contentCaptor.getAllValues().get(0);  
197 - assertEquals(ImMessageTypeEnum.READ.getType(), readPayload.getType()); 201 + assertEquals(ImContentTypeEnum.READ.getType(), contentTypeCaptor.getAllValues().get(0));
  202 + ImMessageReadNotification readPayload = (ImMessageReadNotification) payloadCaptor.getAllValues().get(0);
198 assertEquals(1L, readPayload.getSenderId()); 203 assertEquals(1L, readPayload.getSenderId());
199 assertEquals(2L, readPayload.getReceiverId()); 204 assertEquals(2L, readPayload.getReceiverId());
200 assertEquals(5L, readPayload.getId(), "READ id 应为前端上报的 messageId"); 205 assertEquals(5L, readPayload.getId(), "READ id 应为前端上报的 messageId");
201 206
202 // 第二次:发给对方的 RECEIPT 事件 207 // 第二次:发给对方的 RECEIPT 事件
203 assertEquals(2L, userCaptor.getAllValues().get(1)); 208 assertEquals(2L, userCaptor.getAllValues().get(1));
204 - ImPrivateMessageDTO receiptPayload = contentCaptor.getAllValues().get(1);  
205 - assertEquals(ImMessageTypeEnum.RECEIPT.getType(), receiptPayload.getType()); 209 + assertEquals(ImContentTypeEnum.RECEIPT.getType(), contentTypeCaptor.getAllValues().get(1));
  210 + ImMessageReceiptNotification receiptPayload =
  211 + (ImMessageReceiptNotification) payloadCaptor.getAllValues().get(1);
206 assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为前端上报的 messageId"); 212 assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为前端上报的 messageId");
207 } 213 }
208 214
@@ -228,7 +234,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -228,7 +234,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
228 verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class)); 234 verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class));
229 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class)); 235 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class));
230 // 验证推送了消息(给接收方和发送方) 236 // 验证推送了消息(给接收方和发送方)
231 - verify(imWebSocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 237 + verify(imWebSocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
232 } 238 }
233 239
234 @Test 240 @Test
@@ -304,7 +310,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -304,7 +310,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
304 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); 310 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
305 // 断言:不入库、不推送 311 // 断言:不入库、不推送
306 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); 312 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class));
307 - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 313 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
308 } 314 }
309 315
310 @Test 316 @Test
@@ -319,7 +325,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -319,7 +325,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
319 // 断言:不更新消息状态、不推送 325 // 断言:不更新消息状态、不推送
320 verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus( 326 verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
321 anyLong(), anyLong(), anyLong(), anyInt(), any(ImPrivateMessageDO.class)); 327 anyLong(), anyLong(), anyLong(), anyInt(), any(ImPrivateMessageDO.class));
322 - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 328 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
323 } 329 }
324 330
325 @Test 331 @Test
@@ -332,7 +338,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -332,7 +338,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
332 privateMessageService.readPrivateMessages(1L, 2L, 5L); 338 privateMessageService.readPrivateMessages(1L, 2L, 5L);
333 339
334 // 断言:读位置没前进,不推送 READ / RECEIPT 340 // 断言:读位置没前进,不推送 READ / RECEIPT
335 - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 341 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
336 } 342 }
337 343
338 // ========== getMaxReadMessageId 测试 ========== 344 // ========== getMaxReadMessageId 测试 ==========
@@ -397,8 +403,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -397,8 +403,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
397 assertNotNull(message.getClientMessageId()); 403 assertNotNull(message.getClientMessageId());
398 assertNotNull(message.getSendTime()); 404 assertNotNull(message.getSendTime());
399 // 断言:sender + receiver 双端推送 405 // 断言:sender + receiver 双端推送
400 - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));  
401 - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); 406 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
  407 + verify(imWebSocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
402 } 408 }
403 409
404 @Test 410 @Test
@@ -410,8 +416,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -410,8 +416,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
410 privateMessageService.sendPrivateMessage(1L, dto); 416 privateMessageService.sendPrivateMessage(1L, dto);
411 417
412 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); 418 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class));
413 - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));  
414 - verify(imWebSocketService, never()).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); 419 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
  420 + verify(imWebSocketService, never()).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
415 } 421 }
416 422
417 // ========== getPrivateMessageList ========== 423 // ========== getPrivateMessageList ==========
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java
@@ -143,7 +143,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { @@ -143,7 +143,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
143 143
144 // 断言:成功 1 个;NO_ANSWER 信令推到主叫;不触发 endSession 144 // 断言:成功 1 个;NO_ANSWER 信令推到主叫;不触发 endSession
145 assertEquals(1, result); 145 assertEquals(1, result);
146 - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class)); 146 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
147 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any()); 147 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any());
148 } 148 }
149 149
@@ -238,7 +238,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { @@ -238,7 +238,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
238 rtcCallService.noAnswerCallCheck(100L, "r1"); 238 rtcCallService.noAnswerCallCheck(100L, "r1");
239 239
240 // 断言:NO_ANSWER 信令推到主叫 200L;不触发 endSession 240 // 断言:NO_ANSWER 信令推到主叫 200L;不触发 endSession
241 - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class)); 241 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
242 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any()); 242 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any());
243 } 243 }
244 244
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java
@@ -57,10 +57,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -57,10 +57,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
57 .thenReturn(imWebSocketService); 57 .thenReturn(imWebSocketService);
58 58
59 // 准备 59 // 准备
60 - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L); 60 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  61 + .setType(ImContentTypeEnum.TEXT.getType());
61 62
62 // 调用:无事务,应立即发送 63 // 调用:无事务,应立即发送
63 - imWebSocketService.sendPrivateMessageAsync(2L, dto); 64 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  65 + ImContentTypeEnum.TEXT.getType(), dto);
64 66
65 // 断言 67 // 断言
66 verify(webSocketSenderApi).sendObject( 68 verify(webSocketSenderApi).sendObject(
@@ -79,10 +81,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -79,10 +81,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
79 // 准备:开启事务同步 81 // 准备:开启事务同步
80 TransactionSynchronizationManager.initSynchronization(); 82 TransactionSynchronizationManager.initSynchronization();
81 try { 83 try {
82 - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L); 84 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  85 + .setType(ImContentTypeEnum.TEXT.getType());
83 86
84 // 调用 87 // 调用
85 - imWebSocketService.sendPrivateMessageAsync(2L, dto); 88 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  89 + ImContentTypeEnum.TEXT.getType(), dto);
86 90
87 // 断言:事务未提交,未推送 91 // 断言:事务未提交,未推送
88 verify(webSocketSenderApi, never()).sendObject(anyInt(), anyLong(), anyString(), any()); 92 verify(webSocketSenderApi, never()).sendObject(anyInt(), anyLong(), anyString(), any());
@@ -115,8 +119,10 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -115,8 +119,10 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
115 ImGroupMessageNotification dto = new ImGroupMessageNotification(); 119 ImGroupMessageNotification dto = new ImGroupMessageNotification();
116 dto.setGroupId(10L); 120 dto.setGroupId(10L);
117 dto.setSenderId(1L); 121 dto.setSenderId(1L);
  122 + dto.setType(ImContentTypeEnum.TEXT.getType());
118 123
119 - imWebSocketService.sendGroupMessageAsync(ListUtil.of(1L, 2L, 3L), dto); 124 + imWebSocketService.sendNotificationAsync(ListUtil.of(1L, 2L, 3L),
  125 + ImConversationTypeEnum.GROUP.getType(), ImContentTypeEnum.TEXT.getType(), dto);
120 126
121 verify(webSocketSenderApi).sendObject( 127 verify(webSocketSenderApi).sendObject(
122 eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImNotificationWebSocketDTO.TYPE), 128 eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImNotificationWebSocketDTO.TYPE),
@@ -145,7 +151,8 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -145,7 +151,8 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
145 doThrow(new RuntimeException("user offline")) 151 doThrow(new RuntimeException("user offline"))
146 .when(webSocketSenderApi).sendObject(anyInt(), eq(1L), anyString(), any()); 152 .when(webSocketSenderApi).sendObject(anyInt(), eq(1L), anyString(), any());
147 153
148 - imWebSocketService.sendGroupMessageAsync(ListUtil.of(1L, 2L, 3L), dto); 154 + imWebSocketService.sendNotificationAsync(ListUtil.of(1L, 2L, 3L),
  155 + ImConversationTypeEnum.GROUP.getType(), ImContentTypeEnum.TEXT.getType(), dto);
149 156
150 // 2L 和 3L 也都被推送 157 // 2L 和 3L 也都被推送
151 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any()); 158 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
@@ -154,8 +161,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -154,8 +161,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
154 } 161 }
155 162
156 @Test 163 @Test
157 - public void testDoSendGroupMessage_emptyUserIds_noSend() {  
158 - ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L); 164 + public void testDoSendNotification_emptyUserIds_noSend() {
  165 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
  166 + dto.setGroupId(10L);
  167 + dto.setType(ImContentTypeEnum.TEXT.getType());
  168 + ImNotificationWebSocketDTO notification = buildNotification(ImConversationTypeEnum.GROUP,
  169 + ImContentTypeEnum.TEXT, dto);
159 170
160 imWebSocketService.doSendNotification(Collections.emptyList(), notification); 171 imWebSocketService.doSendNotification(Collections.emptyList(), notification);
161 imWebSocketService.doSendNotification(null, notification); 172 imWebSocketService.doSendNotification(null, notification);
@@ -164,8 +175,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -164,8 +175,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
164 } 175 }
165 176
166 @Test 177 @Test
167 - public void testDoSendGroupMessage_distinctUserIds() {  
168 - ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L); 178 + public void testDoSendNotification_distinctUserIds() {
  179 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
  180 + dto.setGroupId(10L);
  181 + dto.setType(ImContentTypeEnum.TEXT.getType());
  182 + ImNotificationWebSocketDTO notification = buildNotification(ImConversationTypeEnum.GROUP,
  183 + ImContentTypeEnum.TEXT, dto);
169 184
170 imWebSocketService.doSendNotification(Arrays.asList(1L, 2L, 1L, null), notification); 185 imWebSocketService.doSendNotification(Arrays.asList(1L, 2L, 1L, null), notification);
171 186
@@ -187,12 +202,14 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -187,12 +202,14 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
187 .thenReturn(imWebSocketService); 202 .thenReturn(imWebSocketService);
188 203
189 // 准备:sender 抛异常 204 // 准备:sender 抛异常
190 - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L); 205 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  206 + .setType(ImContentTypeEnum.TEXT.getType());
191 doThrow(new RuntimeException("user offline")) 207 doThrow(new RuntimeException("user offline"))
192 .when(webSocketSenderApi).sendObject(anyInt(), anyLong(), anyString(), any()); 208 .when(webSocketSenderApi).sendObject(anyInt(), anyLong(), anyString(), any());
193 209
194 // 调用:异常应被吞掉,不向上抛 210 // 调用:异常应被吞掉,不向上抛
195 - imWebSocketService.sendPrivateMessageAsync(2L, dto); 211 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  212 + ImContentTypeEnum.TEXT.getType(), dto);
196 213
197 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any()); 214 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
198 } 215 }
@@ -206,12 +223,34 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { @@ -206,12 +223,34 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
206 223
207 ImGroupMessageNotification dto = new ImGroupMessageNotification(); 224 ImGroupMessageNotification dto = new ImGroupMessageNotification();
208 dto.setGroupId(10L); 225 dto.setGroupId(10L);
  226 + dto.setType(ImContentTypeEnum.TEXT.getType());
209 227
210 - imWebSocketService.sendGroupMessageAsync(42L, dto); 228 + imWebSocketService.sendNotificationAsync(42L, ImConversationTypeEnum.GROUP.getType(),
  229 + ImContentTypeEnum.TEXT.getType(), dto);
211 230
212 verify(webSocketSenderApi).sendObject( 231 verify(webSocketSenderApi).sendObject(
213 - eq(UserTypeEnum.ADMIN.getValue()), eq(42L), eq(ImGroupMessageDTO.TYPE), eq(dto)); 232 + eq(UserTypeEnum.ADMIN.getValue()), eq(42L), eq(ImNotificationWebSocketDTO.TYPE),
  233 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  234 + ImContentTypeEnum.TEXT, dto)));
214 } 235 }
215 } 236 }
216 237
  238 + private static boolean isNotification(Object object, ImConversationTypeEnum conversationType,
  239 + ImContentTypeEnum contentType, Object payload) {
  240 + if (!(object instanceof ImNotificationWebSocketDTO notification)) {
  241 + return false;
  242 + }
  243 + return conversationType.getType().equals(notification.getConversationType())
  244 + && contentType.getType().equals(notification.getContentType())
  245 + && payload == notification.getPayload();
  246 + }
  247 +
  248 + private static ImNotificationWebSocketDTO buildNotification(ImConversationTypeEnum conversationType,
  249 + ImContentTypeEnum contentType, Object payload) {
  250 + return new ImNotificationWebSocketDTO()
  251 + .setConversationType(conversationType.getType())
  252 + .setContentType(contentType.getType())
  253 + .setPayload(payload);
  254 + }
  255 +
217 } 256 }