diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java index 1b77d44..4c62469 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java @@ -79,6 +79,10 @@ public class ImGroupMemberController { // 3. 转化 VO ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class); + AdminUserRespDTO user = adminUserApi.getUser(member.getUserId()); + if (user != null) { + memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar()); + } hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId); return success(memberVO); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java index 6268fb3..95997c3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java @@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.ibatis.annotations.Mapper; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; @@ -55,4 +57,20 @@ public interface ImFriendMapper extends BaseMapperX { .orderByDesc(ImFriendDO::getId)); } + @SuppressWarnings("UnusedReturnValue") + default int updateReAddFields(Long id, Integer status, LocalDateTime addTime, + Boolean silent, Boolean pinned, Boolean blocked, + String displayName, Integer addSource) { + return update(null, Wrappers.lambdaUpdate() + .eq(ImFriendDO::getId, id) + .set(ImFriendDO::getStatus, status) + .set(ImFriendDO::getAddTime, addTime) + .set(ImFriendDO::getSilent, silent) + .set(ImFriendDO::getPinned, pinned) + .set(ImFriendDO::getBlocked, blocked) + .set(ImFriendDO::getDisplayName, displayName) + .set(ImFriendDO::getAddSource, addSource) + .set(ImFriendDO::getDeleteTime, null)); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java index fcd198f..7ba35a0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java @@ -124,4 +124,18 @@ public interface ImGroupMemberMapper extends BaseMapperX { .set(ImGroupMemberDO::getMuteEndTime, null)); } -} \ No newline at end of file + @SuppressWarnings("UnusedReturnValue") + default int updateRejoinFields(Long id, Integer status, LocalDateTime joinTime, + Integer role, Integer addSource, Long inviterUserId) { + return update(null, Wrappers.lambdaUpdate() + .eq(ImGroupMemberDO::getId, id) + .set(ImGroupMemberDO::getStatus, status) + .set(ImGroupMemberDO::getJoinTime, joinTime) + .set(ImGroupMemberDO::getRole, role) + .set(ImGroupMemberDO::getAddSource, addSource) + .set(ImGroupMemberDO::getInviterUserId, inviterUserId) + .set(ImGroupMemberDO::getQuitTime, null) + .set(ImGroupMemberDO::getMuteEndTime, null)); + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 7dd665f..c304841 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -266,16 +266,8 @@ public class ImFriendServiceImpl implements ImFriendService { } // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 if (exists != null) { - ImFriendDO update = new ImFriendDO().setId(exists.getId()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now()) - .setSilent(false).setPinned(false).setBlocked(false); - if (displayName != null) { - update.setDisplayName(displayName); - } - if (addSource != null) { - update.setAddSource(addSource); - } - friendMapper.updateById(update); + friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now(), + false, false, false, displayName, addSource); return; } // 情况三:不存在记录 → 直接插入新记录 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java index 633dbfb..7242919 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java @@ -128,7 +128,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 *

- * 重置旧成员行时强制重置 role / addSource / inviterUserId,确保留痕反映「本次入群」事件 + * 重置旧成员行时强制重置 role / addSource / inviterUserId / quitTime / muteEndTime */ @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") @@ -139,11 +139,11 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); if (exists != null) { if (CommonStatusEnum.isDisable(exists.getStatus())) { - groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) - .setRole(role).setAddSource(addSource).setInviterUserId(inviterUserId)); + groupMemberMapper.updateRejoinFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, + role, addSource, inviterUserId); exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role) - .setAddSource(addSource).setInviterUserId(inviterUserId); + .setAddSource(addSource).setInviterUserId(inviterUserId) + .setQuitTime(null).setMuteEndTime(null); } return exists; } @@ -193,7 +193,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { // 2.1 先做 update,update 没有并发冲突风险 if (CollUtil.isNotEmpty(updates)) { - groupMemberMapper.updateBatch(updates); + for (ImGroupMemberDO update : updates) { + groupMemberMapper.updateRejoinFields(update.getId(), update.getStatus(), update.getJoinTime(), + update.getRole(), update.getAddSource(), update.getInviterUserId()); + } } // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId), // 会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index 6b0196a..469c6c0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -241,8 +241,14 @@ public class ImGroupServiceImpl implements ImGroupService { @Override @Transactional(rollbackFor = Exception.class) public void quitGroup(Long groupId, Long userId) { - // 1.1 校验群存在 - ImGroupDO group = validateGroupExists(groupId); + // 1.1 校验群存在且未解散 + ImGroupDO group = getSelf().getGroup(groupId); + if (group == null) { + throw exception(GROUP_NOT_EXISTS); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { + throw exception(GROUP_DISSOLVED); + } // 1.2 群主不可退群 if (ObjUtil.equal(group.getOwnerUserId(), userId)) { throw exception(GROUP_OWNER_CANNOT_QUIT); @@ -433,7 +439,7 @@ public class ImGroupServiceImpl implements ImGroupService { pinned.add(messageId); groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); - // 3. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序 + // 3. 推送 GROUP_MESSAGE_PIN 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); } @@ -694,4 +700,4 @@ public class ImGroupServiceImpl implements ImGroupService { .collect(Collectors.joining(",")); } -} \ No newline at end of file +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 0ade2be..e85f6fe 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -354,8 +354,15 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { throw exception(MESSAGE_GROUP_READ_DISABLED); } Assert.notNull(messageId, "已读消息编号不能为空"); - // 1. 校验用户在群中(权限校验) - groupMemberService.validateMemberInGroup(groupId, userId); + // 1.1 校验用户在群中(权限校验) + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); + // 1.2 校验消息属于当前群,且对当前用户可见 + ImGroupMessageDO message = groupMessageMapper.selectById(messageId); + if (message == null + || ObjUtil.notEqual(message.getGroupId(), groupId) + || !isMessageVisible(message, member, userId)) { + throw exception(MESSAGE_NOT_IN_GROUP); + } // 2. 已读位置未前进,直接返回 Long prevMaxMessageId = groupMessageReadRedisDAO.getReadMaxMessageId(groupId, userId); @@ -664,4 +671,4 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { } } -} \ No newline at end of file +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java index 8ab3304..95f4234 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.service.message.dto; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.*; import jakarta.validation.constraints.NotNull; @@ -166,11 +167,14 @@ public class ImGroupMessageSendDTO { .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); } - public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, - cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO message) { + public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, ImGroupMessageDO message) { GroupMessagePinNotification notification = new GroupMessagePinNotification(); + GroupMessagePinNotification.PinnedMessage pinnedMessage = new GroupMessagePinNotification.PinnedMessage() + .setId(message.getId()).setSenderId(message.getSenderId()).setGroupId(message.getGroupId()) + .setType(message.getType()).setContent(message.getContent()).setSendTime(message.getSendTime()) + .setAtUserIds(message.getAtUserIds()).setReceiverUserIds(message.getReceiverUserIds()); notification.setOperatorUserId(operatorUserId); - notification.setMessageId(message.getId()).setMessage(message); + notification.setMessageId(message.getId()).setMessage(pinnedMessage); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java index fe7e9ef..6dd5ebc 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java @@ -1,12 +1,12 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; import lombok.Data; +import java.time.LocalDateTime; +import java.util.List; + /** * 群消息置顶事件通知 - *

- * payload 直接携带被置顶的完整消息对象,前端按 message 直接 push 到 pinnedMessages,避免 GET /im/group/get 命中 @CacheEvict 触发前的旧 cache */ @Data public class GroupMessagePinNotification extends BaseGroupNotification { @@ -16,8 +16,49 @@ public class GroupMessagePinNotification extends BaseGroupNotification { */ private Long messageId; /** - * 被置顶的完整消息对象(避免前端回查群详情) + * 被置顶的消息展示数据 + */ + private PinnedMessage message; + + /** + * 被置顶的消息展示数据 */ - private ImGroupMessageDO message; + @Data + public static class PinnedMessage { + + /** + * 消息编号 + */ + private Long id; + /** + * 发送人编号 + */ + private Long senderId; + /** + * 群编号 + */ + private Long groupId; + /** + * 消息类型 + */ + private Integer type; + /** + * 消息内容 + */ + private String content; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * @ 目标用户编号列表 + */ + private List atUserIds; + /** + * 定向接收用户编号列表 + */ + private List receiverUserIds; + + } } diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java index a489425..a644705 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java @@ -21,9 +21,7 @@ import java.util.List; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -126,8 +124,8 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== @Test - public void testAddFriend0_existingEnabledResetsFields() { - // 准备:已存在且启用 —— 走复用旧记录路径,重置 silent / pinned / blocked + public void testAddFriend0_existingEnabledSkip() { + // 准备:已存在且启用 ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) .status(CommonStatusEnum.ENABLE.getStatus()).silent(true).pinned(true).blocked(true).build(); when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); @@ -135,16 +133,11 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { // 调用 friendService.addFriend0(1L, 2L, null, null); - // 断言:不插入;走 update 重置 silent / pinned / blocked 为 false,并刷新 addTime + // 断言:不插入也不更新 verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(ImFriendDO.class); - verify(imFriendMapper).updateById(captor.capture()); - assertEquals(10L, captor.getValue().getId()); - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); - assertFalse(captor.getValue().getSilent()); - assertFalse(captor.getValue().getPinned()); - assertFalse(captor.getValue().getBlocked()); - assertNotNull(captor.getValue().getAddTime()); + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); + verify(imFriendMapper, never()).updateReAddFields(anyLong(), anyInt(), any(LocalDateTime.class), + anyBoolean(), anyBoolean(), anyBoolean(), any(), any()); } @Test @@ -157,11 +150,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { // 调用 friendService.addFriend0(1L, 2L, null, null); - // 断言:更新 status 为 ENABLE - ArgumentCaptor captor = ArgumentCaptor.forClass(ImFriendDO.class); - verify(imFriendMapper).updateById(captor.capture()); - assertEquals(10L, captor.getValue().getId()); - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); + // 断言:恢复 ENABLE,并清空 deleteTime + verify(imFriendMapper).updateReAddFields(eq(10L), eq(CommonStatusEnum.ENABLE.getStatus()), + any(LocalDateTime.class), eq(false), eq(false), eq(false), isNull(), isNull()); verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); } diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java index 2200687..64687e5 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java @@ -82,14 +82,12 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); - ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); - verify(groupMemberMapper).updateById(captor.capture()); - assertEquals(50L, captor.getValue().getId()); - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); - assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), captor.getValue().getRole()); - assertNotNull(captor.getValue().getJoinTime()); + verify(groupMemberMapper).updateRejoinFields(eq(50L), eq(CommonStatusEnum.ENABLE.getStatus()), + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); + assertNull(result.getQuitTime()); + assertNull(result.getMuteEndTime()); } @Test @@ -124,9 +122,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { groupMemberService.addGroupMembers(10L, List.of(2L, 3L, 4L)); // 断言:updates 只有用户 3;inserts 只有用户 2 - verify(groupMemberMapper).updateBatch(argThat((List list) -> - list.size() == 1 && list.get(0).getId().equals(30L) - && CommonStatusEnum.ENABLE.getStatus().equals(list.get(0).getStatus()))); + verify(groupMemberMapper).updateRejoinFields(eq(30L), eq(CommonStatusEnum.ENABLE.getStatus()), + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); verify(groupMemberMapper).insertBatch(argThat((List list) -> list.size() == 1 && list.get(0).getUserId().equals(2L))); } @@ -145,7 +142,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); - verify(groupMemberMapper).updateBatch(anyList()); + verify(groupMemberMapper, times(2)).updateRejoinFields(anyLong(), eq(CommonStatusEnum.ENABLE.getStatus()), + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); verify(groupMemberMapper, never()).insertBatch(anyList()); } @@ -157,7 +155,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); - verify(groupMemberMapper, never()).updateBatch(anyList()); + verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); verify(groupMemberMapper).insertBatch(anyList()); } @@ -173,7 +171,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { groupMemberService.addGroupMembers(10L, List.of(2L)); - verify(groupMemberMapper, never()).updateBatch(anyList()); + verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); verify(groupMemberMapper, never()).insertBatch(anyList()); } diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java index 84c9928..33810da 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java @@ -525,6 +525,24 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { } } + @Test + public void testQuitGroup_bannedGroupAllowed() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .banned(true).status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + + groupService.quitGroup(10L, 1L); + + verify(groupMemberService).removeGroupMember(10L, 1L); + verify(groupMessageService).deleteReadMaxMessageId(10L, 1L); + verify(groupMessageService).sendGroupMessage(eq(1L), any(ImGroupMessageSendDTO.class)); + } + } + // ========== removeGroupMember ========== @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java index 62af596..5582fab 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; import cn.iocoder.yudao.module.im.dal.redis.message.ImGroupMessageReadRedisDAO; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; @@ -588,6 +589,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { .status(ImMessageStatusEnum.UNREAD.getStatus()) .sendTime(LocalDateTime.now()).build(); when(groupMessageMapper.selectById(50L)).thenReturn(message); + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); // 调用并断言 ServiceException exception = assertThrows(ServiceException.class, @@ -603,6 +607,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { .status(ImMessageStatusEnum.RECALL.getStatus()) .sendTime(LocalDateTime.now()).build(); when(groupMessageMapper.selectById(50L)).thenReturn(message); + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); // 调用并断言 ServiceException exception = assertThrows(ServiceException.class, @@ -618,6 +625,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { .status(ImMessageStatusEnum.UNREAD.getStatus()) .sendTime(LocalDateTime.now().minusMinutes(10)).build(); when(groupMessageMapper.selectById(50L)).thenReturn(message); + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); // 调用并断言 ServiceException exception = assertThrows(ServiceException.class, @@ -641,12 +651,12 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @Test public void testGetReadUserIds_withVisibleScope() { - // 准备:用户 1 是群成员 + // 准备:发送者用户 5 是群成员 ImGroupMemberDO currentMember = ImGroupMemberDO.builder() - .groupId(10L).userId(1L) + .groupId(10L).userId(5L) .status(CommonStatusEnum.ENABLE.getStatus()) .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(currentMember); + when(groupMemberService.validateMemberInGroup(10L, 5L)).thenReturn(currentMember); // 准备:消息由用户 5 发,发送时间在 2026-04-12 10:00 ImGroupMessageDO message = ImGroupMessageDO.builder() @@ -661,6 +671,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { // 用户 5: 发送者,不计入回执 List allMembers = List.of( currentMember, + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()) + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), ImGroupMemberDO.builder().groupId(10L).userId(2L) .status(CommonStatusEnum.ENABLE.getStatus()) .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), @@ -681,7 +694,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions); // 调用:查询 messageId=80 的已读用户 - List readUsers = groupMessageService.getGroupReadUserIds(1L, 10L, 80L); + List readUsers = groupMessageService.getGroupReadUserIds(5L, 10L, 80L); // 断言: // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓ @@ -804,6 +817,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { .groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() + .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L); // readGroupMessageEvent 内部会调 selectListByGroupIdAndPendingReceipt → 返回空简化流程 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 5L, 100L)).thenReturn(List.of()); @@ -876,6 +891,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { .groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() + .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L); // 调用 @@ -886,6 +903,23 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class)); } + @Test + public void testReadGroupMessages_messageNotInGroup() { + // 准备:用户在群,但 messageId 属于其它群 + ImGroupMemberDO member = ImGroupMemberDO.builder() + .groupId(10L).userId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() + .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build()); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> groupMessageService.readGroupMessages(1L, 10L, 100L)); + assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode()); + verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong()); + } + // ========== 回执:DONE 状态迁移 ========== @Test @@ -1105,7 +1139,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { reqVO.setLimit(20); List mockList = List.of( - ImGroupMessageDO.builder().id(99L).groupId(10L).senderId(2L).build() + ImGroupMessageDO.builder().id(99L).groupId(10L).senderId(2L) + .sendTime(joinTime.plusMinutes(1)).build() ); when(groupMessageMapper.selectHistoryList(10L, 100L, 20, joinTime)).thenReturn(mockList);