Commit 3e38e77ee6f68da490e27f86111ecbbc865bc809
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 | } | ... | ... |