Commit 27685b99c8c7ff467382f80119f6d479caff6d3a
1 parent
bb5ac45b
```
✨ feat(im): RTC 通话逻辑收口(displayName / inviteeIds 校验 / 配置化阈值) - fix: pushCallInviteNotification 给被邀请人签 token 时 displayName 改用 invitee 自己的昵称(原来错用 inviter) - feat: 群通话发起 / 追加邀请均强制 inviteeIds 非空,对齐微信交互;超量改抛 RTC_GROUP_INVITEE_OVER_LIMIT,不再静默截断 - refactor: ImGroupMemberService 抽 validateMembersInGroup(groupId, ids) 批量校验,inviteMoreCall / resolveInvitees 复用 - refactor: 僵尸通话清理阈值从 ImRtcCallCleanupJob 静态常量挪到 ImProperties.rtc.cleanupZombieThresholdMinutes - perf: createCall / inviteMoreCall 推 INVITE 前一次 getUserMap 批量拉用户,省 N 次 RPC - refactor: inviteMoreCall 参与者构造改 CollectionUtils.convertList;trim 多余注释 - docs: 补齐 RTC service / controller / job / LiveKitClient 私有方法的 @param / @return - chore: 错误码新增 RTC_GROUP_INVITEE_REQUIRED / RTC_GROUP_INVITEE_OVER_LIMIT ```
Showing
7 changed files
with
145 additions
and
48 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
| ... | ... | @@ -92,5 +92,6 @@ public interface ErrorCodeConstants { |
| 92 | 92 | ErrorCode RTC_INVITE_BUSY = new ErrorCode(1_040_900_008, "通话发起繁忙,请稍后再试"); |
| 93 | 93 | ErrorCode RTC_GROUP_CALL_ACTIVE = new ErrorCode(1_040_900_009, "该群已有进行中通话,请通过胶囊条加入"); |
| 94 | 94 | ErrorCode RTC_GROUP_INVITEE_OVER_LIMIT = new ErrorCode(1_040_900_010, "群通话邀请人数超过最大值"); |
| 95 | + ErrorCode RTC_GROUP_INVITEE_REQUIRED = new ErrorCode(1_040_900_011, "群通话必须选择被邀请人"); | |
| 95 | 96 | |
| 96 | 97 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/job/rtc/ImRtcCallCleanupJob.java
| ... | ... | @@ -4,6 +4,7 @@ import cn.hutool.core.util.NumberUtil; |
| 4 | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | 6 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; |
| 7 | +import cn.iocoder.yudao.module.im.framework.config.ImProperties; | |
| 7 | 8 | import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService; |
| 8 | 9 | import jakarta.annotation.Resource; |
| 9 | 10 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -18,18 +19,16 @@ import org.springframework.stereotype.Component; |
| 18 | 19 | @Slf4j |
| 19 | 20 | public class ImRtcCallCleanupJob implements JobHandler { |
| 20 | 21 | |
| 21 | - /** | |
| 22 | - * 默认扫描阈值(分钟);通话创建超过此值仍未结束才纳入扫描,避开「刚发起还在响铃」的合理零人态 | |
| 23 | - */ | |
| 24 | - private static final int DEFAULT_THRESHOLD_MINUTES = 5; | |
| 25 | - | |
| 26 | 22 | @Resource |
| 27 | 23 | private ImRtcCallService rtcCallService; |
| 28 | 24 | |
| 25 | + @Resource | |
| 26 | + private ImProperties imProperties; | |
| 27 | + | |
| 29 | 28 | /** |
| 30 | 29 | * 执行清理 |
| 31 | 30 | * |
| 32 | - * @param param 阈值(分钟);为空 / 非法走默认 {@link #DEFAULT_THRESHOLD_MINUTES} | |
| 31 | + * @param param 阈值(分钟);为空 / 非法走 {@link ImProperties.Rtc#getCleanupZombieThresholdMinutes()} 默认值 | |
| 33 | 32 | * @return 清理数量描述 |
| 34 | 33 | */ |
| 35 | 34 | @Override |
| ... | ... | @@ -42,14 +41,18 @@ public class ImRtcCallCleanupJob implements JobHandler { |
| 42 | 41 | } |
| 43 | 42 | |
| 44 | 43 | /** |
| 45 | - * 解析 quartz param 为分钟阈值;非法 / 非正数走默认值 | |
| 44 | + * 解析 quartz param 为分钟阈值;非法 / 非正数走配置默认值 | |
| 45 | + * | |
| 46 | + * @param param quartz 调度入参字符串 | |
| 47 | + * @return 分钟阈值 | |
| 46 | 48 | */ |
| 47 | - private static int resolveThresholdMinutes(String param) { | |
| 49 | + private int resolveThresholdMinutes(String param) { | |
| 50 | + int defaultMinutes = imProperties.getRtc().getCleanupZombieThresholdMinutes(); | |
| 48 | 51 | if (StrUtil.isBlank(param) || !NumberUtil.isInteger(param)) { |
| 49 | - return DEFAULT_THRESHOLD_MINUTES; | |
| 52 | + return defaultMinutes; | |
| 50 | 53 | } |
| 51 | 54 | int minutes = Integer.parseInt(param); |
| 52 | - return minutes > 0 ? minutes : DEFAULT_THRESHOLD_MINUTES; | |
| 55 | + return minutes > 0 ? minutes : defaultMinutes; | |
| 53 | 56 | } |
| 54 | 57 | |
| 55 | 58 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
| ... | ... | @@ -161,6 +161,14 @@ public interface ImGroupMemberService { |
| 161 | 161 | ImGroupMemberDO validateMemberInGroup(Long groupId, Long userId); |
| 162 | 162 | |
| 163 | 163 | /** |
| 164 | + * 批量校验用户都是该群的有效成员;任一缺失 / 禁用即抛 {@code GROUP_MEMBER_NOT_IN_GROUP} | |
| 165 | + * | |
| 166 | + * @param groupId 群编号 | |
| 167 | + * @param userIds 用户编号集合;为空直接返回 | |
| 168 | + */ | |
| 169 | + void validateMembersInGroup(Long groupId, Collection<Long> userIds); | |
| 170 | + | |
| 171 | + /** | |
| 164 | 172 | * 更新群成员信息(群内昵称、群名备注、免打扰等) |
| 165 | 173 | * <p> |
| 166 | 174 | * 内部会校验用户是否为群的有效成员 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
| ... | ... | @@ -25,6 +25,7 @@ import java.util.*; |
| 25 | 25 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 26 | 26 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 27 | 27 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| 28 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | |
| 28 | 29 | import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; |
| 29 | 30 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; |
| 30 | 31 | |
| ... | ... | @@ -218,6 +219,23 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 218 | 219 | } |
| 219 | 220 | |
| 220 | 221 | @Override |
| 222 | + public void validateMembersInGroup(Long groupId, Collection<Long> userIds) { | |
| 223 | + if (CollUtil.isEmpty(userIds)) { | |
| 224 | + return; | |
| 225 | + } | |
| 226 | + // 一次性拉取目标 userId 的成员记录,仅保留活跃状态 | |
| 227 | + List<ImGroupMemberDO> members = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); | |
| 228 | + Set<Long> activeUserIds = convertSet(members, ImGroupMemberDO::getUserId, | |
| 229 | + member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())); | |
| 230 | + // 任一 userId 不在活跃集合即抛 | |
| 231 | + for (Long userId : userIds) { | |
| 232 | + if (!activeUserIds.contains(userId)) { | |
| 233 | + throw exception(GROUP_MEMBER_NOT_IN_GROUP); | |
| 234 | + } | |
| 235 | + } | |
| 236 | + } | |
| 237 | + | |
| 238 | + @Override | |
| 221 | 239 | public void updateGroupMemberRole(Long groupId, Collection<Long> userIds, Integer role) { |
| 222 | 240 | if (CollUtil.isEmpty(userIds)) { |
| 223 | 241 | return; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java
| ... | ... | @@ -45,22 +45,33 @@ public interface ImRtcCallService { |
| 45 | 45 | /** |
| 46 | 46 | * 接听通话:参与者 INVITING → JOINED;主表 CREATED → RUNNING(首次有非发起人接通时) |
| 47 | 47 | * |
| 48 | + * @param userId 接听者用户编号 | |
| 49 | + * @param room 业务通话编号 | |
| 48 | 50 | * @return 通话主表 |
| 49 | 51 | */ |
| 50 | 52 | ImRtcCallDO acceptCall(Long userId, String room); |
| 51 | 53 | |
| 52 | 54 | /** |
| 53 | 55 | * 拒绝通话;仅 INVITING 状态可拒;群通话拒绝等同于不参与,房间仍存在 |
| 56 | + * | |
| 57 | + * @param userId 拒接者用户编号 | |
| 58 | + * @param room 业务通话编号 | |
| 54 | 59 | */ |
| 55 | 60 | void rejectCall(Long userId, String room); |
| 56 | 61 | |
| 57 | 62 | /** |
| 58 | 63 | * 取消邀请;主叫在 INVITING 状态主动取消 |
| 64 | + * | |
| 65 | + * @param userId 取消者用户编号(必须是主叫) | |
| 66 | + * @param room 业务通话编号 | |
| 59 | 67 | */ |
| 60 | 68 | void cancelCall(Long userId, String room); |
| 61 | 69 | |
| 62 | 70 | /** |
| 63 | 71 | * 离开通话;RUNNING 状态下离开;私聊任一方离开 = 通话结束;群通话最后一人离开才结束 |
| 72 | + * | |
| 73 | + * @param userId 离开者用户编号 | |
| 74 | + * @param room 业务通话编号 | |
| 64 | 75 | */ |
| 65 | 76 | void leaveCall(Long userId, String room); |
| 66 | 77 | |
| ... | ... | @@ -69,6 +80,8 @@ public interface ImRtcCallService { |
| 69 | 80 | * <p> |
| 70 | 81 | * 仅做校验;签发新 Token 由 Controller 调 {@link #signCallToken} 完成 |
| 71 | 82 | * |
| 83 | + * @param userId 操作人编号 | |
| 84 | + * @param room 业务通话编号 | |
| 72 | 85 | * @return 通话主表 |
| 73 | 86 | */ |
| 74 | 87 | ImRtcCallDO validateCallParticipant(Long userId, String room); |
| ... | ... | @@ -106,6 +119,8 @@ public interface ImRtcCallService { |
| 106 | 119 | * <p> |
| 107 | 120 | * 关键事件:participant_left(成员离开) / room_finished(房间结束)。前端正常 leave 时 |
| 108 | 121 | * 也会触发同样的 LiveKit 事件;此处需做幂等处理,session 已被业务接口移除时直接忽略。 |
| 122 | + * | |
| 123 | + * @param event LiveKit Webhook 事件 | |
| 109 | 124 | */ |
| 110 | 125 | void handleLiveKitEvent(cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO event); |
| 111 | 126 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
| ... | ... | @@ -112,6 +112,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 112 | 112 | |
| 113 | 113 | /** |
| 114 | 114 | * invite 锁内主体:按场景先做活跃通话 / 对端忙线校验,再做发起人自身忙线校验,最后建通话 |
| 115 | + * | |
| 116 | + * @param userId 发起人编号 | |
| 117 | + * @param reqVO 发起请求 | |
| 118 | + * @return 通话主表 | |
| 115 | 119 | */ |
| 116 | 120 | private ImRtcCallDO inviteCall0(Long userId, ImRtcCallInviteReqVO reqVO) { |
| 117 | 121 | // 1.1 按场景检测活跃通话 |
| ... | ... | @@ -180,7 +184,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 180 | 184 | } |
| 181 | 185 | rtcParticipantMapper.insertBatch(participants); |
| 182 | 186 | |
| 183 | - // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人;批量取用户避免 N 次 RPC | |
| 187 | + // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人 | |
| 184 | 188 | AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId); |
| 185 | 189 | Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(invitees); |
| 186 | 190 | for (Long inviteeId : invitees) { |
| ... | ... | @@ -239,13 +243,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 239 | 243 | throw exception(RTC_NOT_PARTICIPANT); |
| 240 | 244 | } |
| 241 | 245 | // 1.4 校验被邀请人必须是该群活跃成员;非本群成员直接抛 |
| 242 | - Set<Long> validMemberIds = new HashSet<>( | |
| 243 | - groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId())); | |
| 244 | - for (Long inviteeId : reqVO.getInviteeIds()) { | |
| 245 | - if (!validMemberIds.contains(inviteeId)) { | |
| 246 | - throw exception(GROUP_MEMBER_NOT_IN_GROUP); | |
| 247 | - } | |
| 248 | - } | |
| 246 | + groupMemberService.validateMembersInGroup(call.getGroupId(), reqVO.getInviteeIds()); | |
| 249 | 247 | // 1.5 排除已在通话池的;剩余即本次新邀请 |
| 250 | 248 | Set<Long> existingUserIds = CollectionUtils.convertSet( |
| 251 | 249 | rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId); |
| ... | ... | @@ -257,18 +255,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 257 | 255 | |
| 258 | 256 | // 2. 批量 INSERT 新邀请人 |
| 259 | 257 | LocalDateTime now = LocalDateTime.now(); |
| 260 | - // TODO @AI:convertList | |
| 261 | - List<ImRtcParticipantDO> participants = new ArrayList<>(incomingUserIds.size()); | |
| 262 | - for (Long inviteeId : incomingUserIds) { | |
| 263 | - participants.add(new ImRtcParticipantDO() | |
| 264 | - .setRoom(call.getRoom()).setUserId(inviteeId) | |
| 265 | - .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) | |
| 266 | - .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) | |
| 267 | - .setInviteTime(now)); | |
| 268 | - } | |
| 258 | + List<ImRtcParticipantDO> participants = CollectionUtils.convertList(incomingUserIds, inviteeId -> | |
| 259 | + new ImRtcParticipantDO().setRoom(call.getRoom()).setUserId(inviteeId) | |
| 260 | + .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) | |
| 261 | + .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()).setInviteTime(now)); | |
| 269 | 262 | rtcParticipantMapper.insertBatch(participants); |
| 270 | 263 | |
| 271 | - // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人;批量取用户避免 N 次 RPC | |
| 264 | + // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人 | |
| 272 | 265 | AdminUserRespDTO inviter = adminUserApi.getUser(userId); |
| 273 | 266 | Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(incomingUserIds); |
| 274 | 267 | for (Long inviteeId : incomingUserIds) { |
| ... | ... | @@ -375,6 +368,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 375 | 368 | * 群通话至少需要 1 个 JOINED 在房才有意义;只剩 INVITING(响铃中)也无价值 |
| 376 | 369 | * <p> |
| 377 | 370 | * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛 |
| 371 | + * | |
| 372 | + * @param room 业务通话编号 | |
| 373 | + * @return true 表示房内已无 JOINED,需要关房 | |
| 378 | 374 | */ |
| 379 | 375 | private boolean shouldCloseGroupRoom(String room) { |
| 380 | 376 | List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByRoomAndStatus(room, |
| ... | ... | @@ -386,6 +382,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 386 | 382 | * 查询两人共同所在的活跃私聊通话 |
| 387 | 383 | * <p> |
| 388 | 384 | * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃 |
| 385 | + * | |
| 386 | + * @param userIdA 用户 A 编号 | |
| 387 | + * @param userIdB 用户 B 编号 | |
| 388 | + * @return 活跃私聊通话;不存在返回 null | |
| 389 | 389 | */ |
| 390 | 390 | private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) { |
| 391 | 391 | ImRtcParticipantDO participantA = rtcParticipantMapper.selectLastOneByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES); |
| ... | ... | @@ -485,6 +485,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 485 | 485 | |
| 486 | 486 | /** |
| 487 | 487 | * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等 |
| 488 | + * | |
| 489 | + * @param event 事件 | |
| 488 | 490 | */ |
| 489 | 491 | private void handleParticipantLeft(LiveKitWebhookEventDTO event) { |
| 490 | 492 | if (ObjUtil.hasNull(event.getRoom(), event.getParticipant())) { |
| ... | ... | @@ -521,6 +523,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 521 | 523 | |
| 522 | 524 | /** |
| 523 | 525 | * 处理 LiveKit 「房间结束」事件:兜底把 call 推到 ENDED |
| 526 | + * | |
| 527 | + * @param event 事件 | |
| 524 | 528 | */ |
| 525 | 529 | private void handleRoomFinished(LiveKitWebhookEventDTO event) { |
| 526 | 530 | if (event.getRoom() == null) { |
| ... | ... | @@ -577,6 +581,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 577 | 581 | |
| 578 | 582 | /** |
| 579 | 583 | * 校验通话存在且活跃:不存在 / 已 ENDED 直接抛 |
| 584 | + * | |
| 585 | + * @param room 业务通话编号 | |
| 586 | + * @return 通话主表 | |
| 580 | 587 | */ |
| 581 | 588 | private ImRtcCallDO validateCallActive(String room) { |
| 582 | 589 | ImRtcCallDO call = rtcCallMapper.selectByRoom(room); |
| ... | ... | @@ -588,6 +595,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 588 | 595 | |
| 589 | 596 | /** |
| 590 | 597 | * 校验本人是该通话的参与者:accept / reject / leave / refreshToken 的共用前置 |
| 598 | + * | |
| 599 | + * @param call 通话主表 | |
| 600 | + * @param userId 用户编号 | |
| 601 | + * @return 参与者记录 | |
| 591 | 602 | */ |
| 592 | 603 | private ImRtcParticipantDO validateParticipant(ImRtcCallDO call, Long userId) { |
| 593 | 604 | ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); |
| ... | ... | @@ -599,6 +610,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 599 | 610 | |
| 600 | 611 | /** |
| 601 | 612 | * 关房判定收口:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关 |
| 613 | + * | |
| 614 | + * @param call 通话主表 | |
| 615 | + * @param operatorId 操作者用户编号;webhook 兜底场景可空 | |
| 616 | + * @param reason 结束原因 | |
| 602 | 617 | */ |
| 603 | 618 | private void endSessionIfTerminal(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason) { |
| 604 | 619 | if (ImConversationTypeEnum.isPrivate(call.getConversationType()) |
| ... | ... | @@ -609,6 +624,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 609 | 624 | |
| 610 | 625 | /** |
| 611 | 626 | * 校验 invite 入参;按场景区分必填字段,私聊补好友校验,群聊补群成员校验 |
| 627 | + * | |
| 628 | + * @param userId 发起人编号 | |
| 629 | + * @param reqVO 发起请求 | |
| 612 | 630 | */ |
| 613 | 631 | private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) { |
| 614 | 632 | Integer scene = reqVO.getScene(); |
| ... | ... | @@ -626,7 +644,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 626 | 644 | if (reqVO.getGroupId() == null) { |
| 627 | 645 | throw exception(RTC_GROUP_REQUIRED); |
| 628 | 646 | } |
| 629 | - // 走单行 SQL;不依赖成员列表缓存,避免新加入成员发起群通话被误判 | |
| 647 | + // 群通话必须前端选中被邀请人(对齐微信);空集合直接拒 | |
| 648 | + if (CollUtil.isEmpty(reqVO.getInviteeIds())) { | |
| 649 | + throw exception(RTC_GROUP_INVITEE_REQUIRED); | |
| 650 | + } | |
| 630 | 651 | groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); |
| 631 | 652 | return; |
| 632 | 653 | } |
| ... | ... | @@ -634,31 +655,23 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 634 | 655 | } |
| 635 | 656 | |
| 636 | 657 | /** |
| 637 | - * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量直接抛错 | |
| 638 | - * <p> | |
| 639 | - * 群聊场景:被邀请人必须是该群活跃成员;防止恶意客户端塞任意 userId 进群房间 | |
| 658 | + * 解析被邀请池:私聊为 peerUserId 单元素;群聊为前端选中子集(超量抛错) | |
| 659 | + * | |
| 660 | + * @param reqVO 发起请求 | |
| 661 | + * @param inviterId 发起人编号;自己不进被邀请池 | |
| 662 | + * @return 被邀请人 userId 集合 | |
| 640 | 663 | */ |
| 641 | 664 | private Set<Long> resolveInvitees(ImRtcCallInviteReqVO reqVO, Long inviterId) { |
| 665 | + // 1. 私聊校验 | |
| 642 | 666 | if (ImConversationTypeEnum.isPrivate(reqVO.getScene())) { |
| 643 | 667 | return CollUtil.newLinkedHashSet(reqVO.getPeerUserId()); |
| 644 | 668 | } |
| 645 | - List<Long> activeMemberIds = groupMemberService | |
| 646 | - .getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId()); | |
| 647 | - Set<Long> initial = new LinkedHashSet<>(); | |
| 648 | - if (CollUtil.isNotEmpty(reqVO.getInviteeIds())) { | |
| 649 | - Set<Long> activeMemberSet = new HashSet<>(activeMemberIds); | |
| 650 | - for (Long inviteeId : reqVO.getInviteeIds()) { | |
| 651 | - if (!activeMemberSet.contains(inviteeId)) { | |
| 652 | - throw exception(GROUP_MEMBER_NOT_IN_GROUP); | |
| 653 | - } | |
| 654 | - initial.add(inviteeId); | |
| 655 | - } | |
| 656 | - } else { | |
| 657 | - initial.addAll(activeMemberIds); | |
| 658 | - } | |
| 659 | - // 不论来源,发起人本人不进被邀请池 | |
| 669 | + | |
| 670 | + // 2. 群聊校验:被邀请人必须是该群活跃成员,防止恶意客户端塞任意 userId | |
| 671 | + groupMemberService.validateMembersInGroup(reqVO.getGroupId(), reqVO.getInviteeIds()); | |
| 672 | + Set<Long> initial = new LinkedHashSet<>(reqVO.getInviteeIds()); | |
| 673 | + // 发起人本人不进被邀请池 | |
| 660 | 674 | initial.remove(inviterId); |
| 661 | - // 超量直接抛错;前端按 max-1 限制选人 | |
| 662 | 675 | int max = imProperties.getRtc().getGroupMaxParticipants(); |
| 663 | 676 | if (initial.size() + 1 > max) { |
| 664 | 677 | throw exception(RTC_GROUP_INVITEE_OVER_LIMIT); |
| ... | ... | @@ -668,6 +681,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 668 | 681 | |
| 669 | 682 | /** |
| 670 | 683 | * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等 |
| 684 | + * | |
| 685 | + * @param call 通话主表;推进成功后会同步内存字段 | |
| 686 | + * @param acceptorId 加入者用户编号;发起人加入不算「首次接通」 | |
| 687 | + * @param now 当前时间 | |
| 671 | 688 | */ |
| 672 | 689 | private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) { |
| 673 | 690 | // 1. 已 RUNNING / ENDED 直接退出;发起人加入不算「首次有人接通」 |
| ... | ... | @@ -696,6 +713,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 696 | 713 | |
| 697 | 714 | /** |
| 698 | 715 | * 关闭会话:主表条件 UPDATE 推到 ENDED → 残留 INVITING 批量改 NO_ANSWER → 推 RTC_CALL_END |
| 716 | + * | |
| 717 | + * @param call 通话主表 | |
| 718 | + * @param operatorId 操作者用户编号;webhook 兜底 / Job 清理场景可空 | |
| 719 | + * @param reason 结束原因 | |
| 699 | 720 | */ |
| 700 | 721 | private void endSession(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason) { |
| 701 | 722 | // 1.1 更新通话主表为已结束;条件 UPDATE 仅在 status 还活跃时生效 |
| ... | ... | @@ -747,6 +768,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 747 | 768 | |
| 748 | 769 | /** |
| 749 | 770 | * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人;persistent=false 不入消息流 |
| 771 | + * | |
| 772 | + * @param call 通话主表 | |
| 773 | + * @param inviter 发起人;可空,缺失时 payload 的 inviterNickname / Avatar 留空 | |
| 774 | + * @param inviteeId 被邀请人用户编号 | |
| 775 | + * @param invitee 被邀请人;可空,缺失时 token 内 displayName 降级为 userId | |
| 750 | 776 | */ |
| 751 | 777 | private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, |
| 752 | 778 | Long inviteeId, AdminUserRespDTO invitee) { |
| ... | ... | @@ -764,6 +790,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 764 | 790 | * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫 |
| 765 | 791 | * <p> |
| 766 | 792 | * 私聊拒绝走 endSession → RTC_CALL_END(reason=REJECT) 入消息流,不在此推 |
| 793 | + * | |
| 794 | + * @param call 通话主表 | |
| 795 | + * @param operatorUserId 拒接者用户编号 | |
| 767 | 796 | */ |
| 768 | 797 | private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { |
| 769 | 798 | AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; |
| ... | ... | @@ -796,6 +825,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 796 | 825 | |
| 797 | 826 | /** |
| 798 | 827 | * 推送参与者事件的公共骨架;按会话类型决定收件人,单次 batch 推送扇出 |
| 828 | + * | |
| 829 | + * @param call 通话主表 | |
| 830 | + * @param type 消息类型;1602 / 1603 | |
| 831 | + * @param actorUserId 触发本次事件的用户编号 | |
| 832 | + * @param payload 业务 payload | |
| 799 | 833 | */ |
| 800 | 834 | private void pushParticipantNotification(ImRtcCallDO call, Integer type, Long actorUserId, Object payload) { |
| 801 | 835 | Collection<Long> receivers = getCallAudienceUserIdList(call); |
| ... | ... | @@ -808,6 +842,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 808 | 842 | |
| 809 | 843 | /** |
| 810 | 844 | * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播;与 RTC_CALL_END 两段式配对 |
| 845 | + * | |
| 846 | + * @param call 通话主表 | |
| 847 | + * @param inviter 发起人;可空 | |
| 811 | 848 | */ |
| 812 | 849 | private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) { |
| 813 | 850 | ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter); |
| ... | ... | @@ -820,6 +857,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 820 | 857 | * RTC_CALL_END:私聊走 imPrivateMessageService.send;群通话走 imGroupMessageService.send |
| 821 | 858 | * <p> |
| 822 | 859 | * senderId 始终用通话发起人,让前端按「谁发起通话」决定气泡左右;操作者从 payload.operatorUserId 拿 |
| 860 | + * | |
| 861 | + * @param call 通话主表 | |
| 862 | + * @param operatorId 操作者用户编号;webhook 兜底 / Job 清理场景可空 | |
| 863 | + * @param reason 结束原因 | |
| 864 | + * @param durationSeconds 通话时长(秒);未接通时为 null | |
| 823 | 865 | */ |
| 824 | 866 | private void pushCallEndNotification(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason, |
| 825 | 867 | Long durationSeconds) { |
| ... | ... | @@ -849,6 +891,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { |
| 849 | 891 | |
| 850 | 892 | /** |
| 851 | 893 | * 签 LiveKit Token |
| 894 | + * | |
| 895 | + * @param userId 用户编号;token 内 identity 由此派生 | |
| 896 | + * @param displayName 房内显示名 | |
| 897 | + * @param room LiveKit 房间名 | |
| 898 | + * @return JWT 字符串 | |
| 852 | 899 | */ |
| 853 | 900 | private String signToken(Long userId, String displayName, String room) { |
| 854 | 901 | return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, room); | ... | ... |