Commit 863a420287d4459b74226a927cd77cdd20db7e81

Authored by YunaiV
1 parent cd0156e0

fix(im):已退的群,允许 read,避免退群无法读

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
1 package cn.iocoder.yudao.module.im.controller.admin.group; 1 package cn.iocoder.yudao.module.im.controller.admin.group;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
@@ -193,13 +194,16 @@ public class ImGroupController { @@ -193,13 +194,16 @@ public class ImGroupController {
193 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds); 194 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
194 // 转换输出 195 // 转换输出
195 return convertList(groups, group -> { 196 return convertList(groups, group -> {
196 - ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);  
197 - if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {  
198 - return vo; 197 + ImGroupRespVO groupVO = BeanUtils.toBean(group, ImGroupRespVO.class);
  198 + // 标记登录用户在该群的成员状态,供前端区分当前群与历史退群
  199 + boolean joined = activeGroupIds.contains(group.getId());
  200 + groupVO.setJoinStatus(joined ? CommonStatusEnum.ENABLE.getStatus() : CommonStatusEnum.DISABLE.getStatus());
  201 + if (!joined || CollUtil.isEmpty(group.getPinnedMessageIds())) {
  202 + return groupVO;
199 } 203 }
200 // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过 204 // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过
201 List<ImGroupMessageDO> pinnedMesages = convertList(group.getPinnedMessageIds(), messageMap::get); 205 List<ImGroupMessageDO> pinnedMesages = convertList(group.getPinnedMessageIds(), messageMap::get);
202 - return vo.setPinnedMessages(BeanUtils.toBean(pinnedMesages, ImGroupMessageRespVO.class)); 206 + return groupVO.setPinnedMessages(BeanUtils.toBean(pinnedMesages, ImGroupMessageRespVO.class));
203 }); 207 });
204 } 208 }
205 209
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
@@ -47,7 +47,10 @@ public class ImGroupRespVO { @@ -47,7 +47,10 @@ public class ImGroupRespVO {
47 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) 47 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
48 private LocalDateTime createTime; 48 private LocalDateTime createTime;
49 49
  50 + @Schema(description = "当前登录用户在该群的成员状态:在群 / 已退群(历史退群群仍返回,供前端展示离线消息的群名 / 头像)")
  51 + private Integer joinStatus; // 参见 CommonStatusEnum 枚举
  52 +
50 @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空") 53 @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
51 private List<ImGroupMessageRespVO> pinnedMessages; 54 private List<ImGroupMessageRespVO> pinnedMessages;
52 55
53 -}  
54 \ No newline at end of file 56 \ No newline at end of file
  57 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -357,8 +357,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -357,8 +357,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
357 throw exception(MESSAGE_GROUP_READ_DISABLED); 357 throw exception(MESSAGE_GROUP_READ_DISABLED);
358 } 358 }
359 Assert.notNull(messageId, "已读消息编号不能为空"); 359 Assert.notNull(messageId, "已读消息编号不能为空");
360 - // 1.1 校验用户在群中(权限校验)  
361 - ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); 360 + // 1.1 获取群成员记录
  361 + ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
362 // 1.2 校验消息属于当前群,且对当前用户可见 362 // 1.2 校验消息属于当前群,且对当前用户可见
363 ImGroupMessageDO message = groupMessageMapper.selectById(messageId); 363 ImGroupMessageDO message = groupMessageMapper.selectById(messageId);
364 if (message == null 364 if (message == null
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
@@ -640,15 +640,17 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -640,15 +640,17 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
640 // ========== 群已读测试 ========== 640 // ========== 群已读测试 ==========
641 641
642 @Test 642 @Test
643 - public void testReadMessages_notInGroup() {  
644 - // 准备:当前用户不在群中  
645 - when(groupMemberService.validateMemberInGroup(10L, 1L))  
646 - .thenThrow(new ServiceException(GROUP_MEMBER_NOT_IN_GROUP.getCode(), GROUP_MEMBER_NOT_IN_GROUP.getMsg())); 643 + public void testReadMessages_messageInvisible() {
  644 + // 准备:用户不是群成员
  645 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(null);
  646 + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
  647 + .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
647 648
648 - // 调用并断言:越权校验 649 + // 调用并断言
649 ServiceException exception = assertThrows(ServiceException.class, 650 ServiceException exception = assertThrows(ServiceException.class,
650 () -> groupMessageService.readGroupMessages(1L, 10L, 100L)); 651 () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
651 - assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); 652 + assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode());
  653 + verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
652 } 654 }
653 655
654 @Test 656 @Test
@@ -817,8 +819,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -817,8 +819,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
817 // 准备:用户在群;Redis 游标从 5 前进到 100 819 // 准备:用户在群;Redis 游标从 5 前进到 100
818 ImGroupMemberDO member = ImGroupMemberDO.builder() 820 ImGroupMemberDO member = ImGroupMemberDO.builder()
819 .groupId(10L).userId(1L) 821 .groupId(10L).userId(1L)
820 - .status(CommonStatusEnum.ENABLE.getStatus()).build();  
821 - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); 822 + .status(CommonStatusEnum.ENABLE.getStatus())
  823 + .joinTime(LocalDateTime.now().minusDays(1)).build();
  824 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
822 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() 825 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
823 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); 826 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
824 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L); 827 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L);
@@ -891,8 +894,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -891,8 +894,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
891 // 准备:已读游标已 >= 目标,直接返回 894 // 准备:已读游标已 >= 目标,直接返回
892 ImGroupMemberDO member = ImGroupMemberDO.builder() 895 ImGroupMemberDO member = ImGroupMemberDO.builder()
893 .groupId(10L).userId(1L) 896 .groupId(10L).userId(1L)
894 - .status(CommonStatusEnum.ENABLE.getStatus()).build();  
895 - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); 897 + .status(CommonStatusEnum.ENABLE.getStatus())
  898 + .joinTime(LocalDateTime.now().minusDays(1)).build();
  899 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
896 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() 900 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
897 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); 901 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
898 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L); 902 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L);
@@ -910,8 +914,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -910,8 +914,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
910 // 准备:用户在群,但 messageId 属于其它群 914 // 准备:用户在群,但 messageId 属于其它群
911 ImGroupMemberDO member = ImGroupMemberDO.builder() 915 ImGroupMemberDO member = ImGroupMemberDO.builder()
912 .groupId(10L).userId(1L) 916 .groupId(10L).userId(1L)
913 - .status(CommonStatusEnum.ENABLE.getStatus()).build();  
914 - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); 917 + .status(CommonStatusEnum.ENABLE.getStatus())
  918 + .joinTime(LocalDateTime.now().minusDays(1)).build();
  919 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
915 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() 920 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
916 .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build()); 921 .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build());
917 922
@@ -922,6 +927,54 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -922,6 +927,54 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
922 verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong()); 927 verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
923 } 928 }
924 929
  930 + @Test
  931 + public void testReadGroupMessages_quitMemberVisibleMessage_success() {
  932 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  933 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupMessageServiceImpl.class)))
  934 + .thenReturn(groupMessageService);
  935 +
  936 + LocalDateTime now = LocalDateTime.now();
  937 + ImGroupMemberDO member = ImGroupMemberDO.builder()
  938 + .groupId(10L).userId(1L)
  939 + .status(CommonStatusEnum.DISABLE.getStatus())
  940 + .joinTime(now.minusDays(1))
  941 + .quitTime(now.plusMinutes(1)).build();
  942 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  943 + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
  944 + .id(100L).groupId(10L).senderId(2L).sendTime(now)
  945 + .receiverUserIds(ListUtil.of(1L, 2L)).build());
  946 + when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(0L);
  947 + when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L)).thenReturn(ListUtil.of());
  948 +
  949 + // 调用
  950 + groupMessageService.readGroupMessages(1L, 10L, 100L);
  951 +
  952 + // 断言:不校验当前在群,直接推进已读位置
  953 + verify(groupMemberService, never()).validateMemberInGroup(10L, 1L);
  954 + verify(groupMessageReadRedisDAO).updateReadMaxMessageId(10L, 1L, 100L);
  955 + }
  956 + }
  957 +
  958 + @Test
  959 + public void testReadGroupMessages_quitMemberInvisibleMessage() {
  960 + LocalDateTime now = LocalDateTime.now();
  961 + ImGroupMemberDO member = ImGroupMemberDO.builder()
  962 + .groupId(10L).userId(1L)
  963 + .status(CommonStatusEnum.DISABLE.getStatus())
  964 + .joinTime(now.minusDays(2))
  965 + .quitTime(now.minusDays(1)).build();
  966 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  967 + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
  968 + .id(100L).groupId(10L).senderId(2L).sendTime(now)
  969 + .receiverUserIds(ListUtil.of(1L, 2L)).build());
  970 +
  971 + // 调用并断言
  972 + ServiceException exception = assertThrows(ServiceException.class,
  973 + () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
  974 + assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode());
  975 + verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
  976 + }
  977 +
925 // ========== 回执:DONE 状态迁移 ========== 978 // ========== 回执:DONE 状态迁移 ==========
926 979
927 @Test 980 @Test