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 1 package cn.iocoder.yudao.module.im.controller.admin.group;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
... ... @@ -193,13 +194,16 @@ public class ImGroupController {
193 194 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
194 195 // 转换输出
195 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 204 // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过
201 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 47 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
48 48 private LocalDateTime createTime;
49 49  
  50 + @Schema(description = "当前登录用户在该群的成员状态:在群 / 已退群(历史退群群仍返回,供前端展示离线消息的群名 / 头像)")
  51 + private Integer joinStatus; // 参见 CommonStatusEnum 枚举
  52 +
50 53 @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
51 54 private List<ImGroupMessageRespVO> pinnedMessages;
52 55  
53   -}
54 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 357 throw exception(MESSAGE_GROUP_READ_DISABLED);
358 358 }
359 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 362 // 1.2 校验消息属于当前群,且对当前用户可见
363 363 ImGroupMessageDO message = groupMessageMapper.selectById(messageId);
364 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 640 // ========== 群已读测试 ==========
641 641  
642 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 650 ServiceException exception = assertThrows(ServiceException.class,
650 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 656 @Test
... ... @@ -817,8 +819,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
817 819 // 准备:用户在群;Redis 游标从 5 前进到 100
818 820 ImGroupMemberDO member = ImGroupMemberDO.builder()
819 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 825 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
823 826 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
824 827 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L);
... ... @@ -891,8 +894,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
891 894 // 准备:已读游标已 >= 目标,直接返回
892 895 ImGroupMemberDO member = ImGroupMemberDO.builder()
893 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 900 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
897 901 .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
898 902 when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L);
... ... @@ -910,8 +914,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
910 914 // 准备:用户在群,但 messageId 属于其它群
911 915 ImGroupMemberDO member = ImGroupMemberDO.builder()
912 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 920 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
916 921 .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build());
917 922  
... ... @@ -922,6 +927,54 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
922 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 978 // ========== 回执:DONE 状态迁移 ==========
926 979  
927 980 @Test
... ...