Commit 4ab95695088f583f0530295c398521ff708d4bc1
1 parent
ca0aadac
✨ feat(im):优化私聊的推送为 ImPrivateMessageDTO
Showing
3 changed files
with
87 additions
and
24 deletions
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
| ... | ... | @@ -13,14 +13,11 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate |
| 13 | 13 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; |
| 14 | 14 | import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage; |
| 15 | 15 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; |
| 16 | -import cn.iocoder.yudao.module.im.enums.message.ImMessageSceneEnum; | |
| 17 | 16 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 18 | 17 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| 19 | 18 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 20 | 19 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 21 | -import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage; | |
| 22 | -import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage; | |
| 23 | -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage; | |
| 20 | +import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; | |
| 24 | 21 | import jakarta.annotation.Resource; |
| 25 | 22 | import lombok.extern.slf4j.Slf4j; |
| 26 | 23 | import org.springframework.scheduling.annotation.Async; |
| ... | ... | @@ -183,7 +180,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { |
| 183 | 180 | ImMessageReceiptMessage receiptMessage = new ImMessageReceiptMessage() |
| 184 | 181 | .setReceiverId(userId).setScene(ImMessageSceneEnum.PRIVATE.getScene()); |
| 185 | 182 | webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), receiverId, |
| 186 | - ImMessageReceiptMessage.TYPE, receiptMessage); | |
| 183 | + ImPrivateMessageDTO.TYPE, receiptDto); | |
| 187 | 184 | } |
| 188 | 185 | |
| 189 | 186 | @Override | ... | ... |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageSendMessage.java renamed to yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.im.websocket; |
| 2 | 2 | |
| 3 | +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.enums.message.ImMessageTypeEnum; | |
| 3 | 6 | import lombok.Data; |
| 4 | 7 | import lombok.experimental.Accessors; |
| 5 | 8 | |
| 6 | 9 | import java.time.LocalDateTime; |
| 7 | 10 | |
| 8 | 11 | /** |
| 9 | - * IM 私聊消息 WebSocket 推送消息 | |
| 12 | + * IM 私聊消息 WebSocket 统一推送 DTO | |
| 13 | + * <p> | |
| 14 | + * 发送、已读、回执、撤回都通过 {@link #type} 字段区分, | |
| 15 | + * 并统一复用 {@link #id} 字段表达目标消息或已读位置。 | |
| 10 | 16 | * |
| 11 | 17 | * @author 芋道源码 |
| 12 | 18 | */ |
| 13 | 19 | @Data |
| 14 | 20 | @Accessors(chain = true) |
| 15 | -public class ImPrivateMessageSendMessage { | |
| 21 | +public class ImPrivateMessageDTO { | |
| 16 | 22 | |
| 17 | - public static final String TYPE = "im-private-message-send"; | |
| 23 | + public static final String TYPE = "im-private-message"; | |
| 18 | 24 | |
| 19 | 25 | /** |
| 20 | 26 | * 消息编号 |
| 27 | + * <p> | |
| 28 | + * 普通消息:当前消息 id | |
| 29 | + * READ:已读位置(我已读到这条消息) | |
| 30 | + * RECEIPT:已读位置(对方已读到这条消息) | |
| 31 | + * RECALL:被撤回的原消息 id | |
| 21 | 32 | */ |
| 22 | 33 | private Long id; |
| 23 | 34 | /** |
| ... | ... | @@ -34,6 +45,8 @@ public class ImPrivateMessageSendMessage { |
| 34 | 45 | private Long receiverId; |
| 35 | 46 | /** |
| 36 | 47 | * 消息类型 |
| 48 | + * <p> | |
| 49 | + * TEXT / IMAGE / FILE / VOICE / VIDEO / READ / RECEIPT / RECALL / TIP_TEXT | |
| 37 | 50 | */ |
| 38 | 51 | private Integer type; |
| 39 | 52 | /** |
| ... | ... | @@ -49,4 +62,44 @@ public class ImPrivateMessageSendMessage { |
| 49 | 62 | */ |
| 50 | 63 | private LocalDateTime sendTime; |
| 51 | 64 | |
| 65 | + // ========== 静态工厂方法 ========== | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 构建发送消息 DTO | |
| 69 | + * | |
| 70 | + * @param message 私聊消息 DO | |
| 71 | + * @return 私聊 DTO | |
| 72 | + */ | |
| 73 | + public static ImPrivateMessageDTO ofSend(ImPrivateMessageDO message) { | |
| 74 | + return BeanUtils.toBean(message, ImPrivateMessageDTO.class); | |
| 75 | + } | |
| 76 | + | |
| 77 | + /** | |
| 78 | + * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个会话") | |
| 79 | + * | |
| 80 | + * @param senderId 当前用户编号 | |
| 81 | + * @param receiverId 对方用户编号 | |
| 82 | + * @param readId 已读位置(最大已读消息编号) | |
| 83 | + * @return 私聊 DTO | |
| 84 | + */ | |
| 85 | + public static ImPrivateMessageDTO ofRead(Long senderId, Long receiverId, Long readId) { | |
| 86 | + return new ImPrivateMessageDTO() | |
| 87 | + .setId(readId).setType(ImMessageTypeEnum.READ.getType()) | |
| 88 | + .setSenderId(senderId).setReceiverId(receiverId); | |
| 89 | + } | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * 构建已读回执 DTO(通知对方"我已读了你的消息") | |
| 93 | + * | |
| 94 | + * @param senderId 已读方的用户编号 | |
| 95 | + * @param receiverId 对方用户编号 | |
| 96 | + * @param readId 已读位置(最大已读消息编号) | |
| 97 | + * @return 私聊 DTO | |
| 98 | + */ | |
| 99 | + public static ImPrivateMessageDTO ofReceipt(Long senderId, Long receiverId, Long readId) { | |
| 100 | + return new ImPrivateMessageDTO() | |
| 101 | + .setId(readId).setType(ImMessageTypeEnum.RECEIPT.getType()) | |
| 102 | + .setSenderId(senderId).setReceiverId(receiverId); | |
| 103 | + } | |
| 104 | + | |
| 52 | 105 | } | ... | ... |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java
| ... | ... | @@ -6,11 +6,10 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate |
| 6 | 6 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; |
| 7 | 7 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; |
| 8 | 8 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 9 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | |
| 9 | 10 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 10 | 11 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 11 | -import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage; | |
| 12 | -import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage; | |
| 13 | -import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage; | |
| 12 | +import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageDTO; | |
| 14 | 13 | import org.junit.jupiter.api.Test; |
| 15 | 14 | import org.mockito.ArgumentCaptor; |
| 16 | 15 | import org.mockito.InjectMocks; |
| ... | ... | @@ -73,12 +72,12 @@ public class ImWebSocketEventTest extends BaseMockitoUnitTest { |
| 73 | 72 | verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), |
| 74 | 73 | typeCaptor.capture(), contentCaptor.capture()); |
| 75 | 74 | |
| 76 | - // 验证消息事件类型 | |
| 77 | - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0)); | |
| 78 | - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1)); | |
| 75 | + // 验证消息事件类型:统一为 im-private-message | |
| 76 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | |
| 77 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | |
| 79 | 78 | |
| 80 | 79 | // 验证 payload 是 DTO 类型 |
| 81 | - ImPrivateMessageSendMessage payload = (ImPrivateMessageSendMessage) contentCaptor.getAllValues().get(0); | |
| 80 | + ImPrivateMessageDTO payload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(0); | |
| 82 | 81 | assertNotNull(payload.getId(), "id 不应为空"); |
| 83 | 82 | assertEquals(1L, payload.getSenderId()); |
| 84 | 83 | assertEquals(2L, payload.getReceiverId()); |
| ... | ... | @@ -91,9 +90,11 @@ public class ImWebSocketEventTest extends BaseMockitoUnitTest { |
| 91 | 90 | springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) |
| 92 | 91 | .thenReturn(privateMessageService); |
| 93 | 92 | |
| 94 | - // 准备:mock 未读消息 | |
| 93 | + // 准备:mock 未读消息(id=1 和 id=5,maxReadId 应为 5) | |
| 95 | 94 | List<ImPrivateMessageDO> unreadMessages = List.of( |
| 96 | 95 | ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L) |
| 96 | + .status(ImMessageStatusEnum.UNREAD.getStatus()).build(), | |
| 97 | + ImPrivateMessageDO.builder().id(5L).senderId(2L).receiverId(1L) | |
| 97 | 98 | .status(ImMessageStatusEnum.UNREAD.getStatus()).build() |
| 98 | 99 | ); |
| 99 | 100 | when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L, |
| ... | ... | @@ -105,15 +106,27 @@ public class ImWebSocketEventTest extends BaseMockitoUnitTest { |
| 105 | 106 | // 捕获 |
| 106 | 107 | ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); |
| 107 | 108 | ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); |
| 109 | + ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | |
| 108 | 110 | verify(webSocketMessageSender, times(2)).sendObject(anyInt(), userCaptor.capture(), |
| 109 | - typeCaptor.capture(), any()); | |
| 111 | + typeCaptor.capture(), contentCaptor.capture()); | |
| 110 | 112 | |
| 111 | - // 第一次:发给自己的 READ 事件 | |
| 112 | - assertEquals(ImMessageReadMessage.TYPE, typeCaptor.getAllValues().get(0)); | |
| 113 | + // 第一次:发给自己的 READ 事件(统一 type = im-private-message) | |
| 114 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | |
| 113 | 115 | assertEquals(1L, userCaptor.getAllValues().get(0)); |
| 114 | - // 第二次:发给对方的 RECEIPT 事件 | |
| 115 | - assertEquals(ImMessageReceiptMessage.TYPE, typeCaptor.getAllValues().get(1)); | |
| 116 | + ImPrivateMessageDTO readPayload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(0); | |
| 117 | + assertEquals(ImMessageTypeEnum.READ.getType(), readPayload.getType()); | |
| 118 | + assertEquals(1L, readPayload.getSenderId()); | |
| 119 | + assertEquals(2L, readPayload.getReceiverId()); | |
| 120 | + assertEquals(5L, readPayload.getId(), "READ id 应为 maxReadId"); | |
| 121 | + | |
| 122 | + // 第二次:发给对方的 RECEIPT 事件(统一 type = im-private-message) | |
| 123 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | |
| 116 | 124 | assertEquals(2L, userCaptor.getAllValues().get(1)); |
| 125 | + ImPrivateMessageDTO receiptPayload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(1); | |
| 126 | + assertEquals(ImMessageTypeEnum.RECEIPT.getType(), receiptPayload.getType()); | |
| 127 | + assertEquals(1L, receiptPayload.getSenderId()); | |
| 128 | + assertEquals(2L, receiptPayload.getReceiverId()); | |
| 129 | + assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为 maxReadId"); | |
| 117 | 130 | } |
| 118 | 131 | } |
| 119 | 132 | |
| ... | ... | @@ -140,9 +153,9 @@ public class ImWebSocketEventTest extends BaseMockitoUnitTest { |
| 140 | 153 | verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), |
| 141 | 154 | typeCaptor.capture(), contentCaptor.capture()); |
| 142 | 155 | |
| 143 | - // 验证:推送的是 TIP_TEXT 消息(通过 sendPrivateMessageEvent) | |
| 144 | - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0)); | |
| 145 | - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1)); | |
| 156 | + // 验证:推送的是 TIP_TEXT 消息(统一 type = im-private-message) | |
| 157 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | |
| 158 | + assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | |
| 146 | 159 | } |
| 147 | 160 | } |
| 148 | 161 | ... | ... |