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,7 +95,7 @@ public class ImRtcCallController {
95 @Operation(summary = "重新签发 Token;客户端重连或 Token 过期续期") 95 @Operation(summary = "重新签发 Token;客户端重连或 Token 过期续期")
96 @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567") 96 @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
97 public CommonResult<ImRtcCallRespVO> refreshToken(@RequestParam("room") String room) { 97 public CommonResult<ImRtcCallRespVO> refreshToken(@RequestParam("room") String room) {
98 - ImRtcCallDO call = rtcCallService.refreshCallToken(getLoginUserId(), room); 98 + ImRtcCallDO call = rtcCallService.validateCallParticipant(getLoginUserId(), room);
99 return success(buildCallResp(call, getLoginUserId())); 99 return success(buildCallResp(call, getLoginUserId()));
100 } 100 }
101 101
@@ -113,7 +113,7 @@ public class ImRtcCallController { @@ -113,7 +113,7 @@ public class ImRtcCallController {
113 if (call == null) { 113 if (call == null) {
114 return null; 114 return null;
115 } 115 }
116 - List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipants(call.getRoom()); 116 + List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
117 return new ImRtcCallRespVO() 117 return new ImRtcCallRespVO()
118 .setRoom(call.getRoom()) 118 .setRoom(call.getRoom())
119 .setLivekitUrl(imProperties.getRtc().getLivekitUrl()) 119 .setLivekitUrl(imProperties.getRtc().getLivekitUrl())
@@ -129,7 +129,7 @@ public class ImRtcCallController { @@ -129,7 +129,7 @@ public class ImRtcCallController {
129 if (call == null) { 129 if (call == null) {
130 return null; 130 return null;
131 } 131 }
132 - List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipants(call.getRoom()); 132 + List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
133 return new ImRtcGroupCallRespVO() 133 return new ImRtcGroupCallRespVO()
134 .setRoom(call.getRoom()) 134 .setRoom(call.getRoom())
135 .setGroupId(call.getGroupId()).setMediaType(call.getMediaType()) 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,12 +65,13 @@ public interface ImRtcCallService {
65 void leaveCall(Long userId, String room); 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 * @return 通话主表 72 * @return 通话主表
72 */ 73 */
73 - ImRtcCallDO refreshCallToken(Long userId, String room); 74 + ImRtcCallDO validateCallParticipant(Long userId, String room);
74 75
75 /** 76 /**
76 * 查询当前正在进行的通话;目前仅群聊场景(胶囊条),私聊未来扩展再补 peerUserId 参数 77 * 查询当前正在进行的通话;目前仅群聊场景(胶囊条),私聊未来扩展再补 peerUserId 参数
@@ -89,7 +90,7 @@ public interface ImRtcCallService { @@ -89,7 +90,7 @@ public interface ImRtcCallService {
89 * @param room 业务通话编号 90 * @param room 业务通话编号
90 * @return 参与者明细列表 91 * @return 参与者明细列表
91 */ 92 */
92 - List<ImRtcParticipantDO> getCallParticipants(String room); 93 + List<ImRtcParticipantDO> getCallParticipantList(String room);
93 94
94 /** 95 /**
95 * 签发指定用户进入该通话的 LiveKit Token;供 Controller 拼接到响应 VO 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,6 +2,8 @@ package cn.iocoder.yudao.module.im.service.rtc;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.IdUtil; 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 7 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteMoreReqVO; 8 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteMoreReqVO;
7 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO; 9 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO;
@@ -68,8 +70,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -68,8 +70,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
68 70
69 @Resource 71 @Resource
70 private ImRtcCallMapper rtcCallMapper; 72 private ImRtcCallMapper rtcCallMapper;
71 - // TODO DONE @AI:可以搞个 ImRtcParticipantService 简化本类么 —— 暂不抽:参与表 CRUD 与通话状态机紧耦合(INVITING→JOINED→LEFT 等转换都跟主表 status 联动),独立 service 会沦为薄包装且需双方互调,反而增加复杂度  
72 - // TODO @AI:要不试着拆出来,看看 ImRtcParticipantService 能不能稍微简化一些些;  
73 @Resource 73 @Resource
74 private ImRtcParticipantMapper rtcParticipantMapper; 74 private ImRtcParticipantMapper rtcParticipantMapper;
75 @Resource 75 @Resource
@@ -100,7 +100,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -100,7 +100,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
100 @Override 100 @Override
101 @SneakyThrows 101 @SneakyThrows
102 public ImRtcCallDO inviteCall(Long userId, ImRtcCallInviteReqVO reqVO) { 102 public ImRtcCallDO inviteCall(Long userId, ImRtcCallInviteReqVO reqVO) {
103 - ensureEnabled(); 103 + validateEnabled();
104 // 1. 校验入参与场景 104 // 1. 校验入参与场景
105 validateInviteScene(userId, reqVO); 105 validateInviteScene(userId, reqVO);
106 106
@@ -136,7 +136,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -136,7 +136,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
136 throw exception(RTC_PEER_BUSY); 136 throw exception(RTC_PEER_BUSY);
137 } 137 }
138 } 138 }
139 - // 1.2 通用检测:发起人在另一通通话中(与上面的私聊「已是参与者」不同语义;这里覆盖跨好友 / 跨群的兜底) 139 + // 1.2 兜底校验:发起人在另一通通话中(跨好友 / 跨群)
140 ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus( 140 ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
141 userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES); 141 userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
142 if (selfActive != null) { 142 if (selfActive != null) {
@@ -181,7 +181,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -181,7 +181,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
181 } 181 }
182 rtcParticipantMapper.insertBatch(participants); 182 rtcParticipantMapper.insertBatch(participants);
183 183
184 - // TODO @AI:需要把 3.1、3.2 顺序调整下么?你分析下。  
185 // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人 184 // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人
186 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId); 185 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId);
187 for (Long inviteeId : invitees) { 186 for (Long inviteeId : invitees) {
@@ -196,7 +195,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -196,7 +195,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
196 195
197 @Override 196 @Override
198 public ImRtcCallDO joinCall(Long userId, String room) { 197 public ImRtcCallDO joinCall(Long userId, String room) {
199 - ensureEnabled(); 198 + validateEnabled();
200 // 1.1 校验通话存在且活跃 199 // 1.1 校验通话存在且活跃
201 ImRtcCallDO call = validateCallActive(room); 200 ImRtcCallDO call = validateCallActive(room);
202 // 1.2 仅群通话支持「旁观者加入」 201 // 1.2 仅群通话支持「旁观者加入」
@@ -227,230 +226,215 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -227,230 +226,215 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
227 226
228 @Override 227 @Override
229 public void inviteMoreCall(Long userId, ImRtcCallInviteMoreReqVO reqVO) { 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 ImRtcCallDO call = validateCallActive(reqVO.getRoom()); 231 ImRtcCallDO call = validateCallActive(reqVO.getRoom());
233 - // 仅群通话支持追加邀请 232 + // 1.2 仅群通话支持追加邀请
234 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) { 233 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
235 throw exception(RTC_GROUP_REQUIRED); 234 throw exception(RTC_GROUP_REQUIRED);
236 } 235 }
237 - // 操作者必须是已在房间的参与者 236 + // 1.3 操作者必须是已在房间的参与者
238 ImRtcParticipantDO operator = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId); 237 ImRtcParticipantDO operator = rtcParticipantMapper.selectByRoomAndUserId(call.getRoom(), userId);
239 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) { 238 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) {
240 throw exception(RTC_NOT_PARTICIPANT); 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 groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId())); 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 return; 255 return;
256 } 256 }
257 257
258 - // INSERT 新邀请人 + 推 INVITE  
259 - // TODO @AI:batch 插入; 258 + // 2. 批量 INSERT 新邀请人
260 LocalDateTime now = LocalDateTime.now(); 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 .setRoom(call.getRoom()).setUserId(inviteeId) 264 .setRoom(call.getRoom()).setUserId(inviteeId)
264 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) 265 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole())
265 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) 266 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
266 .setInviteTime(now)); 267 .setInviteTime(now));
267 } 268 }
  269 + rtcParticipantMapper.insertBatch(participants);
268 270
269 - // TODO @AI:这里在写推送的; 271 + // 3. 推送通知:RTC_CALL(INVITE) 给每个新邀请人
270 AdminUserRespDTO inviter = adminUserApi.getUser(userId); 272 AdminUserRespDTO inviter = adminUserApi.getUser(userId);
271 - for (Long inviteeId : incoming) { 273 + for (Long inviteeId : incomingUserIds) {
272 pushCallInviteNotification(call, inviter, inviteeId); 274 pushCallInviteNotification(call, inviter, inviteeId);
273 } 275 }
274 } 276 }
275 277
276 @Override 278 @Override
277 public ImRtcCallDO acceptCall(Long userId, String room) { 279 public ImRtcCallDO acceptCall(Long userId, String room) {
278 - ensureEnabled();  
279 - // TODO @AI:按照 inviteMoreCall 风格,优化下注释; 280 + validateEnabled();
  281 + // 1.1 校验通话存在且活跃
280 ImRtcCallDO call = validateCallActive(room); 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 LocalDateTime now = LocalDateTime.now(); 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 maybeMarkOngoing(call, userId, now); 302 maybeMarkOngoing(call, userId, now);
302 return call; 303 return call;
303 } 304 }
304 305
305 @Override 306 @Override
306 public void rejectCall(Long userId, String room) { 307 public void rejectCall(Long userId, String room) {
307 - ensureEnabled();  
308 - // TODO @AI:按照 inviteMoreCall 风格,优化下注释; 308 + validateEnabled();
  309 + // 1.1 校验通话存在且活跃
309 ImRtcCallDO call = validateCallActive(room); 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 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) { 314 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
316 throw exception(RTC_SESSION_NOT_EXISTS); 315 throw exception(RTC_SESSION_NOT_EXISTS);
317 } 316 }
318 317
319 - // TODO @AI:这里写个注释 318 + // 2. INVITING → REJECTED;并发已变更则忽略
320 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(), 319 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
321 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.REJECTED.getStatus()).setLeaveTime(LocalDateTime.now())); 320 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.REJECTED.getStatus()).setLeaveTime(LocalDateTime.now()));
322 if (updated == 0) { 321 if (updated == 0) {
323 return; 322 return;
324 } 323 }
325 324
326 - // TODO @AI:3.1 3.2 这样; 325 + // 3.1 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体
327 // TODO @AI:群聊如果拒绝,需要做类似的 endSession 逻辑么? 326 // TODO @AI:群聊如果拒绝,需要做类似的 endSession 逻辑么?
328 - // 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体  
329 if (ImConversationTypeEnum.isGroup(call.getConversationType())) { 327 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
330 pushCallRejectNotification(call, userId); 328 pushCallRejectNotification(call, userId);
331 return; 329 return;
332 } 330 }
333 - // 私聊拒绝:走 endSession(推 RTC_CALL_END(reason=REJECT)) 331 + // 3.2 私聊拒绝:走 endSession(推 RTC_CALL_END(reason=REJECT))
334 endSession(call, userId, ImRtcCallEndReasonEnum.REJECT); 332 endSession(call, userId, ImRtcCallEndReasonEnum.REJECT);
335 } 333 }
336 334
337 @Override 335 @Override
338 public void cancelCall(Long userId, String room) { 336 public void cancelCall(Long userId, String room) {
339 - ensureEnabled();  
340 - // TODO @AI:按照 inviteMoreCall 风格,优化下注释; 337 + validateEnabled();
  338 + // 1.1 校验通话存在且活跃
341 ImRtcCallDO call = validateCallActive(room); 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 throw exception(RTC_NOT_PARTICIPANT); 342 throw exception(RTC_NOT_PARTICIPANT);
345 } 343 }
346 - // 仅 CREATED 状态可取消(RUNNING 应走 leave) 344 + // 1.3 仅 CREATED 状态可取消(RUNNING 应走 leave)
347 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) { 345 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
348 throw exception(RTC_SESSION_NOT_EXISTS); 346 throw exception(RTC_SESSION_NOT_EXISTS);
349 } 347 }
350 348
351 - // TODO @AI:这里写下注释; 349 + // 2. 关会话并推 RTC_CALL_END(reason=CANCEL)
352 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL); 350 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL);
353 } 351 }
354 352
355 @Override 353 @Override
356 public void leaveCall(Long userId, String room) { 354 public void leaveCall(Long userId, String room) {
357 - ensureEnabled();  
358 - // TODO @AI:按照 inviteMoreCall 风格,优化下注释; 355 + validateEnabled();
  356 + // 1.1 校验通话存在且活跃
359 ImRtcCallDO call = validateCallActive(room); 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 LocalDateTime now = LocalDateTime.now(); 362 LocalDateTime now = LocalDateTime.now();
367 rtcParticipantMapper.updateByIdAndStatus(participant.getId(), participant.getStatus(), 363 rtcParticipantMapper.updateByIdAndStatus(participant.getId(), participant.getStatus(),
368 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(now)); 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 // 群通话单人离开:LiveKit `ParticipantDisconnected` 自动通知房内成员;业务后端不另外推 367 // 群通话单人离开:LiveKit `ParticipantDisconnected` 自动通知房内成员;业务后端不另外推
  368 + endSessionIfTerminal(call, userId, ImRtcCallEndReasonEnum.HANGUP);
378 } 369 }
379 370
380 /** 371 /**
381 * 群通话是否应该关闭:房内没有 JOINED 就关 372 * 群通话是否应该关闭:房内没有 JOINED 就关
382 * <p> 373 * <p>
383 - * 群通话至少需要 1 个 JOINED 在房才有意义;如果只剩 INVITING(响铃中)的,接通后也只是 1 人独房,无价值 374 + * 群通话至少需要 1 个 JOINED 在房才有意义;只剩 INVITING(响铃中)也无价值
384 * <p> 375 * <p>
385 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛 376 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛
386 */ 377 */
387 private boolean shouldCloseGroupRoom(String room) { 378 private boolean shouldCloseGroupRoom(String room) {
388 - // TODO @AI:通过查询,计算情况?;感觉所有群聊的关闭条件:都是只有 1 个 join,剩余都处于结束,说白了没人可以和他电话了。。。。  
389 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByRoomAndStatus(room, 379 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByRoomAndStatus(room,
390 ImRtcParticipantStatusEnum.JOINED.getStatus()); 380 ImRtcParticipantStatusEnum.JOINED.getStatus());
391 return CollUtil.isEmpty(joined); 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 * <p> 386 * <p>
399 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃 387 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃
400 */ 388 */
401 private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) { 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 return null; 392 return null;
406 } 393 }
407 - ImRtcCallDO call = rtcCallMapper.selectByRoom(aPart.getRoom()); 394 + ImRtcCallDO call = rtcCallMapper.selectByRoom(participantA.getRoom());
408 if (call == null 395 if (call == null
409 || !ImConversationTypeEnum.isPrivate(call.getConversationType()) 396 || !ImConversationTypeEnum.isPrivate(call.getConversationType())
410 || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 397 || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
411 return null; 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 return null; 403 return null;
417 } 404 }
418 return call; 405 return call;
419 } 406 }
420 407
421 - // TODO @AI:怎么感觉这个要改下?是不是根据 userid + callid 更合理?  
422 @Override 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 ImRtcCallDO call = validateCallActive(room); 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 return call; 415 return call;
432 } 416 }
433 417
434 @Override 418 @Override
435 public ImRtcCallDO getActiveCall(Long userId, Long groupId) { 419 public ImRtcCallDO getActiveCall(Long userId, Long groupId) {
436 - ensureEnabled(); 420 + validateEnabled();
437 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存) 421 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存)
438 groupMemberService.validateMemberInGroup(groupId, userId); 422 groupMemberService.validateMemberInGroup(groupId, userId);
439 // 2. 查询活跃通话 423 // 2. 查询活跃通话
440 return rtcCallMapper.selectLastOneByGroupIdAndStatusIn(groupId, ImRtcCallStatusEnum.ACTIVE_STATUSES); 424 return rtcCallMapper.selectLastOneByGroupIdAndStatusIn(groupId, ImRtcCallStatusEnum.ACTIVE_STATUSES);
441 } 425 }
442 426
443 - // TODO @AI:方法名,复数应该使用 List,不使用 s;  
444 @Override 427 @Override
445 - public List<ImRtcParticipantDO> getCallParticipants(String room) { 428 + public List<ImRtcParticipantDO> getCallParticipantList(String room) {
446 return rtcParticipantMapper.selectListByRoom(room); 429 return rtcParticipantMapper.selectListByRoom(room);
447 } 430 }
448 431
449 @Override 432 @Override
450 public String signCallToken(Long userId, String room) { 433 public String signCallToken(Long userId, String room) {
451 AdminUserRespDTO user = adminUserApi.getUser(userId); 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 return signToken(userId, displayName, room); 438 return signToken(userId, displayName, room);
455 } 439 }
456 440
@@ -476,11 +460,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -476,11 +460,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
476 * @param event 事件 460 * @param event 事件
477 */ 461 */
478 private void handleParticipantJoined(LiveKitWebhookEventDTO event) { 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 return; 464 return;
482 } 465 }
483 - // 前置检查 466 + // 1. 前置检查:通话存在且活跃
484 String room = event.getRoom().getName(); 467 String room = event.getRoom().getName();
485 ImRtcCallDO call = rtcCallMapper.selectByRoom(room); 468 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
486 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 469 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
@@ -491,7 +474,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -491,7 +474,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
491 return; 474 return;
492 } 475 }
493 476
494 - // 推 1602 通知参与方 / 全群参与方 477 + // 2. 推 1602 通知参与方 / 全群参与方
495 pushParticipantConnectedNotification(call, userId); 478 pushParticipantConnectedNotification(call, userId);
496 } 479 }
497 480
@@ -499,11 +482,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -499,11 +482,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
499 * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等 482 * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等
500 */ 483 */
501 private void handleParticipantLeft(LiveKitWebhookEventDTO event) { 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 return; 486 return;
505 } 487 }
506 - // 前置检查 488 + // 1. 前置检查:通话存在且活跃 + 参与者仍在 JOINED
507 String room = event.getRoom().getName(); 489 String room = event.getRoom().getName();
508 ImRtcCallDO call = rtcCallMapper.selectByRoom(room); 490 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
509 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 491 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
@@ -518,7 +500,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -518,7 +500,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
518 return; 500 return;
519 } 501 }
520 502
521 - // TODO @AI:注释。带序号的; 503 + // 2. JOINED → LEFT;正常 leave 接口已改过则 update 影响 0 行直接退出
522 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.JOINED.getStatus(), 504 int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.JOINED.getStatus(),
523 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(LocalDateTime.now())); 505 new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(LocalDateTime.now()));
524 if (updated == 0) { 506 if (updated == 0) {
@@ -526,14 +508,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -526,14 +508,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
526 } 508 }
527 log.info("[handleParticipantLeft][room={} userId={} 由 LiveKit Webhook 兜底]", room, userId); 509 log.info("[handleParticipantLeft][room={} userId={} 由 LiveKit Webhook 兜底]", room, userId);
528 510
529 - // 推 1603 通知参与方 / 全群参与方 511 + // 3. 推 1603 通知参与方 / 全群参与方
530 pushParticipantDisconnectedNotification(call, userId); 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,14 +521,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
543 if (event.getRoom() == null) { 521 if (event.getRoom() == null) {
544 return; 522 return;
545 } 523 }
546 - // 前置检查 524 + // 1. 前置检查:通话存在且活跃
547 String room = event.getRoom().getName(); 525 String room = event.getRoom().getName();
548 ImRtcCallDO call = rtcCallMapper.selectByRoom(room); 526 ImRtcCallDO call = rtcCallMapper.selectByRoom(room);
549 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 527 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
550 return; 528 return;
551 } 529 }
552 -  
553 - // TODO @AI:注释。带序号的; 530 + // 2. 关会话
554 log.info("[handleRoomFinished][room={} 由 LiveKit Webhook 兜底]", room); 531 log.info("[handleRoomFinished][room={} 由 LiveKit Webhook 兜底]", room);
555 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP); 532 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP);
556 } 533 }
@@ -587,11 +564,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -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 if (!imProperties.getRtc().isEnabled()) { 568 if (!imProperties.getRtc().isEnabled()) {
596 throw exception(RTC_NOT_ENABLED); 569 throw exception(RTC_NOT_ENABLED);
597 } 570 }
@@ -609,10 +582,28 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -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 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) { 608 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) {
618 Integer scene = reqVO.getScene(); 609 Integer scene = reqVO.getScene();
@@ -630,18 +621,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -630,18 +621,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
630 if (reqVO.getGroupId() == null) { 621 if (reqVO.getGroupId() == null) {
631 throw exception(RTC_GROUP_REQUIRED); 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 return; 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 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断 632 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断
647 * <p> 633 * <p>
@@ -677,17 +663,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -677,17 +663,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
677 return initial; 663 return initial;
678 } 664 }
679 665
680 - // TODO @AI:按照 updateCallXXX 这种风格?  
681 - // TODO @AI:注释,不用写这么细致。。。通过方法名,可以看的懂的。  
682 /** 666 /**
683 * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等 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 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) { 669 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) {
690 - // TODO @AI:写下注释 670 + // 1. 已 RUNNING / ENDED 直接退出;发起人加入不算「首次有人接通」
691 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) { 671 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
692 return; 672 return;
693 } 673 }
@@ -695,24 +675,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -695,24 +675,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
695 return; 675 return;
696 } 676 }
697 677
698 - // TODO @AI:写下注释 678 + // 2. CREATED → RUNNING 条件 UPDATE;多人并发只有一人成功
699 int updated = rtcCallMapper.updateByIdAndStatus(call.getId(), ImRtcCallStatusEnum.CREATED.getStatus(), 679 int updated = rtcCallMapper.updateByIdAndStatus(call.getId(), ImRtcCallStatusEnum.CREATED.getStatus(),
700 new ImRtcCallDO().setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now)); 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 return; 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,13 +726,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
750 log.info("[endSession][room={} operator={} reason={}]", call.getRoom(), operatorId, reason); 726 log.info("[endSession][room={} operator={} reason={}]", call.getRoom(), operatorId, reason);
751 } 727 }
752 728
753 - // TODO @AI:getCallAudienceUserIdList?  
754 /** 729 /**
755 * 通话事件的收件人池:1)私聊为双方参与者;2)群聊为群活跃成员 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 if (ImConversationTypeEnum.isGroup(call.getConversationType())) { 736 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
761 return groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId()); 737 return groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId());
762 } 738 }
@@ -764,15 +740,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -764,15 +740,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
764 rtcParticipantMapper.selectListByRoom(call.getRoom()), ImRtcParticipantDO::getUserId); 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 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) { 748 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) {
778 String token = signToken(inviteeId, inviter == null ? null : inviter.getNickname(), call.getRoom()); 749 String token = signToken(inviteeId, inviter == null ? null : inviter.getNickname(), call.getRoom());
@@ -784,8 +755,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -784,8 +755,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
784 755
785 /** 756 /**
786 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫 757 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫
  758 + * <p>
  759 + * 私聊拒绝走 endSession → RTC_CALL_END(reason=REJECT) 入消息流,不在此推
787 */ 760 */
788 - // TODO @AI:有点奇怪,单聊不推送么???  
789 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { 761 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) {
790 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; 762 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
791 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator); 763 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
@@ -819,7 +791,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -819,7 +791,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
819 * 推送参与者事件的公共骨架;按会话类型决定收件人,单次 batch 推送扇出 791 * 推送参与者事件的公共骨架;按会话类型决定收件人,单次 batch 推送扇出
820 */ 792 */
821 private void pushParticipantNotification(ImRtcCallDO call, Integer type, Long actorUserId, Object payload) { 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 if (CollUtil.isEmpty(receivers)) { 795 if (CollUtil.isEmpty(receivers)) {
824 return; 796 return;
825 } 797 }
@@ -827,11 +799,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -827,11 +799,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
827 webSocketService.sendPrivateMessageAsync(receivers, dto); 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 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) { 805 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) {
837 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter); 806 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter);
@@ -874,9 +843,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -874,9 +843,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
874 /** 843 /**
875 * 签 LiveKit Token 844 * 签 LiveKit Token
876 */ 845 */
877 - // TODO @AI:sign 方法,是不是传递 adminuserdo?  
878 private String signToken(Long userId, String displayName, String room) { 846 private String signToken(Long userId, String displayName, String room) {
879 - // TODO @AI:是不是抽一个 signJoinToken(userid, displayName, room)做减法  
880 return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, room); 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,18 +64,18 @@ public class ImRtcCallNotification {
64 */ 64 */
65 private String inviterAvatar; 65 private String inviterAvatar;
66 66
67 - // ========== 非 INVITE 信令的操作者 ========== 67 + // ========== REJECT 专属字段 ==========
68 68
69 /** 69 /**
70 - * 操作者用户编号;ACCEPT / REJECT / CANCEL / HUNGUP 触发本次状态变迁的人 70 + * 操作者用户编号;REJECT 触发本次状态变迁的人
71 */ 71 */
72 private Long operatorUserId; 72 private Long operatorUserId;
73 /** 73 /**
74 - * 操作者昵称;前端按需展示(被某某拒接 / 取消 / 挂断),普通文案不依赖 74 + * 操作者昵称;前端按需展示(被某某拒接);普通文案不依赖
75 */ 75 */
76 private String operatorNickname; 76 private String operatorNickname;
77 /** 77 /**
78 - * 操作者头像;前端按需展示,普通文案不依赖 78 + * 操作者头像;前端按需展示;普通文案不依赖
79 */ 79 */
80 private String operatorAvatar; 80 private String operatorAvatar;
81 81
@@ -101,19 +101,6 @@ public class ImRtcCallNotification { @@ -101,19 +101,6 @@ public class ImRtcCallNotification {
101 return notification; 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 * 构造 REJECT 信令;仅群通话场景;推主叫 105 * 构造 REJECT 信令;仅群通话场景;推主叫
119 * 106 *
@@ -123,41 +110,7 @@ public class ImRtcCallNotification { @@ -123,41 +110,7 @@ public class ImRtcCallNotification {
123 * @return REJECT 信令 110 * @return REJECT 信令
124 */ 111 */
125 public static ImRtcCallNotification ofReject(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) { 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 notification.operatorUserId = operatorUserId; 114 notification.operatorUserId = operatorUserId;
162 if (operator != null) { 115 if (operator != null) {
163 notification.operatorNickname = operator.getNickname(); 116 notification.operatorNickname = operator.getNickname();
@@ -167,7 +120,7 @@ public class ImRtcCallNotification { @@ -167,7 +120,7 @@ public class ImRtcCallNotification {
167 } 120 }
168 121
169 /** 122 /**
170 - * 各 ofXxx 工厂的公共骨架;填充 call 上下文 + status 123 + * 公共骨架;填充 call 上下文 + status
171 */ 124 */
172 private static ImRtcCallNotification baseOf(ImRtcCallDO call, Integer status) { 125 private static ImRtcCallNotification baseOf(ImRtcCallDO call, Integer status) {
173 ImRtcCallNotification notification = new ImRtcCallNotification(); 126 ImRtcCallNotification notification = new ImRtcCallNotification();