Commit 4ab95695088f583f0530295c398521ff708d4bc1

Authored by YunaiV
1 parent ca0aadac

✨ feat(im):优化私聊的推送为 ImPrivateMessageDTO

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  
... ...