Commit 27685b99c8c7ff467382f80119f6d479caff6d3a

Authored by YunaiV
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
```
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
... ... @@ -165,6 +165,11 @@ public class ImProperties {
165 165 */
166 166 private int groupMaxParticipants = 16;
167 167  
  168 + /**
  169 + * 僵尸通话清理阈值(分钟);通话创建超过此值仍未结束才纳入扫描,避开「刚发起还在响铃」的合理零人态
  170 + */
  171 + private int cleanupZombieThresholdMinutes = 5;
  172 +
168 173 }
169 174  
170 175 }
... ...
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);
... ...