Commit 9ed7c050b03c696bf92cba5e2a5332ff29b82411

Authored by YunaiV
1 parent 79de9233

feat(im):优化已读上报、群详情缓存与 RTC 通话状态

- 已读上报增加本地读位置覆盖判断,避免切换会话和当前会话自动已读时重复调用 read 接口
- 标记会话已读时同步推进本地 read 游标并写入 IndexedDB,接口失败仅记录日志
- 缓存私聊对方 maxReadMessageId,并在状态补拉、回执更新和退出 IM 时维护缓存
- 增加群详情 infoLoaded 内存标记,减少切群时重复拉取群详情,手动刷新和关键通知仍强制刷新
- 同步 GROUP_INFO_UPDATE 的 joinApproval,避免群审批配置在前端缓存中陈旧
- 优化群通话胶囊条状态,记录 participantsLoaded,按需补齐参与者并在通话无人时移除胶囊
- RTC_CALL_START 生成群通话最小胶囊条,后续由参与者事件和 getActiveCall 补齐
- 退出 IM 时清理 RTC 状态和群通话缓存
- Vben antd/antd-next 调整媒体元素为函数 ref,修复 MediaStream 与元素挂载时序问题
- 修复 Vben 消息历史弹窗回调类型标注
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -149,7 +149,9 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -149,7 +149,9 @@ public class ImGroupServiceImpl implements ImGroupService {
149 boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName()); 149 boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName());
150 boolean noticeChanged = updateReqVO.getNotice() != null; 150 boolean noticeChanged = updateReqVO.getNotice() != null;
151 boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar()); 151 boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar());
152 - if (nameChanged || noticeChanged || avatarChanged) { 152 + boolean joinApprovalChanged = updateReqVO.getJoinApproval() != null
  153 + && ObjUtil.notEqual(group.getJoinApproval(), updateReqVO.getJoinApproval());
  154 + if (nameChanged || noticeChanged || avatarChanged || joinApprovalChanged) {
153 List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId); 155 List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId);
154 if (nameChanged) { 156 if (nameChanged) {
155 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate( 157 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate(
@@ -161,7 +163,11 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -161,7 +163,11 @@ public class ImGroupServiceImpl implements ImGroupService {
161 } 163 }
162 if (avatarChanged) { 164 if (avatarChanged) {
163 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate( 165 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate(
164 - groupId, userId, group.getAvatar(), updateReqVO.getAvatar())); 166 + groupId, userId, group.getAvatar(), updateReqVO.getAvatar(), null, null));
  167 + }
  168 + if (joinApprovalChanged) {
  169 + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate(
  170 + groupId, userId, null, null, group.getJoinApproval(), updateReqVO.getJoinApproval()));
165 } 171 }
166 } 172 }
167 173
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
@@ -63,11 +63,15 @@ public class ImGroupMessageSendDTO { @@ -63,11 +63,15 @@ public class ImGroupMessageSendDTO {
63 .setType(ImContentTypeEnum.GROUP_CREATE.getType()).setContent(notification); 63 .setType(ImContentTypeEnum.GROUP_CREATE.getType()).setContent(notification);
64 } 64 }
65 65
66 - public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) { 66 + public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId,
  67 + String oldAvatar, String newAvatar,
  68 + Boolean oldJoinApproval, Boolean newJoinApproval) {
67 GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification(); 69 GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification();
68 notification.setOperatorUserId(operatorUserId); 70 notification.setOperatorUserId(operatorUserId);
69 notification.setOldAvatar(oldAvatar); 71 notification.setOldAvatar(oldAvatar);
70 notification.setNewAvatar(newAvatar); 72 notification.setNewAvatar(newAvatar);
  73 + notification.setOldJoinApproval(oldJoinApproval);
  74 + notification.setNewJoinApproval(newJoinApproval);
71 return new ImGroupMessageSendDTO().setGroupId(groupId) 75 return new ImGroupMessageSendDTO().setGroupId(groupId)
72 .setType(ImContentTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification); 76 .setType(ImContentTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification);
73 } 77 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupInfoUpdateNotification.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.im.service.websocket.notification.group; @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.im.service.websocket.notification.group;
3 import lombok.Data; 3 import lombok.Data;
4 4
5 /** 5 /**
6 - * 群信息变更事件通知(当前承载头像变更,NAME / NOTICE 走独立事件) 6 + * 群信息变更事件通知(NAME / NOTICE 走独立事件)
7 */ 7 */
8 @Data 8 @Data
9 public class GroupInfoUpdateNotification extends BaseGroupNotification { 9 public class GroupInfoUpdateNotification extends BaseGroupNotification {
@@ -16,5 +16,13 @@ public class GroupInfoUpdateNotification extends BaseGroupNotification { @@ -16,5 +16,13 @@ public class GroupInfoUpdateNotification extends BaseGroupNotification {
16 * 新群头像 16 * 新群头像
17 */ 17 */
18 private String newAvatar; 18 private String newAvatar;
  19 + /**
  20 + * 旧进群审批开关
  21 + */
  22 + private Boolean oldJoinApproval;
  23 + /**
  24 + * 新进群审批开关
  25 + */
  26 + private Boolean newJoinApproval;
19 27
20 } 28 }
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
@@ -209,6 +209,58 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -209,6 +209,58 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
209 } 209 }
210 } 210 }
211 211
  212 + @Test
  213 + public void testUpdateGroup_joinApprovalChanged() {
  214 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  215 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  216 + .thenReturn(groupService);
  217 +
  218 + // 准备
  219 + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(1L)
  220 + .joinApproval(false).status(CommonStatusEnum.ENABLE.getStatus()).build();
  221 + when(groupMapper.selectById(10L)).thenReturn(group);
  222 +
  223 + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO();
  224 + reqVO.setId(10L);
  225 + reqVO.setJoinApproval(true);
  226 +
  227 + // 调用
  228 + ImGroupDO result = groupService.updateGroup(reqVO, 1L);
  229 +
  230 + // 断言
  231 + verify(groupMapper).updateById(any(ImGroupDO.class));
  232 + assertTrue(result.getJoinApproval());
  233 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  234 + verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
  235 + assertEquals(ImContentTypeEnum.GROUP_INFO_UPDATE.getType(), dtoCaptor.getValue().getType());
  236 + }
  237 + }
  238 +
  239 + @Test
  240 + public void testUpdateGroup_joinApprovalSame() {
  241 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  242 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  243 + .thenReturn(groupService);
  244 +
  245 + // 准备
  246 + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(1L)
  247 + .joinApproval(true).status(CommonStatusEnum.ENABLE.getStatus()).build();
  248 + when(groupMapper.selectById(10L)).thenReturn(group);
  249 +
  250 + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO();
  251 + reqVO.setId(10L);
  252 + reqVO.setJoinApproval(true);
  253 +
  254 + // 调用
  255 + ImGroupDO result = groupService.updateGroup(reqVO, 1L);
  256 +
  257 + // 断言
  258 + verify(groupMapper).updateById(any(ImGroupDO.class));
  259 + assertTrue(result.getJoinApproval());
  260 + verify(groupMessageService, never()).sendGroupMessage(anyLong(), anyCollection(), any());
  261 + }
  262 + }
  263 +
212 // ========== dissolveGroup ========== 264 // ========== dissolveGroup ==========
213 265
214 @Test 266 @Test