diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java index 92db90dd1e..e70a32f7be 100644 --- a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java +++ b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java @@ -13,7 +13,8 @@ import lombok.Getter; public enum ImConversationTypeEnum { PRIVATE(1, "单聊"), - GROUP(2, "群聊"); + GROUP(2, "群聊"), + NOTICE(4, "通知会话"); /** * 类型 diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationPageReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationPageReqVO.java index 152be2980b..68f1416760 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationPageReqVO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationPageReqVO.java @@ -24,7 +24,7 @@ public class ImConversationPageReqVO extends PageParam { private Integer conversationType; @Schema(description = "单聊时,用户编号;群聊时,群编号", example = "21454") - private String targetId; + private Long targetId; @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId") private String no; diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationRespVO.java index 682ce2e811..6d63dbc421 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationRespVO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationRespVO.java @@ -26,7 +26,7 @@ public class ImConversationRespVO { @Schema(description = "单聊时,用户编号;群聊时,群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21454") @ExcelProperty("单聊时,用户编号;群聊时,群编号") - private String targetId; + private Long targetId; @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId") diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationSaveReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationSaveReqVO.java index 4282846597..c2aaa8f1ce 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationSaveReqVO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationSaveReqVO.java @@ -24,7 +24,7 @@ public class ImConversationSaveReqVO { @Schema(description = "单聊时,用户编号;群聊时,群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21454") @NotEmpty(message = "单聊时,用户编号;群聊时,群编号不能为空") - private String targetId; + private Long targetId; @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId不能为空") diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ImConversationDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ImConversationDO.java index 822b78f450..f41f274ce6 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ImConversationDO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ImConversationDO.java @@ -39,7 +39,7 @@ public class ImConversationDO extends BaseDO { /** * 单聊时,用户编号;群聊时,群编号 */ - private String targetId; + private Long targetId; /** * 会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId */ diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationService.java index 504591d95e..676fe60821 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationService.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationService.java @@ -52,4 +52,11 @@ public interface ImConversationService { PageResult getConversationPage(ImConversationPageReqVO pageReqVO); + /** + * 保存私聊会话 + * + * @param fromUserId 发送者 + * @param receiverId 接收者 + */ + void savePrivateConversation(Long fromUserId, Long receiverId); } \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationServiceImpl.java index 8ff151441e..3f7634220e 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationServiceImpl.java @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversatio import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationSaveReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationDO; import cn.iocoder.yudao.module.im.dal.mysql.conversation.ConversationMapper; +import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -67,4 +69,28 @@ public class ImConversationServiceImpl implements ImConversationService { return conversationMapper.selectPage(pageReqVO); } + @Override + public void savePrivateConversation(Long fromUserId, Long receiverId) { + // 创建并保存会话 + createAndSaveConversation(fromUserId, receiverId); + createAndSaveConversation(receiverId, fromUserId); + } + + private void createAndSaveConversation(Long userId, Long targetId) { + // 创建会话 + ImConversationDO conversation = new ImConversationDO(); + conversation.setUserId(userId); + conversation.setConversationType(ImConversationTypeEnum.PRIVATE.getType()); + conversation.setTargetId(targetId); + conversation.setNo("s_" + userId + "_" + targetId); + conversation.setPinned(false); + + // 根据 no 查询是否存在,不存在则新增 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("no", conversation.getNo()); + if (conversationMapper.selectOne(queryWrapper) == null) { + conversationMapper.insert(conversation); + } + } + } \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java index 073f89d9d2..e1a3e87983 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java @@ -60,7 +60,7 @@ public interface ImMessageService { * @param fromUserId 发送人编号 * @return id */ - Long savePrivateMessage(ImSendMessage imSendMessage, Long fromUserId); + ImMessageDO savePrivateMessage(ImSendMessage imSendMessage, Long fromUserId); /** * 更新消息状态 diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java index 8e08493e09..d946ddc1f7 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java @@ -76,24 +76,25 @@ public class ImMessageServiceImpl implements ImMessageService { @Override - public Long savePrivateMessage(ImSendMessage message, Long senderId) { - ImMessageSaveReqVO imMessageSaveReqVO = new ImMessageSaveReqVO(); - imMessageSaveReqVO.setClientMessageId(message.getClientMessageId()); - imMessageSaveReqVO.setSenderId(senderId); - imMessageSaveReqVO.setReceiverId(message.getReceiverId()); + public ImMessageDO savePrivateMessage(ImSendMessage message, Long senderId) { + ImMessageDO imMessageDO = new ImMessageDO(); + imMessageDO.setClientMessageId(message.getClientMessageId()); + imMessageDO.setSenderId(senderId); + imMessageDO.setReceiverId(message.getReceiverId()); //查询发送人昵称和发送人头像 AdminUserRespDTO user = adminUserApi.getUser(senderId); - imMessageSaveReqVO.setSenderNickname(user.getNickname()); - imMessageSaveReqVO.setSenderAvatar(user.getAvatar()); - imMessageSaveReqVO.setConversationType(message.getConversationType()); - imMessageSaveReqVO.setContentType(message.getContentType()); - imMessageSaveReqVO.setConversationNo(senderId + "_" + message.getReceiverId()); - imMessageSaveReqVO.setContent(message.getContent()); + imMessageDO.setSenderNickname(user.getNickname()); + imMessageDO.setSenderAvatar(user.getAvatar()); + imMessageDO.setConversationType(message.getConversationType()); + imMessageDO.setContentType(message.getContentType()); + imMessageDO.setConversationNo(senderId + "_" + message.getReceiverId()); + imMessageDO.setContent(message.getContent()); //消息来源 100-用户发送;200-系统发送(一般是通知);不能为空 - imMessageSaveReqVO.setSendFrom(100); - imMessageSaveReqVO.setSendTime(TimeUtil.now()); - imMessageSaveReqVO.setMessageStatus(ImMessageStatusEnum.SENDING.getStatus()); - return createMessage(imMessageSaveReqVO); + imMessageDO.setSendFrom(100); + imMessageDO.setSendTime(TimeUtil.now()); + imMessageDO.setMessageStatus(ImMessageStatusEnum.SENDING.getStatus()); + imMessageMapper.insert(imMessageDO); + return imMessageDO; } @Override diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java index 7263eb67a3..6bcb175fcc 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.im.websocket; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; -import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.ImInboxSaveReqVO; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; import cn.iocoder.yudao.module.im.dal.redis.inbox.SequenceGeneratorRedisDao; import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; @@ -19,8 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; -import java.util.List; - /** * WebSocket im * @@ -31,55 +29,100 @@ import java.util.List; public class ImWebSocketMessageListener implements WebSocketMessageListener { @Resource - private WebSocketMessageSender webSocketMessageSender; + private WebSocketMessageSender webSocketMessageSender; // WebSocket消息发送器 @Resource - private ImMessageService imMessageService; + private ImMessageService imMessageService; // IM消息服务 @Resource - private ImConversationService imConversationService; + private ImConversationService imConversationService; // IM会话服务 @Resource - private ImInboxService imInboxService; + private ImInboxService imInboxService; // IM收件箱服务 @Resource - private SequenceGeneratorRedisDao sequenceGeneratorRedisDao; - @Resource - private WebSocketSessionManager webSocketSessionManager; + private SequenceGeneratorRedisDao sequenceGeneratorRedisDao; // 序列生成器Redis DAO + /** + * 处理WebSocket消息 + * + * @param session WebSocket会话 + * @param message 发送的IM消息 + */ @Override public void onMessage(WebSocketSession session, ImSendMessage message) { - Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session); - //1、插入消息表 - Long messageId = imMessageService.savePrivateMessage(message, fromUserId); + Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session); // 获取登录用户ID - // 私聊 + // 如果是私人消息,处理私人消息 if (message.getConversationType().equals(ImConversationTypeEnum.PRIVATE.getType())) { - //2、插入收件箱表(私聊:两条,群聊:每个群有一条) - imInboxService.createInbox(new ImInboxSaveReqVO(message.getReceiverId(), messageId, sequenceGeneratorRedisDao.generateSequence(message.getReceiverId()))); - imInboxService.createInbox(new ImInboxSaveReqVO(fromUserId, messageId, sequenceGeneratorRedisDao.generateSequence(fromUserId))); - - //3、推送消息 - // 3.1判断是否在线 - List sessions = (List) webSocketSessionManager.getSessionList(UserTypeEnum.ADMIN.getValue(), message.getReceiverId()); - if (sessions.isEmpty()) { - //更新消息状态,为发送失败 - imMessageService.updateMessageStatus(messageId, ImMessageStatusEnum.FAILURE.getStatus()); - return; - } - //3.2发送 - ImReceiveMessage toMessage = new ImReceiveMessage(); - toMessage.setFromId(fromUserId); - toMessage.setConversationType(ImConversationTypeEnum.PRIVATE.getType()); - toMessage.setContentType(message.getContentType()); - toMessage.setContent(message.getContent()); - webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), // 给指定用户 - "im-message-receive", toMessage); - //4、更新消息状态,为发送成功 - imMessageService.updateMessageStatus(messageId, ImMessageStatusEnum.SUCCESS.getStatus()); - + ImMessageDO imMessageDO = imMessageService.savePrivateMessage(message, fromUserId); // 保存私人消息 + handlePrivateMessage(fromUserId, imMessageDO, message); } } + /** + * 处理私人消息 + * + * @param fromUserId 发送者用户ID + * @param imMessageDO IM消息数据对象 + * @param message 发送的IM消息 + */ + private void handlePrivateMessage(Long fromUserId, ImMessageDO imMessageDO, ImSendMessage message) { + Long fromUserSequence = sequenceGeneratorRedisDao.generateSequence(fromUserId); // 生成发送者序列 + Long fromUserInboxId = createAndSaveInbox(fromUserId, imMessageDO.getId(), fromUserSequence); // 创建并保存发送者收件箱 + Long receiverSequence = sequenceGeneratorRedisDao.generateSequence(fromUserId); // 生成接收者序列 + Long receiverInboxId = createAndSaveInbox(message.getReceiverId(), imMessageDO.getId(), receiverSequence); // 创建并保存接收者收件箱 + + // 发送消息给接收者和发送者 + sendMessage(fromUserId, receiverInboxId, imMessageDO, message, "im-message-receive", fromUserSequence); + sendMessage(fromUserId, fromUserInboxId, imMessageDO, message, "im-message-receive", receiverSequence); + + // 更新消息状态为成功 + imMessageService.updateMessageStatus(imMessageDO.getId(), ImMessageStatusEnum.SUCCESS.getStatus()); + // 保存私人会话 + imConversationService.savePrivateConversation(fromUserId, message.getReceiverId()); + } + + /** + * 创建并保存收件箱 + * + * @param userId 用户ID + * @param messageId 消息ID + * @param sequence 序列 + * @return 收件箱ID + */ + private Long createAndSaveInbox(Long userId, Long messageId, Long sequence) { + ImInboxSaveReqVO inboxSaveReqVO = new ImInboxSaveReqVO(userId, messageId, sequence); // 创建收件箱保存请求VO + return imInboxService.createInbox(inboxSaveReqVO); // 创建收件箱 + } + + /** + * 发送消息 + * + * @param fromUserId 发送者用户ID + * @param inboxId 收件箱ID + * @param imMessageDO IM消息数据对象 + * @param message 发送的IM消息 + * @param messageType 消息类型 + * @param sequence 序列 + */ + private void sendMessage(Long fromUserId, Long inboxId, ImMessageDO imMessageDO, ImSendMessage message, String messageType, Long sequence) { + ImReceiveMessage receiveMessage = new ImReceiveMessage(); // 创建接收消息 + receiveMessage.setFromId(fromUserId); // 设置发送者ID + receiveMessage.setConversationType(ImConversationTypeEnum.PRIVATE.getType()); // 设置会话类型为私人 + receiveMessage.setContentType(message.getContentType()); // 设置内容类型 + receiveMessage.setContent(message.getContent()); // 设置内容 + receiveMessage.setMessageId(imMessageDO.getId()); // 设置消息ID + receiveMessage.setInboxId(inboxId); // 设置收件箱ID + receiveMessage.setSendTime(imMessageDO.getSendTime()); // 设置发送时间 + receiveMessage.setSequence(sequence); // 设置序列 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), fromUserId, messageType, receiveMessage); // 发送消息 + } + + /** + * 获取类型 + * + * @return 类型 + */ @Override public String getType() { return "im-message-send"; } -} +} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java index 73c65a7c75..51f1b8d9a9 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java @@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.im.websocket.message; import cn.iocoder.yudao.module.im.dal.dataobject.message.body.ImMessageBody; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.time.LocalDateTime; + @Schema(description = "管理后台 - 消息发送 receive") @Data public class ImReceiveMessage { @@ -20,4 +23,16 @@ public class ImReceiveMessage { @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED) private String content; + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12454") + private Long messageId; + + @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime sendTime; + + @Schema(description = "收件箱编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18389") + private Long inboxId; + + @Schema(description = "序号,按照 user 递增", requiredMode = Schema.RequiredMode.REQUIRED) + private Long sequence; + } \ No newline at end of file