Commit 8996b947951cd858315fe1a04f7575fa43b3cd06

Authored by YunaiV
1 parent 07f3bab7

✨ feat(im): 优化 ImRtcCallServiceImpl 实现类的各种注释,以及编码风格。

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/ImRtcCallController.java
... ... @@ -95,7 +95,7 @@ public class ImRtcCallController {
95 95 @Operation(summary = "重新签发 Token;客户端重连或 Token 过期续期")
96 96 @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
97 97 public CommonResult<ImRtcCallRespVO> refreshToken(@RequestParam("room") String room) {
98   - ImRtcCallDO call = rtcCallService.refreshCallToken(getLoginUserId(), room);
  98 + ImRtcCallDO call = rtcCallService.validateCallParticipant(getLoginUserId(), room);
99 99 return success(buildCallResp(call, getLoginUserId()));
100 100 }
101 101  
... ... @@ -113,7 +113,7 @@ public class ImRtcCallController {
113 113 if (call == null) {
114 114 return null;
115 115 }
116   - List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipants(call.getRoom());
  116 + List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
117 117 return new ImRtcCallRespVO()
118 118 .setRoom(call.getRoom())
119 119 .setLivekitUrl(imProperties.getRtc().getLivekitUrl())
... ... @@ -129,7 +129,7 @@ public class ImRtcCallController {
129 129 if (call == null) {
130 130 return null;
131 131 }
132   - List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipants(call.getRoom());
  132 + List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
133 133 return new ImRtcGroupCallRespVO()
134 134 .setRoom(call.getRoom())
135 135 .setGroupId(call.getGroupId()).setMediaType(call.getMediaType())
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java
... ... @@ -65,12 +65,13 @@ public interface ImRtcCallService {
65 65 void leaveCall(Long userId, String room);
66 66  
67 67 /**
68   - * 校验用户仍是该通话的参与者,用于客户端重连或 Token 过期续期前的合法性检查;
69   - * 签发新 Token 由 Controller 调 {@link #signCallToken} 完成
  68 + * 校验通话活跃且本人是参与者;用于客户端重连或 Token 过期续期前的合法性检查
  69 + * <p>
  70 + * 仅做校验;签发新 Token 由 Controller 调 {@link #signCallToken} 完成
70 71 *
71 72 * @return 通话主表
72 73 */
73   - ImRtcCallDO refreshCallToken(Long userId, String room);
  74 + ImRtcCallDO validateCallParticipant(Long userId, String room);
74 75  
75 76 /**
76 77 * 查询当前正在进行的通话;目前仅群聊场景(胶囊条),私聊未来扩展再补 peerUserId 参数
... ... @@ -89,7 +90,7 @@ public interface ImRtcCallService {
89 90 * @param room 业务通话编号
90 91 * @return 参与者明细列表
91 92 */
92   - List<ImRtcParticipantDO> getCallParticipants(String room);
  93 + List<ImRtcParticipantDO> getCallParticipantList(String room);
93 94  
94 95 /**
95 96 * 签发指定用户进入该通话的 LiveKit Token;供 Controller 拼接到响应 VO
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
... ... @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.im.service.rtc;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
5 7 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 8 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteMoreReqVO;
7 9 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO;
... ... @@ -68,8 +70,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
68 70  
69 71 @Resource
70 72 private ImRtcCallMapper rtcCallMapper;
71   - // TODO DONE @AI:可以搞个 ImRtcParticipantService 简化本类么 —— 暂不抽:参与表 CRUD 与通话状态机紧耦合(INVITING→JOINED→LEFT 等转换都跟主表 status 联动),独立 service 会沦为薄包装且需双方互调,反而增加复杂度
72   - // TODO @AI:要不试着拆出来,看看 ImRtcParticipantService 能不能稍微简化一些些;
73 73 @Resource
74 74 private ImRtcParticipantMapper rtcParticipantMapper;
75 75 @Resource
... ... @@ -100,7 +100,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
100 100 @Override
101 101 @SneakyThrows
102 102 public ImRtcCallDO inviteCall(Long userId, ImRtcCallInviteReqVO reqVO) {
103   - ensureEnabled();
  103 + validateEnabled();
104 104 // 1. 校验入参与场景
105 105 validateInviteScene(userId, reqVO);
106 106  
... ... @@ -136,7 +136,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
136 136 throw exception(RTC_PEER_BUSY);
137 137 }
138 138 }
139   - // 1.2 通用检测:发起人在另一通通话中(与上面的私聊「已是参与者」不同语义;这里覆盖跨好友 / 跨群的兜底)
  139 + // 1.2 兜底校验:发起人在另一通通话中(跨好友 / 跨群)
140 140 ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
141 141 userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
142 142 if (selfActive != null) {
... ... @@ -181,7 +181,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
181 181 }
182 182 rtcParticipantMapper.insertBatch(participants);
183 183  
184   - // TODO @AI:需要把 3.1、3.2 顺序调整下么?你分析下。
185 184 // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人
186 185 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId);
187 186 for (Long inviteeId : invitees) {
... ... @@ -196,7 +195,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
196 195  
197 196 @Override
198 197 public ImRtcCallDO joinCall(Long userId, String room) {
199   - ensureEnabled();
  198 + validateEnabled();
200 199 // 1.1 校验通话存在且活跃
201 200 ImRtcCallDO call = validateCallActive(room);
202 201 // 1.2 仅群通话支持「旁观者加入」
... ... @@ -227,230 +226,215 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
227 226  
228 227 @Override
229 228 public void inviteMoreCall(Long userId, ImRtcCallInviteMoreReqVO reqVO) {
230   - ensureEnabled();
231   - // TODO @AI:校验的,按照 1.1 1.2 1.3 这样;
  229 + validateEnabled();
  230 + // 1.1 校验通话存在且活跃
232 231 ImRtcCallDO call = validateCallActive(reqVO.getRoom());
233   - // 仅群通话支持追加邀请
  232 + // 1.2 仅群通话支持追加邀请
234 233 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
235 234 throw exception(RTC_GROUP_REQUIRED);
236 235 }
237   - // 操作者必须是已在房间的参与者
  236 + // 1.3 操作者必须是已在房间的参与者
238 237 ImRtcParticipantDO operator = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
239 238 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) {
240 239 throw exception(RTC_NOT_PARTICIPANT);
241 240 }
242   - // TODO @AI:如果存在非本群里,直接抛出异常。
243   - // 校验被邀请人必须是该群活跃成员;且未在通话池
244   - Set<Long> validMemberIds = new LinkedHashSet<>(
  241 + // 1.4 校验被邀请人必须是该群活跃成员;非本群成员直接抛
  242 + Set<Long> validMemberIds = new HashSet<>(
245 243 groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId()));
246   - // TODO @AI:convertSet;
247   - Set<Long> existingUserIds = rtcParticipantMapper.selectListByRoom(call.getRoom()).stream()
248   - .map(ImRtcParticipantDO::getUserId)
249   - .collect(Collectors.toSet());
250   - // TODO @AI:incomingUserIds;直接 remove 原始的元素;没必要;单独的;
251   - List<Long> incoming = reqVO.getInviteeIds().stream()
252   - .filter(id -> validMemberIds.contains(id) && !existingUserIds.contains(id))
253   - .toList();
254   - if (CollUtil.isEmpty(incoming)) {
  244 + for (Long inviteeId : reqVO.getInviteeIds()) {
  245 + if (!validMemberIds.contains(inviteeId)) {
  246 + throw exception(GROUP_MEMBER_NOT_IN_GROUP);
  247 + }
  248 + }
  249 + // 1.5 排除已在通话池的;剩余即本次新邀请
  250 + Set<Long> existingUserIds = CollectionUtils.convertSet(
  251 + rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId);
  252 + Set<Long> incomingUserIds = new LinkedHashSet<>(reqVO.getInviteeIds());
  253 + incomingUserIds.removeAll(existingUserIds);
  254 + if (CollUtil.isEmpty(incomingUserIds)) {
255 255 return;
256 256 }
257 257  
258   - // INSERT 新邀请人 + 推 INVITE
259   - // TODO @AI:batch 插入;
  258 + // 2. 批量 INSERT 新邀请人
260 259 LocalDateTime now = LocalDateTime.now();
261   - for (Long inviteeId : incoming) {
262   - rtcParticipantMapper.insert(new ImRtcParticipantDO()
  260 + // TODO @AI:convertList
  261 + List<ImRtcParticipantDO> participants = new ArrayList<>(incomingUserIds.size());
  262 + for (Long inviteeId : incomingUserIds) {
  263 + participants.add(new ImRtcParticipantDO()
263 264 .setRoom(call.getRoom()).setUserId(inviteeId)
264 265 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole())
265 266 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
266 267 .setInviteTime(now));
267 268 }
  269 + rtcParticipantMapper.insertBatch(participants);
268 270  
269   - // TODO @AI:这里在写推送的;
  271 + // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人
270 272 AdminUserRespDTO inviter = adminUserApi.getUser(userId);
271   - for (Long inviteeId : incoming) {
  273 + for (Long inviteeId : incomingUserIds) {
272 274 pushCallInviteNotification(call, inviter, inviteeId);
273 275 }
274 276 }
275 277  
276 278 @Override
277 279 public ImRtcCallDO acceptCall(Long userId, String room) {
278   - ensureEnabled();
279   - // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  280 + validateEnabled();
  281 + // 1.1 校验通话存在且活跃
280 282 ImRtcCallDO call = validateCallActive(room);
281   - ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
282   - if (participant == null) {
283   - throw exception(RTC_NOT_PARTICIPANT);
  283 + // 1.2 校验本人是该通话的参与者
  284 + ImRtcParticipantDO participant = validateParticipant(call, userId);
  285 +
  286 + // 2.1 已 JOINED 直接幂等返回
  287 + if (ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
  288 + return call;
  289 + }
  290 + // 2.2 仅 INVITING → JOINED;其它状态拒
  291 + if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
  292 + throw exception(RTC_SESSION_NOT_EXISTS);
284 293 }
285   - // 已 JOINED 视为重复 accept,幂等返回
286 294 LocalDateTime now = LocalDateTime.now();
287   - // TODO @AI:如果已经是,就 if return,简化层级。
288   - if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
289   - // 仅 INVITING → JOINED;其它状态拒
290   - if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
291   - throw exception(RTC_SESSION_NOT_EXISTS);
292   - }
293   - int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
294   - new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now));
295   - if (updated == 0) {
296   - throw exception(RTC_SESSION_NOT_EXISTS);
297   - }
  295 + int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
  296 + new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now));
  297 + if (updated == 0) {
  298 + throw exception(RTC_SESSION_NOT_EXISTS);
298 299 }
299 300  
300   - // 主表 CREATED → RUNNING(首次有非发起人接通)
  301 + // 3. 主表 CREATED → RUNNING(首次有非发起人接通)
301 302 maybeMarkOngoing(call, userId, now);
302 303 return call;
303 304 }
304 305  
305 306 @Override
306 307 public void rejectCall(Long userId, String room) {
307   - ensureEnabled();
308   - // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  308 + validateEnabled();
  309 + // 1.1 校验通话存在且活跃
309 310 ImRtcCallDO call = validateCallActive(room);
310   - ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
311   - if (participant == null) {
312   - throw exception(RTC_NOT_PARTICIPANT);
313   - }
314   - // 仅 INVITING 状态可拒
  311 + // 1.2 校验本人是该通话的参与者
  312 + ImRtcParticipantDO participant = validateParticipant(call, userId);
  313 + // 1.3 仅 INVITING 状态可拒
315 314 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
316 315 throw exception(RTC_SESSION_NOT_EXISTS);
317 316 }
318 317  
319   - // TODO @AI:这里写个注释
  318 + // 2. INVITING → REJECTED;并发已变更则忽略
320 319 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
321 320 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.REJECTED.getStatus()).setLeaveTime(LocalDateTime.now()));
322 321 if (updated == 0) {
323 322 return;
324 323 }
325 324  
326   - // TODO @AI:3.1 3.2 这样;
  325 + // 3.1 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体
327 326 // TODO @AI:群聊如果拒绝,需要做类似的 endSession 逻辑么?
328   - // 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体
329 327 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
330 328 pushCallRejectNotification(call, userId);
331 329 return;
332 330 }
333   - // 私聊拒绝:走 endSession(推 RTC_CALL_END(reason=REJECT))
  331 + // 3.2 私聊拒绝:走 endSession(推 RTC_CALL_END(reason=REJECT))
334 332 endSession(call, userId, ImRtcCallEndReasonEnum.REJECT);
335 333 }
336 334  
337 335 @Override
338 336 public void cancelCall(Long userId, String room) {
339   - ensureEnabled();
340   - // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  337 + validateEnabled();
  338 + // 1.1 校验通话存在且活跃
341 339 ImRtcCallDO call = validateCallActive(room);
342   - // TODO @AI:notEquals;
343   - if (!Objects.equals(call.getInviterUserId(), userId)) {
  340 + // 1.2 仅主叫可取消
  341 + if (ObjUtil.notEqual(call.getInviterUserId(), userId)) {
344 342 throw exception(RTC_NOT_PARTICIPANT);
345 343 }
346   - // 仅 CREATED 状态可取消(RUNNING 应走 leave)
  344 + // 1.3 仅 CREATED 状态可取消(RUNNING 应走 leave)
347 345 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
348 346 throw exception(RTC_SESSION_NOT_EXISTS);
349 347 }
350 348  
351   - // TODO @AI:这里写下注释;
  349 + // 2. 关会话并推 RTC_CALL_END(reason=CANCEL)
352 350 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL);
353 351 }
354 352  
355 353 @Override
356 354 public void leaveCall(Long userId, String room) {
357   - ensureEnabled();
358   - // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  355 + validateEnabled();
  356 + // 1.1 校验通话存在且活跃
359 357 ImRtcCallDO call = validateCallActive(room);
360   - ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
361   - if (participant == null) {
362   - throw exception(RTC_NOT_PARTICIPANT);
363   - }
  358 + // 1.2 校验本人是该通话的参与者
  359 + ImRtcParticipantDO participant = validateParticipant(call, userId);
364 360  
365   - // TODO @AI:这里要写下注释。
  361 + // 2. 当前状态 → LEFT;条件 UPDATE 防并发反复改
366 362 LocalDateTime now = LocalDateTime.now();
367 363 rtcParticipantMapper.updateByIdAndStatus(participant.getId(), participant.getStatus(),
368 364 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(now));
369 365  
370   - // TODO @AI:是不是搞个 endSessionIfXXX;
371   - // TODO @AI:群聊的关房,本质和单聊,是不是一样的?是否有必要统一掉?
372   - // 关房条件:私聊任一方离开 = 关;群通话仅在「无人在房 + 无人响铃」时关
373   - if (ImConversationTypeEnum.isPrivate(call.getConversationType())
374   - || shouldCloseGroupRoom(call.getRoom())) {
375   - endSession(call, userId, ImRtcCallEndReasonEnum.HANGUP);
376   - }
  366 + // 3. 触发关房判定:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关
377 367 // 群通话单人离开:LiveKit `ParticipantDisconnected` 自动通知房内成员;业务后端不另外推
  368 + endSessionIfTerminal(call, userId, ImRtcCallEndReasonEnum.HANGUP);
378 369 }
379 370  
380 371 /**
381 372 * 群通话是否应该关闭:房内没有 JOINED 就关
382 373 * <p>
383   - * 群通话至少需要 1 个 JOINED 在房才有意义;如果只剩 INVITING(响铃中)的,接通后也只是 1 人独房,无价值
  374 + * 群通话至少需要 1 个 JOINED 在房才有意义;只剩 INVITING(响铃中)也无价值
384 375 * <p>
385 376 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛
386 377 */
387 378 private boolean shouldCloseGroupRoom(String room) {
388   - // TODO @AI:通过查询,计算情况?;感觉所有群聊的关闭条件:都是只有 1 个 join,剩余都处于结束,说白了没人可以和他电话了。。。。
389 379 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByRoomAndStatus(room,
390 380 ImRtcParticipantStatusEnum.JOINED.getStatus());
391 381 return CollUtil.isEmpty(joined);
392 382 }
393 383  
394   - // TODO @AI:把“拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃”;写到方法注释里,更好理解点。
395   - // TODO @AI:不用说 “由应用层 Redisson 锁串行化,最多一个”;感觉意义不大呀。
396 384 /**
397   - * 查询两人共同所在的活跃私聊通话;由应用层 Redisson 锁串行化,最多一个
  385 + * 查询两人共同所在的活跃私聊通话
398 386 * <p>
399 387 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃
400 388 */
401 389 private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) {
402   - // TODO @AI:是不是用 participantA;和上面的命名更统一点。
403   - ImRtcParticipantDO aPart = rtcParticipantMapper.selectLastOneByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
404   - if (aPart == null) {
  390 + ImRtcParticipantDO participantA = rtcParticipantMapper.selectLastOneByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
  391 + if (participantA == null) {
405 392 return null;
406 393 }
407   - ImRtcCallDO call = rtcCallMapper.selectByRoom(aPart.getRoom());
  394 + ImRtcCallDO call = rtcCallMapper.selectByRoom(participantA.getRoom());
408 395 if (call == null
409 396 || !ImConversationTypeEnum.isPrivate(call.getConversationType())
410 397 || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
411 398 return null;
412 399 }
413   - ImRtcParticipantDO bPart = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userIdB);
414   - if (bPart == null
415   - || !ImRtcParticipantStatusEnum.ACTIVE_STATUSES.contains(bPart.getStatus())) {
  400 + ImRtcParticipantDO participantB = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userIdB);
  401 + if (participantB == null
  402 + || !ImRtcParticipantStatusEnum.ACTIVE_STATUSES.contains(participantB.getStatus())) {
416 403 return null;
417 404 }
418 405 return call;
419 406 }
420 407  
421   - // TODO @AI:怎么感觉这个要改下?是不是根据 userid + callid 更合理?
422 408 @Override
423   - public ImRtcCallDO refreshCallToken(Long userId, String room) {
424   - ensureEnabled();
425   - // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  409 + public ImRtcCallDO validateCallParticipant(Long userId, String room) {
  410 + validateEnabled();
  411 + // 1.1 校验通话存在且活跃
426 412 ImRtcCallDO call = validateCallActive(room);
427   - ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
428   - if (participant == null) {
429   - throw exception(RTC_NOT_PARTICIPANT);
430   - }
  413 + // 1.2 校验本人是该通话的参与者
  414 + validateParticipant(call, userId);
431 415 return call;
432 416 }
433 417  
434 418 @Override
435 419 public ImRtcCallDO getActiveCall(Long userId, Long groupId) {
436   - ensureEnabled();
  420 + validateEnabled();
437 421 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存)
438 422 groupMemberService.validateMemberInGroup(groupId, userId);
439 423 // 2. 查询活跃通话
440 424 return rtcCallMapper.selectLastOneByGroupIdAndStatusIn(groupId, ImRtcCallStatusEnum.ACTIVE_STATUSES);
441 425 }
442 426  
443   - // TODO @AI:方法名,复数应该使用 List,不使用 s;
444 427 @Override
445   - public List<ImRtcParticipantDO> getCallParticipants(String room) {
  428 + public List<ImRtcParticipantDO> getCallParticipantList(String room) {
446 429 return rtcParticipantMapper.selectListByRoom(room);
447 430 }
448 431  
449 432 @Override
450 433 public String signCallToken(Long userId, String room) {
451 434 AdminUserRespDTO user = adminUserApi.getUser(userId);
452   - // TODO @AI:不能为空。。。nickname 为空,则使用 userid;
453   - String displayName = user == null ? null : user.getNickname();
  435 + // displayName 不能为空;nickname 缺失时降级为 userId 字符串
  436 + String displayName = StrUtil.blankToDefault(
  437 + user == null ? null : user.getNickname(), String.valueOf(userId));
454 438 return signToken(userId, displayName, room);
455 439 }
456 440  
... ... @@ -476,11 +460,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
476 460 * @param event 事件
477 461 */
478 462 private void handleParticipantJoined(LiveKitWebhookEventDTO event) {
479   - // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
480   - if (event.getRoom() == null || event.getParticipant() == null) {
  463 + if (ObjUtil.hasNull(event.getRoom(), event.getParticipant())) {
481 464 return;
482 465 }
483   - // 前置检查
  466 + // 1. 前置检查:通话存在且活跃
484 467 String room = event.getRoom().getName();
485 468 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
486 469 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
... ... @@ -491,7 +474,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
491 474 return;
492 475 }
493 476  
494   - // 推 1602 通知参与方 / 全群参与方
  477 + // 2. 推 1602 通知参与方 / 全群参与方
495 478 pushParticipantConnectedNotification(call, userId);
496 479 }
497 480  
... ... @@ -499,11 +482,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
499 482 * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等
500 483 */
501 484 private void handleParticipantLeft(LiveKitWebhookEventDTO event) {
502   - // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
503   - if (event.getRoom() == null || event.getParticipant() == null) {
  485 + if (ObjUtil.hasNull(event.getRoom(), event.getParticipant())) {
504 486 return;
505 487 }
506   - // 前置检查
  488 + // 1. 前置检查:通话存在且活跃 + 参与者仍在 JOINED
507 489 String room = event.getRoom().getName();
508 490 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
509 491 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
... ... @@ -518,7 +500,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
518 500 return;
519 501 }
520 502  
521   - // TODO @AI:注释。带序号的;
  503 + // 2. JOINED → LEFT;正常 leave 接口已改过则 update 影响 0 行直接退出
522 504 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.JOINED.getStatus(),
523 505 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(LocalDateTime.now()));
524 506 if (updated == 0) {
... ... @@ -526,14 +508,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
526 508 }
527 509 log.info("[handleParticipantLeft][room={} userId={} 由 LiveKit Webhook 兜底]", room, userId);
528 510  
529   - // 推 1603 通知参与方 / 全群参与方
  511 + // 3. 推 1603 通知参与方 / 全群参与方
530 512 pushParticipantDisconnectedNotification(call, userId);
531   - // 关房条件与 leave 一致;群通话仅在「无人在房 + 无人响铃」时关
532   - // TODO @AI:注释风格;感觉可以抽个 endSessionIfXXX;收口更统一;
533   - if (ImConversationTypeEnum.isPrivate(call.getConversationType())
534   - || shouldCloseGroupRoom(call.getRoom())) {
535   - endSession(call, userId, ImRtcCallEndReasonEnum.HANGUP);
536   - }
  513 + // 4. 触发关房判定:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关
  514 + endSessionIfTerminal(call, userId, ImRtcCallEndReasonEnum.HANGUP);
537 515 }
538 516  
539 517 /**
... ... @@ -543,14 +521,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
543 521 if (event.getRoom() == null) {
544 522 return;
545 523 }
546   - // 前置检查
  524 + // 1. 前置检查:通话存在且活跃
547 525 String room = event.getRoom().getName();
548 526 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
549 527 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
550 528 return;
551 529 }
552   -
553   - // TODO @AI:注释。带序号的;
  530 + // 2. 关会话
554 531 log.info("[handleRoomFinished][room={} 由 LiveKit Webhook 兜底]", room);
555 532 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP);
556 533 }
... ... @@ -587,11 +564,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
587 564  
588 565 // ========== 内部辅助 ==========
589 566  
590   - // TODO @AI:目前是叫 ensureEnabled,还是 validateXXX;
591   - /**
592   - * 校验通话功能开关
593   - */
594   - private void ensureEnabled() {
  567 + private void validateEnabled() {
595 568 if (!imProperties.getRtc().isEnabled()) {
596 569 throw exception(RTC_NOT_ENABLED);
597 570 }
... ... @@ -609,10 +582,28 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
609 582 }
610 583  
611 584 /**
612   - * 校验 invite 入参
613   - * <p>
614   - * 1. 按场景区分必填字段;
615   - * 2. 私聊补好友 / 黑名单校验,群聊补群成员校验
  585 + * 校验本人是该通话的参与者:accept / reject / leave / refreshToken 的共用前置
  586 + */
  587 + private ImRtcParticipantDO validateParticipant(ImRtcCallDO call, Long userId) {
  588 + ImRtcParticipantDO participant = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
  589 + if (participant == null) {
  590 + throw exception(RTC_NOT_PARTICIPANT);
  591 + }
  592 + return participant;
  593 + }
  594 +
  595 + /**
  596 + * 关房判定收口:私聊任一方离开必关;群通话仅在「无人在房 + 无人响铃」时关
  597 + */
  598 + private void endSessionIfTerminal(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason) {
  599 + if (ImConversationTypeEnum.isPrivate(call.getConversationType())
  600 + || shouldCloseGroupRoom(call.getRoom())) {
  601 + endSession(call, operatorId, reason);
  602 + }
  603 + }
  604 +
  605 + /**
  606 + * 校验 invite 入参;按场景区分必填字段,私聊补好友校验,群聊补群成员校验
616 607 */
617 608 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) {
618 609 Integer scene = reqVO.getScene();
... ... @@ -630,18 +621,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
630 621 if (reqVO.getGroupId() == null) {
631 622 throw exception(RTC_GROUP_REQUIRED);
632 623 }
633   - // TODO @AI:是不是 db 里,提供校验的方法,完事了。groupId + userid;
634   - List<Long> activeMemberIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId());
635   - if (!activeMemberIds.contains(userId)) {
636   - throw exception(GROUP_MEMBER_NOT_IN_GROUP);
637   - }
  624 + // 走单行 SQL;不依赖成员列表缓存,避免新加入成员发起群通话被误判
  625 + groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId);
638 626 return;
639 627 }
640   - // TODO @AI:位置参数,illegalArgumentException;
641   - throw exception(RTC_PRIVATE_INVITEE_REQUIRED);
  628 + throw new IllegalArgumentException("非法的 scene: " + scene);
642 629 }
643 630  
644   - // TODO @AI:前端必须传递成员,不允许过多;
645 631 /**
646 632 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断
647 633 * <p>
... ... @@ -677,17 +663,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
677 663 return initial;
678 664 }
679 665  
680   - // TODO @AI:按照 updateCallXXX 这种风格?
681   - // TODO @AI:注释,不用写这么细致。。。通过方法名,可以看的懂的。
682 666 /**
683 667 * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等
684   - * <p>
685   - * 与 cancel / endSession 并发时 update 可能影响 0 行;reload 看真实状态:
686   - * ENDED → 通话已被另一路径终结,抛 RTC_SESSION_NOT_EXISTS 让 accept 失败而不是返回假成功 token
687   - * RUNNING → 已被另一接听者抢先推进,幂等同步内存
688 668 */
689 669 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) {
690   - // TODO @AI:写下注释
  670 + // 1. 已 RUNNING / ENDED 直接退出;发起人加入不算「首次有人接通」
691 671 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
692 672 return;
693 673 }
... ... @@ -695,24 +675,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
695 675 return;
696 676 }
697 677  
698   - // TODO @AI:写下注释
  678 + // 2. CREATED → RUNNING 条件 UPDATE;多人并发只有一人成功
699 679 int updated = rtcCallMapper.updateByIdAndStatus(call.getId(), ImRtcCallStatusEnum.CREATED.getStatus(),
700 680 new ImRtcCallDO().setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now));
701   - // TODO @AI:是不是 if == 0,然后里面做判断。然后下面统一搞成 call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now);
702   - if (updated > 0) {
703   - // 内存值同步给后续判断
704   - call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now);
  681 + if (updated == 0) {
  682 + // 3. 竞争失败:reload 看真实终态;已 ENDED 抛错,否则同步内存
  683 + ImRtcCallDO latest = rtcCallMapper.selectById(call.getId());
  684 + if (latest == null || ImRtcCallStatusEnum.isEnded(latest.getStatus())) {
  685 + throw exception(RTC_SESSION_NOT_EXISTS);
  686 + }
  687 + call.setStatus(latest.getStatus()).setAcceptTime(latest.getAcceptTime());
705 688 return;
706 689 }
707   -
708   - // TODO @AI:写下注释
709   - // updated == 0:与并发路径竞争失败;reload 看真实终态
710   - ImRtcCallDO latest = rtcCallMapper.selectById(call.getId());
711   - if (latest == null || ImRtcCallStatusEnum.isEnded(latest.getStatus())) {
712   - // TODO @AI:不用会话把?用 RTC_CALL 吧?
713   - throw exception(RTC_SESSION_NOT_EXISTS);
714   - }
715   - call.setStatus(latest.getStatus()).setAcceptTime(latest.getAcceptTime());
  690 + // 4. 推进成功:同步内存给后续判断
  691 + call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now);
716 692 }
717 693  
718 694 /**
... ... @@ -750,13 +726,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
750 726 log.info("[endSession][room={} operator={} reason={}]", call.getRoom(), operatorId, reason);
751 727 }
752 728  
753   - // TODO @AI:getCallAudienceUserIdList?
754 729 /**
755 730 * 通话事件的收件人池:1)私聊为双方参与者;2)群聊为群活跃成员
756 731 *
757   - * TODO @params 补全;
  732 + * @param call 通话主表
  733 + * @return 收件人 userId 集合
758 734 */
759   - private Collection<Long> getCallAudience(ImRtcCallDO call) {
  735 + private Collection<Long> getCallAudienceUserIdList(ImRtcCallDO call) {
760 736 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
761 737 return groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId());
762 738 }
... ... @@ -764,15 +740,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
764 740 rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId);
765 741 }
766 742  
767   - // TODO @AI:这里的 ========== 太短了;
768   - // ========== 推送 ==========
769   - // TODO @AI:@param 注释要补全
  743 + // ========== 通知推送 ==========
770 744  
771 745 /**
772   - * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人
773   - * <p>
774   - * TODO @AI:为啥下面多写了个 “不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知”注释;
775   - * 不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知
  746 + * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人;persistent=false 不入消息流
776 747 */
777 748 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) {
778 749 String token = signToken(inviteeId, inviter == null ? null : inviter.getNickname(), call.getRoom());
... ... @@ -784,8 +755,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
784 755  
785 756 /**
786 757 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫
  758 + * <p>
  759 + * 私聊拒绝走 endSession → RTC_CALL_END(reason=REJECT) 入消息流,不在此推
787 760 */
788   - // TODO @AI:有点奇怪,单聊不推送么???
789 761 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) {
790 762 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
791 763 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
... ... @@ -819,7 +791,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
819 791 * 推送参与者事件的公共骨架;按会话类型决定收件人,单次 batch 推送扇出
820 792 */
821 793 private void pushParticipantNotification(ImRtcCallDO call, Integer type, Long actorUserId, Object payload) {
822   - Collection<Long> receivers = getCallAudience(call);
  794 + Collection<Long> receivers = getCallAudienceUserIdList(call);
823 795 if (CollUtil.isEmpty(receivers)) {
824 796 return;
825 797 }
... ... @@ -827,11 +799,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
827 799 webSocketService.sendPrivateMessageAsync(receivers, dto);
828 800 }
829 801  
830   - // TODO @AI:“自增 id 自然保证聊天流顺序”,是不是你不用写这个注释?
831 802 /**
832   - * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播
833   - * <p>
834   - * 与 RTC_CALL_END 两段式配对:START 在本接口(invite)事务里 INSERT,END 在 cancel / leave 接口事务里 INSERT,自增 id 自然保证聊天流顺序
  803 + * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播;与 RTC_CALL_END 两段式配对
835 804 */
836 805 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) {
837 806 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter);
... ... @@ -874,9 +843,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
874 843 /**
875 844 * 签 LiveKit Token
876 845 */
877   - // TODO @AI:sign 方法,是不是传递 adminuserdo?
878 846 private String signToken(Long userId, String displayName, String room) {
879   - // TODO @AI:是不是抽一个 signJoinToken(userid, displayName, room)做减法
880 847 return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, room);
881 848 }
882 849  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcCallNotification.java
... ... @@ -64,18 +64,18 @@ public class ImRtcCallNotification {
64 64 */
65 65 private String inviterAvatar;
66 66  
67   - // ========== 非 INVITE 信令的操作者 ==========
  67 + // ========== REJECT 专属字段 ==========
68 68  
69 69 /**
70   - * 操作者用户编号;ACCEPT / REJECT / CANCEL / HUNGUP 触发本次状态变迁的人
  70 + * 操作者用户编号;REJECT 触发本次状态变迁的人
71 71 */
72 72 private Long operatorUserId;
73 73 /**
74   - * 操作者昵称;前端按需展示(被某某拒接 / 取消 / 挂断),普通文案不依赖
  74 + * 操作者昵称;前端按需展示(被某某拒接);普通文案不依赖
75 75 */
76 76 private String operatorNickname;
77 77 /**
78   - * 操作者头像;前端按需展示,普通文案不依赖
  78 + * 操作者头像;前端按需展示;普通文案不依赖
79 79 */
80 80 private String operatorAvatar;
81 81  
... ... @@ -101,19 +101,6 @@ public class ImRtcCallNotification {
101 101 return notification;
102 102 }
103 103  
104   - // TODO @AI:为什么没使用?
105   - /**
106   - * 构造 ACCEPT 信令;推除接听者外的其它参与方,让主叫尽早结束响铃 UI
107   - *
108   - * @param call 通话主表
109   - * @param operatorUserId 接听者用户编号
110   - * @param operator 接听者;可空,缺失时 operatorNickname / operatorAvatar 留空
111   - * @return ACCEPT 信令
112   - */
113   - public static ImRtcCallNotification ofAccept(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
114   - return ofOperatorAction(call, ImRtcParticipantStatusEnum.JOINED.getStatus(), operatorUserId, operator);
115   - }
116   -
117 104 /**
118 105 * 构造 REJECT 信令;仅群通话场景;推主叫
119 106 *
... ... @@ -123,41 +110,7 @@ public class ImRtcCallNotification {
123 110 * @return REJECT 信令
124 111 */
125 112 public static ImRtcCallNotification ofReject(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
126   - return ofOperatorAction(call, ImRtcParticipantStatusEnum.REJECTED.getStatus(), operatorUserId, operator);
127   - }
128   -
129   - // TODO @AI:为什么没使用?
130   - /**
131   - * 构造 CANCEL 信令;推被邀请方,invitees 状态变为 NO_ANSWER
132   - *
133   - * @param call 通话主表
134   - * @param operatorUserId 取消者用户编号(主叫)
135   - * @param operator 取消者;可空,缺失时 operatorNickname / operatorAvatar 留空
136   - * @return CANCEL 信令
137   - */
138   - public static ImRtcCallNotification ofCancel(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
139   - return ofOperatorAction(call, ImRtcParticipantStatusEnum.NO_ANSWER.getStatus(), operatorUserId, operator);
140   - }
141   -
142   - // TODO @AI:为什么没使用?
143   - /**
144   - * 构造 HUNGUP 信令;推其它参与方,操作者状态变为 LEFT
145   - *
146   - * @param call 通话主表
147   - * @param operatorUserId 挂断者用户编号
148   - * @param operator 挂断者;可空,缺失时 operatorNickname / operatorAvatar 留空
149   - * @return HUNGUP 信令
150   - */
151   - public static ImRtcCallNotification ofHungUp(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
152   - return ofOperatorAction(call, ImRtcParticipantStatusEnum.LEFT.getStatus(), operatorUserId, operator);
153   - }
154   -
155   - /**
156   - * ACCEPT / REJECT / CANCEL / HUNGUP 4 个工厂的公共骨架;填充 call 上下文 + status + operator
157   - */
158   - private static ImRtcCallNotification ofOperatorAction(ImRtcCallDO call, Integer status,
159   - Long operatorUserId, AdminUserRespDTO operator) {
160   - ImRtcCallNotification notification = baseOf(call, status);
  113 + ImRtcCallNotification notification = baseOf(call, ImRtcParticipantStatusEnum.REJECTED.getStatus());
161 114 notification.operatorUserId = operatorUserId;
162 115 if (operator != null) {
163 116 notification.operatorNickname = operator.getNickname();
... ... @@ -167,7 +120,7 @@ public class ImRtcCallNotification {
167 120 }
168 121  
169 122 /**
170   - * 各 ofXxx 工厂的公共骨架;填充 call 上下文 + status
  123 + * 公共骨架;填充 call 上下文 + status
171 124 */
172 125 private static ImRtcCallNotification baseOf(ImRtcCallDO call, Integer status) {
173 126 ImRtcCallNotification notification = new ImRtcCallNotification();
... ...