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 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 7 import io.swagger.v3.oas.annotations.media.Schema;
  8 +import jakarta.validation.constraints.AssertTrue;
4 9 import jakarta.validation.constraints.NotNull;
5 10 import lombok.Data;
6 11  
... ... @@ -12,16 +17,28 @@ public class ImRtcCallCreateReqVO {
12 17  
13 18 @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
14 19 @NotNull(message = "会话类型不能为空")
15   - private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类
  20 + @InEnum(ImConversationTypeEnum.class)
  21 + private Integer conversationType;
16 22  
17 23 @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
18 24 @NotNull(message = "媒体类型不能为空")
19   - private Integer mediaType; // 参见 ImCallMediaTypeEnum 枚举类
  25 + @InEnum(ImRtcCallMediaTypeEnum.class)
  26 + private Integer mediaType;
20 27  
21 28 @Schema(description = "群编号;群聊场景必填", example = "2048")
22 29 private Long groupId;
23 30  
24 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 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotBlank;
5 5 import jakarta.validation.constraints.NotEmpty;
  6 +import jakarta.validation.constraints.NotNull;
6 7 import lombok.Data;
7 8  
8 9 import java.util.Set;
... ... @@ -17,6 +18,6 @@ public class ImRtcCallInviteReqVO {
17 18  
18 19 @Schema(description = "新邀请的用户编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
19 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 141 if (reqVO.getDisplayName() == null && reqVO.getSilent() == null && reqVO.getPinned() == null) {
142 142 return;
143 143 }
144   - // 1.2 校验好友关系存在
  144 + // 1.2 校验好友关系启用
145 145 ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId());
146   - if (friend == null) {
  146 + if (friend == null || !CommonStatusEnum.isEnable(friend.getStatus())) {
147 147 throw exception(FRIEND_NOT_FRIEND);
148 148 }
149 149  
... ... @@ -209,7 +209,9 @@ public class ImFriendServiceImpl implements ImFriendService {
209 209 @Transactional(rollbackFor = Exception.class)
210 210 public void deleteFriend(Long userId, Long friendUserId, Boolean clear) {
211 211 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
212   - deleteFriend0(userId, friendUserId);
  212 + if (!deleteFriend0(userId, friendUserId)) {
  213 + return;
  214 + }
213 215  
214 216 // 2. 走 sendPrivateMessage + persistent=false:不入库 + 仅推 userId 多端(friendUserId 不感知);clear 透传让多端清理动作一致
215 217 // clear=false 时前端按 type=FRIEND_DELETE 渲染「你已删除好友」会话气泡(瞬时);
... ... @@ -311,13 +313,14 @@ public class ImFriendServiceImpl implements ImFriendService {
311 313 * <p>
312 314 * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
313 315 */
314   - public void deleteFriend0(Long userId, Long friendUserId) {
  316 + public boolean deleteFriend0(Long userId, Long friendUserId) {
315 317 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
316 318 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
317   - return;
  319 + return false;
318 320 }
319 321 friendMapper.updateById(new ImFriendDO().setId(exists.getId())
320 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 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
8 8 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
9 9 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  10 +import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
10 11 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
11 12 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
12 13 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
... ... @@ -85,6 +86,24 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
85 86 }
86 87  
87 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 107 public void testUpdateFriend_displayNameOnly() {
89 108 // 准备:只传备注、不传 silent —— 走差量更新
90 109 ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO();
... ... @@ -178,8 +197,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
178 197 .status(CommonStatusEnum.DISABLE.getStatus()).build();
179 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 203 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
184 204 }
185 205  
... ... @@ -189,14 +209,41 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
189 209 .status(CommonStatusEnum.ENABLE.getStatus()).build();
190 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 215 ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class);
195 216 verify(imFriendMapper).updateById(captor.capture());
196 217 assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus());
197 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 249 @Test
... ...