diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java index 98920c0..3bd5c28 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java @@ -92,5 +92,6 @@ public interface ErrorCodeConstants { ErrorCode RTC_INVITE_BUSY = new ErrorCode(1_040_900_008, "通话发起繁忙,请稍后再试"); ErrorCode RTC_GROUP_CALL_ACTIVE = new ErrorCode(1_040_900_009, "该群已有进行中通话,请通过胶囊条加入"); ErrorCode RTC_GROUP_INVITEE_OVER_LIMIT = new ErrorCode(1_040_900_010, "群通话邀请人数超过最大值"); + ErrorCode RTC_GROUP_INVITEE_REQUIRED = new ErrorCode(1_040_900_011, "群通话必须选择被邀请人"); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java index 2ce6136..19e8f56 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java @@ -165,6 +165,11 @@ public class ImProperties { */ private int groupMaxParticipants = 16; + /** + * 僵尸通话清理阈值(分钟);通话创建超过此值仍未结束才纳入扫描,避开「刚发起还在响铃」的合理零人态 + */ + private int cleanupZombieThresholdMinutes = 5; + } } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/job/rtc/ImRtcCallCleanupJob.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/job/rtc/ImRtcCallCleanupJob.java index 9cce516..664e3cf 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/job/rtc/ImRtcCallCleanupJob.java +++ b/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; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.im.framework.config.ImProperties; import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -18,18 +19,16 @@ import org.springframework.stereotype.Component; @Slf4j public class ImRtcCallCleanupJob implements JobHandler { - /** - * 默认扫描阈值(分钟);通话创建超过此值仍未结束才纳入扫描,避开「刚发起还在响铃」的合理零人态 - */ - private static final int DEFAULT_THRESHOLD_MINUTES = 5; - @Resource private ImRtcCallService rtcCallService; + @Resource + private ImProperties imProperties; + /** * 执行清理 * - * @param param 阈值(分钟);为空 / 非法走默认 {@link #DEFAULT_THRESHOLD_MINUTES} + * @param param 阈值(分钟);为空 / 非法走 {@link ImProperties.Rtc#getCleanupZombieThresholdMinutes()} 默认值 * @return 清理数量描述 */ @Override @@ -42,14 +41,18 @@ public class ImRtcCallCleanupJob implements JobHandler { } /** - * 解析 quartz param 为分钟阈值;非法 / 非正数走默认值 + * 解析 quartz param 为分钟阈值;非法 / 非正数走配置默认值 + * + * @param param quartz 调度入参字符串 + * @return 分钟阈值 */ - private static int resolveThresholdMinutes(String param) { + private int resolveThresholdMinutes(String param) { + int defaultMinutes = imProperties.getRtc().getCleanupZombieThresholdMinutes(); if (StrUtil.isBlank(param) || !NumberUtil.isInteger(param)) { - return DEFAULT_THRESHOLD_MINUTES; + return defaultMinutes; } int minutes = Integer.parseInt(param); - return minutes > 0 ? minutes : DEFAULT_THRESHOLD_MINUTES; + return minutes > 0 ? minutes : defaultMinutes; } } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java index dfddbe0..2b4374f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java @@ -161,6 +161,14 @@ public interface ImGroupMemberService { ImGroupMemberDO validateMemberInGroup(Long groupId, Long userId); /** + * 批量校验用户都是该群的有效成员;任一缺失 / 禁用即抛 {@code GROUP_MEMBER_NOT_IN_GROUP} + * + * @param groupId 群编号 + * @param userIds 用户编号集合;为空直接返回 + */ + void validateMembersInGroup(Long groupId, Collection userIds); + + /** * 更新群成员信息(群内昵称、群名备注、免打扰等) *

* 内部会校验用户是否为群的有效成员 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java index 8b8f925..633dbfb 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java @@ -25,6 +25,7 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; @@ -218,6 +219,23 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } @Override + public void validateMembersInGroup(Long groupId, Collection userIds) { + if (CollUtil.isEmpty(userIds)) { + return; + } + // 一次性拉取目标 userId 的成员记录,仅保留活跃状态 + List members = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); + Set activeUserIds = convertSet(members, ImGroupMemberDO::getUserId, + member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())); + // 任一 userId 不在活跃集合即抛 + for (Long userId : userIds) { + if (!activeUserIds.contains(userId)) { + throw exception(GROUP_MEMBER_NOT_IN_GROUP); + } + } + } + + @Override public void updateGroupMemberRole(Long groupId, Collection userIds, Integer role) { if (CollUtil.isEmpty(userIds)) { return; 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 545177a..a13b0a9 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 @@ -45,22 +45,33 @@ public interface ImRtcCallService { /** * 接听通话:参与者 INVITING → JOINED;主表 CREATED → RUNNING(首次有非发起人接通时) * + * @param userId 接听者用户编号 + * @param room 业务通话编号 * @return 通话主表 */ ImRtcCallDO acceptCall(Long userId, String room); /** * 拒绝通话;仅 INVITING 状态可拒;群通话拒绝等同于不参与,房间仍存在 + * + * @param userId 拒接者用户编号 + * @param room 业务通话编号 */ void rejectCall(Long userId, String room); /** * 取消邀请;主叫在 INVITING 状态主动取消 + * + * @param userId 取消者用户编号(必须是主叫) + * @param room 业务通话编号 */ void cancelCall(Long userId, String room); /** * 离开通话;RUNNING 状态下离开;私聊任一方离开 = 通话结束;群通话最后一人离开才结束 + * + * @param userId 离开者用户编号 + * @param room 业务通话编号 */ void leaveCall(Long userId, String room); @@ -69,6 +80,8 @@ public interface ImRtcCallService { *

* 仅做校验;签发新 Token 由 Controller 调 {@link #signCallToken} 完成 * + * @param userId 操作人编号 + * @param room 业务通话编号 * @return 通话主表 */ ImRtcCallDO validateCallParticipant(Long userId, String room); @@ -106,6 +119,8 @@ public interface ImRtcCallService { *

* 关键事件:participant_left(成员离开) / room_finished(房间结束)。前端正常 leave 时 * 也会触发同样的 LiveKit 事件;此处需做幂等处理,session 已被业务接口移除时直接忽略。 + * + * @param event LiveKit Webhook 事件 */ void handleLiveKitEvent(cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO event); 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 708a1fe..2fa78eb 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 @@ -112,6 +112,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * invite 锁内主体:按场景先做活跃通话 / 对端忙线校验,再做发起人自身忙线校验,最后建通话 + * + * @param userId 发起人编号 + * @param reqVO 发起请求 + * @return 通话主表 */ private ImRtcCallDO inviteCall0(Long userId, ImRtcCallInviteReqVO reqVO) { // 1.1 按场景检测活跃通话 @@ -180,7 +184,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { } rtcParticipantMapper.insertBatch(participants); - // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人;批量取用户避免 N 次 RPC + // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId); Map inviteeMap = adminUserApi.getUserMap(invitees); for (Long inviteeId : invitees) { @@ -239,13 +243,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { throw exception(RTC_NOT_PARTICIPANT); } // 1.4 校验被邀请人必须是该群活跃成员;非本群成员直接抛 - Set validMemberIds = new HashSet<>( - groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId())); - for (Long inviteeId : reqVO.getInviteeIds()) { - if (!validMemberIds.contains(inviteeId)) { - throw exception(GROUP_MEMBER_NOT_IN_GROUP); - } - } + groupMemberService.validateMembersInGroup(call.getGroupId(), reqVO.getInviteeIds()); // 1.5 排除已在通话池的;剩余即本次新邀请 Set existingUserIds = CollectionUtils.convertSet( rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId); @@ -257,18 +255,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { // 2. 批量 INSERT 新邀请人 LocalDateTime now = LocalDateTime.now(); - // TODO @AI:convertList - List participants = new ArrayList<>(incomingUserIds.size()); - for (Long inviteeId : incomingUserIds) { - participants.add(new ImRtcParticipantDO() - .setRoom(call.getRoom()).setUserId(inviteeId) - .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) - .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) - .setInviteTime(now)); - } + List participants = CollectionUtils.convertList(incomingUserIds, inviteeId -> + new ImRtcParticipantDO().setRoom(call.getRoom()).setUserId(inviteeId) + .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) + .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()).setInviteTime(now)); rtcParticipantMapper.insertBatch(participants); - // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人;批量取用户避免 N 次 RPC + // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人 AdminUserRespDTO inviter = adminUserApi.getUser(userId); Map inviteeMap = adminUserApi.getUserMap(incomingUserIds); for (Long inviteeId : incomingUserIds) { @@ -375,6 +368,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { * 群通话至少需要 1 个 JOINED 在房才有意义;只剩 INVITING(响铃中)也无价值 *

* 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛 + * + * @param room 业务通话编号 + * @return true 表示房内已无 JOINED,需要关房 */ private boolean shouldCloseGroupRoom(String room) { List joined = rtcParticipantMapper.selectListByRoomAndStatus(room, @@ -386,6 +382,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { * 查询两人共同所在的活跃私聊通话 *

* 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃 + * + * @param userIdA 用户 A 编号 + * @param userIdB 用户 B 编号 + * @return 活跃私聊通话;不存在返回 null */ private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) { ImRtcParticipantDO participantA = rtcParticipantMapper.selectLastOneByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES); @@ -485,6 +485,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等 + * + * @param event 事件 */ private void handleParticipantLeft(LiveKitWebhookEventDTO event) { if (ObjUtil.hasNull(event.getRoom(), event.getParticipant())) { @@ -521,6 +523,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 处理 LiveKit 「房间结束」事件:兜底把 call 推到 ENDED + * + * @param event 事件 */ private void handleRoomFinished(LiveKitWebhookEventDTO event) { if (event.getRoom() == null) { @@ -577,6 +581,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 校验通话存在且活跃:不存在 / 已 ENDED 直接抛 + * + * @param room 业务通话编号 + * @return 通话主表 */ private ImRtcCallDO validateCallActive(String room) { ImRtcCallDO call = rtcCallMapper.selectByRoom(room); @@ -588,6 +595,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 校验本人是该通话的参与者:accept / reject / leave / refreshToken 的共用前置 + * + * @param call 通话主表 + * @param userId 用户编号 + * @return 参与者记录 */ private ImRtcParticipantDO validateParticipant(ImRtcCallDO call, Long userId) { ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); @@ -599,6 +610,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 关房判定收口:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关 + * + * @param call 通话主表 + * @param operatorId 操作者用户编号;webhook 兜底场景可空 + * @param reason 结束原因 */ private void endSessionIfTerminal(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason) { if (ImConversationTypeEnum.isPrivate(call.getConversationType()) @@ -609,6 +624,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 校验 invite 入参;按场景区分必填字段,私聊补好友校验,群聊补群成员校验 + * + * @param userId 发起人编号 + * @param reqVO 发起请求 */ private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) { Integer scene = reqVO.getScene(); @@ -626,7 +644,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { if (reqVO.getGroupId() == null) { throw exception(RTC_GROUP_REQUIRED); } - // 走单行 SQL;不依赖成员列表缓存,避免新加入成员发起群通话被误判 + // 群通话必须前端选中被邀请人(对齐微信);空集合直接拒 + if (CollUtil.isEmpty(reqVO.getInviteeIds())) { + throw exception(RTC_GROUP_INVITEE_REQUIRED); + } groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); return; } @@ -634,31 +655,23 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { } /** - * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量直接抛错 - *

- * 群聊场景:被邀请人必须是该群活跃成员;防止恶意客户端塞任意 userId 进群房间 + * 解析被邀请池:私聊为 peerUserId 单元素;群聊为前端选中子集(超量抛错) + * + * @param reqVO 发起请求 + * @param inviterId 发起人编号;自己不进被邀请池 + * @return 被邀请人 userId 集合 */ private Set resolveInvitees(ImRtcCallInviteReqVO reqVO, Long inviterId) { + // 1. 私聊校验 if (ImConversationTypeEnum.isPrivate(reqVO.getScene())) { return CollUtil.newLinkedHashSet(reqVO.getPeerUserId()); } - List activeMemberIds = groupMemberService - .getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId()); - Set initial = new LinkedHashSet<>(); - if (CollUtil.isNotEmpty(reqVO.getInviteeIds())) { - Set activeMemberSet = new HashSet<>(activeMemberIds); - for (Long inviteeId : reqVO.getInviteeIds()) { - if (!activeMemberSet.contains(inviteeId)) { - throw exception(GROUP_MEMBER_NOT_IN_GROUP); - } - initial.add(inviteeId); - } - } else { - initial.addAll(activeMemberIds); - } - // 不论来源,发起人本人不进被邀请池 + + // 2. 群聊校验:被邀请人必须是该群活跃成员,防止恶意客户端塞任意 userId + groupMemberService.validateMembersInGroup(reqVO.getGroupId(), reqVO.getInviteeIds()); + Set initial = new LinkedHashSet<>(reqVO.getInviteeIds()); + // 发起人本人不进被邀请池 initial.remove(inviterId); - // 超量直接抛错;前端按 max-1 限制选人 int max = imProperties.getRtc().getGroupMaxParticipants(); if (initial.size() + 1 > max) { throw exception(RTC_GROUP_INVITEE_OVER_LIMIT); @@ -668,6 +681,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等 + * + * @param call 通话主表;推进成功后会同步内存字段 + * @param acceptorId 加入者用户编号;发起人加入不算「首次接通」 + * @param now 当前时间 */ private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) { // 1. 已 RUNNING / ENDED 直接退出;发起人加入不算「首次有人接通」 @@ -696,6 +713,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 关闭会话:主表条件 UPDATE 推到 ENDED → 残留 INVITING 批量改 NO_ANSWER → 推 RTC_CALL_END + * + * @param call 通话主表 + * @param operatorId 操作者用户编号;webhook 兜底 / Job 清理场景可空 + * @param reason 结束原因 */ private void endSession(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason) { // 1.1 更新通话主表为已结束;条件 UPDATE 仅在 status 还活跃时生效 @@ -747,6 +768,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人;persistent=false 不入消息流 + * + * @param call 通话主表 + * @param inviter 发起人;可空,缺失时 payload 的 inviterNickname / Avatar 留空 + * @param inviteeId 被邀请人用户编号 + * @param invitee 被邀请人;可空,缺失时 token 内 displayName 降级为 userId */ private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId, AdminUserRespDTO invitee) { @@ -764,6 +790,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫 *

* 私聊拒绝走 endSession → RTC_CALL_END(reason=REJECT) 入消息流,不在此推 + * + * @param call 通话主表 + * @param operatorUserId 拒接者用户编号 */ private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; @@ -796,6 +825,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 推送参与者事件的公共骨架;按会话类型决定收件人,单次 batch 推送扇出 + * + * @param call 通话主表 + * @param type 消息类型;1602 / 1603 + * @param actorUserId 触发本次事件的用户编号 + * @param payload 业务 payload */ private void pushParticipantNotification(ImRtcCallDO call, Integer type, Long actorUserId, Object payload) { Collection receivers = getCallAudienceUserIdList(call); @@ -808,6 +842,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播;与 RTC_CALL_END 两段式配对 + * + * @param call 通话主表 + * @param inviter 发起人;可空 */ private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) { ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter); @@ -820,6 +857,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { * RTC_CALL_END:私聊走 imPrivateMessageService.send;群通话走 imGroupMessageService.send *

* senderId 始终用通话发起人,让前端按「谁发起通话」决定气泡左右;操作者从 payload.operatorUserId 拿 + * + * @param call 通话主表 + * @param operatorId 操作者用户编号;webhook 兜底 / Job 清理场景可空 + * @param reason 结束原因 + * @param durationSeconds 通话时长(秒);未接通时为 null */ private void pushCallEndNotification(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason, Long durationSeconds) { @@ -849,6 +891,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { /** * 签 LiveKit Token + * + * @param userId 用户编号;token 内 identity 由此派生 + * @param displayName 房内显示名 + * @param room LiveKit 房间名 + * @return JWT 字符串 */ private String signToken(Long userId, String displayName, String room) { return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, room);