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 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 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 7 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
7 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 10 import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage;
11 11 import jakarta.annotation.Resource;
12 12 import lombok.extern.slf4j.Slf4j;
... ... @@ -52,8 +52,9 @@ public class AdminUserProfileUpdateConsumer {
52 52 try {
53 53 FriendInfoUpdatedNotification payload = (FriendInfoUpdatedNotification) new FriendInfoUpdatedNotification()
54 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 58 successCount++;
58 59 } catch (Exception e) {
59 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 14 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
15 15 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
16 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 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
19 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
20 21 import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestApprovedNotification;
... ... @@ -104,8 +105,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
104 105 if (fromUser != null) {
105 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 111 // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程
111 112 // 回调内 try/catch 兜底 —— afterCommit 异常会被 Spring 静默吞掉,否则同意失败时申请方永远等不到 APPROVED
... ... @@ -191,8 +192,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
191 192 FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification)
192 193 new FriendRequestApprovedNotification().setRequestId(request.getId())
193 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 199 @Override
... ... @@ -216,8 +217,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
216 217 new FriendRequestRejectedNotification().setRequestId(request.getId())
217 218 .setHandleContent(handleContent)
218 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 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 11 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
12 12 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
13 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 16 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
16 17 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
17 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 20 import jakarta.annotation.Resource;
21 21 import lombok.extern.slf4j.Slf4j;
22 22 import org.springframework.cache.annotation.CacheEvict;
... ... @@ -160,8 +160,8 @@ public class ImFriendServiceImpl implements ImFriendService {
160 160 FriendUpdateNotification payload = (FriendUpdateNotification) new FriendUpdateNotification()
161 161 .setDisplayName(reqVO.getDisplayName()).setSilent(reqVO.getSilent()).setPinned(reqVO.getPinned())
162 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 167 @Override
... ... @@ -183,7 +183,7 @@ public class ImFriendServiceImpl implements ImFriendService {
183 183 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
184 184 .setOperatorUserId(fromUserId).setFriendUserId(toUserId);
185 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 189 @Override
... ... @@ -202,7 +202,7 @@ public class ImFriendServiceImpl implements ImFriendService {
202 202 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
203 203 .setOperatorUserId(friendUserId).setFriendUserId(friendUserId);
204 204 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
205   - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType())
  205 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_ADD.getType())
206 206 .setContent(payload).setPersistent(false));
207 207 }
208 208  
... ... @@ -224,7 +224,7 @@ public class ImFriendServiceImpl implements ImFriendService {
224 224 FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification()
225 225 .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear);
226 226 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
227   - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_DELETE.getType())
  227 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_DELETE.getType())
228 228 .setContent(payload).setPersistent(false));
229 229 }
230 230  
... ... @@ -250,8 +250,8 @@ public class ImFriendServiceImpl implements ImFriendService {
250 250 // 3. 推 FRIEND_BLOCK 给 A 多端
251 251 FriendBlockNotification payload = (FriendBlockNotification) new FriendBlockNotification()
252 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 257 @Override
... ... @@ -276,8 +276,8 @@ public class ImFriendServiceImpl implements ImFriendService {
276 276 // 3. 推 FRIEND_UNBLOCK 给 A 多端
277 277 FriendUnblockNotification payload = (FriendUnblockNotification) new FriendUnblockNotification()
278 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 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
14 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
15 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 18 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 19 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
19 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
... ... @@ -100,8 +101,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
100 101 AdminUserRespDTO applyUser = adminUserApi.getUser(userId);
101 102 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
102 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 107 return request;
107 108 }
... ... @@ -200,8 +201,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
200 201 AdminUserRespDTO applyUser = userMap.get(request.getUserId());
201 202 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
202 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 406 Set<Long> receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group));
406 407 receivers.add(applicantUserId);
407 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 97 */
98 98 @Async
99 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 104 private ImChannelMessageServiceImpl getSelf() {
... ... @@ -121,11 +122,12 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
121 122 channelMessageMapper.insert(message);
122 123  
123 124 // 3. 异步推 WebSocket:指定用户走点对点;全员(receiverUserIds 为空)走广播
124   - ImChannelMessageDTO dto = ImChannelMessageDTO.ofSend(message);
  125 + ImChannelMessageNotification dto = ImChannelMessageNotification.ofSend(message);
125 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 129 } else {
128   - webSocketService.broadcastChannelMessageAsync(dto);
  130 + webSocketService.broadcastNotificationAsync(ImConversationTypeEnum.CHANNEL.getType(), dto.getType(), dto);
129 131 }
130 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 118 }
119 119  
120 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 124 return message;
123 125 }
124 126  
... ... @@ -149,7 +151,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
149 151 }
150 152  
151 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 157 return message;
154 158 }
155 159  
... ... @@ -347,8 +351,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
347 351 @SuppressWarnings("DataFlowIssue")
348 352 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) {
349 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 358 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息
354 359 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt(
... ... @@ -378,8 +383,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
378 383 }
379 384  
380 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 103 }
104 104  
105 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 111 return message;
110 112 }
111 113  
... ... @@ -131,11 +133,13 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
131 133 }
132 134  
133 135 // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知
134   - ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
  136 + ImPrivateMessageNotification notification = ImPrivateMessageNotification.ofSend(message);
135 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 143 return message;
140 144 }
141 145  
... ... @@ -256,10 +260,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
256 260 }
257 261  
258 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 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 56 * 并发幂等:同好友对 / 同群活跃唯一性走 {@link ImRtcCallLockRedisDAO} 分布式锁 + 锁内 SELECT 兜底;webhook 兜底走条件 UPDATE;
57 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 60 * 1602 / 1603 PARTICIPANT_CONNECTED / DISCONNECTED → {@link ImWebSocketService} 推参与方 + 群通话场景广播全群;
61 61 * 1610 RTC_CALL_START + 1611 RTC_CALL_END → {@link ImPrivateMessageService} / {@link ImGroupMessageService} 入消息流当聊天 tip
62 62 * (START 仅群通话;两者分别在 invite / cancel(leave) 事务里 INSERT,自增 id 自然保证顺序)
... ... @@ -1001,8 +1001,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1001 1001 String token = signToken(inviteeId, resolveDisplayName(invitee, inviteeId), call.getRoom());
1002 1002 ImRtcCallNotification payload = ImRtcCallNotification.ofInvite(
1003 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 1017 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
1018 1018 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
1019 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 1034 private void pushCallNoAnswerNotification(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
1035 1035 ImRtcCallNotification payload = ImRtcCallNotification.ofNoAnswer(call, operatorUserId, operator);
1036 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 1074 if (CollUtil.isEmpty(receivers)) {
1075 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 34 private WebSocketSenderApi webSocketSenderApi;
35 35  
36 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 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 112 assertNull(result);
113 113 verify(friendService).silentReAddFriend(eq(1L), eq(2L), eq("老张"), eq(1));
114 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 118 @Test
... ... @@ -157,7 +157,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
157 157 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), saved.getHandleResult());
158 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 163 @Test
... ... @@ -210,7 +210,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
210 210 any(LocalDateTime.class));
211 211 assertEquals(100L, result.getId());
212 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 216 // ========== agreeFriendRequest ==========
... ... @@ -230,7 +230,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
230 230 // 断言:双向建立好友 + 推 APPROVED 给发起方
231 231 verify(adminUserApi).validateUserList(ListUtil.of(1L, 2L));
232 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 236 @Test
... ... @@ -305,7 +305,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
305 305 eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), captor.capture());
306 306 assertEquals(ImFriendRequestHandleResultEnum.REFUSED.getResult(), captor.getValue().getHandleResult());
307 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 311 @Test
... ... @@ -320,7 +320,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
320 320 ServiceException exception = assertThrows(ServiceException.class,
321 321 () -> friendRequestService.refuseFriendRequest(2L, 100L, "x"));
322 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 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 68 assertEquals(100L, captor.getValue().getId());
69 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 74 @Test
... ... @@ -100,7 +100,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
100 100 () -> friendService.updateFriend(1L, reqVO));
101 101 assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode());
102 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 106 @Test
... ... @@ -123,7 +123,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
123 123 assertEquals("老张", captor.getValue().getDisplayName());
124 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 129 @Test
... ... @@ -138,7 +138,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
138 138 // 断言:没查记录、没触发 SQL 更新 / 没发 WebSocket 推送
139 139 verify(imFriendMapper, never()).selectByUserIdAndFriendUserId(anyLong(), anyLong());
140 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 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 116 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), anyInt(), anyLong());
117 117 verify(groupRequestMapper).insert(any(ImGroupRequestDO.class));
118 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 122 @Test
... ... @@ -149,7 +149,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
149 149 eq(ImGroupAddSourceEnum.SEARCH.getSource()), any());
150 150 assertEquals(50L, result.getId());
151 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 155 @Test
... ... @@ -208,7 +208,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
208 208 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
209 209 assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
210 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 214 @Test
... ... @@ -278,7 +278,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
278 278  
279 279 // 不写群成员;推 1506 给申请人 + 群主(同一人 1L 时去重为 1 + 申请人 2L = 2 条)
280 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 284 // ==================== createInviteRequestList ====================
... ... @@ -305,7 +305,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
305 305 // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧
306 306 ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class);
307 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 309 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值
310 310 Collection<ImGroupRequestDO> inserted = captor.getAllValues();
311 311 assertEquals(2, inserted.size());
... ... @@ -337,7 +337,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
337 337 // 断言:复用并重置旧邀请申请
338 338 verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L),
339 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 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 342 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
343 343 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
344 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 346 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
347 347 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
348 348 assertEquals(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
... ... @@ -526,7 +526,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
526 526  
527 527 // 断言:不会触发添加、不会推送
528 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 88 channelMessageService.readChannelMessages(1L, 10L, 100L);
89 89  
90 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 116 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
117 117  
118 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 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 159  
153 160 // 断言:固化快照必含发送者,推送目标补回发送者 = {1,2,3}
154 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 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 218 // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3})
212 219 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
213 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 222 .sendTime(LocalDateTime.now()).build();
216 223 when(groupMessageMapper.selectById(500L)).thenReturn(original);
217 224  
... ... @@ -237,7 +244,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
237 244 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
238 245 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
239 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 248 .sendTime(LocalDateTime.now()).build();
242 249 when(groupMessageMapper.selectById(500L)).thenReturn(original);
243 250 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> {
... ... @@ -390,9 +397,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
390 397 verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class));
391 398 verify(groupMessageMapper).insert(any(ImGroupMessageDO.class));
392 399 // 验证:给 2 个活跃成员推送撤回提示
393   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  400 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
394 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 595 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
589 596 // 断言:不入库、不推送
590 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 656  
650 657 // 断言:已读位置更新 + READ 事件
651 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 671 assertEquals(MESSAGE_GROUP_READ_DISABLED.getCode(), exception.getCode());
665 672 // 断言:已读位置不写、不推送
666 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 677 @Test
... ... @@ -726,7 +733,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
726 733  
727 734 // 断言:调用了 CAS 更新但读位置未前进 → 不推送
728 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 739 @Test
... ... @@ -787,7 +794,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
787 794 assertEquals(100L, captor.getValue().getId());
788 795 assertEquals(ImMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus());
789 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 800 @Test
... ... @@ -824,7 +831,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
824 831 // 断言:回执状态保持 PENDING,不更新 DB
825 832 verify(groupMessageMapper, never()).updateById(any(ImGroupMessageDO.class));
826 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 837 // ========== sendGroupMessage(senderId, dto):receiverUserIds 定向过滤 ==========
... ... @@ -833,17 +840,17 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
833 840 public void testSendGroupMessage_noReceiverUserIds_broadcastsToAllMembers() {
834 841 // 准备:无定向 → 应发给所有 ENABLE 成员(含发送者自己,多端同步)
835 842 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
836   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  843 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
837 844 .setContent("{\"messageId\":1}");
838 845 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
839 846 .thenReturn(List.of(1L, 2L, 3L, 4L));
840 847  
841 848 groupMessageService.sendGroupMessage(1L, dto);
842 849  
843   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  850 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
844 851 ids.size() == 4 && ids.contains(1L) && ids.contains(2L)
845 852 && ids.contains(3L) && ids.contains(4L)),
846   - any(ImGroupMessageDTO.class));
  853 + anyInt(), anyInt(), any());
847 854 }
848 855  
849 856 @Test
... ... @@ -851,7 +858,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
851 858 // 准备:定向给 {2,3},发送者 1;群成员 {1,2,3,4}
852 859 // 预期推送目标:{1,2,3}(发送者自己多端同步 + 定向列表);4 被过滤
853 860 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
854   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  861 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
855 862 .setContent("{\"messageId\":1}")
856 863 .setReceiverUserIds(List.of(2L, 3L));
857 864 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
... ... @@ -859,10 +866,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
859 866  
860 867 groupMessageService.sendGroupMessage(1L, dto);
861 868  
862   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  869 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
863 870 ids.size() == 3 && ids.contains(1L) && ids.contains(2L)
864 871 && ids.contains(3L) && !ids.contains(4L)),
865   - any(ImGroupMessageDTO.class));
  872 + anyInt(), anyInt(), any());
866 873 }
867 874  
868 875 @Test
... ... @@ -870,7 +877,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
870 877 // 边界:发送者不在群成员缓存里(理论上应先被 validateMemberInGroup 挡住,这里纯防御)
871 878 // 预期:定向用户 + 发送者自己(始终纳入快照保证多端同步);非定向成员 4 被过滤
872 879 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
873   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  880 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
874 881 .setContent("{\"messageId\":1}")
875 882 .setReceiverUserIds(List.of(2L, 3L));
876 883 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
... ... @@ -878,10 +885,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
878 885  
879 886 groupMessageService.sendGroupMessage(99L, dto);
880 887  
881   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  888 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
882 889 ids.size() == 3 && ids.contains(2L) && ids.contains(3L)
883 890 && ids.contains(99L) && !ids.contains(4L)),
884   - any(ImGroupMessageDTO.class));
  891 + anyInt(), anyInt(), any());
885 892 }
886 893  
887 894 // ========== sendGroupMessage(senderId, dto):helper 行为 ==========
... ... @@ -935,7 +942,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
935 942 groupMessageService.sendGroupMessage(1L, dto);
936 943  
937 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 948 @Test
... ... @@ -949,7 +956,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
949 956  
950 957 // 断言:不读取 active members,按调用方 targets 推送
951 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 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 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
8 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
  10 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
10 11 import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
11 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
12 13 import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
... ... @@ -16,8 +17,10 @@ import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
16 17 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
17 18 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
18 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 24 import org.junit.jupiter.api.Test;
22 25 import org.mockito.ArgumentCaptor;
23 26 import org.mockito.InjectMocks;
... ... @@ -61,7 +64,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
61 64 ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO();
62 65 reqVO.setClientMessageId("test-uuid-001");
63 66 reqVO.setReceiverId(2L);
64   - reqVO.setType(ImMessageTypeEnum.TEXT.getType());
  67 + reqVO.setType(ImContentTypeEnum.TEXT.getType());
65 68 reqVO.setContent("{\"content\":\"你好\"}");
66 69 return reqVO;
67 70 }
... ... @@ -87,7 +90,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
87 90 assertNotNull(result);
88 91 assertEquals(1L, result.getSenderId());
89 92 assertEquals(2L, result.getReceiverId());
90   - assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType());
  93 + assertEquals(ImContentTypeEnum.TEXT.getType(), result.getType());
91 94 assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
92 95 assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus(),
93 96 "用户私聊消息默认需要回执(PENDING)");
... ... @@ -98,8 +101,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
98 101 verify(sensitiveWordService).validateText(reqVO.getContent());
99 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 108 @Test
... ... @@ -187,22 +190,25 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
187 190  
188 191 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确
189 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 199 // 第一次:发给自己的 READ 事件
195 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 203 assertEquals(1L, readPayload.getSenderId());
199 204 assertEquals(2L, readPayload.getReceiverId());
200 205 assertEquals(5L, readPayload.getId(), "READ id 应为前端上报的 messageId");
201 206  
202 207 // 第二次:发给对方的 RECEIPT 事件
203 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 212 assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为前端上报的 messageId");
207 213 }
208 214  
... ... @@ -228,7 +234,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
228 234 verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class));
229 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 240 @Test
... ... @@ -304,7 +310,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
304 310 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
305 311 // 断言:不入库、不推送
306 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 316 @Test
... ... @@ -319,7 +325,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
319 325 // 断言:不更新消息状态、不推送
320 326 verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
321 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 331 @Test
... ... @@ -332,7 +338,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
332 338 privateMessageService.readPrivateMessages(1L, 2L, 5L);
333 339  
334 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 344 // ========== getMaxReadMessageId 测试 ==========
... ... @@ -397,8 +403,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
397 403 assertNotNull(message.getClientMessageId());
398 404 assertNotNull(message.getSendTime());
399 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 410 @Test
... ... @@ -410,8 +416,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
410 416 privateMessageService.sendPrivateMessage(1L, dto);
411 417  
412 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 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 143  
144 144 // 断言:成功 1 个;NO_ANSWER 信令推到主叫;不触发 endSession
145 145 assertEquals(1, result);
146   - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class));
  146 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
147 147 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any());
148 148 }
149 149  
... ... @@ -238,7 +238,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
238 238 rtcCallService.noAnswerCallCheck(100L, "r1");
239 239  
240 240 // 断言:NO_ANSWER 信令推到主叫 200L;不触发 endSession
241   - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class));
  241 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
242 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 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 68 verify(webSocketSenderApi).sendObject(
... ... @@ -79,10 +81,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
79 81 // 准备:开启事务同步
80 82 TransactionSynchronizationManager.initSynchronization();
81 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 92 verify(webSocketSenderApi, never()).sendObject(anyInt(), anyLong(), anyString(), any());
... ... @@ -115,8 +119,10 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
115 119 ImGroupMessageNotification dto = new ImGroupMessageNotification();
116 120 dto.setGroupId(10L);
117 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 127 verify(webSocketSenderApi).sendObject(
122 128 eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImNotificationWebSocketDTO.TYPE),
... ... @@ -145,7 +151,8 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
145 151 doThrow(new RuntimeException("user offline"))
146 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 157 // 2L 和 3L 也都被推送
151 158 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
... ... @@ -154,8 +161,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
154 161 }
155 162  
156 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 171 imWebSocketService.doSendNotification(Collections.emptyList(), notification);
161 172 imWebSocketService.doSendNotification(null, notification);
... ... @@ -164,8 +175,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
164 175 }
165 176  
166 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 185 imWebSocketService.doSendNotification(Arrays.asList(1L, 2L, 1L, null), notification);
171 186  
... ... @@ -187,12 +202,14 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
187 202 .thenReturn(imWebSocketService);
188 203  
189 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 207 doThrow(new RuntimeException("user offline"))
192 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 214 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
198 215 }
... ... @@ -206,12 +223,34 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
206 223  
207 224 ImGroupMessageNotification dto = new ImGroupMessageNotification();
208 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 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 }
... ...