From 2ab72872bee985729d6ff7bf6d68ce8d421337b1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 22 Apr 2026 12:30:08 +0800 Subject: [PATCH] ✨ feat(im): 添加 WebSocket 服务和消息推送功能 --- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java | 3 +++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java | 24 ------------------------ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java | 24 ------------------------ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java | 28 ---------------------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java | 32 -------------------------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java | 24 ------------------------ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java | 18 ------------------ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java | 16 ---------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java | 36 ------------------------------------ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++-------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java | 43 ++++++++++++++++--------------------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java | 11 +++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java | 80 +++++++++++++++++++++++++++++--------------------------------------------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java | 22 ++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java | 24 ++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java | 24 ++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java | 28 ++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java | 32 ++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java | 24 ++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java | 18 ++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java | 16 ++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java | 119 ----------------------------------------------------------------------------------------------------------------------- yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java | 103 ------------------------------------------------------------------------------------------------------- yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java | 2 +- 29 files changed, 693 insertions(+), 517 deletions(-) delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java create mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java delete mode 100644 yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java index 428e1ca..ee8a164 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java +++ b/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 { @Schema(description = "回执状态", example = "0") private Integer receiptStatus; + @Schema(description = "已读人数(回执消息、且发送人为当前用户时有值)", example = "3") + private Integer readCount; + } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java deleted file mode 100644 index 0b399bd..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 语音消息内容 - */ -@Data -public class AudioMessage { - - /** - * 语音文件 URL - */ - private String url; - /** - * 语音时长(秒) - */ - private Integer duration; - /** - * 文件大小(字节) - */ - private Long size; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java deleted file mode 100644 index fd1d082..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 名片消息内容 - */ -@Data -public class CardMessage { - - /** - * 用户编号 - */ - private Long userId; - /** - * 用户昵称 - */ - private String nickname; - /** - * 用户头像 - */ - private String avatar; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java deleted file mode 100644 index aabba54..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 文件消息内容 - */ -@Data -public class FileMessage { - - /** - * 文件 URL - */ - private String url; - /** - * 文件名 - */ - private String name; - /** - * 文件大小(字节) - */ - private Long size; - /** - * 文件类型(扩展名) - */ - private String type; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java deleted file mode 100644 index 547937c..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 图片消息内容 - */ -@Data -public class ImageMessage { - - /** - * 图片 URL - */ - private String url; - /** - * 缩略图 URL - */ - private String thumbnailUrl; - /** - * 图片宽度 - */ - private Integer width; - /** - * 图片高度 - */ - private Integer height; - /** - * 图片大小(字节) - */ - private Long size; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java deleted file mode 100644 index fdbb7c7..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 位置消息内容 - */ -@Data -public class LocationMessage { - - /** - * 位置描述 - */ - private String description; - /** - * 经度 - */ - private Double longitude; - /** - * 纬度 - */ - private Double latitude; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java deleted file mode 100644 index a9ba575..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; -import lombok.experimental.Accessors; - -/** - * 撤回提示消息内容 - */ -@Data -@Accessors(chain = true) -public class RecallMessage { - - /** - * 被撤回的原消息编号 - */ - private Long messageId; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java deleted file mode 100644 index 73a45b0..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 文本消息内容 - */ -@Data -public class TextMessage { - - /** - * 文本内容 - */ - private String content; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java deleted file mode 100644 index 822042c..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.message.content; - -import lombok.Data; - -/** - * 视频消息内容 - */ -@Data -public class VideoMessage { - - /** - * 视频文件 URL - */ - private String url; - /** - * 视频封面 URL - */ - private String coverUrl; - /** - * 视频时长(秒) - */ - private Integer duration; - /** - * 视频宽度 - */ - private Integer width; - /** - * 视频高度 - */ - private Integer height; - /** - * 文件大小(字节) - */ - private Long size; - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 5f0b18b..d149c90 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -1,11 +1,15 @@ package cn.iocoder.yudao.module.im.service.friend; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; +import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; +import cn.iocoder.yudao.module.im.service.websocket.dto.friend.FriendUpdateMessage; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.hutool.extra.spring.SpringUtil; import jakarta.annotation.Resource; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -19,13 +23,17 @@ import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND; -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE; /** * IM 好友关系 Service 实现类 * * @author 芋道源码 */ +@Slf4j @Service @Validated public class ImFriendServiceImpl implements ImFriendService { @@ -36,6 +44,11 @@ public class ImFriendServiceImpl implements ImFriendService { @Resource private AdminUserApi adminUserApi; + @Resource + private ImWebSocketService imWebSocketService; + @Resource + private ImPrivateMessageService privateMessageService; + @Override @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null") public boolean isFriend(Long userId, Long friendUserId) { @@ -62,10 +75,13 @@ public class ImFriendServiceImpl implements ImFriendService { } // 2. 更新好友属性(目前仅免打扰) - ImFriendDO updateObj = new ImFriendDO(); - updateObj.setId(friend.getId()); - updateObj.setMuted(reqVO.getMuted()); - imFriendMapper.updateById(updateObj); + imFriendMapper.updateById(new ImFriendDO().setId(friend.getId()).setMuted(reqVO.getMuted())); + + // 3. 推送好友更新通知(多端同步) + FriendUpdateMessage websocketContent = new FriendUpdateMessage() + .setFriendUserId(reqVO.getFriendUserId()).setMuted(reqVO.getMuted()); + imWebSocketService.sendPrivateMessageAsync(userId, + ImPrivateMessageDTO.ofFriendUpdate(userId, reqVO.getFriendUserId(), websocketContent)); } @Override @@ -82,19 +98,29 @@ public class ImFriendServiceImpl implements ImFriendService { getSelf().addFriend0(userId, friendUserId); getSelf().addFriend0(friendUserId, userId); - // TODO @芋艿:好友添加成功后:1)插入 TIP_TEXT 系统提示消息并推送双方;2)通知双方好友列表变更。 - // 依赖 WebSocket 事件类型扩展(当前无 FRIEND_NEW),待统一后补上。 + // 3.1 插入 TIP_TEXT 系统提示消息并推送双方 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_ADD_TIP_MESSAGE); + // 3.2 推送好友添加通知给双方 + imWebSocketService.sendPrivateMessageAsync(userId, + ImPrivateMessageDTO.ofFriendAdd(friendUserId, userId)); + imWebSocketService.sendPrivateMessageAsync(friendUserId, + ImPrivateMessageDTO.ofFriendAdd(userId, friendUserId)); } @Override @Transactional(rollbackFor = Exception.class) public void deleteFriend(Long userId, Long friendUserId) { - // 双向标记为 DISABLE + 记录 deleteTime + // 1. 双向标记为 DISABLE + 记录 deleteTime getSelf().deleteFriend0(userId, friendUserId); getSelf().deleteFriend0(friendUserId, userId); - // TODO @芋艿:【对齐】这里缺少了好友删除的系统提示 + FRIEND_DEL WebSocket 推送, - // 待 WS 事件类型扩展后补上。 + // 2.1 插入 TIP_TEXT 系统提示消息并推送双方 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE); + // 2.2 推送好友删除通知给双方 + imWebSocketService.sendPrivateMessageAsync(userId, + ImPrivateMessageDTO.ofFriendDelete(userId, friendUserId)); + imWebSocketService.sendPrivateMessageAsync(friendUserId, + ImPrivateMessageDTO.ofFriendDelete(friendUserId, userId)); } /** @@ -102,11 +128,14 @@ public class ImFriendServiceImpl implements ImFriendService { * - 首次:新增记录(status=ENABLE,addTime=now) * - 已存在且 ENABLE:无需重复操作 * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime) + *

+ * 并发安全:依靠 im_friend 表的唯一索引 uk_user_friend(user_id, friend_user_id) 保证幂等, + * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 */ @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId") public void addFriend0(Long userId, Long friendUserId) { ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); - // 情况一:恢复已删除的好友关系 + // 情况一:已存在记录 → 恢复或跳过 if (exists != null) { if (CommonStatusEnum.isEnable(exists.getStatus())) { return; @@ -115,10 +144,16 @@ public class ImFriendServiceImpl implements ImFriendService { .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now())); return; } - // 情况二:新增好有关系 + // 情况二:新增好友关系 ImFriendDO friend = ImFriendDO.builder().userId(userId).friendUserId(friendUserId) .muted(false).status(CommonStatusEnum.ENABLE.getStatus()).addTime(LocalDateTime.now()).build(); - imFriendMapper.insert(friend); + try { + imFriendMapper.insert(friend); + } catch (DuplicateKeyException e) { + // 并发场景:另一个请求已先一步插入,降级走已存在逻辑 + log.warn("[addFriend0][userId({}) friendUserId({}) 并发插入冲突,降级处理]", userId, friendUserId); + addFriend0(userId, friendUserId); + } } /** diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 5a7cc89..6efc128 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/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; import cn.hutool.core.util.ObjUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage; +import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; @@ -23,7 +21,8 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; import cn.iocoder.yudao.module.im.service.group.ImGroupService; import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; -import cn.iocoder.yudao.module.im.websocket.ImGroupMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -69,7 +68,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { private ImSensitiveWordService sensitiveWordService; @Resource - private WebSocketMessageSender webSocketMessageSender; + private ImWebSocketService imWebSocketService; @Override public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) { @@ -98,7 +97,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { groupMessageMapper.insert(message); // 3. WebSocket 推送给群成员(异步) - getSelf().sendGroupMessageEvent(message); + sendGroupMessageEvent(message); return message; } @@ -161,7 +160,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { // 4. 按本轮消息最大 id 推进内部游标,跳过这段不可见区间;若游标未前进则直接停止 Long maxMessageId = getMaxValue(messages, ImGroupMessageDO::getId); - if (maxMessageId <= activeMinId) { + if (maxMessageId == null || maxMessageId <= activeMinId) { return Collections.emptyList(); } activeMinId = maxMessageId; @@ -314,7 +313,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { groupMessageMapper.insert(recallMessage); // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示) - getSelf().sendGroupMessageEvent(recallMessage); + sendGroupMessageEvent(recallMessage); return recallMessage; } @@ -363,21 +362,12 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { /** * 发送群聊消息 WebSocket 事件 */ - @Async public void sendGroupMessageEvent(ImGroupMessageDO message) { ImGroupMessageDTO dto = ImGroupMessageDTO.ofSend(message); - // 广播给群内有效成员(含发送方自己,支持多端同步) + // 广播给群内有效成员中的可见用户(含发送方自己,支持多端同步;定向消息自动过滤) List members = groupMemberService.getActiveGroupMemberListByGroupId(message.getGroupId()); - // 定向消息仍需过滤,非目标用户不应收到推送 - List receiverList = message.getReceiverUserIds(); - for (ImGroupMemberDO member : members) { - if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId()) - && ObjUtil.notEqual(member.getUserId(), message.getSenderId())) { - continue; - } - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), member.getUserId(), - ImGroupMessageDTO.TYPE, dto); - } + Set targetUserIds = getVisibleUserIds(message, members); + imWebSocketService.sendGroupMessageAsync(targetUserIds, dto); } /** @@ -389,11 +379,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { * @param newMaxMessageId 本次已读位置 */ @Async + @SuppressWarnings("DataFlowIssue") public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) { // 1. 发送 READ 事件给自己的其他终端(多端同步) - ImGroupMessageDTO readDTO = ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId); - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, - ImGroupMessageDTO.TYPE, readDTO); + imWebSocketService.sendGroupMessageAsync(userId, + ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId)); // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息 List pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt( @@ -422,10 +412,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { } // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度) - ImGroupMessageDTO receiptDTO = ImGroupMessageDTO.ofReceipt( - message.getId(), groupId, readCount, newReceiptStatus); - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(), - ImGroupMessageDTO.TYPE, receiptDTO); + imWebSocketService.sendGroupMessageAsync(message.getSenderId(), + ImGroupMessageDTO.ofReceipt(message.getId(), groupId, readCount, newReceiptStatus)); } } @@ -483,6 +471,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { member -> isMessageVisible(msg, member, member.getUserId())); } + private ImGroupMessageServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java index d8d2d3e..0f21bd1 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java +++ b/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 { */ List getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO); + /** + * 发送系统提示消息(TIP_TEXT) + *

+ * 跳过好友校验、敏感词过滤,用于系统自动生成的提示消息。 + * + * @param senderId 发送人编号 + * @param receiverId 接收人编号 + * @param content 提示文本 + */ + void sendTipPrivateMessage(Long senderId, Long receiverId, String content); + } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java index ba4627c..64ee4a0 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java +++ b/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; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO; import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage; import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -31,9 +28,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_PRIVATE_PULL_MAX_DAYS; -import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; /** * IM 私聊消息 Service 实现类 @@ -54,7 +49,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { private ImSensitiveWordService sensitiveWordService; @Resource - private WebSocketMessageSender webSocketMessageSender; + private ImWebSocketService imWebSocketService; @Override public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) { @@ -80,8 +75,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())); privateMessageMapper.insert(message); - // 3. WebSocket 推送给接收方(异步) - getSelf().sendPrivateMessageEvent(message); + // 3. WebSocket 异步推送(接收方 + 发送方多端同步) + ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message); + imWebSocketService.sendPrivateMessageAsync(message.getReceiverId(), websocketMessage); + imWebSocketService.sendPrivateMessageAsync(message.getSenderId(), websocketMessage); return message; } @@ -116,7 +113,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { // 2. 异步发送 READ + RECEIPT 事件(携带已读位置) Long maxReadId = CollUtil.max(messageIds); - getSelf().readPrivateMessageEvent(userId, receiverId, maxReadId); + imWebSocketService.sendPrivateMessageAsync(userId, + ImPrivateMessageDTO.ofRead(userId, receiverId, maxReadId)); + imWebSocketService.sendPrivateMessageAsync(receiverId, + ImPrivateMessageDTO.ofReceipt(userId, receiverId, maxReadId)); } @Override @@ -153,52 +153,30 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { privateMessageMapper.insert(tipMessage); // 4. 异步推送撤回提示消息(前端据此更新原消息状态 + 插入撤回提示) - getSelf().sendPrivateMessageEvent(tipMessage); + ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(tipMessage); + imWebSocketService.sendPrivateMessageAsync(tipMessage.getReceiverId(), websocketMessage); + imWebSocketService.sendPrivateMessageAsync(tipMessage.getSenderId(), websocketMessage); return tipMessage; } - /** - * 发送私聊消息 WebSocket 事件 - */ - @Async - public void sendPrivateMessageEvent(ImPrivateMessageDO message) { - ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofSend(message); - // 推送给接收方 - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), - ImPrivateMessageDTO.TYPE, dto); - - // 推送给发送方自己的其他终端(多端同步) - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(), - ImPrivateMessageDTO.TYPE, dto); - } - - /** - * 发送已读 + 已读回执 WebSocket 事件 - * - * @param userId 当前用户编号 - * @param receiverId 对方用户编号 - * @param maxReadId 已读位置(最大已读消息编号) - */ - @Async - public void readPrivateMessageEvent(Long userId, Long receiverId, Long maxReadId) { - // 1. 发送 READ 事件给自己的其他终端(多端同步) - ImPrivateMessageDTO readDTO = ImPrivateMessageDTO.ofRead(userId, receiverId, maxReadId); - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, - ImPrivateMessageDTO.TYPE, readDTO); - - // 2. 发送 RECEIPT 事件给对方(已读回执) - ImPrivateMessageDTO receiptDTO = ImPrivateMessageDTO.ofReceipt(userId, receiverId, maxReadId); - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), receiverId, - ImPrivateMessageDTO.TYPE, receiptDTO); - } - @Override public List getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO) { return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit()); } - private ImPrivateMessageServiceImpl getSelf() { - return SpringUtil.getBean(getClass()); + @Override + public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) { + // 1. 插入 TIP_TEXT 消息 + ImPrivateMessageDO tipMessage = new ImPrivateMessageDO() + .setClientMessageId(IdUtil.fastSimpleUUID()).setSenderId(senderId).setReceiverId(receiverId) + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content) + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()); + privateMessageMapper.insert(tipMessage); + + // 2. 推送给双方 + ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofSend(tipMessage); + imWebSocketService.sendPrivateMessageAsync(receiverId, dto); + imWebSocketService.sendPrivateMessageAsync(senderId, dto); } } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java new file mode 100644 index 0000000..f7457fe --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.im.service.websocket; + +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; + +import java.util.Collection; +import java.util.Collections; + +/** + * IM WebSocket 推送 Service 接口 + *

+ * 统一封装 WebSocket 消息推送,所有方法默认异步执行。 + * + * @author 芋道源码 + */ +public interface ImWebSocketService { + + /** + * 异步推送私聊消息给指定用户 + * + * @param userId 目标用户编号 + * @param dto 私聊消息 DTO + */ + void sendPrivateMessageAsync(Long userId, ImPrivateMessageDTO dto); + + /** + * 异步推送群聊消息给指定用户 + * + * @param userId 目标用户编号 + * @param dto 群聊消息 DTO + */ + default void sendGroupMessageAsync(Long userId, ImGroupMessageDTO dto) { + sendGroupMessageAsync(Collections.singleton(userId), dto); + } + + /** + * 异步批量推送群聊消息给多个用户 + * + * @param userIds 目标用户编号列表 + * @param dto 群聊消息 DTO + */ + void sendGroupMessageAsync(Collection userIds, ImGroupMessageDTO dto); + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java new file mode 100644 index 0000000..93642bb --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.im.service.websocket; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +/** + * IM WebSocket 推送 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class ImWebSocketServiceImpl implements ImWebSocketService { + + @Resource + private WebSocketMessageSender webSocketMessageSender; + + @Async + @Override + public void sendPrivateMessageAsync(Long userId, ImPrivateMessageDTO dto) { + try { + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, + ImPrivateMessageDTO.TYPE, dto); + } catch (Exception e) { + log.error("[sendPrivateMessageAsync][userId({}) dto({}) 发送失败]", userId, dto, e); + } + } + + @Async + @Override + public void sendGroupMessageAsync(Collection userIds, ImGroupMessageDTO dto) { + for (Long userId : userIds) { + try { + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, + ImGroupMessageDTO.TYPE, dto); + } catch (Exception e) { + log.error("[sendGroupMessageAsync][userId({}) dto({}) 发送失败]", userId, dto, e); + } + } + } + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java new file mode 100644 index 0000000..2a83c84 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * IM 群聊消息 WebSocket 统一推送 DTO + *

+ * 发送、已读、回执都通过 {@link #type} 字段区分, + * 并统一复用 {@link #id} 字段表达目标消息或已读位置。 + * + * @author 芋道源码 + */ +@Data +public class ImGroupMessageDTO { + + public static final String TYPE = "im-group-message"; + + /** + * 消息编号 + *

+ * 普通消息:当前消息 id + * READ:无(群已读不需要) + * RECEIPT:需要回执的目标消息 id + */ + private Long id; + /** + * 客户端消息编号 + */ + private String clientMessageId; + /** + * 发送人编号 + */ + private Long senderId; + /** + * 群编号 + */ + private Long groupId; + /** + * 消息类型 + */ + private Integer type; + /** + * 消息内容 + */ + private String content; + /** + * 消息状态 + */ + private Integer status; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * @ 目标用户编号列表 + */ + private List atUserIds; + /** + * 定向接收用户编号列表 + */ + private List receiverUserIds; + /** + * 群回执状态 + */ + private Integer receiptStatus; + /** + * 群回执已读人数 + */ + private Integer readCount; + + // ========== 静态工厂方法 ========== + + /** + * 构建发送消息 DTO + * + * @param message 群聊消息 DO + * @return 群聊 DTO + */ + public static ImGroupMessageDTO ofSend(ImGroupMessageDO message) { + return BeanUtils.toBean(message, ImGroupMessageDTO.class); + } + + /** + * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个群") + * + * @param senderId 当前用户编号 + * @param groupId 群编号 + * @param readId 已读位置(最大已读消息编号) + * @return 群聊 DTO + */ + public static ImGroupMessageDTO ofRead(Long senderId, Long groupId, Long readId) { + return new ImGroupMessageDTO() + .setId(readId).setType(ImMessageTypeEnum.READ.getType()) + .setSenderId(senderId).setGroupId(groupId); + } + + /** + * 构建群回执 DTO(广播回执状态和已读人数) + * + * @param messageId 消息编号 + * @param groupId 群编号 + * @param readCount 已读人数 + * @param receiptStatus 回执状态 + * @return 群聊 DTO + */ + public static ImGroupMessageDTO ofReceipt(Long messageId, Long groupId, + Integer readCount, Integer receiptStatus) { + return new ImGroupMessageDTO() + .setId(messageId).setType(ImMessageTypeEnum.RECEIPT.getType()) + .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus); + } + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java new file mode 100644 index 0000000..9aa9339 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java @@ -0,0 +1,147 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; +import cn.iocoder.yudao.module.im.service.websocket.dto.friend.FriendUpdateMessage; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * IM 私聊消息 WebSocket 统一推送 DTO + *

+ * 发送、已读、回执、撤回都通过 {@link #type} 字段区分, + * 并统一复用 {@link #id} 字段表达目标消息或已读位置。 + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class ImPrivateMessageDTO { + + public static final String TYPE = "im-private-message"; + + /** + * 消息编号 + *

+ * 普通消息:当前消息 id + * READ:已读位置(我已读到这条消息) + * RECEIPT:已读位置(对方已读到这条消息) + * RECALL:被撤回的原消息 id + */ + private Long id; + /** + * 客户端消息编号 + */ + private String clientMessageId; + /** + * 发送人编号 + */ + private Long senderId; + /** + * 接收人编号 + */ + private Long receiverId; + /** + * 消息类型 + */ + private Integer type; + /** + * 消息内容 + */ + private String content; + /** + * 消息状态 + */ + private Integer status; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + + // ========== 静态工厂方法 ========== + + /** + * 构建发送消息 DTO + * + * @param message 私聊消息 DO + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofSend(ImPrivateMessageDO message) { + return BeanUtils.toBean(message, ImPrivateMessageDTO.class); + } + + /** + * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个会话") + * + * @param senderId 当前用户编号 + * @param receiverId 对方用户编号 + * @param readId 已读位置(最大已读消息编号) + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofRead(Long senderId, Long receiverId, Long readId) { + return new ImPrivateMessageDTO() + .setId(readId).setType(ImMessageTypeEnum.READ.getType()) + .setSenderId(senderId).setReceiverId(receiverId); + } + + /** + * 构建已读回执 DTO(通知对方"我已读了你的消息") + * + * @param senderId 已读方的用户编号 + * @param receiverId 对方用户编号 + * @param readId 已读位置(最大已读消息编号) + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofReceipt(Long senderId, Long receiverId, Long readId) { + return new ImPrivateMessageDTO() + .setId(readId).setType(ImMessageTypeEnum.RECEIPT.getType()) + .setSenderId(senderId).setReceiverId(receiverId); + } + + /** + * 构建好友添加推送 DTO(通知对方"你有一位新好友") + * + * @param userId 当前用户编号(即发起添加的用户) + * @param friendUserId 好友的用户编号(即接收通知的用户) + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofFriendAdd(Long userId, Long friendUserId) { + return new ImPrivateMessageDTO() + .setType(ImMessageTypeEnum.FRIEND_ADD.getType()) + .setSenderId(userId).setReceiverId(friendUserId) + .setSendTime(LocalDateTime.now()); + } + + /** + * 构建好友删除推送 DTO(通知对方"好友关系已解除") + * + * @param userId 当前用户编号(即发起删除的用户) + * @param friendUserId 好友的用户编号(即接收通知的用户) + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofFriendDelete(Long userId, Long friendUserId) { + return new ImPrivateMessageDTO() + .setType(ImMessageTypeEnum.FRIEND_DELETE.getType()) + .setSenderId(userId).setReceiverId(friendUserId) + .setSendTime(LocalDateTime.now()); + } + /** + * 构建好友更新推送 DTO(通知自己多端同步好友属性变更) + * + * @param userId 当前用户编号 + * @param friendUserId 好友的用户编号 + * @param content 更新后的好友属性 + * @return 私聊 DTO + */ + public static ImPrivateMessageDTO ofFriendUpdate(Long userId, Long friendUserId, FriendUpdateMessage content) { + return new ImPrivateMessageDTO() + .setType(ImMessageTypeEnum.FRIEND_UPDATE.getType()) + .setSenderId(userId).setReceiverId(friendUserId) + .setContent(JsonUtils.toJsonString(content)).setSendTime(LocalDateTime.now()); + } + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java new file mode 100644 index 0000000..c818cbf --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/friend/FriendUpdateMessage.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.friend; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 好友更新消息内容(目前仅免打扰) + */ +@Data +@Accessors(chain = true) +public class FriendUpdateMessage { + + /** + * 好友的用户编号 + */ + private Long friendUserId; + /** + * 是否免打扰 + */ + private Boolean muted; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java new file mode 100644 index 0000000..884e1b0 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 语音消息内容 + */ +@Data +public class AudioMessage { + + /** + * 语音文件 URL + */ + private String url; + /** + * 语音时长(秒) + */ + private Integer duration; + /** + * 文件大小(字节) + */ + private Long size; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java new file mode 100644 index 0000000..e567b27 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 名片消息内容 + */ +@Data +public class CardMessage { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java new file mode 100644 index 0000000..7965179 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 文件消息内容 + */ +@Data +public class FileMessage { + + /** + * 文件 URL + */ + private String url; + /** + * 文件名 + */ + private String name; + /** + * 文件大小(字节) + */ + private Long size; + /** + * 文件类型(扩展名) + */ + private String type; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java new file mode 100644 index 0000000..c3d272d --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 图片消息内容 + */ +@Data +public class ImageMessage { + + /** + * 图片 URL + */ + private String url; + /** + * 缩略图 URL + */ + private String thumbnailUrl; + /** + * 图片宽度 + */ + private Integer width; + /** + * 图片高度 + */ + private Integer height; + /** + * 图片大小(字节) + */ + private Long size; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java new file mode 100644 index 0000000..8c36d1a --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 位置消息内容 + */ +@Data +public class LocationMessage { + + /** + * 位置描述 + */ + private String description; + /** + * 经度 + */ + private Double longitude; + /** + * 纬度 + */ + private Double latitude; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java new file mode 100644 index 0000000..5df78f2 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 撤回提示消息内容 + */ +@Data +@Accessors(chain = true) +public class RecallMessage { + + /** + * 被撤回的原消息编号 + */ + private Long messageId; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java new file mode 100644 index 0000000..747fabd --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 文本消息内容 + */ +@Data +public class TextMessage { + + /** + * 文本内容 + */ + private String content; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java new file mode 100644 index 0000000..3aac314 --- /dev/null +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.message; + +import lombok.Data; + +/** + * 视频消息内容 + */ +@Data +public class VideoMessage { + + /** + * 视频文件 URL + */ + private String url; + /** + * 视频封面 URL + */ + private String coverUrl; + /** + * 视频时长(秒) + */ + private Integer duration; + /** + * 视频宽度 + */ + private Integer width; + /** + * 视频高度 + */ + private Integer height; + /** + * 文件大小(字节) + */ + private Long size; + +} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java deleted file mode 100644 index bc9fd2a..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageDTO.java +++ /dev/null @@ -1,119 +0,0 @@ -package cn.iocoder.yudao.module.im.websocket; - -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; -import lombok.Data; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * IM 群聊消息 WebSocket 统一推送 DTO - *

- * 发送、已读、回执都通过 {@link #type} 字段区分, - * 并统一复用 {@link #id} 字段表达目标消息或已读位置。 - * - * @author 芋道源码 - */ -@Data -public class ImGroupMessageDTO { - - public static final String TYPE = "im-group-message"; - - /** - * 消息编号 - *

- * 普通消息:当前消息 id - * READ:无(群已读不需要) - * RECEIPT:需要回执的目标消息 id - */ - private Long id; - /** - * 客户端消息编号 - */ - private String clientMessageId; - /** - * 发送人编号 - */ - private Long senderId; - /** - * 群编号 - */ - private Long groupId; - /** - * 消息类型 - */ - private Integer type; - /** - * 消息内容 - */ - private String content; - /** - * 消息状态 - */ - private Integer status; - /** - * 发送时间 - */ - private LocalDateTime sendTime; - /** - * @ 目标用户编号列表 - */ - private List atUserIds; - /** - * 定向接收用户编号列表 - */ - private List receiverUserIds; - /** - * 群回执状态 - */ - private Integer receiptStatus; - /** - * 群回执已读人数 - */ - private Integer readCount; - - // ========== 静态工厂方法 ========== - - /** - * 构建发送消息 DTO - * - * @param message 群聊消息 DO - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofSend(ImGroupMessageDO message) { - return BeanUtils.toBean(message, ImGroupMessageDTO.class); - } - - /** - * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个群") - * - * @param senderId 当前用户编号 - * @param groupId 群编号 - * @param readId 已读位置(最大已读消息编号) - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofRead(Long senderId, Long groupId, Long readId) { - return new ImGroupMessageDTO() - .setId(readId).setType(ImMessageTypeEnum.READ.getType()) - .setSenderId(senderId).setGroupId(groupId); - } - - /** - * 构建群回执 DTO(广播回执状态和已读人数) - * - * @param messageId 消息编号 - * @param groupId 群编号 - * @param readCount 已读人数 - * @param receiptStatus 回执状态 - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofReceipt(Long messageId, Long groupId, - Integer readCount, Integer receiptStatus) { - return new ImGroupMessageDTO() - .setId(messageId).setType(ImMessageTypeEnum.RECEIPT.getType()) - .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus); - } - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java deleted file mode 100644 index 471c75f..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java +++ /dev/null @@ -1,103 +0,0 @@ -package cn.iocoder.yudao.module.im.websocket; - -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.time.LocalDateTime; - -/** - * IM 私聊消息 WebSocket 统一推送 DTO - *

- * 发送、已读、回执、撤回都通过 {@link #type} 字段区分, - * 并统一复用 {@link #id} 字段表达目标消息或已读位置。 - * - * @author 芋道源码 - */ -@Data -@Accessors(chain = true) -public class ImPrivateMessageDTO { - - public static final String TYPE = "im-private-message"; - - /** - * 消息编号 - *

- * 普通消息:当前消息 id - * READ:已读位置(我已读到这条消息) - * RECEIPT:已读位置(对方已读到这条消息) - * RECALL:被撤回的原消息 id - */ - private Long id; - /** - * 客户端消息编号 - */ - private String clientMessageId; - /** - * 发送人编号 - */ - private Long senderId; - /** - * 接收人编号 - */ - private Long receiverId; - /** - * 消息类型 - */ - private Integer type; - /** - * 消息内容 - */ - private String content; - /** - * 消息状态 - */ - private Integer status; - /** - * 发送时间 - */ - private LocalDateTime sendTime; - - // ========== 静态工厂方法 ========== - - /** - * 构建发送消息 DTO - * - * @param message 私聊消息 DO - * @return 私聊 DTO - */ - public static ImPrivateMessageDTO ofSend(ImPrivateMessageDO message) { - return BeanUtils.toBean(message, ImPrivateMessageDTO.class); - } - - /** - * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个会话") - * - * @param senderId 当前用户编号 - * @param receiverId 对方用户编号 - * @param readId 已读位置(最大已读消息编号) - * @return 私聊 DTO - */ - public static ImPrivateMessageDTO ofRead(Long senderId, Long receiverId, Long readId) { - return new ImPrivateMessageDTO() - .setId(readId).setType(ImMessageTypeEnum.READ.getType()) - .setSenderId(senderId).setReceiverId(receiverId); - } - - /** - * 构建已读回执 DTO(通知对方"我已读了你的消息") - * - * @param senderId 已读方的用户编号 - * @param receiverId 对方用户编号 - * @param readId 已读位置(最大已读消息编号) - * @return 私聊 DTO - */ - public static ImPrivateMessageDTO ofReceipt(Long senderId, Long receiverId, Long readId) { - return new ImPrivateMessageDTO() - .setId(readId).setType(ImMessageTypeEnum.RECEIPT.getType()) - .setSenderId(senderId).setReceiverId(receiverId); - } - -} diff --git a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java index d8d7299..3dd887a 100644 --- a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java +++ b/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; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; -- libgit2 0.21.4