diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java index 0ddaf79..ec88708 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Data; import java.util.List; @@ -12,6 +13,7 @@ public class ImGroupCreateReqVO { @Schema(description = "群名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道技术交流群") @NotBlank(message = "群名称不能为空") + @Size(max = 64, message = "群名称长度不能超过 64") private String name; @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java index 128cdd9..99bdfb3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java @@ -1,7 +1,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; @Schema(description = "管理后台 - 群更新 Request VO") @@ -13,15 +17,30 @@ public class ImGroupUpdateReqVO { private Long id; @Schema(description = "群名称", example = "芋道技术交流群") + @Size(max = 64, message = "群名称长度不能超过 64") private String name; @Schema(description = "群头像") + @Size(max = 512, message = "群头像长度不能超过 512") private String avatar; @Schema(description = "群公告") + @Size(max = 2048, message = "群公告长度不能超过 2048") private String notice; @Schema(description = "进群是否需群主 / 管理员审批", example = "true") private Boolean joinApproval; + @AssertTrue(message = "群名称不能为空") + @JsonIgnore + public boolean isNameValid() { + return name == null || StrUtil.isNotBlank(name); + } + + @AssertTrue(message = "群头像不能为空") + @JsonIgnore + public boolean isAvatarValid() { + return avatar == null || StrUtil.isNotBlank(avatar); + } + } 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 95997c3..83752a9 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 @@ -47,6 +47,15 @@ public interface ImFriendMapper extends BaseMapperX { .eq(ImFriendDO::getStatus, status)); } + default List selectListByUserIdsAndFriendUserIdAndStatus(Collection userIds, + Long friendUserId, + Integer status) { + return selectList(new LambdaQueryWrapperX() + .in(ImFriendDO::getUserId, userIds) + .eq(ImFriendDO::getFriendUserId, friendUserId) + .eq(ImFriendDO::getStatus, status)); + } + default PageResult selectPage(ImFriendManagerPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ImFriendDO::getUserId, reqVO.getUserId()) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java index 0c6f103..2354f05 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java @@ -81,7 +81,6 @@ public interface ImGroupMessageMapper extends BaseMapperX { default List selectHistoryList(Long groupId, Long maxId, Integer limit, LocalDateTime joinTime) { QueryWrapperX wrapper = new QueryWrapperX<>(); wrapper.eq("group_id", groupId) - .ne("status", ImMessageStatusEnum.RECALL.getStatus()) .lt(maxId != null, "id", maxId) .ge(joinTime != null, "send_time", joinTime) .orderByDesc("id"); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java index 32c0516..9b62b45 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import org.apache.ibatis.annotations.Mapper; import java.time.LocalDateTime; @@ -56,7 +55,6 @@ public interface ImPrivateMessageMapper extends BaseMapperX wrapper.and(w -> w.eq("sender_id", userId).eq("receiver_id", receiverId) .or() .eq("sender_id", receiverId).eq("receiver_id", userId)) - .ne("status", ImMessageStatusEnum.RECALL.getStatus()) .lt(maxId != null, "id", maxId) .orderByDesc("id"); wrapper.limitN(limit); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java index 9d4e5f5..7ef83a3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java @@ -35,8 +35,8 @@ public class AdminUserProfileUpdateConsumer { public void onMessage(AdminUserProfileUpdateMessage message) { log.info("[onMessage][消息内容({})]", message); Long userId = message.getUserId(); - // 1. SQL 层过滤 ENABLE 好友 - List friends = friendService.getEnableFriendList(userId); + // 1. 过滤双向有效好友 + List friends = friendService.getMutualEnableFriendList(userId); if (friends.isEmpty()) { return; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java index 20564ef..081720e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java @@ -143,10 +143,12 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @Override @Transactional(rollbackFor = Exception.class) public void agreeFriendRequest(Long userId, Long requestId) { - // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底) + // 1.1 校验申请存在、未处理、操作人是接收方 ImFriendRequestDO request = validateRequestForHandle(userId, requestId); + // 1.2 复验双方用户有效 + adminUserApi.validateUserList(List.of(request.getFromUserId(), request.getToUserId())); - // 2. 乐观锁更新申请:handleResult=AGREED + handleTime;并发同意会有一方 affectedRows=0 + // 2. 乐观锁更新申请处理结果 ImFriendRequestDO updateObj = new ImFriendRequestDO() .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(LocalDateTime.now()); int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(), diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java index f3ed003..84a6de9 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java @@ -48,6 +48,11 @@ public interface ImFriendService { List getEnableFriendList(Long userId); /** + * 获得当前用户的双向有效好友列表(双方均 ENABLE 状态) + */ + List getMutualEnableFriendList(Long userId); + + /** * 获得当前用户与指定用户之间的有效好友列表(仅 ENABLE 状态) */ List getActiveFriendList(Long userId, Collection friendUserIds); 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 5d16b26..a11a8d4 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 @@ -31,8 +31,11 @@ import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND_STATE; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_BLOCKED_BY_PEER; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED; @@ -101,6 +104,24 @@ public class ImFriendServiceImpl implements ImFriendService { } @Override + public List getMutualEnableFriendList(Long userId) { + // 1. 查询本端启用好友 + List friends = getEnableFriendList(userId); + if (CollUtil.isEmpty(friends)) { + return Collections.emptyList(); + } + + // 2. 查询对端启用好友关系 + Set friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); + List mutualFriends = friendMapper.selectListByUserIdsAndFriendUserIdAndStatus(friendUserIds, userId, + CommonStatusEnum.ENABLE.getStatus()); + Set mutualUserIds = convertSet(mutualFriends, ImFriendDO::getUserId); + + // 3. 过滤双向启用好友 + return filterList(friends, friend -> mutualUserIds.contains(friend.getFriendUserId())); + } + + @Override public List getActiveFriendList(Long userId, Collection friendUserIds) { if (CollUtil.isEmpty(friendUserIds)) { return Collections.emptyList(); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java index ae4efbf..0b8517d 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java @@ -812,6 +812,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { Set initial = new LinkedHashSet<>(reqVO.getInviteeIds()); // 发起人本人不进被邀请池 initial.remove(inviterId); + if (CollUtil.isEmpty(initial)) { + throw exception(RTC_GROUP_INVITEE_REQUIRED); + } int max = imProperties.getRtc().getGroupMaxParticipants(); if (initial.size() + 1 > max) { throw exception(RTC_GROUP_INVITEE_OVER_LIMIT); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java index 1f68fe3..8b33b64 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java @@ -15,6 +15,9 @@ import org.springframework.transaction.support.TransactionSynchronizationManager import org.springframework.validation.annotation.Validated; import java.util.Collection; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertLinkedSet; /** * IM WebSocket 推送 Service 实现类 @@ -59,7 +62,7 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { */ @Async public void doSendPrivateMessage(Collection userIds, ImPrivateMessageDTO dto) { - for (Long userId : userIds) { + for (Long userId : getDistinctUserIds(userIds)) { try { webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, ImPrivateMessageDTO.TYPE, dto); @@ -74,7 +77,7 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { */ @Async public void doSendGroupMessage(Collection userIds, ImGroupMessageDTO dto) { - for (Long userId : userIds) { + for (Long userId : getDistinctUserIds(userIds)) { try { webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, ImGroupMessageDTO.TYPE, dto); @@ -89,7 +92,7 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { */ @Async public void doSendChannelMessage(Collection userIds, ImChannelMessageDTO dto) { - for (Long userId : userIds) { + for (Long userId : getDistinctUserIds(userIds)) { try { webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, ImChannelMessageDTO.TYPE, dto); @@ -113,6 +116,10 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { } } + private static Set getDistinctUserIds(Collection userIds) { + return convertLinkedSet(userIds, userId -> userId); + } + /** * 事务感知的任务调度: * - 有事务:注册 afterCommit 回调,事务提交后再执行,防止客户端拿到消息去查库时数据还没落盘 diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java index 735afbb..9c71fd2 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java @@ -165,7 +165,7 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { } @Test - public void testSelectHistoryList_excludeRecall() { + public void testSelectHistoryList_includeRecall() { ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); mapper.insert(normal); ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); @@ -173,8 +173,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { List result = mapper.selectHistoryList(10L, null, 100, null); - assertEquals(1, result.size()); - assertEquals(normal.getId(), result.get(0).getId()); + assertEquals(2, result.size()); + assertEquals(recalled.getId(), result.get(0).getId()); + assertEquals(normal.getId(), result.get(1).getId()); } @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java index f49d18c..e67b3e5 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java @@ -120,7 +120,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { } @Test - public void testSelectHistoryList_excludeRecall() { + public void testSelectHistoryList_includeRecall() { // 准备 ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); mapper.insert(msg1); @@ -130,9 +130,10 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { // 调用 List result = mapper.selectHistoryList(1L, 2L, null, 100); - // 断言:撤回的不返回 - assertEquals(1, result.size()); - assertEquals(msg1.getId(), result.get(0).getId()); + // 断言:撤回消息一并返回 + assertEquals(2, result.size()); + assertEquals(msg2.getId(), result.get(0).getId()); + assertEquals(msg1.getId(), result.get(1).getId()); } @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java index 1123ab6..0b7585f 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java @@ -19,6 +19,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.util.List; + import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -198,6 +200,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { friendRequestService.agreeFriendRequest(2L, 100L); // 断言:双向建立好友 + 推 APPROVED 给发起方 + verify(adminUserApi).validateUserList(List.of(1L, 2L)); verify(friendService).becomeFriends(request); verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); } 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 df1edff..549e950 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 @@ -222,4 +222,23 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { verify(imFriendMapper, never()).selectListByUserIdAndFriendUserIdsAndStatus(anyLong(), anyCollection(), anyInt()); } + @Test + public void testGetMutualEnableFriendList_filterSingleSideDeleted() { + ImFriendDO friend2 = ImFriendDO.builder().id(1L).userId(1L).friendUserId(2L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + ImFriendDO friend3 = ImFriendDO.builder().id(2L).userId(1L).friendUserId(3L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(imFriendMapper.selectListByUserIdAndStatus(1L, CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(List.of(friend2, friend3)); + when(imFriendMapper.selectListByUserIdsAndFriendUserIdAndStatus(anyCollection(), eq(1L), + eq(CommonStatusEnum.ENABLE.getStatus()))).thenReturn(List.of( + ImFriendDO.builder().userId(2L).friendUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build())); + + List result = friendService.getMutualEnableFriendList(1L); + + assertEquals(1, result.size()); + assertEquals(2L, result.get(0).getFriendUserId()); + } + } diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java index 7177882..24f3dac 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.im.service.rtc; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallCreateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO; import cn.iocoder.yudao.module.im.dal.mysql.rtc.ImRtcCallMapper; import cn.iocoder.yudao.module.im.dal.mysql.rtc.ImRtcParticipantMapper; +import cn.iocoder.yudao.module.im.dal.redis.rtc.ImRtcCallLockRedisDAO; import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum; import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum; @@ -27,7 +29,10 @@ import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.RTC_GROUP_INVITEE_REQUIRED; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.RTC_SELF_BUSY; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -43,6 +48,8 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { @Mock private ImRtcCallMapper rtcCallMapper; @Mock + private ImRtcCallLockRedisDAO rtcCallLockRedisDAO; + @Mock private AdminUserApi adminUserApi; @Mock private ImWebSocketService webSocketService; @@ -228,6 +235,28 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any()); } + // ========== createCall ========== + + @Test + public void testCreateCall_groupOnlyInviteSelf_throwInviteeRequired() throws Exception { + when(imProperties.getRtc()).thenReturn(new ImProperties.Rtc()); + when(rtcCallLockRedisDAO.lockGroup(eq(10L), any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + Callable callable = invocation.getArgument(1); + return callable.call(); + }); + ImRtcCallCreateReqVO reqVO = new ImRtcCallCreateReqVO(); + reqVO.setConversationType(ImConversationTypeEnum.GROUP.getType()); + reqVO.setGroupId(10L); + reqVO.setInviteeIds(Set.of(100L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> rtcCallService.createCall(100L, reqVO)); + + assertEquals(RTC_GROUP_INVITEE_REQUIRED.getCode(), exception.getCode()); + verify(rtcParticipantMapper, never()).insertBatch(anyList()); + } + // ========== acceptCall / joinCall 忙线校验 ========== @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java index aa42d35..f1c3339 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java @@ -14,6 +14,8 @@ import org.mockito.MockedStatic; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -137,6 +139,29 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testDoSendGroupMessage_emptyUserIds_noSend() { + ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L); + + imWebSocketService.doSendGroupMessage(Collections.emptyList(), dto); + imWebSocketService.doSendGroupMessage(null, dto); + + verifyNoInteractions(webSocketMessageSender); + } + + @Test + public void testDoSendGroupMessage_distinctUserIds() { + ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L); + + imWebSocketService.doSendGroupMessage(Arrays.asList(1L, 2L, 1L, null), dto); + + verify(webSocketMessageSender).sendObject( + eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImGroupMessageDTO.TYPE), eq(dto)); + verify(webSocketMessageSender).sendObject( + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImGroupMessageDTO.TYPE), eq(dto)); + verifyNoMoreInteractions(webSocketMessageSender); + } + + @Test public void testSendPrivateMessageAsync_exceptionSwallowed() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))