Commit 36c30a431a0f4b3eb947a401823e786e8962d125
1 parent
49241c31
fix: 加强 IM 上传 URL 与 RTC 来电载荷校验
Showing
4 changed files
with
79 additions
and
11 deletions
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 |