diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallCreateReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallCreateReqVO.java index 3ccad17..9dc6a75 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallCreateReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallCreateReqVO.java @@ -1,6 +1,11 @@ package cn.iocoder.yudao.module.im.controller.admin.rtc.vo; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallMediaTypeEnum; +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 lombok.Data; @@ -12,16 +17,28 @@ public class ImRtcCallCreateReqVO { @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "会话类型不能为空") - private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类 + @InEnum(ImConversationTypeEnum.class) + private Integer conversationType; @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") @NotNull(message = "媒体类型不能为空") - private Integer mediaType; // 参见 ImCallMediaTypeEnum 枚举类 + @InEnum(ImRtcCallMediaTypeEnum.class) + private Integer mediaType; @Schema(description = "群编号;群聊场景必填", example = "2048") private Long groupId; @Schema(description = "被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人") - private Set inviteeIds; + private Set<@NotNull(message = "被邀请用户编号不能为空") Long> inviteeIds; + + /** + * 通话仅支持私聊和群聊 + */ + @AssertTrue(message = "会话类型不支持") + @JsonIgnore + public boolean isConversationTypeSupported() { + return conversationType == null || ImConversationTypeEnum.isPrivate(conversationType) + || ImConversationTypeEnum.isGroup(conversationType); + } } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallInviteReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallInviteReqVO.java index 689c26e..7fcde58 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallInviteReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallInviteReqVO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.controller.admin.rtc.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.Set; @@ -17,6 +18,6 @@ public class ImRtcCallInviteReqVO { @Schema(description = "新邀请的用户编号集合", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "请至少选择一位成员") - private Set inviteeIds; + private Set<@NotNull(message = "被邀请用户编号不能为空") Long> inviteeIds; } 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 a11a8d4..1919aae 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 @@ -141,9 +141,9 @@ public class ImFriendServiceImpl implements ImFriendService { if (reqVO.getDisplayName() == null && reqVO.getSilent() == null && reqVO.getPinned() == null) { return; } - // 1.2 校验好友关系存在 + // 1.2 校验好友关系启用 ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId()); - if (friend == null) { + if (friend == null || !CommonStatusEnum.isEnable(friend.getStatus())) { throw exception(FRIEND_NOT_FRIEND); } @@ -209,7 +209,9 @@ public class ImFriendServiceImpl implements ImFriendService { @Transactional(rollbackFor = Exception.class) public void deleteFriend(Long userId, Long friendUserId, Boolean clear) { // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 - deleteFriend0(userId, friendUserId); + if (!deleteFriend0(userId, friendUserId)) { + return; + } // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(friendUserId 不感知);clear 透传让多端清理动作一致 // clear=false 时前端按 type=FRIEND_DELETE 渲染「你已删除好友」会话气泡(瞬时); @@ -311,13 +313,14 @@ public class ImFriendServiceImpl implements ImFriendService { *

* FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict */ - public void deleteFriend0(Long userId, Long friendUserId) { + public boolean deleteFriend0(Long userId, Long friendUserId) { ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { - return; + return false; } friendMapper.updateById(new ImFriendDO().setId(exists.getId()) .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now())); + return true; } // ==================== 管理后台 ==================== 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 549e950..81d03f9 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 @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; +import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; @@ -85,6 +86,24 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testUpdateFriend_disabledFriend() { + // 准备 + ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); + reqVO.setFriendUserId(2L); + reqVO.setSilent(true); + ImFriendDO friend = ImFriendDO.builder().id(100L).userId(1L).friendUserId(2L) + .status(CommonStatusEnum.DISABLE.getStatus()).build(); + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> friendService.updateFriend(1L, reqVO)); + assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode()); + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); + verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class)); + } + + @Test public void testUpdateFriend_displayNameOnly() { // 准备:只传备注、不传 silent —— 走差量更新 ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); @@ -178,8 +197,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { .status(CommonStatusEnum.DISABLE.getStatus()).build(); when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); - friendService.deleteFriend0(1L, 2L); + boolean result = friendService.deleteFriend0(1L, 2L); + assertFalse(result); verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); } @@ -189,14 +209,41 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); - friendService.deleteFriend0(1L, 2L); + boolean result = friendService.deleteFriend0(1L, 2L); + assertTrue(result); ArgumentCaptor captor = ArgumentCaptor.forClass(ImFriendDO.class); verify(imFriendMapper).updateById(captor.capture()); assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); assertNotNull(captor.getValue().getDeleteTime()); } + @Test + public void testDeleteFriend_alreadyDisabledSkipNotification() { + // 准备:已经是 DISABLE,不再推本端删除通知 + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) + .status(CommonStatusEnum.DISABLE.getStatus()).build(); + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); + + friendService.deleteFriend(1L, 2L, true); + + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); + verify(privateMessageService, never()).sendPrivateMessage(anyLong(), any(ImPrivateMessageSendDTO.class)); + } + + @Test + public void testDeleteFriend_enabledSendNotification() { + // 准备 + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); + + friendService.deleteFriend(1L, 2L, true); + + verify(imFriendMapper).updateById(any(ImFriendDO.class)); + verify(privateMessageService).sendPrivateMessage(eq(1L), any(ImPrivateMessageSendDTO.class)); + } + // ========== 其它读方法 ========== @Test