Commit 2ab72872bee985729d6ff7bf6d68ce8d421337b1

Authored by YunaiV
1 parent e2eab4fe

✨ feat(im): 添加 WebSocket 服务和消息推送功能

新增 IM WebSocket 服务接口及实现,支持异步推送私聊和群聊消息。
同时,重构相关消息发送逻辑,提升代码可读性和维护性。

- 新增 ImWebSocketService 接口
- 实现 ImWebSocketServiceImpl 类
- 更新 ImPrivateMessageService 和 ImGroupMessageService 以使用 WebSocket 推送
Showing 19 changed files with 279 additions and 103 deletions
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java
@@ -46,4 +46,7 @@ public class ImGroupMessageRespVO { @@ -46,4 +46,7 @@ public class ImGroupMessageRespVO {
46 @Schema(description = "回执状态", example = "0") 46 @Schema(description = "回执状态", example = "0")
47 private Integer receiptStatus; 47 private Integer receiptStatus;
48 48
  49 + @Schema(description = "已读人数(回执消息、且发送人为当前用户时有值)", example = "3")
  50 + private Integer readCount;
  51 +
49 } 52 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
  3 +import cn.hutool.extra.spring.SpringUtil;
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; 5 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 6 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
6 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; 7 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
  8 +import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  9 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  10 +import cn.iocoder.yudao.module.im.service.websocket.dto.friend.FriendUpdateMessage;
  11 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
7 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 12 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
8 -import cn.hutool.extra.spring.SpringUtil;  
9 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
10 import org.springframework.cache.annotation.CacheEvict; 14 import org.springframework.cache.annotation.CacheEvict;
11 import org.springframework.cache.annotation.Cacheable; 15 import org.springframework.cache.annotation.Cacheable;
@@ -19,13 +23,17 @@ import java.util.Objects; @@ -19,13 +23,17 @@ import java.util.Objects;
19 23
20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 24 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
21 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND; 25 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND;
22 -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 26 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
  27 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
  28 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE;
  29 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE;
23 30
24 /** 31 /**
25 * IM 好友关系 Service 实现类 32 * IM 好友关系 Service 实现类
26 * 33 *
27 * @author 芋道源码 34 * @author 芋道源码
28 */ 35 */
  36 +@Slf4j
29 @Service 37 @Service
30 @Validated 38 @Validated
31 public class ImFriendServiceImpl implements ImFriendService { 39 public class ImFriendServiceImpl implements ImFriendService {
@@ -36,6 +44,11 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -36,6 +44,11 @@ public class ImFriendServiceImpl implements ImFriendService {
36 @Resource 44 @Resource
37 private AdminUserApi adminUserApi; 45 private AdminUserApi adminUserApi;
38 46
  47 + @Resource
  48 + private ImWebSocketService imWebSocketService;
  49 + @Resource
  50 + private ImPrivateMessageService privateMessageService;
  51 +
39 @Override 52 @Override
40 @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null") 53 @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null")
41 public boolean isFriend(Long userId, Long friendUserId) { 54 public boolean isFriend(Long userId, Long friendUserId) {
@@ -62,10 +75,13 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -62,10 +75,13 @@ public class ImFriendServiceImpl implements ImFriendService {
62 } 75 }
63 76
64 // 2. 更新好友属性(目前仅免打扰) 77 // 2. 更新好友属性(目前仅免打扰)
65 - ImFriendDO updateObj = new ImFriendDO();  
66 - updateObj.setId(friend.getId());  
67 - updateObj.setMuted(reqVO.getMuted());  
68 - imFriendMapper.updateById(updateObj); 78 + imFriendMapper.updateById(new ImFriendDO().setId(friend.getId()).setMuted(reqVO.getMuted()));
  79 +
  80 + // 3. 推送好友更新通知(多端同步)
  81 + FriendUpdateMessage websocketContent = new FriendUpdateMessage()
  82 + .setFriendUserId(reqVO.getFriendUserId()).setMuted(reqVO.getMuted());
  83 + imWebSocketService.sendPrivateMessageAsync(userId,
  84 + ImPrivateMessageDTO.ofFriendUpdate(userId, reqVO.getFriendUserId(), websocketContent));
69 } 85 }
70 86
71 @Override 87 @Override
@@ -82,19 +98,29 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -82,19 +98,29 @@ public class ImFriendServiceImpl implements ImFriendService {
82 getSelf().addFriend0(userId, friendUserId); 98 getSelf().addFriend0(userId, friendUserId);
83 getSelf().addFriend0(friendUserId, userId); 99 getSelf().addFriend0(friendUserId, userId);
84 100
85 - // TODO @芋艿:好友添加成功后:1)插入 TIP_TEXT 系统提示消息并推送双方;2)通知双方好友列表变更。  
86 - // 依赖 WebSocket 事件类型扩展(当前无 FRIEND_NEW),待统一后补上。 101 + // 3.1 插入 TIP_TEXT 系统提示消息并推送双方
  102 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_ADD_TIP_MESSAGE);
  103 + // 3.2 推送好友添加通知给双方
  104 + imWebSocketService.sendPrivateMessageAsync(userId,
  105 + ImPrivateMessageDTO.ofFriendAdd(friendUserId, userId));
  106 + imWebSocketService.sendPrivateMessageAsync(friendUserId,
  107 + ImPrivateMessageDTO.ofFriendAdd(userId, friendUserId));
87 } 108 }
88 109
89 @Override 110 @Override
90 @Transactional(rollbackFor = Exception.class) 111 @Transactional(rollbackFor = Exception.class)
91 public void deleteFriend(Long userId, Long friendUserId) { 112 public void deleteFriend(Long userId, Long friendUserId) {
92 - // 双向标记为 DISABLE + 记录 deleteTime 113 + // 1. 双向标记为 DISABLE + 记录 deleteTime
93 getSelf().deleteFriend0(userId, friendUserId); 114 getSelf().deleteFriend0(userId, friendUserId);
94 getSelf().deleteFriend0(friendUserId, userId); 115 getSelf().deleteFriend0(friendUserId, userId);
95 116
96 - // TODO @芋艿:【对齐】这里缺少了好友删除的系统提示 + FRIEND_DEL WebSocket 推送,  
97 - // 待 WS 事件类型扩展后补上。 117 + // 2.1 插入 TIP_TEXT 系统提示消息并推送双方
  118 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE);
  119 + // 2.2 推送好友删除通知给双方
  120 + imWebSocketService.sendPrivateMessageAsync(userId,
  121 + ImPrivateMessageDTO.ofFriendDelete(userId, friendUserId));
  122 + imWebSocketService.sendPrivateMessageAsync(friendUserId,
  123 + ImPrivateMessageDTO.ofFriendDelete(friendUserId, userId));
98 } 124 }
99 125
100 /** 126 /**
@@ -102,11 +128,14 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -102,11 +128,14 @@ public class ImFriendServiceImpl implements ImFriendService {
102 * - 首次:新增记录(status=ENABLE,addTime=now) 128 * - 首次:新增记录(status=ENABLE,addTime=now)
103 * - 已存在且 ENABLE:无需重复操作 129 * - 已存在且 ENABLE:无需重复操作
104 * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime) 130 * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime)
  131 + * <p>
  132 + * 并发安全:依靠 im_friend 表的唯一索引 uk_user_friend(user_id, friend_user_id) 保证幂等,
  133 + * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。
105 */ 134 */
106 @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId") 135 @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId")
107 public void addFriend0(Long userId, Long friendUserId) { 136 public void addFriend0(Long userId, Long friendUserId) {
108 ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); 137 ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
109 - // 情况一:恢复已删除的好友关系 138 + // 情况一:已存在记录 → 恢复或跳过
110 if (exists != null) { 139 if (exists != null) {
111 if (CommonStatusEnum.isEnable(exists.getStatus())) { 140 if (CommonStatusEnum.isEnable(exists.getStatus())) {
112 return; 141 return;
@@ -115,10 +144,16 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -115,10 +144,16 @@ public class ImFriendServiceImpl implements ImFriendService {
115 .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now())); 144 .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now()));
116 return; 145 return;
117 } 146 }
118 - // 情况二:新增好有关系 147 + // 情况二:新增好友关系
119 ImFriendDO friend = ImFriendDO.builder().userId(userId).friendUserId(friendUserId) 148 ImFriendDO friend = ImFriendDO.builder().userId(userId).friendUserId(friendUserId)
120 .muted(false).status(CommonStatusEnum.ENABLE.getStatus()).addTime(LocalDateTime.now()).build(); 149 .muted(false).status(CommonStatusEnum.ENABLE.getStatus()).addTime(LocalDateTime.now()).build();
121 - imFriendMapper.insert(friend); 150 + try {
  151 + imFriendMapper.insert(friend);
  152 + } catch (DuplicateKeyException e) {
  153 + // 并发场景:另一个请求已先一步插入,降级走已存在逻辑
  154 + log.warn("[addFriend0][userId({}) friendUserId({}) 并发插入冲突,降级处理]", userId, friendUserId);
  155 + addFriend0(userId, friendUserId);
  156 + }
122 } 157 }
123 158
124 /** 159 /**
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -6,15 +6,13 @@ import cn.hutool.core.util.IdUtil; @@ -6,15 +6,13 @@ import cn.hutool.core.util.IdUtil;
6 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
7 import cn.hutool.extra.spring.SpringUtil; 7 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
9 -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;  
10 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
12 -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;  
13 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; 11 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
14 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; 12 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
15 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
16 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 14 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
17 -import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage; 15 +import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
18 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; 16 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
19 import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; 17 import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO;
20 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 18 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
@@ -23,7 +21,8 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; @@ -23,7 +21,8 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
23 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; 21 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
24 import cn.iocoder.yudao.module.im.service.group.ImGroupService; 22 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
25 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 23 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
26 -import cn.iocoder.yudao.module.im.websocket.ImGroupMessageDTO; 24 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  25 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
27 import jakarta.annotation.Resource; 26 import jakarta.annotation.Resource;
28 import lombok.extern.slf4j.Slf4j; 27 import lombok.extern.slf4j.Slf4j;
29 import org.springframework.scheduling.annotation.Async; 28 import org.springframework.scheduling.annotation.Async;
@@ -69,7 +68,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -69,7 +68,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
69 private ImSensitiveWordService sensitiveWordService; 68 private ImSensitiveWordService sensitiveWordService;
70 69
71 @Resource 70 @Resource
72 - private WebSocketMessageSender webSocketMessageSender; 71 + private ImWebSocketService imWebSocketService;
73 72
74 @Override 73 @Override
75 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) { 74 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) {
@@ -98,7 +97,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -98,7 +97,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
98 groupMessageMapper.insert(message); 97 groupMessageMapper.insert(message);
99 98
100 // 3. WebSocket 推送给群成员(异步) 99 // 3. WebSocket 推送给群成员(异步)
101 - getSelf().sendGroupMessageEvent(message); 100 + sendGroupMessageEvent(message);
102 return message; 101 return message;
103 } 102 }
104 103
@@ -161,7 +160,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -161,7 +160,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
161 160
162 // 4. 按本轮消息最大 id 推进内部游标,跳过这段不可见区间;若游标未前进则直接停止 161 // 4. 按本轮消息最大 id 推进内部游标,跳过这段不可见区间;若游标未前进则直接停止
163 Long maxMessageId = getMaxValue(messages, ImGroupMessageDO::getId); 162 Long maxMessageId = getMaxValue(messages, ImGroupMessageDO::getId);
164 - if (maxMessageId <= activeMinId) { 163 + if (maxMessageId == null || maxMessageId <= activeMinId) {
165 return Collections.emptyList(); 164 return Collections.emptyList();
166 } 165 }
167 activeMinId = maxMessageId; 166 activeMinId = maxMessageId;
@@ -314,7 +313,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -314,7 +313,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
314 groupMessageMapper.insert(recallMessage); 313 groupMessageMapper.insert(recallMessage);
315 314
316 // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示) 315 // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示)
317 - getSelf().sendGroupMessageEvent(recallMessage); 316 + sendGroupMessageEvent(recallMessage);
318 return recallMessage; 317 return recallMessage;
319 } 318 }
320 319
@@ -363,21 +362,12 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -363,21 +362,12 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
363 /** 362 /**
364 * 发送群聊消息 WebSocket 事件 363 * 发送群聊消息 WebSocket 事件
365 */ 364 */
366 - @Async  
367 public void sendGroupMessageEvent(ImGroupMessageDO message) { 365 public void sendGroupMessageEvent(ImGroupMessageDO message) {
368 ImGroupMessageDTO dto = ImGroupMessageDTO.ofSend(message); 366 ImGroupMessageDTO dto = ImGroupMessageDTO.ofSend(message);
369 - // 广播给群内有效成员(含发送方自己,支持多端同步) 367 + // 广播给群内有效成员中的可见用户(含发送方自己,支持多端同步;定向消息自动过滤)
370 List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(message.getGroupId()); 368 List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(message.getGroupId());
371 - // 定向消息仍需过滤,非目标用户不应收到推送  
372 - List<Long> receiverList = message.getReceiverUserIds();  
373 - for (ImGroupMemberDO member : members) {  
374 - if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId())  
375 - && ObjUtil.notEqual(member.getUserId(), message.getSenderId())) {  
376 - continue;  
377 - }  
378 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), member.getUserId(),  
379 - ImGroupMessageDTO.TYPE, dto);  
380 - } 369 + Set<Long> targetUserIds = getVisibleUserIds(message, members);
  370 + imWebSocketService.sendGroupMessageAsync(targetUserIds, dto);
381 } 371 }
382 372
383 /** 373 /**
@@ -389,11 +379,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -389,11 +379,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
389 * @param newMaxMessageId 本次已读位置 379 * @param newMaxMessageId 本次已读位置
390 */ 380 */
391 @Async 381 @Async
  382 + @SuppressWarnings("DataFlowIssue")
392 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) { 383 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) {
393 // 1. 发送 READ 事件给自己的其他终端(多端同步) 384 // 1. 发送 READ 事件给自己的其他终端(多端同步)
394 - ImGroupMessageDTO readDTO = ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId);  
395 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,  
396 - ImGroupMessageDTO.TYPE, readDTO); 385 + imWebSocketService.sendGroupMessageAsync(userId,
  386 + ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId));
397 387
398 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息 388 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息
399 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt( 389 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt(
@@ -422,10 +412,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -422,10 +412,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
422 } 412 }
423 413
424 // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度) 414 // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度)
425 - ImGroupMessageDTO receiptDTO = ImGroupMessageDTO.ofReceipt(  
426 - message.getId(), groupId, readCount, newReceiptStatus);  
427 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(),  
428 - ImGroupMessageDTO.TYPE, receiptDTO); 415 + imWebSocketService.sendGroupMessageAsync(message.getSenderId(),
  416 + ImGroupMessageDTO.ofReceipt(message.getId(), groupId, readCount, newReceiptStatus));
429 } 417 }
430 } 418 }
431 419
@@ -483,6 +471,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -483,6 +471,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
483 member -> isMessageVisible(msg, member, member.getUserId())); 471 member -> isMessageVisible(msg, member, member.getUserId()));
484 } 472 }
485 473
  474 +
486 private ImGroupMessageServiceImpl getSelf() { 475 private ImGroupMessageServiceImpl getSelf() {
487 return SpringUtil.getBean(getClass()); 476 return SpringUtil.getBean(getClass());
488 } 477 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
@@ -58,4 +58,15 @@ public interface ImPrivateMessageService { @@ -58,4 +58,15 @@ public interface ImPrivateMessageService {
58 */ 58 */
59 List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO); 59 List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO);
60 60
  61 + /**
  62 + * 发送系统提示消息(TIP_TEXT)
  63 + * <p>
  64 + * 跳过好友校验、敏感词过滤,用于系统自动生成的提示消息。
  65 + *
  66 + * @param senderId 发送人编号
  67 + * @param receiverId 接收人编号
  68 + * @param content 提示文本
  69 + */
  70 + void sendTipPrivateMessage(Long senderId, Long receiverId, String content);
  71 +
61 } 72 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
@@ -3,24 +3,21 @@ package cn.iocoder.yudao.module.im.service.message; @@ -3,24 +3,21 @@ package cn.iocoder.yudao.module.im.service.message;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.IdUtil; 4 import cn.hutool.core.util.IdUtil;
5 import cn.hutool.core.util.ObjUtil; 5 import cn.hutool.core.util.ObjUtil;
6 -import cn.hutool.extra.spring.SpringUtil;  
7 -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;  
8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;  
11 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO; 8 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
12 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 9 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
14 -import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;  
15 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 11 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
16 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 13 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
18 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 14 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
19 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 15 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
20 -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; 16 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  17 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  18 +import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
21 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
22 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
23 -import org.springframework.scheduling.annotation.Async;  
24 import org.springframework.stereotype.Service; 21 import org.springframework.stereotype.Service;
25 import org.springframework.transaction.annotation.Transactional; 22 import org.springframework.transaction.annotation.Transactional;
26 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
@@ -31,9 +28,7 @@ import java.util.List; @@ -31,9 +28,7 @@ import java.util.List;
31 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 28 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
32 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 29 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
33 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 30 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
34 -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE;  
35 -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_PRIVATE_PULL_MAX_DAYS;  
36 -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; 31 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*;
37 32
38 /** 33 /**
39 * IM 私聊消息 Service 实现类 34 * IM 私聊消息 Service 实现类
@@ -54,7 +49,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -54,7 +49,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
54 private ImSensitiveWordService sensitiveWordService; 49 private ImSensitiveWordService sensitiveWordService;
55 50
56 @Resource 51 @Resource
57 - private WebSocketMessageSender webSocketMessageSender; 52 + private ImWebSocketService imWebSocketService;
58 53
59 @Override 54 @Override
60 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) { 55 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) {
@@ -80,8 +75,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -80,8 +75,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
80 .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())); 75 .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()));
81 privateMessageMapper.insert(message); 76 privateMessageMapper.insert(message);
82 77
83 - // 3. WebSocket 推送给接收方(异步)  
84 - getSelf().sendPrivateMessageEvent(message); 78 + // 3. WebSocket 异步推送(接收方 + 发送方多端同步)
  79 + ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
  80 + imWebSocketService.sendPrivateMessageAsync(message.getReceiverId(), websocketMessage);
  81 + imWebSocketService.sendPrivateMessageAsync(message.getSenderId(), websocketMessage);
85 return message; 82 return message;
86 } 83 }
87 84
@@ -116,7 +113,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -116,7 +113,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
116 113
117 // 2. 异步发送 READ + RECEIPT 事件(携带已读位置) 114 // 2. 异步发送 READ + RECEIPT 事件(携带已读位置)
118 Long maxReadId = CollUtil.max(messageIds); 115 Long maxReadId = CollUtil.max(messageIds);
119 - getSelf().readPrivateMessageEvent(userId, receiverId, maxReadId); 116 + imWebSocketService.sendPrivateMessageAsync(userId,
  117 + ImPrivateMessageDTO.ofRead(userId, receiverId, maxReadId));
  118 + imWebSocketService.sendPrivateMessageAsync(receiverId,
  119 + ImPrivateMessageDTO.ofReceipt(userId, receiverId, maxReadId));
120 } 120 }
121 121
122 @Override 122 @Override
@@ -153,52 +153,30 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -153,52 +153,30 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
153 privateMessageMapper.insert(tipMessage); 153 privateMessageMapper.insert(tipMessage);
154 154
155 // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示) 155 // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示)
156 - getSelf().sendPrivateMessageEvent(tipMessage); 156 + ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(tipMessage);
  157 + imWebSocketService.sendPrivateMessageAsync(tipMessage.getReceiverId(), websocketMessage);
  158 + imWebSocketService.sendPrivateMessageAsync(tipMessage.getSenderId(), websocketMessage);
157 return tipMessage; 159 return tipMessage;
158 } 160 }
159 161
160 - /**  
161 - * 发送私聊消息 WebSocket 事件  
162 - */  
163 - @Async  
164 - public void sendPrivateMessageEvent(ImPrivateMessageDO message) {  
165 - ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofSend(message);  
166 - // 推送给接收方  
167 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(),  
168 - ImPrivateMessageDTO.TYPE, dto);  
169 -  
170 - // 推送给发送方自己的其他终端(多端同步)  
171 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(),  
172 - ImPrivateMessageDTO.TYPE, dto);  
173 - }  
174 -  
175 - /**  
176 - * 发送已读 + 已读回执 WebSocket 事件  
177 - *  
178 - * @param userId 当前用户编号  
179 - * @param receiverId 对方用户编号  
180 - * @param maxReadId 已读位置(最大已读消息编号)  
181 - */  
182 - @Async  
183 - public void readPrivateMessageEvent(Long userId, Long receiverId, Long maxReadId) {  
184 - // 1. 发送 READ 事件给自己的其他终端(多端同步)  
185 - ImPrivateMessageDTO readDTO = ImPrivateMessageDTO.ofRead(userId, receiverId, maxReadId);  
186 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,  
187 - ImPrivateMessageDTO.TYPE, readDTO);  
188 -  
189 - // 2. 发送 RECEIPT 事件给对方(已读回执)  
190 - ImPrivateMessageDTO receiptDTO = ImPrivateMessageDTO.ofReceipt(userId, receiverId, maxReadId);  
191 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), receiverId,  
192 - ImPrivateMessageDTO.TYPE, receiptDTO);  
193 - }  
194 -  
195 @Override 162 @Override
196 public List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO) { 163 public List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO) {
197 return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit()); 164 return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit());
198 } 165 }
199 166
200 - private ImPrivateMessageServiceImpl getSelf() {  
201 - return SpringUtil.getBean(getClass()); 167 + @Override
  168 + public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) {
  169 + // 1. 插入 TIP_TEXT 消息
  170 + ImPrivateMessageDO tipMessage = new ImPrivateMessageDO()
  171 + .setClientMessageId(IdUtil.fastSimpleUUID()).setSenderId(senderId).setReceiverId(receiverId)
  172 + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content)
  173 + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now());
  174 + privateMessageMapper.insert(tipMessage);
  175 +
  176 + // 2. 推送给双方
  177 + ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofSend(tipMessage);
  178 + imWebSocketService.sendPrivateMessageAsync(receiverId, dto);
  179 + imWebSocketService.sendPrivateMessageAsync(senderId, dto);
202 } 180 }
203 181
204 } 182 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket;
  2 +
  3 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  4 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  5 +
  6 +import java.util.Collection;
  7 +import java.util.Collections;
  8 +
  9 +/**
  10 + * IM WebSocket 推送 Service 接口
  11 + * <p>
  12 + * 统一封装 WebSocket 消息推送,所有方法默认异步执行。
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public interface ImWebSocketService {
  17 +
  18 + /**
  19 + * 异步推送私聊消息给指定用户
  20 + *
  21 + * @param userId 目标用户编号
  22 + * @param dto 私聊消息 DTO
  23 + */
  24 + void sendPrivateMessageAsync(Long userId, ImPrivateMessageDTO dto);
  25 +
  26 + /**
  27 + * 异步推送群聊消息给指定用户
  28 + *
  29 + * @param userId 目标用户编号
  30 + * @param dto 群聊消息 DTO
  31 + */
  32 + default void sendGroupMessageAsync(Long userId, ImGroupMessageDTO dto) {
  33 + sendGroupMessageAsync(Collections.singleton(userId), dto);
  34 + }
  35 +
  36 + /**
  37 + * 异步批量推送群聊消息给多个用户
  38 + *
  39 + * @param userIds 目标用户编号列表
  40 + * @param dto 群聊消息 DTO
  41 + */
  42 + void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto);
  43 +
  44 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  5 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  6 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  7 +import jakarta.annotation.Resource;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.springframework.scheduling.annotation.Async;
  10 +import org.springframework.stereotype.Service;
  11 +
  12 +import java.util.Collection;
  13 +
  14 +/**
  15 + * IM WebSocket 推送 Service 实现类
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Service
  20 +@Slf4j
  21 +public class ImWebSocketServiceImpl implements ImWebSocketService {
  22 +
  23 + @Resource
  24 + private WebSocketMessageSender webSocketMessageSender;
  25 +
  26 + @Async
  27 + @Override
  28 + public void sendPrivateMessageAsync(Long userId, ImPrivateMessageDTO dto) {
  29 + try {
  30 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
  31 + ImPrivateMessageDTO.TYPE, dto);
  32 + } catch (Exception e) {
  33 + log.error("[sendPrivateMessageAsync][userId({}) dto({}) 发送失败]", userId, dto, e);
  34 + }
  35 + }
  36 +
  37 + @Async
  38 + @Override
  39 + public void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto) {
  40 + for (Long userId : userIds) {
  41 + try {
  42 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
  43 + ImGroupMessageDTO.TYPE, dto);
  44 + } catch (Exception e) {
  45 + log.error("[sendGroupMessageAsync][userId({}) dto({}) 发送失败]", userId, dto, e);
  46 + }
  47 + }
  48 + }
  49 +
  50 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java
1 -package cn.iocoder.yudao.module.im.websocket; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto;
2 2
3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java
1 -package cn.iocoder.yudao.module.im.websocket; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto;
2 2
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 6 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  7 +import cn.iocoder.yudao.module.im.service.websocket.dto.friend.FriendUpdateMessage;
6 import lombok.Data; 8 import lombok.Data;
7 import lombok.experimental.Accessors; 9 import lombok.experimental.Accessors;
8 10
@@ -100,4 +102,46 @@ public class ImPrivateMessageDTO { @@ -100,4 +102,46 @@ public class ImPrivateMessageDTO {
100 .setSenderId(senderId).setReceiverId(receiverId); 102 .setSenderId(senderId).setReceiverId(receiverId);
101 } 103 }
102 104
  105 + /**
  106 + * 构建好友添加推送 DTO(通知对方"你有一位新好友")
  107 + *
  108 + * @param userId 当前用户编号(即发起添加的用户)
  109 + * @param friendUserId 好友的用户编号(即接收通知的用户)
  110 + * @return 私聊 DTO
  111 + */
  112 + public static ImPrivateMessageDTO ofFriendAdd(Long userId, Long friendUserId) {
  113 + return new ImPrivateMessageDTO()
  114 + .setType(ImMessageTypeEnum.FRIEND_ADD.getType())
  115 + .setSenderId(userId).setReceiverId(friendUserId)
  116 + .setSendTime(LocalDateTime.now());
  117 + }
  118 +
  119 + /**
  120 + * 构建好友删除推送 DTO(通知对方"好友关系已解除")
  121 + *
  122 + * @param userId 当前用户编号(即发起删除的用户)
  123 + * @param friendUserId 好友的用户编号(即接收通知的用户)
  124 + * @return 私聊 DTO
  125 + */
  126 + public static ImPrivateMessageDTO ofFriendDelete(Long userId, Long friendUserId) {
  127 + return new ImPrivateMessageDTO()
  128 + .setType(ImMessageTypeEnum.FRIEND_DELETE.getType())
  129 + .setSenderId(userId).setReceiverId(friendUserId)
  130 + .setSendTime(LocalDateTime.now());
  131 + }
  132 + /**
  133 + * 构建好友更新推送 DTO(通知自己多端同步好友属性变更)
  134 + *
  135 + * @param userId 当前用户编号
  136 + * @param friendUserId 好友的用户编号
  137 + * @param content 更新后的好友属性
  138 + * @return 私聊 DTO
  139 + */
  140 + public static ImPrivateMessageDTO ofFriendUpdate(Long userId, Long friendUserId, FriendUpdateMessage content) {
  141 + return new ImPrivateMessageDTO()
  142 + .setType(ImMessageTypeEnum.FRIEND_UPDATE.getType())
  143 + .setSenderId(userId).setReceiverId(friendUserId)
  144 + .setContent(JsonUtils.toJsonString(content)).setSendTime(LocalDateTime.now());
  145 + }
  146 +
103 } 147 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.friend;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * 好友更新消息内容(目前仅免打扰)
  8 + */
  9 +@Data
  10 +@Accessors(chain = true)
  11 +public class FriendUpdateMessage {
  12 +
  13 + /**
  14 + * 好友的用户编号
  15 + */
  16 + private Long friendUserId;
  17 + /**
  18 + * 是否免打扰
  19 + */
  20 + private Boolean muted;
  21 +
  22 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 import lombok.experimental.Accessors; 4 import lombok.experimental.Accessors;
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java
1 -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; 1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
9 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 9 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
10 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 10 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
11 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 11 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
12 -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; 12 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
13 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.api.Test;
14 import org.mockito.ArgumentCaptor; 14 import org.mockito.ArgumentCaptor;
15 import org.mockito.InjectMocks; 15 import org.mockito.InjectMocks;