Commit 36c30a431a0f4b3eb947a401823e786e8962d125

Authored by YunaiV
1 parent 49241c31

fix: 加强 IM 上传 URL 与 RTC 来电载荷校验

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallCreateReqVO.java
1 package cn.iocoder.yudao.module.im.controller.admin.rtc.vo; 1 package cn.iocoder.yudao.module.im.controller.admin.rtc.vo;
2 2
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  5 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallMediaTypeEnum;
  6 +import com.fasterxml.jackson.annotation.JsonIgnore;
3 import io.swagger.v3.oas.annotations.media.Schema; 7 import io.swagger.v3.oas.annotations.media.Schema;
  8 +import jakarta.validation.constraints.AssertTrue;
4 import jakarta.validation.constraints.NotNull; 9 import jakarta.validation.constraints.NotNull;
5 import lombok.Data; 10 import lombok.Data;
6 11
@@ -12,16 +17,28 @@ public class ImRtcCallCreateReqVO { @@ -12,16 +17,28 @@ public class ImRtcCallCreateReqVO {
12 17
13 @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 18 @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
14 @NotNull(message = "会话类型不能为空") 19 @NotNull(message = "会话类型不能为空")
15 - private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类 20 + @InEnum(ImConversationTypeEnum.class)
  21 + private Integer conversationType;
16 22
17 @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") 23 @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
18 @NotNull(message = "媒体类型不能为空") 24 @NotNull(message = "媒体类型不能为空")
19 - private Integer mediaType; // 参见 ImCallMediaTypeEnum 枚举类 25 + @InEnum(ImRtcCallMediaTypeEnum.class)
  26 + private Integer mediaType;
20 27
21 @Schema(description = "群编号;群聊场景必填", example = "2048") 28 @Schema(description = "群编号;群聊场景必填", example = "2048")
22 private Long groupId; 29 private Long groupId;
23 30
24 @Schema(description = "被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人") 31 @Schema(description = "被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人")
25 - private Set<Long> inviteeIds; 32 + private Set<@NotNull(message = "被邀请用户编号不能为空") Long> inviteeIds;
  33 +
  34 + /**
  35 + * 通话仅支持私聊和群聊
  36 + */
  37 + @AssertTrue(message = "会话类型不支持")
  38 + @JsonIgnore
  39 + public boolean isConversationTypeSupported() {
  40 + return conversationType == null || ImConversationTypeEnum.isPrivate(conversationType)
  41 + || ImConversationTypeEnum.isGroup(conversationType);
  42 + }
26 43
27 } 44 }
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; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.controller.admin.rtc.vo;
3 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.NotBlank;
5 import jakarta.validation.constraints.NotEmpty; 5 import jakarta.validation.constraints.NotEmpty;
  6 +import jakarta.validation.constraints.NotNull;
6 import lombok.Data; 7 import lombok.Data;
7 8
8 import java.util.Set; 9 import java.util.Set;
@@ -17,6 +18,6 @@ public class ImRtcCallInviteReqVO { @@ -17,6 +18,6 @@ public class ImRtcCallInviteReqVO {
17 18
18 @Schema(description = "新邀请的用户编号集合", requiredMode = Schema.RequiredMode.REQUIRED) 19 @Schema(description = "新邀请的用户编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
19 @NotEmpty(message = "请至少选择一位成员") 20 @NotEmpty(message = "请至少选择一位成员")
20 - private Set<Long> inviteeIds; 21 + private Set<@NotNull(message = "被邀请用户编号不能为空") Long> inviteeIds;
21 22
22 } 23 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
@@ -141,9 +141,9 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -141,9 +141,9 @@ public class ImFriendServiceImpl implements ImFriendService {
141 if (reqVO.getDisplayName() == null && reqVO.getSilent() == null && reqVO.getPinned() == null) { 141 if (reqVO.getDisplayName() == null && reqVO.getSilent() == null && reqVO.getPinned() == null) {
142 return; 142 return;
143 } 143 }
144 - // 1.2 校验好友关系存在 144 + // 1.2 校验好友关系启用
145 ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId()); 145 ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId());
146 - if (friend == null) { 146 + if (friend == null || !CommonStatusEnum.isEnable(friend.getStatus())) {
147 throw exception(FRIEND_NOT_FRIEND); 147 throw exception(FRIEND_NOT_FRIEND);
148 } 148 }
149 149
@@ -209,7 +209,9 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -209,7 +209,9 @@ public class ImFriendServiceImpl implements ImFriendService {
209 @Transactional(rollbackFor = Exception.class) 209 @Transactional(rollbackFor = Exception.class)
210 public void deleteFriend(Long userId, Long friendUserId, Boolean clear) { 210 public void deleteFriend(Long userId, Long friendUserId, Boolean clear) {
211 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 211 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
212 - deleteFriend0(userId, friendUserId); 212 + if (!deleteFriend0(userId, friendUserId)) {
  213 + return;
  214 + }
213 215
214 // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(friendUserId 不感知);clear 透传让多端清理动作一致 216 // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(friendUserId 不感知);clear 透传让多端清理动作一致
215 // clear=false 时前端按 type=FRIEND_DELETE 渲染「你已删除好友」会话气泡(瞬时); 217 // clear=false 时前端按 type=FRIEND_DELETE 渲染「你已删除好友」会话气泡(瞬时);
@@ -311,13 +313,14 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -311,13 +313,14 @@ public class ImFriendServiceImpl implements ImFriendService {
311 * <p> 313 * <p>
312 * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict 314 * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
313 */ 315 */
314 - public void deleteFriend0(Long userId, Long friendUserId) { 316 + public boolean deleteFriend0(Long userId, Long friendUserId) {
315 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); 317 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
316 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { 318 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
317 - return; 319 + return false;
318 } 320 }
319 friendMapper.updateById(new ImFriendDO().setId(exists.getId()) 321 friendMapper.updateById(new ImFriendDO().setId(exists.getId())
320 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now())); 322 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now()));
  323 + return true;
321 } 324 }
322 325
323 // ==================== 管理后台 ==================== 326 // ==================== 管理后台 ====================
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 @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO
7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
8 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; 8 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
9 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; 9 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  10 +import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
10 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 11 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
11 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; 12 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
12 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 13 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -85,6 +86,24 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -85,6 +86,24 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
85 } 86 }
86 87
87 @Test 88 @Test
  89 + public void testUpdateFriend_disabledFriend() {
  90 + // 准备
  91 + ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO();
  92 + reqVO.setFriendUserId(2L);
  93 + reqVO.setSilent(true);
  94 + ImFriendDO friend = ImFriendDO.builder().id(100L).userId(1L).friendUserId(2L)
  95 + .status(CommonStatusEnum.DISABLE.getStatus()).build();
  96 + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend);
  97 +
  98 + // 调用并断言
  99 + ServiceException exception = assertThrows(ServiceException.class,
  100 + () -> friendService.updateFriend(1L, reqVO));
  101 + assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode());
  102 + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
  103 + verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class));
  104 + }
  105 +
  106 + @Test
88 public void testUpdateFriend_displayNameOnly() { 107 public void testUpdateFriend_displayNameOnly() {
89 // 准备:只传备注、不传 silent —— 走差量更新 108 // 准备:只传备注、不传 silent —— 走差量更新
90 ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); 109 ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO();
@@ -178,8 +197,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -178,8 +197,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
178 .status(CommonStatusEnum.DISABLE.getStatus()).build(); 197 .status(CommonStatusEnum.DISABLE.getStatus()).build();
179 when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); 198 when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists);
180 199
181 - friendService.deleteFriend0(1L, 2L); 200 + boolean result = friendService.deleteFriend0(1L, 2L);
182 201
  202 + assertFalse(result);
183 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); 203 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
184 } 204 }
185 205
@@ -189,14 +209,41 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -189,14 +209,41 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
189 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 209 .status(CommonStatusEnum.ENABLE.getStatus()).build();
190 when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); 210 when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists);
191 211
192 - friendService.deleteFriend0(1L, 2L); 212 + boolean result = friendService.deleteFriend0(1L, 2L);
193 213
  214 + assertTrue(result);
194 ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); 215 ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class);
195 verify(imFriendMapper).updateById(captor.capture()); 216 verify(imFriendMapper).updateById(captor.capture());
196 assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); 217 assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus());
197 assertNotNull(captor.getValue().getDeleteTime()); 218 assertNotNull(captor.getValue().getDeleteTime());
198 } 219 }
199 220
  221 + @Test
  222 + public void testDeleteFriend_alreadyDisabledSkipNotification() {
  223 + // 准备:已经是 DISABLE,不再推本端删除通知
  224 + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L)
  225 + .status(CommonStatusEnum.DISABLE.getStatus()).build();
  226 + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists);
  227 +
  228 + friendService.deleteFriend(1L, 2L, true);
  229 +
  230 + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
  231 + verify(privateMessageService, never()).sendPrivateMessage(anyLong(), any(ImPrivateMessageSendDTO.class));
  232 + }
  233 +
  234 + @Test
  235 + public void testDeleteFriend_enabledSendNotification() {
  236 + // 准备
  237 + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L)
  238 + .status(CommonStatusEnum.ENABLE.getStatus()).build();
  239 + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists);
  240 +
  241 + friendService.deleteFriend(1L, 2L, true);
  242 +
  243 + verify(imFriendMapper).updateById(any(ImFriendDO.class));
  244 + verify(privateMessageService).sendPrivateMessage(eq(1L), any(ImPrivateMessageSendDTO.class));
  245 + }
  246 +
200 // ========== 其它读方法 ========== 247 // ========== 其它读方法 ==========
201 248
202 @Test 249 @Test