Commit 863a420287d4459b74226a927cd77cdd20db7e81
1 parent
cd0156e0
fix(im):已退的群,允许 read,避免退群无法读
Showing
4 changed files
with
79 additions
and
19 deletions
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 |