Commit 2ab72872bee985729d6ff7bf6d68ce8d421337b1
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
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
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
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
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
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
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
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
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; |