diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java index cc82205..e9a7477 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java @@ -134,14 +134,14 @@ public class ImGroupController { @PutMapping("/pin-message") @Operation(summary = "置顶群消息(群主 / 管理员)") public CommonResult pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) { - groupService.pinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId()); + groupService.pinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId()); return success(true); } @PutMapping("/unpin-message") @Operation(summary = "取消置顶群消息(群主 / 管理员)") public CommonResult unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) { - groupService.unpinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId()); + groupService.unpinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId()); return success(true); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java index 72b518c..e4183f0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java @@ -13,7 +13,7 @@ public class ImGroupAdminAddReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") @NotEmpty(message = "目标用户编号列表不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java index 3444128..7c35dfb 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java @@ -13,7 +13,7 @@ public class ImGroupAdminRemoveReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") @NotEmpty(message = "目标用户编号列表不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java index 4781f82..2e2243a 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java @@ -10,7 +10,7 @@ public class ImGroupCancelMuteMemberReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "用户编号不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java index 0e4dfac..49c14ff 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java @@ -13,7 +13,7 @@ public class ImGroupMessagePinReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9527") @NotNull(message = "消息编号不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java index faa42ac..0a23cb0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java @@ -8,10 +8,9 @@ import lombok.Data; @Data public class ImGroupMuteAllReqVO { - // TODO @AI:改成 id 是不是更好? @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "是否全群禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否全群禁言不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java index a9927b3..1847648 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java @@ -11,7 +11,7 @@ public class ImGroupMuteMemberReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "被禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "用户编号不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java index 9879099..226d07e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java @@ -10,7 +10,7 @@ public class ImGroupTransferOwnerReqVO { @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") @NotNull(message = "群编号不能为空") - private Long groupId; + private Long id; @Schema(description = "新群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202") @NotNull(message = "新群主用户编号不能为空") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java index ea43d9e..72fe756 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java @@ -27,12 +27,6 @@ public interface ImGroupRequestMapper extends BaseMapperX { .eq(ImGroupRequestDO::getUserId, userId)); } - default List selectListByGroupIdAndUserIds(Long groupId, Collection userIds) { - return selectList(new LambdaQueryWrapperX() - .eq(ImGroupRequestDO::getGroupId, groupId) - .in(ImGroupRequestDO::getUserId, userIds)); - } - default List selectListByGroupIdsAndHandleResult(Collection groupIds, Integer handleResult) { return selectList(new LambdaQueryWrapperX() .in(ImGroupRequestDO::getGroupId, groupIds) @@ -73,14 +67,9 @@ public interface ImGroupRequestMapper extends BaseMapperX { .set(ImGroupRequestDO::getUpdateTime, updateTime)); } - /** - * 复用邀请审批的旧记录(批量):盖写 inviterUserId / addSource,重置为未处理 + 清空旧处理痕迹 + 刷 update_time - */ - default int updateInviteByGroupIdAndUserIdsReset(Long groupId, Collection userIds, - Long inviterUserId, Integer addSource, LocalDateTime updateTime) { + default int updateInviteByIdReset(Long id, Long inviterUserId, Integer addSource, LocalDateTime updateTime) { return update(null, new LambdaUpdateWrapper() - .eq(ImGroupRequestDO::getGroupId, groupId) - .in(ImGroupRequestDO::getUserId, userIds) + .eq(ImGroupRequestDO::getId, id) .set(ImGroupRequestDO::getInviterUserId, inviterUserId) .set(ImGroupRequestDO::getAddSource, addSource) .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantRoleEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantRoleEnum.java index 583f41b..125c5f0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantRoleEnum.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantRoleEnum.java @@ -17,8 +17,7 @@ public enum ImRtcParticipantRoleEnum implements ArrayValuable { INVITER(1, "发起人"), INVITEE(2, "被邀请者"), - // TODO @AI:改成 joiner?还是 joinee?看看这样拼写是不是对的。 - ACTIVE_JOIN(3, "主动加入者"); // 仅群通话场景:旁观者点胶囊条加入已有通话 + JOINER(3, "主动加入者"); // 仅群通话场景:旁观者点胶囊条加入已有通话 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImRtcParticipantRoleEnum::getRole).toArray(Integer[]::new); 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 39f6423..12b5fb1 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 @@ -24,6 +24,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; @@ -92,22 +93,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { return null; } - // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹 - ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId); - if (request != null) { - LocalDateTime now = LocalDateTime.now(); - friendRequestMapper.updateByIdReset(request.getId(), - reqVO.getApplyContent(), reqVO.getDisplayName(), reqVO.getAddSource(), now); - request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName()) - .setAddSource(reqVO.getAddSource()) - .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) - .setHandleContent(null).setHandleTime(null).setUpdateTime(now); - } else { - request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class) - .setFromUserId(fromUserId).setToUserId(toUserId) - .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); - friendRequestMapper.insert(request); - } + // 2. 落库:同一申请人和接收人唯一,已有记录覆盖申请内容并重置为未处理 + ImFriendRequestDO request = createOrResetRequest(fromUserId, reqVO); // 3. 推送 FRIEND_REQUEST_RECEIVED 给 toUser 多端;payload 携带申请方昵称 / 头像,前端按 requestId 直推 push 进列表 AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId); @@ -141,6 +128,44 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { return request; } + /** + * 创建或重置好友申请 + * + * @param fromUserId 申请人用户编号 + * @param reqVO 申请请求 + * @return 申请记录 + */ + private ImFriendRequestDO createOrResetRequest(Long fromUserId, ImFriendRequestApplyReqVO reqVO) { + Long toUserId = reqVO.getToUserId(); + ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId); + if (request == null) { + // 1. 无旧申请:创建新申请;唯一键冲突时回查并复用并发写入的记录 + request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class) + .setFromUserId(fromUserId).setToUserId(toUserId) + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); + try { + friendRequestMapper.insert(request); + return request; + } catch (DuplicateKeyException ex) { + request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId); + if (request == null) { + throw ex; + } + } + } + + // 2. 复用旧申请:覆盖本次申请内容,并重置为未处理 + LocalDateTime now = LocalDateTime.now(); + friendRequestMapper.updateByIdReset(request.getId(), + reqVO.getApplyContent(), reqVO.getDisplayName(), reqVO.getAddSource(), now); + // 同步内存对象,后续通知和自动通过直接复用 + request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName()) + .setAddSource(reqVO.getAddSource()) + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) + .setHandleContent(null).setHandleTime(null).setUpdateTime(now); + return request; + } + @Override @Transactional(rollbackFor = Exception.class) public void agreeFriendRequest(Long userId, Long requestId) { diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java index 43de7be..bd604f6 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java @@ -19,7 +19,7 @@ public interface ImGroupRequestService { /** * 用户主动申请加群 *

- * 群未开启审批时直接入群 + 1510 全员广播;开启审批则 upsert 一条 inviterUserId=null 的待审批记录 + 1503 推送 + * 群未开启审批时直接入群 + 1510 全员广播;开启审批则创建或复用一条待审批记录 + 1503 推送 * * @param userId 申请人用户编号 * @param reqVO 申请请求 @@ -45,7 +45,7 @@ public interface ImGroupRequestService { void refuseGroupRequest(Long userId, Long requestId, String handleContent); /** - * 邀请创建审批申请;inviteGroupMember 在群开启审批时调用,每个被邀请人 upsert 一条 inviterUserId=操作人 的记录 + * 邀请创建审批申请;inviteGroupMember 在群开启审批时调用,每个被邀请人创建或复用一条待审批记录 * * @param groupId 群编号 * @param inviterUserId 邀请人用户编号 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java index 6328ec1..71d3d76 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java @@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -93,22 +94,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { return null; } - // 3. 情况二:群开启了审批;upsert 一条主动申请记录(inviterUserId=null) - ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); - if (request != null) { - LocalDateTime now = LocalDateTime.now(); - groupRequestMapper.updateApplyByIdReset(request.getId(), - reqVO.getApplyContent(), reqVO.getAddSource(), now); - request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) - .setInviterUserId(null).setHandleUserId(null) - .setHandleContent(null).setHandleTime(null).setUpdateTime(now); - } else { - request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) - .setUserId(userId).setInviterUserId(null) - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); - groupRequestMapper.insert(request); - } + // 3. 情况二:群开启了审批,创建或复用一条主动申请记录 + ImGroupRequestDO request = createOrResetApplyRequest(groupId, userId, reqVO); // 4. 1503 私聊定向推群主 + 全部管理员(多端同步);payload 携带申请方昵称 / 头像 AdminUserRespDTO applyUser = adminUserApi.getUser(userId); @@ -203,39 +190,13 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { } ImGroupDO group = groupService.validateGroupExists(groupId); Integer inviteSource = ImGroupAddSourceEnum.INVITE.getSource(); - Integer unhandled = ImGroupRequestHandleResultEnum.UNHANDLED.getResult(); - LocalDateTime now = LocalDateTime.now(); + // 1. 逐条创建或复用邀请申请 + List requests = convertList(invitedUserIds, userId -> + createOrResetInviteRequest(groupId, inviterUserId, userId, inviteSource)); - // 1. 批量查现有记录,分流为「复用旧记录」+「新增」 - List existingList = groupRequestMapper.selectListByGroupIdAndUserIds(groupId, invitedUserIds); - Set existingUserIds = convertSet(existingList, ImGroupRequestDO::getUserId); - Collection newUserIds = CollUtil.subtract(invitedUserIds, existingUserIds); - - // 2. 复用旧记录:一条 update WHERE IN 完成 - if (CollUtil.isNotEmpty(existingList)) { - groupRequestMapper.updateInviteByGroupIdAndUserIdsReset(groupId, existingUserIds, - inviterUserId, inviteSource, now); - // 同步内存对象,下面 buildRequestNotification 要读 inviterUserId / addSource - existingList.forEach(r -> r.setInviterUserId(inviterUserId).setAddSource(inviteSource) - .setHandleResult(unhandled) - .setHandleUserId(null).setHandleContent(null).setHandleTime(null) - .setUpdateTime(now)); - } - - // 3. 新增:批量 insert - List newList = convertList(newUserIds, userId -> new ImGroupRequestDO() - .setGroupId(groupId).setUserId(userId).setInviterUserId(inviterUserId) - .setAddSource(inviteSource).setHandleResult(unhandled)); - if (CollUtil.isNotEmpty(newList)) { - groupRequestMapper.insertBatch(newList); - } - - // 4. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 + // 2. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 Map userMap = adminUserApi.getUserMap(invitedUserIds); List ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group); - List requests = new ArrayList<>(existingList.size() + newList.size()); - requests.addAll(existingList); - requests.addAll(newList); for (ImGroupRequestDO request : requests) { AdminUserRespDTO applyUser = userMap.get(request.getUserId()); GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); @@ -284,6 +245,109 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { } /** + * 创建或重置主动加群申请 + * + * @param groupId 群编号 + * @param userId 申请人用户编号 + * @param reqVO 申请请求 + * @return 申请记录 + */ + private ImGroupRequestDO createOrResetApplyRequest(Long groupId, Long userId, ImGroupRequestApplyReqVO reqVO) { + // 1. 已有申请:覆盖本次申请内容,并重置为未处理 + ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); + if (request != null) { + resetApplyRequest(request, reqVO); + return request; + } + // 2. 无旧申请:创建主动申请记录 + request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) + .setUserId(userId).setInviterUserId(null) + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); + try { + groupRequestMapper.insert(request); + return request; + } catch (DuplicateKeyException ex) { + // 3. 唯一键冲突:回查并复用并发写入的记录 + request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); + if (request == null) { + throw ex; + } + resetApplyRequest(request, reqVO); + return request; + } + } + + /** + * 创建或重置邀请加群申请 + * + * @param groupId 群编号 + * @param inviterUserId 邀请人用户编号 + * @param userId 被邀请人用户编号 + * @param inviteSource 邀请来源 + * @return 申请记录 + */ + private ImGroupRequestDO createOrResetInviteRequest(Long groupId, Long inviterUserId, + Long userId, Integer inviteSource) { + // 1. 已有申请:覆盖邀请人和来源,并重置为未处理 + ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); + if (request != null) { + resetInviteRequest(request, inviterUserId, inviteSource); + return request; + } + // 2. 无旧申请:创建邀请申请记录 + request = new ImGroupRequestDO().setGroupId(groupId).setUserId(userId).setInviterUserId(inviterUserId) + .setAddSource(inviteSource).setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); + try { + groupRequestMapper.insert(request); + return request; + } catch (DuplicateKeyException ex) { + // 3. 唯一键冲突:回查并复用并发写入的记录 + request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); + if (request == null) { + throw ex; + } + resetInviteRequest(request, inviterUserId, inviteSource); + return request; + } + } + + /** + * 重置主动加群申请 + * + * @param request 申请记录 + * @param reqVO 申请请求 + */ + private void resetApplyRequest(ImGroupRequestDO request, ImGroupRequestApplyReqVO reqVO) { + // 1. 更新申请内容、来源和处理状态 + LocalDateTime now = LocalDateTime.now(); + groupRequestMapper.updateApplyByIdReset(request.getId(), + reqVO.getApplyContent(), reqVO.getAddSource(), now); + // 2. 同步内存对象,后续通知构建直接复用 + request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) + .setInviterUserId(null).setHandleUserId(null) + .setHandleContent(null).setHandleTime(null).setUpdateTime(now); + } + + /** + * 重置邀请加群申请 + * + * @param request 申请记录 + * @param inviterUserId 邀请人用户编号 + * @param inviteSource 邀请来源 + */ + private void resetInviteRequest(ImGroupRequestDO request, Long inviterUserId, Integer inviteSource) { + // 1. 更新邀请人、来源和处理状态 + LocalDateTime now = LocalDateTime.now(); + groupRequestMapper.updateInviteByIdReset(request.getId(), inviterUserId, inviteSource, now); + // 2. 同步内存对象,后续通知构建直接复用 + request.setInviterUserId(inviterUserId).setAddSource(inviteSource) + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) + .setHandleUserId(null).setHandleContent(null).setHandleTime(null) + .setUpdateTime(now); + } + + /** * 校验申请可被「当前用户」处理:申请存在 + 未处理 + 操作人是群主 / 管理员 */ private ImGroupRequestDO validateRequestForHandle(Long userId, Long requestId) { diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index 527c5a1..541aaf3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -316,7 +316,7 @@ public class ImGroupServiceImpl implements ImGroupService { @Override @Transactional(rollbackFor = Exception.class) public void addGroupAdmin(Long userId, ImGroupAdminAddReqVO reqVO) { - Long groupId = reqVO.getGroupId(); + Long groupId = reqVO.getId(); Set targetUserIds = new HashSet<>(reqVO.getUserIds()); // 1.1 仅群主可操作 validateGroupOwnerForUpdate(groupId, userId); @@ -354,7 +354,7 @@ public class ImGroupServiceImpl implements ImGroupService { @Override @Transactional(rollbackFor = Exception.class) public void removeGroupAdmin(Long userId, ImGroupAdminRemoveReqVO reqVO) { - Long groupId = reqVO.getGroupId(); + Long groupId = reqVO.getId(); Set targetUserIds = new HashSet<>(reqVO.getUserIds()); // 1.1 仅群主可操作 validateGroupOwnerForUpdate(groupId, userId); @@ -398,10 +398,10 @@ public class ImGroupServiceImpl implements ImGroupService { } @Override - @CacheEvict(cacheNames = GROUP, key = "#transferReqVO.groupId") + @CacheEvict(cacheNames = GROUP, key = "#transferReqVO.id") @Transactional(rollbackFor = Exception.class) public void transferGroupOwner(Long userId, ImGroupTransferOwnerReqVO transferReqVO) { - Long groupId = transferReqVO.getGroupId(); + Long groupId = transferReqVO.getId(); Long newOwnerUserId = transferReqVO.getNewOwnerUserId(); // 1.1 仅老群主可执行 validateGroupOwnerForUpdate(groupId, userId); @@ -677,19 +677,19 @@ public class ImGroupServiceImpl implements ImGroupService { // ==================== 群禁言 ==================== @Override - @CacheEvict(cacheNames = GROUP, key = "#reqVO.groupId") + @CacheEvict(cacheNames = GROUP, key = "#reqVO.id") @Transactional(rollbackFor = Exception.class) public void muteAll(Long userId, ImGroupMuteAllReqVO reqVO) { // 1. 校验群主或管理员 - validateGroupOwnerOrAdmin(reqVO.getGroupId(), userId); + validateGroupOwnerOrAdmin(reqVO.getId(), userId); // 2. 更新 mutedAll - groupMapper.updateById(new ImGroupDO().setId(reqVO.getGroupId()).setMutedAll(reqVO.getMutedAll())); + groupMapper.updateById(new ImGroupDO().setId(reqVO.getId()).setMutedAll(reqVO.getMutedAll())); // 3. 广播通知 ImGroupMessageSendDTO messageSendDTO = Boolean.TRUE.equals(reqVO.getMutedAll()) - ? ImGroupMessageSendDTO.ofGroupMuted(reqVO.getGroupId(), userId) - : ImGroupMessageSendDTO.ofGroupCancelMuted(reqVO.getGroupId(), userId); + ? ImGroupMessageSendDTO.ofGroupMuted(reqVO.getId(), userId) + : ImGroupMessageSendDTO.ofGroupCancelMuted(reqVO.getId(), userId); groupMessageService.sendGroupMessage(userId, messageSendDTO); } @@ -701,21 +701,21 @@ public class ImGroupServiceImpl implements ImGroupService { throw exception(GROUP_MUTE_MEMBER_SELF); } // 1.2 校验群存在且未封禁 - validateGroupExists(reqVO.getGroupId()); + validateGroupExists(reqVO.getId()); // 1.3 校验操作人和目标都在群中 - ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); - ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getId(), userId); + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getId(), reqVO.getUserId()); // 1.4 三档权限校验 validateMutePermission(operatorMember, targetMember); // 2. 设置 muteEndTime LocalDateTime muteEndTime = reqVO.getMutedSeconds() == 0 ? ImGroupMemberDO.PERMANENT_MUTE_END_TIME : LocalDateTime.now().plusSeconds(reqVO.getMutedSeconds()); - groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), muteEndTime); + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getId(), reqVO.getUserId(), muteEndTime); // 3. 广播通知 groupMessageService.sendGroupMessage(userId, - ImGroupMessageSendDTO.ofGroupMemberMuted(reqVO.getGroupId(), userId, + ImGroupMessageSendDTO.ofGroupMemberMuted(reqVO.getId(), userId, reqVO.getUserId(), muteEndTime)); } @@ -723,19 +723,19 @@ public class ImGroupServiceImpl implements ImGroupService { @Transactional(rollbackFor = Exception.class) public void cancelMuteMember(Long userId, ImGroupCancelMuteMemberReqVO reqVO) { // 1.1 校验群存在且未封禁 - validateGroupExists(reqVO.getGroupId()); + validateGroupExists(reqVO.getId()); // 1.2 校验操作人和目标都在群中 - ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); - ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getId(), userId); + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getId(), reqVO.getUserId()); // 1.3 三档权限校验 validateMutePermission(operatorMember, targetMember); // 2. 取消禁言(清空 muteEndTime) - groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), null); + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getId(), reqVO.getUserId(), null); // 3. 广播通知 groupMessageService.sendGroupMessage(userId, - ImGroupMessageSendDTO.ofGroupMemberCancelMuted(reqVO.getGroupId(), userId, reqVO.getUserId())); + ImGroupMessageSendDTO.ofGroupMemberCancelMuted(reqVO.getId(), userId, reqVO.getUserId())); } /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java index 33be85f..5be2c7c 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java @@ -34,7 +34,7 @@ public interface ImRtcCallService { void inviteCall(Long userId, ImRtcCallInviteReqVO reqVO); /** - * 加入已有群通话:用于群胶囊条「加入」按钮;旁观者作为 ACTIVE_JOIN 加入,邀请池内成员转 JOINED + * 加入已有群通话:用于群胶囊条「加入」按钮;旁观者作为 JOINER 加入,邀请池内成员转 JOINED * * @param userId 加入者用户编号 * @param room 业务通话编号;从胶囊条 activeCall 拿 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 c2a967a..28bf9b8 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 @@ -37,6 +37,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -256,24 +257,12 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { } // 1.3 校验当前用户是该群有效成员;防止仅凭 room 就拿到 LiveKit token 越权入会 groupMemberService.validateMemberInGroup(call.getGroupId(), userId); - validateUserNotJoinedOtherCall(userId, call.getRoom()); + // 1.4 校验当前用户没有其它活跃通话 + validateUserNotInOtherCall(userId, call.getRoom()); - // 2. 入参与表:已有记录切回 JOINED;不在记录则以 ACTIVE_JOIN 角色 INSERT + // 2. 入参与表:已有记录切回 JOINED;不在记录则以 JOINER 角色 INSERT LocalDateTime now = LocalDateTime.now(); - ImRtcParticipantDO existing = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); - if (existing != null) { - // 非 JOINED 状态切回 JOINED + accept_time;INVITER / INVITEE 重连、旁观者点击胶囊条加入,都走这条 - if (!ImRtcParticipantStatusEnum.isJoined(existing.getStatus())) { - rtcParticipantMapper.updateById(new ImRtcParticipantDO().setId(existing.getId()) - .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now)); - } - } else { - // 旁观者主动加入:INSERT role=ACTIVE_JOIN - rtcParticipantMapper.insert(new ImRtcParticipantDO() - .setCallId(call.getId()).setRoom(call.getRoom()) - .setUserId(userId).setRole(ImRtcParticipantRoleEnum.ACTIVE_JOIN.getRole()) - .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setInviteTime(now).setAcceptTime(now)); - } + joinParticipant(call, userId, now); // 3. 主表 CREATED → RUNNING(首次有非发起人加入) maybeMarkOngoing(call, userId, now); @@ -291,13 +280,19 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { // 1.1 校验被邀请人都是群活跃成员 groupMemberService.validateMembersInGroup(call.getGroupId(), inviteeIds); // 1.2 排除已在通话池的;剩余即本次新邀请 - Set existingUserIds = CollectionUtils.convertSet( - rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId); + List existingParticipants = rtcParticipantMapper.selectListByRoom(call.getRoom()); + Set existingUserIds = CollectionUtils.convertSet(existingParticipants, ImRtcParticipantDO::getUserId); Set incomingUserIds = new LinkedHashSet<>(inviteeIds); incomingUserIds.removeAll(existingUserIds); if (CollUtil.isEmpty(incomingUserIds)) { return; } + long activeCount = existingParticipants.stream() + .filter(participant -> ImRtcParticipantStatusEnum.ACTIVE_STATUSES.contains(participant.getStatus())) + .count(); + if (activeCount + incomingUserIds.size() > imProperties.getRtc().getGroupMaxParticipants()) { + throw exception(RTC_GROUP_INVITEE_OVER_LIMIT); + } // 2. 批量 INSERT 新邀请人 LocalDateTime now = LocalDateTime.now(); @@ -328,7 +323,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { if (ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) { return call; } - validateUserNotJoinedOtherCall(userId, call.getRoom()); + validateUserNotInOtherCall(userId, call.getRoom()); // 2.2 仅 INVITING → JOINED;其它状态拒 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) { throw exception(RTC_SESSION_NOT_EXISTS); @@ -760,15 +755,70 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { return participant; } - private void validateUserNotJoinedOtherCall(Long userId, String room) { - ImRtcParticipantDO joined = rtcParticipantMapper.selectLastOneByUserIdAndStatusInAndRoomNot( - userId, Collections.singleton(ImRtcParticipantStatusEnum.JOINED.getStatus()), room); - if (joined != null) { + /** + * 校验用户不在其它活跃通话中 + * + * @param userId 用户编号 + * @param room 当前房间标识 + */ + private void validateUserNotInOtherCall(Long userId, String room) { + // 查询当前房间外的活跃参与记录 + ImRtcParticipantDO participant = rtcParticipantMapper.selectLastOneByUserIdAndStatusInAndRoomNot( + userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES, room); + // 存在活跃参与记录,则当前用户忙线 + if (participant != null) { throw exception(RTC_SELF_BUSY); } } /** + * 加入群通话参与者列表 + * + * @param call 通话主表 + * @param userId 用户编号 + * @param now 当前时间 + */ + private void joinParticipant(ImRtcCallDO call, Long userId, LocalDateTime now) { + // 1. 已有参与记录:切回 JOINED + ImRtcParticipantDO existing = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); + if (existing != null) { + updateParticipantJoined(existing, now); + return; + } + + // 2. 无参与记录:以主动加入者身份新增 + try { + rtcParticipantMapper.insert(new ImRtcParticipantDO() + .setCallId(call.getId()).setRoom(call.getRoom()) + .setUserId(userId).setRole(ImRtcParticipantRoleEnum.JOINER.getRole()) + .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setInviteTime(now).setAcceptTime(now)); + } catch (DuplicateKeyException ex) { + // 3. 唯一键冲突:回查并复用并发写入的记录 + existing = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); + if (existing == null) { + throw ex; + } + updateParticipantJoined(existing, now); + } + } + + /** + * 将参与者更新为已加入 + * + * @param participant 参与者记录 + * @param now 当前时间 + */ + private void updateParticipantJoined(ImRtcParticipantDO participant, LocalDateTime now) { + // 已是 JOINED 直接返回 + if (ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) { + return; + } + // 更新状态和接听时间 + rtcParticipantMapper.updateById(new ImRtcParticipantDO().setId(participant.getId()) + .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now)); + } + + /** * 关房判定收口:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关 *

* end reason 按 call.status 自动推:CREATED(没人接通过)= CANCEL;RUNNING(有人接通过)= HANGUP @@ -958,7 +1008,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { } /** - * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫 + * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 推给群通话受众 *

* 私聊拒绝走 endSession → RTC_CALL_END(reason=REJECT) 入消息流,不在此推 * @@ -968,12 +1018,14 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator); - webSocketService.sendPrivateMessageAsync(call.getInviterUserId(), ImPrivateMessageDTO.ofRtcNotification( - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, call.getInviterUserId(), payload)); + for (Long receiverUserId : getCallAudienceUserIdList(call)) { + webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification( + ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload)); + } } /** - * RTC_CALL(NO_ANSWER):仅群通话场景;振铃超时由 Job 触发;走 webSocketService 直推主叫 + * RTC_CALL(NO_ANSWER):仅群通话场景;振铃超时由 Job 触发;走 webSocketService 推给群通话受众 *

* 私聊未接听走 endSession → RTC_CALL_END(reason=NO_ANSWER) 入消息流,不在此推 * @@ -983,8 +1035,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { */ private void pushCallNoAnswerNotification(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) { ImRtcCallNotification payload = ImRtcCallNotification.ofNoAnswer(call, operatorUserId, operator); - webSocketService.sendPrivateMessageAsync(call.getInviterUserId(), ImPrivateMessageDTO.ofRtcNotification( - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, call.getInviterUserId(), payload)); + for (Long receiverUserId : getCallAudienceUserIdList(call)) { + webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification( + ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload)); + } } /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java index 186835d..cc31ad7 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java @@ -3,5 +3,5 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; /** * 添加管理员事件通知(memberUserIds 为被设管理员的成员) */ -public class GroupAdminAddNotification extends GroupMembersAffectedNotification { +public class GroupAdminAddNotification extends GroupMemberListNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java index f6bc3ea..0f0e8d2 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java @@ -3,5 +3,5 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; /** * 撤销管理员事件通知(memberUserIds 为被撤销管理员的成员) */ -public class GroupAdminRemoveNotification extends GroupMembersAffectedNotification { +public class GroupAdminRemoveNotification extends GroupMemberListNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java index 4c3b6fc..b7e6563 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java @@ -3,5 +3,5 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; /** * 群创建事件通知(memberUserIds 含创建者 + 初始邀请成员) */ -public class GroupCreateNotification extends GroupMembersAffectedNotification { +public class GroupCreateNotification extends GroupMemberListNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java index 0092bfe..5877ed1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java @@ -3,5 +3,5 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; /** * 成员加入事件通知(memberUserIds 为被邀请人) */ -public class GroupMemberInviteNotification extends GroupMembersAffectedNotification { +public class GroupMemberInviteNotification extends GroupMemberListNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java index 3ac811d..30bbc34 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java @@ -3,5 +3,5 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; /** * 成员被移出事件通知(memberUserIds 为被移出成员) */ -public class GroupMemberKickNotification extends GroupMembersAffectedNotification { +public class GroupMemberKickNotification extends GroupMemberListNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMembersAffectedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberListNotification.java index add8349..4601858 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMembersAffectedNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberListNotification.java @@ -4,13 +4,13 @@ import lombok.Data; import java.util.List; -// TODO DONE @AI:本身已是 BaseGroupNotification 的子类,名字不带 Group 前缀是因为是抽象基类、被多种 Group* 通知共用 -// TODO @AI:它目前只被 group 使用。别过度设计,名字按照我说的调整下; /** - * 涉及成员列表的群事件通知基类:用于 Create / Invite / Kick / AdminAdd / AdminRemove + * 群事件成员列表通知基类 + * + * @author 芋道源码 */ @Data -public abstract class GroupMembersAffectedNotification extends BaseGroupNotification { +public abstract class GroupMemberListNotification extends BaseGroupNotification { /** * 受影响的成员用户编号列表 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 16e63d9..6c094cf 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 @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.springframework.dao.DuplicateKeyException; import java.time.LocalDateTime; import java.util.List; @@ -187,6 +188,31 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { assertNull(result.getHandleTime()); } + @Test + public void testApplyFriend_insertDuplicateKey_reuseOldRequest() { + // 准备:首次查询不存在,插入时命中唯一键,回查到并发写入的旧申请 + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); + reqVO.setToUserId(2L).setApplyContent("并发申请").setDisplayName("老张").setAddSource(2); + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); + when(friendService.getFriend(2L, 1L)).thenReturn(null); + ImFriendRequestDO old = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) + .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()); + when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null, old); + when(friendRequestMapper.insert(any(ImFriendRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); + when(adminUserApi.getUser(1L)).thenReturn(null); + when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); + + // 调用 + ImFriendRequestDO result = friendRequestService.applyFriend(1L, reqVO); + + // 断言:复用并重置旧申请,不向上抛数据库异常 + verify(friendRequestMapper).updateByIdReset(eq(100L), eq("并发申请"), eq("老张"), eq(2), + any(LocalDateTime.class)); + assertEquals(100L, result.getId()); + assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); + verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); + } + // ========== agreeFriendRequest ========== @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java index 590a156..4a19651 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.springframework.dao.DuplicateKeyException; import java.util.Collection; import java.util.List; @@ -118,6 +119,39 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testApplyJoinGroup_insertDuplicateKey_reuseOldRequest() { + // 准备:群是 APPLY 模式,首次查询不存在,插入时命中唯一键 + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .joinApproval(true) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupService.validateGroupExists(10L)).thenReturn(group); + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( + ImGroupMemberDO.builder().groupId(10L).userId(99L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()) + .status(CommonStatusEnum.ENABLE.getStatus()).build())); + when(adminUserApi.getUser(1L)).thenReturn(buildUser(1L, "申请人")); + ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(1L) + .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); + when(groupRequestMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null, old); + when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); + + ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); + reqVO.setGroupId(10L); + reqVO.setApplyContent("我想进群"); + reqVO.setAddSource(ImGroupAddSourceEnum.SEARCH.getSource()); + + // 调用 + ImGroupRequestDO result = groupRequestService.applyJoinGroup(1L, reqVO); + + // 断言:复用并重置旧申请 + verify(groupRequestMapper).updateApplyByIdReset(eq(50L), eq("我想进群"), + eq(ImGroupAddSourceEnum.SEARCH.getSource()), any()); + assertEquals(50L, result.getId()); + assertEquals(ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); + verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class)); + } + + @Test public void testApplyJoinGroup_alreadyMember_throws() { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) @@ -254,8 +288,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); - when(groupRequestMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) - .thenReturn(List.of()); + when(groupRequestMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) @@ -267,13 +300,12 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { // 调用:邀请人 1L 邀请 2L、3L(都没有旧记录) groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); - // 断言:批量插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧 - @SuppressWarnings("unchecked") - ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); - verify(groupRequestMapper).insertBatch(captor.capture()); + // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧 + ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); + verify(groupRequestMapper, times(2)).insert(captor.capture()); verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 - Collection inserted = captor.getValue(); + Collection inserted = captor.getAllValues(); assertEquals(2, inserted.size()); inserted.forEach(insert -> { assertEquals(1L, insert.getInviterUserId()); @@ -281,6 +313,31 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { }); } + @Test + public void testCreateInviteRequestList_insertDuplicateKey_reuseOldRequest() { + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .joinApproval(true) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupService.validateGroupExists(10L)).thenReturn(group); + ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(2L) + .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); + when(groupRequestMapper.selectByGroupIdAndUserId(10L, 2L)).thenReturn(null, old); + when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( + ImGroupMemberDO.builder().groupId(10L).userId(99L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()) + .status(CommonStatusEnum.ENABLE.getStatus()).build())); + when(adminUserApi.getUserMap(anyCollection())).thenReturn(java.util.Map.of(2L, buildUser(2L, "用户A"))); + + // 调用 + groupRequestService.createInviteRequestList(10L, 1L, List.of(2L)); + + // 断言:复用并重置旧邀请申请 + verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L), + eq(ImGroupAddSourceEnum.INVITE.getSource()), any()); + verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class)); + } + private AdminUserRespDTO buildUser(Long id, String nickname) { AdminUserRespDTO user = new AdminUserRespDTO(); user.setId(id); diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java index c62c1ae..b430f0b 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java @@ -769,7 +769,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { eq(ImGroupMemberRoleEnum.ADMIN.getRole()))).thenReturn(1); ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(3L)); groupService.addGroupAdmin(1L, reqVO); @@ -797,7 +797,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .thenReturn(3L); ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(5L)); ServiceException exception = assertThrows(ServiceException.class, @@ -821,7 +821,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .role(ImGroupMemberRoleEnum.OWNER.getRole()).build())); ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(1L)); ServiceException exception = assertThrows(ServiceException.class, @@ -845,7 +845,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(2L)); groupService.addGroupAdmin(1L, reqVO); @@ -873,7 +873,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { eq(ImGroupMemberRoleEnum.NORMAL.getRole()))).thenReturn(1); ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(2L)); groupService.removeGroupAdmin(1L, reqVO); @@ -899,7 +899,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setUserIds(List.of(2L)); groupService.removeGroupAdmin(1L, reqVO); @@ -928,7 +928,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { eq(ImGroupMemberRoleEnum.NORMAL.getRole()))).thenReturn(1); ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setNewOwnerUserId(2L); groupService.transferGroupOwner(1L, reqVO); @@ -956,7 +956,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group); ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setNewOwnerUserId(1L); ServiceException exception = assertThrows(ServiceException.class, @@ -977,7 +977,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group); ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); - reqVO.setGroupId(10L); + reqVO.setId(10L); reqVO.setNewOwnerUserId(2L); ServiceException exception = assertThrows(ServiceException.class, @@ -1046,7 +1046,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build()); ImGroupMuteMemberReqVO reqVO = new ImGroupMuteMemberReqVO(); - reqVO.setGroupId(10L).setUserId(2L).setMutedSeconds(60); + reqVO.setId(10L).setUserId(2L).setMutedSeconds(60); ServiceException exception = assertThrows(ServiceException.class, () -> groupService.muteMember(1L, reqVO)); 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 24f3dac..b39c024 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 @@ -3,6 +3,7 @@ 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.controller.admin.rtc.vo.ImRtcCallInviteReqVO; 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; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.springframework.dao.DuplicateKeyException; import java.time.Duration; import java.time.LocalDateTime; @@ -33,6 +35,7 @@ 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_GROUP_INVITEE_OVER_LIMIT; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.RTC_SELF_BUSY; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -125,6 +128,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { .thenReturn(1); ImRtcCallDO call = buildCall("r1", 200L, ImConversationTypeEnum.GROUP, 999L); when(rtcCallMapper.selectByRoom("r1")).thenReturn(call); + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(999L)).thenReturn(List.of(200L, 201L)); // 房内 2 个 JOINED + 1 个 INVITING → shouldCloseGroupRoom 返 false when(rtcParticipantMapper.selectListByRoom("r1")).thenReturn(List.of( buildParticipant(20L, "r1", 200L, ImRtcParticipantStatusEnum.JOINED), @@ -221,6 +225,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { .thenReturn(1); ImRtcCallDO call = buildCall("r1", 200L, ImConversationTypeEnum.GROUP, 999L); when(rtcCallMapper.selectByRoom("r1")).thenReturn(call); + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(999L)).thenReturn(List.of(200L, 201L)); // 让 shouldCloseGroupRoom 返 false when(rtcParticipantMapper.selectListByRoom("r1")).thenReturn(List.of( buildParticipant(20L, "r1", 200L, ImRtcParticipantStatusEnum.JOINED), @@ -295,6 +300,57 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest { verify(rtcParticipantMapper, never()).updateById(any(ImRtcParticipantDO.class)); } + @Test + public void testJoinCall_insertDuplicateKey_reuseExistingParticipant() { + // 准备:群通话活跃,首次查询无参与者,插入时命中唯一键 + when(imProperties.getRtc()).thenReturn(new ImProperties.Rtc()); + ImRtcCallDO call = buildCall("r1", 200L, ImConversationTypeEnum.GROUP, 999L); + when(rtcCallMapper.selectByRoom("r1")).thenReturn(call); + when(groupMemberService.validateMemberInGroup(999L, 100L)).thenReturn(new ImGroupMemberDO()); + when(rtcParticipantMapper.selectLastOneByUserIdAndStatusInAndRoomNot(eq(100L), anyCollection(), eq("r1"))) + .thenReturn(null); + when(rtcParticipantMapper.selectByRoomAndUserId("r1", 100L)) + .thenReturn(null, buildParticipant(10L, "r1", 100L, ImRtcParticipantStatusEnum.JOINED)); + when(rtcParticipantMapper.insert(any(ImRtcParticipantDO.class))).thenThrow(new DuplicateKeyException("dup")); + + // 调用 + ImRtcCallDO result = rtcCallService.joinCall(100L, "r1"); + + // 断言:不向上抛数据库异常 + assertSame(call, result); + verify(rtcParticipantMapper, never()).updateById(any(ImRtcParticipantDO.class)); + } + + @Test + public void testInviteCall_overLimit_throws() throws Exception { + ImProperties.Rtc rtcConfig = new ImProperties.Rtc(); + rtcConfig.setGroupMaxParticipants(3); + when(imProperties.getRtc()).thenReturn(rtcConfig); + ImRtcCallDO call = buildCall("r1", 200L, ImConversationTypeEnum.GROUP, 999L); + when(rtcCallMapper.selectByRoom("r1")).thenReturn(call); + when(rtcParticipantMapper.selectByRoomAndUserId("r1", 200L)) + .thenReturn(buildParticipant(10L, "r1", 200L, ImRtcParticipantStatusEnum.JOINED)); + when(rtcCallLockRedisDAO.lockGroup(eq(999L), any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + Callable callable = invocation.getArgument(1); + return callable.call(); + }); + when(rtcParticipantMapper.selectListByRoom("r1")).thenReturn(List.of( + buildParticipant(10L, "r1", 200L, ImRtcParticipantStatusEnum.JOINED), + buildParticipant(11L, "r1", 201L, ImRtcParticipantStatusEnum.JOINED), + buildParticipant(12L, "r1", 202L, ImRtcParticipantStatusEnum.INVITING) + )); + ImRtcCallInviteReqVO reqVO = new ImRtcCallInviteReqVO(); + reqVO.setRoom("r1"); + reqVO.setInviteeIds(Set.of(203L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> rtcCallService.inviteCall(200L, reqVO)); + + assertEquals(RTC_GROUP_INVITEE_OVER_LIMIT.getCode(), exception.getCode()); + verify(rtcParticipantMapper, never()).insertBatch(anyList()); + } + // ========== 测试数据构造 ========== private ImRtcParticipantDO buildParticipant(Long id, String room, Long userId, ImRtcParticipantStatusEnum status) { diff --git a/yudao-module-im/src/test/resources/sql/create_tables.sql b/yudao-module-im/src/test/resources/sql/create_tables.sql index 427522a..2505fbb 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -219,7 +219,8 @@ CREATE TABLE IF NOT EXISTS "im_rtc_participant" ( "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL DEFAULT 0, - PRIMARY KEY ("id") + PRIMARY KEY ("id"), + CONSTRAINT "uk_im_rtc_participant_room_user" UNIQUE ("room", "user_id", "tenant_id") ) COMMENT 'IM 通话参与者表'; CREATE TABLE IF NOT EXISTS "im_face_user_item" (