Commit d0aac3824d08478127333357e9c7604922097a1b

Authored by YunaiV
1 parent b6830315

✨ feat(im): rtc 增加代码评审,进一步优化代码风格;

yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -153,7 +153,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> { @@ -153,7 +153,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
153 * @param queryWrapper 查询条件 153 * @param queryWrapper 查询条件
154 * @return 最新一条;不存在返回 null 154 * @return 最新一条;不存在返回 null
155 */ 155 */
156 - default T selectLast(LambdaQueryWrapper<T> queryWrapper) { 156 + default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
157 return CollUtil.getLast(selectList(queryWrapper)); 157 return CollUtil.getLast(selectList(queryWrapper));
158 } 158 }
159 159
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/ImRtcCallController.java
@@ -26,6 +26,7 @@ import java.util.Set; @@ -26,6 +26,7 @@ import java.util.Set;
26 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 26 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
27 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 27 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
28 28
  29 +// TODO @AI:需要讨论,到底使用 roomName 还是 callId 更好?前端传参;然后,DO 那种,是不是只存储 callId,roomName 二选 1,是不是就行了。或者统一成 room?uuid 完事;callid 还给 call.id 更好理解;
29 @Tag(name = "管理后台 - IM 实时通话") 30 @Tag(name = "管理后台 - IM 实时通话")
30 @RestController 31 @RestController
31 @RequestMapping("/im/rtc") 32 @RequestMapping("/im/rtc")
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcCallMapper.java
@@ -26,8 +26,8 @@ public interface ImRtcCallMapper extends BaseMapperX&lt;ImRtcCallDO&gt; { @@ -26,8 +26,8 @@ public interface ImRtcCallMapper extends BaseMapperX&lt;ImRtcCallDO&gt; {
26 return selectOne(ImRtcCallDO::getRoomName, roomName); 26 return selectOne(ImRtcCallDO::getRoomName, roomName);
27 } 27 }
28 28
29 - default ImRtcCallDO selectLastByGroupIdAndStatusIn(Long groupId, Collection<Integer> statuses) {  
30 - return selectLast(new LambdaQueryWrapperX<ImRtcCallDO>() 29 + default ImRtcCallDO selectLastOneByGroupIdAndStatusIn(Long groupId, Collection<Integer> statuses) {
  30 + return selectLastOne(new LambdaQueryWrapperX<ImRtcCallDO>()
31 .eq(ImRtcCallDO::getGroupId, groupId) 31 .eq(ImRtcCallDO::getGroupId, groupId)
32 .in(ImRtcCallDO::getStatus, statuses)); 32 .in(ImRtcCallDO::getStatus, statuses));
33 } 33 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapper.java
@@ -33,8 +33,8 @@ public interface ImRtcParticipantMapper extends BaseMapperX&lt;ImRtcParticipantDO&gt; @@ -33,8 +33,8 @@ public interface ImRtcParticipantMapper extends BaseMapperX&lt;ImRtcParticipantDO&gt;
33 .eq(ImRtcParticipantDO::getStatus, status)); 33 .eq(ImRtcParticipantDO::getStatus, status));
34 } 34 }
35 35
36 - default ImRtcParticipantDO selectLastByUserIdAndStatus(Long userId, Collection<Integer> statuses) {  
37 - return selectLast(new LambdaQueryWrapperX<ImRtcParticipantDO>() 36 + default ImRtcParticipantDO selectLastOneByUserIdAndStatus(Long userId, Collection<Integer> statuses) {
  37 + return selectLastOne(new LambdaQueryWrapperX<ImRtcParticipantDO>()
38 .eq(ImRtcParticipantDO::getUserId, userId) 38 .eq(ImRtcParticipantDO::getUserId, userId)
39 .in(ImRtcParticipantDO::getStatus, statuses)); 39 .in(ImRtcParticipantDO::getStatus, statuses));
40 } 40 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantRoleEnum.java
@@ -17,6 +17,7 @@ public enum ImRtcParticipantRoleEnum implements ArrayValuable&lt;Integer&gt; { @@ -17,6 +17,7 @@ public enum ImRtcParticipantRoleEnum implements ArrayValuable&lt;Integer&gt; {
17 17
18 INVITER(1, "发起人"), 18 INVITER(1, "发起人"),
19 INVITEE(2, "被邀请者"), 19 INVITEE(2, "被邀请者"),
  20 + // TODO @AI:改成 joiner?还是 joinee?看看这样拼写是不是对的。
20 ACTIVE_JOIN(3, "主动加入者"); // 仅群通话场景:旁观者点胶囊条加入已有通话 21 ACTIVE_JOIN(3, "主动加入者"); // 仅群通话场景:旁观者点胶囊条加入已有通话
21 22
22 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImRtcParticipantRoleEnum::getRole).toArray(Integer[]::new); 23 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImRtcParticipantRoleEnum::getRole).toArray(Integer[]::new);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
@@ -13,21 +13,21 @@ import cn.iocoder.yudao.module.im.dal.redis.rtc.ImRtcCallLockRedisDAO; @@ -13,21 +13,21 @@ import cn.iocoder.yudao.module.im.dal.redis.rtc.ImRtcCallLockRedisDAO;
13 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; 13 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
14 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
15 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallEndReasonEnum; 15 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallEndReasonEnum;
  16 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
16 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum; 17 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum;
17 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum; 18 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
18 -import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;  
19 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
20 import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitClient; 20 import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitClient;
  21 +import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO;
21 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 22 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
22 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; 23 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
23 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 24 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
24 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; 25 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
25 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; 26 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
26 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO; 27 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
27 -import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO;  
28 -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc.*;  
29 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 28 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
30 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; 29 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  30 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc.*;
31 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 31 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
32 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 32 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
33 import jakarta.annotation.Resource; 33 import jakarta.annotation.Resource;
@@ -68,7 +68,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -68,7 +68,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
68 68
69 @Resource 69 @Resource
70 private ImRtcCallMapper rtcCallMapper; 70 private ImRtcCallMapper rtcCallMapper;
71 - // TODO @AI:可以搞个 ImRtcParticipantService,从而简化这个类的大小么? 71 + // TODO DONE @AI:可以搞个 ImRtcParticipantService 简化本类么 —— 暂不抽:参与表 CRUD 与通话状态机紧耦合(INVITING→JOINED→LEFT 等转换都跟主表 status 联动),独立 service 会沦为薄包装且需双方互调,反而增加复杂度
  72 + // TODO @AI:要不试着拆出来,看看 ImRtcParticipantService 能不能稍微简化一些些;
72 @Resource 73 @Resource
73 private ImRtcParticipantMapper rtcParticipantMapper; 74 private ImRtcParticipantMapper rtcParticipantMapper;
74 @Resource 75 @Resource
@@ -102,8 +103,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -102,8 +103,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
102 ensureEnabled(); 103 ensureEnabled();
103 // 1. 校验入参与场景 104 // 1. 校验入参与场景
104 validateInviteScene(userId, reqVO); 105 validateInviteScene(userId, reqVO);
105 - // TODO @AI:2 这个注释,有点复杂。略微重复了。应该就是加锁 + 所内逻辑。为什么加锁:xxxx  
106 - // 2. 包锁:同好友 / 同群串行;锁内 SELECT 命中已有活跃通话改走加入分支,否则 INSERT 新通话 106 +
  107 + // 2. 加锁后跑业务主体;同好友对 / 同群串行,避免并发各开一通的竞态
107 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) { 108 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) {
108 return rtcCallLockRedisDAO.lockGroup(reqVO.getGroupId(), () -> inviteCall0(userId, reqVO)); 109 return rtcCallLockRedisDAO.lockGroup(reqVO.getGroupId(), () -> inviteCall0(userId, reqVO));
109 } 110 }
@@ -111,73 +112,84 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -111,73 +112,84 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
111 } 112 }
112 113
113 /** 114 /**
114 - * invite 锁内主体;SELECT 已有活跃通话 → 直接抛错(群走 joinCall,私聊用户已是参与者);否则 INSERT 新通话 115 + * invite 锁内主体:按场景先做活跃通话 / 对端忙线校验,再做发起人自身忙线校验,最后建通话
115 */ 116 */
116 private ImRtcCallDO inviteCall0(Long userId, ImRtcCallInviteReqVO reqVO) { 117 private ImRtcCallDO inviteCall0(Long userId, ImRtcCallInviteReqVO reqVO) {
117 - // TODO @AI:是不是可以改成:私聊的检测、群聊的检测;通用的检测;现在 if else 有点混乱。  
118 - // 1.1 已有活跃通话:群通话提示走加入按钮;私聊一定是当前用户已在通话中(自身忙线)  
119 - ImRtcCallDO active = ImConversationTypeEnum.isGroup(reqVO.getScene())  
120 - // TODO @AI:selectLastOneByXXX;然后 basemapperx 也改成这样的 selectLastOne;这样整体风格对齐;  
121 - ? rtcCallMapper.selectLastByGroupIdAndStatusIn(reqVO.getGroupId(), ImRtcCallStatusEnum.ACTIVE_STATUSES)  
122 - : getActivePrivateCallByPair(userId, reqVO.getPeerUserId());  
123 - if (active != null) {  
124 - throw exception(ImConversationTypeEnum.isGroup(reqVO.getScene())  
125 - ? RTC_GROUP_CALL_ACTIVE : RTC_SELF_BUSY);  
126 - }  
127 - // 1.2 自身忙线:当前用户已在另一通话;私聊场景再额外检查对端 TODO @AI:是不是要拆成 2 段注释。就是“私聊场景再额外检查对端”下面的“ImConversationTypeEnum.isPrivate(reqVO.getScene())”  
128 - ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastByUserIdAndStatus(userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);  
129 - if (selfActive != null) {  
130 - throw exception(RTC_SELF_BUSY);  
131 - }  
132 - if (ImConversationTypeEnum.isPrivate(reqVO.getScene())) {  
133 - ImRtcParticipantDO peerActive = rtcParticipantMapper.selectLastByUserIdAndStatus(reqVO.getPeerUserId(), ImRtcParticipantStatusEnum.ACTIVE_STATUSES); 118 + // 1.1 按场景检测活跃通话
  119 + if (ImConversationTypeEnum.isGroup(reqVO.getScene())) {
  120 + // 群通话已存在则引导走 joinCall(旁观者加入按钮),不再开新通话
  121 + ImRtcCallDO active = rtcCallMapper.selectLastOneByGroupIdAndStatusIn(
  122 + reqVO.getGroupId(), ImRtcCallStatusEnum.ACTIVE_STATUSES);
  123 + if (active != null) {
  124 + throw exception(RTC_GROUP_CALL_ACTIVE);
  125 + }
  126 + } else {
  127 + // 私聊已在通话中说明发起人自己已是参与者,当作自身忙线
  128 + ImRtcCallDO active = getActivePrivateCallByPair(userId, reqVO.getPeerUserId());
  129 + if (active != null) {
  130 + throw exception(RTC_SELF_BUSY);
  131 + }
  132 + // 私聊额外检查对端忙线
  133 + ImRtcParticipantDO peerActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
  134 + reqVO.getPeerUserId(), ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
134 if (peerActive != null) { 135 if (peerActive != null) {
135 throw exception(RTC_PEER_BUSY); 136 throw exception(RTC_PEER_BUSY);
136 } 137 }
137 } 138 }
  139 + // 1.2 通用检测:发起人在另一通通话中(与上面的私聊「已是参与者」不同语义;这里覆盖跨好友 / 跨群的兜底)
  140 + ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
  141 + userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
  142 + if (selfActive != null) {
  143 + throw exception(RTC_SELF_BUSY);
  144 + }
138 145
139 // 2. 新建通话 146 // 2. 新建通话
140 - return doInsertNewCall(userId, reqVO); 147 + return createCall(userId, reqVO);
141 } 148 }
142 149
143 - // TODO @AI:改成 createCall 方法;  
144 - // TODO @AI:希望逻辑是:1 构造参数,2.1 插入主表、2.2 插入参与表;3. 推送通知;  
145 - private ImRtcCallDO doInsertNewCall(Long inviterId, ImRtcCallInviteReqVO reqVO) {  
146 - // 1. 派生 callId + roomName 150 + /**
  151 + * 新建通话
  152 + *
  153 + * @param inviterId 发起人编号
  154 + * @param reqVO 发起请求
  155 + * @return 通话主表
  156 + */
  157 + private ImRtcCallDO createCall(Long inviterId, ImRtcCallInviteReqVO reqVO) {
  158 + // 1. 构造参数:派生 callId / roomName,解析被邀请池(私聊单元素;群聊 invitee 子集或全员)
  159 + // TODO @AI:callId、roomName 都抽成小方法。
147 String callId = IdUtil.fastSimpleUUID(); 160 String callId = IdUtil.fastSimpleUUID();
148 String roomName = ImRtcCallDO.ROOM_NAME_PREFIX + callId; 161 String roomName = ImRtcCallDO.ROOM_NAME_PREFIX + callId;
149 LocalDateTime now = LocalDateTime.now(); 162 LocalDateTime now = LocalDateTime.now();
150 - // 2. 构建被邀请池(私聊为 peerUserId 单元素;群聊取 invitee 子集 / 全员)  
151 Set<Long> invitees = resolveInvitees(reqVO, inviterId); 163 Set<Long> invitees = resolveInvitees(reqVO, inviterId);
152 164
153 - // 3. INSERT 主表;群聊发起人即时 JOINED 但通话仍处 CREATED,等首个非发起人接通才切 RUNNING  
154 - ImRtcCallDO call = new ImRtcCallDO()  
155 - .setCallId(callId).setRoomName(roomName) 165 + // 2.1 INSERT 主表;群聊发起人即时 JOINED 但通话仍处 CREATED,等首个非发起人接通才切 RUNNING
  166 + ImRtcCallDO call = new ImRtcCallDO().setCallId(callId).setRoomName(roomName)
156 .setConversationType(reqVO.getScene()).setMediaType(reqVO.getMediaType()) 167 .setConversationType(reqVO.getScene()).setMediaType(reqVO.getMediaType())
157 .setInviterUserId(inviterId).setGroupId(reqVO.getGroupId()) 168 .setInviterUserId(inviterId).setGroupId(reqVO.getGroupId())
158 - .setStatus(ImRtcCallStatusEnum.CREATED.getStatus())  
159 - .setStartTime(now); 169 + .setStatus(ImRtcCallStatusEnum.CREATED.getStatus()).setStartTime(now);
160 rtcCallMapper.insert(call); 170 rtcCallMapper.insert(call);
161 - // TODO @AI:是不是 4 和 5,改成 批量插入;  
162 - // 4. INSERT 发起人 participant  
163 - rtcParticipantMapper.insert(new ImRtcParticipantDO()  
164 - .setCallId(callId).setUserId(inviterId) 171 + // 2.2 批量 INSERT 参与表:发起人即时 JOINED,被邀请人 INVITING 等接通
  172 + List<ImRtcParticipantDO> participants = new ArrayList<>(invitees.size() + 1);
  173 + participants.add(new ImRtcParticipantDO().setCallId(callId).setUserId(inviterId)
165 .setRole(ImRtcParticipantRoleEnum.INVITER.getRole()) 174 .setRole(ImRtcParticipantRoleEnum.INVITER.getRole())
166 .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()) 175 .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus())
167 .setInviteTime(now).setAcceptTime(now)); 176 .setInviteTime(now).setAcceptTime(now));
168 - // 5. INSERT 被邀请人 participants  
169 for (Long inviteeId : invitees) { 177 for (Long inviteeId : invitees) {
170 - rtcParticipantMapper.insert(new ImRtcParticipantDO() 178 + participants.add(new ImRtcParticipantDO()
171 .setCallId(callId).setUserId(inviteeId) 179 .setCallId(callId).setUserId(inviteeId)
172 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) 180 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole())
173 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) 181 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
174 .setInviteTime(now)); 182 .setInviteTime(now));
175 } 183 }
176 - // 6. 推送 RTC_CALL(INVITE) 给被邀请人;群通话再向全群广播 RTC_CALL_START 入聊天流 184 + rtcParticipantMapper.insertBatch(participants);
  185 +
  186 + // TODO @AI:需要把 3.1、3.2 顺序调整下么?你分析下。
  187 + // 3.1 推送通知:RTC_CALL(INVITE) 给每个被邀请人
177 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId); 188 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId);
178 for (Long inviteeId : invitees) { 189 for (Long inviteeId : invitees) {
179 pushCallInviteNotification(call, inviterUser, inviteeId); 190 pushCallInviteNotification(call, inviterUser, inviteeId);
180 } 191 }
  192 + // 3.2 群通话再向全群广播 RTC_CALL_START 入聊天流
181 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) { 193 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) {
182 pushCallStartNotification(call, inviterUser); 194 pushCallStartNotification(call, inviterUser);
183 } 195 }
@@ -188,47 +200,38 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -188,47 +200,38 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
188 public ImRtcCallDO joinCall(Long userId, String roomName) { 200 public ImRtcCallDO joinCall(Long userId, String roomName) {
189 ensureEnabled(); 201 ensureEnabled();
190 // 1.1 校验通话存在且活跃 202 // 1.1 校验通话存在且活跃
191 - ImRtcCallDO call = requireActiveCall(roomName); 203 + ImRtcCallDO call = validateCallActive(roomName);
192 // 1.2 仅群通话支持「旁观者加入」 204 // 1.2 仅群通话支持「旁观者加入」
193 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) { 205 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
194 throw exception(RTC_GROUP_REQUIRED); 206 throw exception(RTC_GROUP_REQUIRED);
195 } 207 }
196 208
197 - // 2. TODO 把 joinExistingCall 拿进来?避免方法太多太碎。  
198 - return joinExistingCall(call, userId);  
199 - }  
200 -  
201 - /**  
202 - * 命中已有活跃通话;当前用户加入或重新进入;不再触发 INVITE / START 广播  
203 - */  
204 - private ImRtcCallDO joinExistingCall(ImRtcCallDO active, Long userId) {  
205 - ImRtcParticipantDO existing = rtcParticipantMapper.selectByCallIdAndUserId(active.getCallId(), userId); 209 + // 2. 入参与表:已有记录切回 JOINED;不在记录则以 ACTIVE_JOIN 角色 INSERT
206 LocalDateTime now = LocalDateTime.now(); 210 LocalDateTime now = LocalDateTime.now();
  211 + ImRtcParticipantDO existing = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
207 if (existing != null) { 212 if (existing != null) {
208 // 非 JOINED 状态切回 JOINED + accept_time;INVITER / INVITEE 重连 / 旁观点胶囊条都走这条 213 // 非 JOINED 状态切回 JOINED + accept_time;INVITER / INVITEE 重连 / 旁观点胶囊条都走这条
209 if (!ImRtcParticipantStatusEnum.isJoined(existing.getStatus())) { 214 if (!ImRtcParticipantStatusEnum.isJoined(existing.getStatus())) {
210 - rtcParticipantMapper.updateById(new ImRtcParticipantDO()  
211 - .setId(existing.getId())  
212 - .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus())  
213 - .setAcceptTime(now)); 215 + rtcParticipantMapper.updateById(new ImRtcParticipantDO().setId(existing.getId())
  216 + .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now));
214 } 217 }
215 } else { 218 } else {
216 // 旁观者主动加入:INSERT role=ACTIVE_JOIN 219 // 旁观者主动加入:INSERT role=ACTIVE_JOIN
217 - rtcParticipantMapper.insert(new ImRtcParticipantDO()  
218 - .setCallId(active.getCallId()).setUserId(userId)  
219 - .setRole(ImRtcParticipantRoleEnum.ACTIVE_JOIN.getRole())  
220 - .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus())  
221 - .setInviteTime(now).setAcceptTime(now)); 220 + rtcParticipantMapper.insert(new ImRtcParticipantDO().setCallId(call.getCallId())
  221 + .setUserId(userId).setRole(ImRtcParticipantRoleEnum.ACTIVE_JOIN.getRole())
  222 + .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setInviteTime(now).setAcceptTime(now));
222 } 223 }
223 - // 主表 CREATED → RUNNING(首次有非发起人加入)  
224 - maybeMarkOngoing(active, userId, now);  
225 - return active; 224 +
  225 + // 3. 主表 CREATED → RUNNING(首次有非发起人加入)
  226 + maybeMarkOngoing(call, userId, now);
  227 + return call;
226 } 228 }
227 229
228 @Override 230 @Override
229 public void inviteMoreCall(Long userId, ImRtcCallInviteMoreReqVO reqVO) { 231 public void inviteMoreCall(Long userId, ImRtcCallInviteMoreReqVO reqVO) {
230 ensureEnabled(); 232 ensureEnabled();
231 - ImRtcCallDO call = requireActiveCall(reqVO.getRoomName()); 233 + // TODO @AI:校验的,按照 1.1 1.2 1.3 这样;
  234 + ImRtcCallDO call = validateCallActive(reqVO.getRoomName());
232 // 仅群通话支持追加邀请 235 // 仅群通话支持追加邀请
233 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) { 236 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
234 throw exception(RTC_GROUP_REQUIRED); 237 throw exception(RTC_GROUP_REQUIRED);
@@ -238,19 +241,24 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -238,19 +241,24 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
238 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) { 241 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) {
239 throw exception(RTC_NOT_PARTICIPANT); 242 throw exception(RTC_NOT_PARTICIPANT);
240 } 243 }
  244 + // TODO @AI:如果存在非本群里,直接抛出异常。
241 // 校验被邀请人必须是该群活跃成员;且未在通话池 245 // 校验被邀请人必须是该群活跃成员;且未在通话池
242 Set<Long> validMemberIds = new LinkedHashSet<>( 246 Set<Long> validMemberIds = new LinkedHashSet<>(
243 groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId())); 247 groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId()));
  248 + // TODO @AI:convertSet;
244 Set<Long> existingUserIds = rtcParticipantMapper.selectListByCallId(call.getCallId()).stream() 249 Set<Long> existingUserIds = rtcParticipantMapper.selectListByCallId(call.getCallId()).stream()
245 .map(ImRtcParticipantDO::getUserId) 250 .map(ImRtcParticipantDO::getUserId)
246 .collect(Collectors.toSet()); 251 .collect(Collectors.toSet());
  252 + // TODO @AI:incomingUserIds;直接 remove 原始的元素;没必要;单独的;
247 List<Long> incoming = reqVO.getInviteeIds().stream() 253 List<Long> incoming = reqVO.getInviteeIds().stream()
248 .filter(id -> validMemberIds.contains(id) && !existingUserIds.contains(id)) 254 .filter(id -> validMemberIds.contains(id) && !existingUserIds.contains(id))
249 .toList(); 255 .toList();
250 if (CollUtil.isEmpty(incoming)) { 256 if (CollUtil.isEmpty(incoming)) {
251 return; 257 return;
252 } 258 }
  259 +
253 // INSERT 新邀请人 + 推 INVITE 260 // INSERT 新邀请人 + 推 INVITE
  261 + // TODO @AI:batch 插入;
254 LocalDateTime now = LocalDateTime.now(); 262 LocalDateTime now = LocalDateTime.now();
255 for (Long inviteeId : incoming) { 263 for (Long inviteeId : incoming) {
256 rtcParticipantMapper.insert(new ImRtcParticipantDO() 264 rtcParticipantMapper.insert(new ImRtcParticipantDO()
@@ -259,6 +267,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -259,6 +267,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
259 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) 267 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
260 .setInviteTime(now)); 268 .setInviteTime(now));
261 } 269 }
  270 +
  271 + // TODO @AI:这里在写推送的;
262 AdminUserRespDTO inviter = adminUserApi.getUser(userId); 272 AdminUserRespDTO inviter = adminUserApi.getUser(userId);
263 for (Long inviteeId : incoming) { 273 for (Long inviteeId : incoming) {
264 pushCallInviteNotification(call, inviter, inviteeId); 274 pushCallInviteNotification(call, inviter, inviteeId);
@@ -268,28 +278,27 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -268,28 +278,27 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
268 @Override 278 @Override
269 public ImRtcCallDO acceptCall(Long userId, String roomName) { 279 public ImRtcCallDO acceptCall(Long userId, String roomName) {
270 ensureEnabled(); 280 ensureEnabled();
271 - ImRtcCallDO call = requireActiveCall(roomName); 281 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  282 + ImRtcCallDO call = validateCallActive(roomName);
272 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId); 283 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
273 if (participant == null) { 284 if (participant == null) {
274 throw exception(RTC_NOT_PARTICIPANT); 285 throw exception(RTC_NOT_PARTICIPANT);
275 } 286 }
276 // 已 JOINED 视为重复 accept,幂等返回 287 // 已 JOINED 视为重复 accept,幂等返回
277 LocalDateTime now = LocalDateTime.now(); 288 LocalDateTime now = LocalDateTime.now();
  289 + // TODO @AI:如果已经是,就 if return,简化层级。
278 if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) { 290 if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
279 // 仅 INVITING → JOINED;其它状态拒 291 // 仅 INVITING → JOINED;其它状态拒
280 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) { 292 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
281 throw exception(RTC_SESSION_NOT_EXISTS); 293 throw exception(RTC_SESSION_NOT_EXISTS);
282 } 294 }
283 - int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(),  
284 - ImRtcParticipantStatusEnum.INVITING.getStatus(),  
285 - new ImRtcParticipantDO()  
286 - .setId(participant.getId())  
287 - .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus())  
288 - .setAcceptTime(now)); 295 + int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
  296 + new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setAcceptTime(now));
289 if (updated == 0) { 297 if (updated == 0) {
290 throw exception(RTC_SESSION_NOT_EXISTS); 298 throw exception(RTC_SESSION_NOT_EXISTS);
291 } 299 }
292 } 300 }
  301 +
293 // 主表 CREATED → RUNNING(首次有非发起人接通) 302 // 主表 CREATED → RUNNING(首次有非发起人接通)
294 maybeMarkOngoing(call, userId, now); 303 maybeMarkOngoing(call, userId, now);
295 return call; 304 return call;
@@ -298,7 +307,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -298,7 +307,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
298 @Override 307 @Override
299 public void rejectCall(Long userId, String roomName) { 308 public void rejectCall(Long userId, String roomName) {
300 ensureEnabled(); 309 ensureEnabled();
301 - ImRtcCallDO call = requireActiveCall(roomName); 310 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  311 + ImRtcCallDO call = validateCallActive(roomName);
302 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId); 312 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
303 if (participant == null) { 313 if (participant == null) {
304 throw exception(RTC_NOT_PARTICIPANT); 314 throw exception(RTC_NOT_PARTICIPANT);
@@ -307,15 +317,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -307,15 +317,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
307 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) { 317 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
308 throw exception(RTC_SESSION_NOT_EXISTS); 318 throw exception(RTC_SESSION_NOT_EXISTS);
309 } 319 }
310 - int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(),  
311 - ImRtcParticipantStatusEnum.INVITING.getStatus(),  
312 - new ImRtcParticipantDO()  
313 - .setId(participant.getId())  
314 - .setStatus(ImRtcParticipantStatusEnum.REJECTED.getStatus())  
315 - .setLeaveTime(LocalDateTime.now())); 320 +
  321 + // TODO @AI:这里写个注释
  322 + int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
  323 + new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.REJECTED.getStatus()).setLeaveTime(LocalDateTime.now()));
316 if (updated == 0) { 324 if (updated == 0) {
317 return; 325 return;
318 } 326 }
  327 +
  328 + // TODO @AI:3.1 3.2 这样;
  329 + // TODO @AI:群聊如果拒绝,需要做类似的 endSession 逻辑么?
319 // 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体 330 // 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体
320 if (ImConversationTypeEnum.isGroup(call.getConversationType())) { 331 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
321 pushCallRejectNotification(call, userId); 332 pushCallRejectNotification(call, userId);
@@ -328,7 +339,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -328,7 +339,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
328 @Override 339 @Override
329 public void cancelCall(Long userId, String roomName) { 340 public void cancelCall(Long userId, String roomName) {
330 ensureEnabled(); 341 ensureEnabled();
331 - ImRtcCallDO call = requireActiveCall(roomName); 342 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  343 + ImRtcCallDO call = validateCallActive(roomName);
  344 + // TODO @AI:notEquals;
332 if (!Objects.equals(call.getInviterUserId(), userId)) { 345 if (!Objects.equals(call.getInviterUserId(), userId)) {
333 throw exception(RTC_NOT_PARTICIPANT); 346 throw exception(RTC_NOT_PARTICIPANT);
334 } 347 }
@@ -336,23 +349,28 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -336,23 +349,28 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
336 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) { 349 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
337 throw exception(RTC_SESSION_NOT_EXISTS); 350 throw exception(RTC_SESSION_NOT_EXISTS);
338 } 351 }
  352 +
  353 + // TODO @AI:这里写下注释;
339 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL); 354 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL);
340 } 355 }
341 356
342 @Override 357 @Override
343 public void leaveCall(Long userId, String roomName) { 358 public void leaveCall(Long userId, String roomName) {
344 ensureEnabled(); 359 ensureEnabled();
345 - ImRtcCallDO call = requireActiveCall(roomName); 360 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  361 + ImRtcCallDO call = validateCallActive(roomName);
346 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId); 362 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
347 if (participant == null) { 363 if (participant == null) {
348 throw exception(RTC_NOT_PARTICIPANT); 364 throw exception(RTC_NOT_PARTICIPANT);
349 } 365 }
  366 +
  367 + // TODO @AI:这里要写下注释。
350 LocalDateTime now = LocalDateTime.now(); 368 LocalDateTime now = LocalDateTime.now();
351 rtcParticipantMapper.updateByIdAndStatus(participant.getId(), participant.getStatus(), 369 rtcParticipantMapper.updateByIdAndStatus(participant.getId(), participant.getStatus(),
352 - new ImRtcParticipantDO()  
353 - .setId(participant.getId())  
354 - .setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus())  
355 - .setLeaveTime(now)); 370 + new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(now));
  371 +
  372 + // TODO @AI:是不是搞个 endSessionIfXXX;
  373 + // TODO @AI:群聊的关房,本质和单聊,是不是一样的?是否有必要统一掉?
356 // 关房条件:私聊任一方离开 = 关;群通话仅在「无人在房 + 无人响铃」时关 374 // 关房条件:私聊任一方离开 = 关;群通话仅在「无人在房 + 无人响铃」时关
357 if (ImConversationTypeEnum.isPrivate(call.getConversationType()) 375 if (ImConversationTypeEnum.isPrivate(call.getConversationType())
358 || shouldCloseGroupRoom(call.getCallId())) { 376 || shouldCloseGroupRoom(call.getCallId())) {
@@ -369,18 +387,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -369,18 +387,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
369 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛 387 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛
370 */ 388 */
371 private boolean shouldCloseGroupRoom(String callId) { 389 private boolean shouldCloseGroupRoom(String callId) {
  390 + // TODO @AI:通过查询,计算情况?;感觉所有群聊的关闭条件:都是只有 1 个 join,剩余都处于结束,说白了没人可以和他电话了。。。。
372 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByCallIdAndStatus(callId, 391 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByCallIdAndStatus(callId,
373 ImRtcParticipantStatusEnum.JOINED.getStatus()); 392 ImRtcParticipantStatusEnum.JOINED.getStatus());
374 return CollUtil.isEmpty(joined); 393 return CollUtil.isEmpty(joined);
375 } 394 }
376 395
  396 + // TODO @AI:把“拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃”;写到方法注释里,更好理解点。
  397 + // TODO @AI:不用说 “由应用层 Redisson 锁串行化,最多一个”;感觉意义不大呀。
377 /** 398 /**
378 * 查询两人共同所在的活跃私聊通话;由应用层 Redisson 锁串行化,最多一个 399 * 查询两人共同所在的活跃私聊通话;由应用层 Redisson 锁串行化,最多一个
379 * <p> 400 * <p>
380 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃 401 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃
381 */ 402 */
382 private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) { 403 private ImRtcCallDO getActivePrivateCallByPair(Long userIdA, Long userIdB) {
383 - ImRtcParticipantDO aPart = rtcParticipantMapper.selectLastByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES); 404 + // TODO @AI:是不是用 participantA;和上面的命名更统一点。
  405 + ImRtcParticipantDO aPart = rtcParticipantMapper.selectLastOneByUserIdAndStatus(userIdA, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);
384 if (aPart == null) { 406 if (aPart == null) {
385 return null; 407 return null;
386 } 408 }
@@ -398,10 +420,12 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -398,10 +420,12 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
398 return call; 420 return call;
399 } 421 }
400 422
  423 + // TODO @AI:怎么感觉这个要改下?是不是根据 userid + callid 更合理?
401 @Override 424 @Override
402 public ImRtcCallDO refreshCallToken(Long userId, String roomName) { 425 public ImRtcCallDO refreshCallToken(Long userId, String roomName) {
403 ensureEnabled(); 426 ensureEnabled();
404 - ImRtcCallDO call = requireActiveCall(roomName); 427 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  428 + ImRtcCallDO call = validateCallActive(roomName);
405 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId); 429 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
406 if (participant == null) { 430 if (participant == null) {
407 throw exception(RTC_NOT_PARTICIPANT); 431 throw exception(RTC_NOT_PARTICIPANT);
@@ -415,9 +439,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -415,9 +439,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
415 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存) 439 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存)
416 groupMemberService.validateMemberInGroup(groupId, userId); 440 groupMemberService.validateMemberInGroup(groupId, userId);
417 // 2. 查询活跃通话 441 // 2. 查询活跃通话
418 - return rtcCallMapper.selectLastByGroupIdAndStatusIn(groupId, ImRtcCallStatusEnum.ACTIVE_STATUSES); 442 + return rtcCallMapper.selectLastOneByGroupIdAndStatusIn(groupId, ImRtcCallStatusEnum.ACTIVE_STATUSES);
419 } 443 }
420 444
  445 + // TODO @AI:方法名,复数应该使用 List,不使用 s;
421 @Override 446 @Override
422 public List<ImRtcParticipantDO> getCallParticipants(String callId) { 447 public List<ImRtcParticipantDO> getCallParticipants(String callId) {
423 return rtcParticipantMapper.selectListByCallId(callId); 448 return rtcParticipantMapper.selectListByCallId(callId);
@@ -426,12 +451,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -426,12 +451,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
426 @Override 451 @Override
427 public String signCallToken(Long userId, String roomName) { 452 public String signCallToken(Long userId, String roomName) {
428 AdminUserRespDTO user = adminUserApi.getUser(userId); 453 AdminUserRespDTO user = adminUserApi.getUser(userId);
  454 + // TODO @AI:不能为空。。。nickname 为空,则使用 userid;
429 String displayName = user == null ? null : user.getNickname(); 455 String displayName = user == null ? null : user.getNickname();
430 return signToken(userId, displayName, roomName); 456 return signToken(userId, displayName, roomName);
431 } 457 }
432 458
  459 + // TODO DONE @AI:兼容 JDK8 —— 项目 Spring Boot 3 已要求 JDK17+,switch arrow case 可直接用
433 @Override 460 @Override
434 - // TODO @AI:这个方法的编写,需要兼容 JDK8  
435 public void handleLiveKitEvent(LiveKitWebhookEventDTO event) { 461 public void handleLiveKitEvent(LiveKitWebhookEventDTO event) {
436 if (event == null || event.getEvent() == null) { 462 if (event == null || event.getEvent() == null) {
437 return; 463 return;
@@ -447,12 +473,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -447,12 +473,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
447 } 473 }
448 474
449 /** 475 /**
450 - * 处理 LiveKit 「成员加入」事件;DB 接通态由 accept 接口写入,此处仅做 1602 转推 476 + * 处理 LiveKit 「成员加入」事件:DB 接通态由 {@link #acceptCall(Long, String)} 接口写入,此处仅做 1602 转推
  477 + *
  478 + * @param event 事件
451 */ 479 */
452 private void handleParticipantJoined(LiveKitWebhookEventDTO event) { 480 private void handleParticipantJoined(LiveKitWebhookEventDTO event) {
  481 + // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
453 if (event.getRoom() == null || event.getParticipant() == null) { 482 if (event.getRoom() == null || event.getParticipant() == null) {
454 return; 483 return;
455 } 484 }
  485 + // 前置检查
456 String roomName = event.getRoom().getName(); 486 String roomName = event.getRoom().getName();
457 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName); 487 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
458 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 488 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
@@ -462,16 +492,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -462,16 +492,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
462 if (userId == null) { 492 if (userId == null) {
463 return; 493 return;
464 } 494 }
  495 +
  496 + // 推 1602 通知参与方 / 全群参与方
465 pushParticipantConnectedNotification(call, userId); 497 pushParticipantConnectedNotification(call, userId);
466 } 498 }
467 499
468 /** 500 /**
469 - * 处理 LiveKit 「成员离开」事件;正常 leave 接口已经清理过的话条件 UPDATE 自动幂等 501 + * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等
470 */ 502 */
471 private void handleParticipantLeft(LiveKitWebhookEventDTO event) { 503 private void handleParticipantLeft(LiveKitWebhookEventDTO event) {
  504 + // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
472 if (event.getRoom() == null || event.getParticipant() == null) { 505 if (event.getRoom() == null || event.getParticipant() == null) {
473 return; 506 return;
474 } 507 }
  508 + // 前置检查
475 String roomName = event.getRoom().getName(); 509 String roomName = event.getRoom().getName();
476 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName); 510 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
477 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 511 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
@@ -485,19 +519,19 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -485,19 +519,19 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
485 if (participant == null || !ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) { 519 if (participant == null || !ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
486 return; 520 return;
487 } 521 }
488 - int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(),  
489 - ImRtcParticipantStatusEnum.JOINED.getStatus(),  
490 - new ImRtcParticipantDO()  
491 - .setId(participant.getId())  
492 - .setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus())  
493 - .setLeaveTime(LocalDateTime.now())); 522 +
  523 + // TODO @AI:注释。带序号的;
  524 + int updated = rtcParticipantMapper.updateByIdAndStatus(participant.getId(), ImRtcParticipantStatusEnum.JOINED.getStatus(),
  525 + new ImRtcParticipantDO().setId(participant.getId()).setStatus(ImRtcParticipantStatusEnum.LEFT.getStatus()).setLeaveTime(LocalDateTime.now()));
494 if (updated == 0) { 526 if (updated == 0) {
495 return; 527 return;
496 } 528 }
497 log.info("[handleParticipantLeft][roomName={} userId={} 由 LiveKit Webhook 兜底]", roomName, userId); 529 log.info("[handleParticipantLeft][roomName={} userId={} 由 LiveKit Webhook 兜底]", roomName, userId);
  530 +
498 // 推 1603 通知参与方 / 全群参与方 531 // 推 1603 通知参与方 / 全群参与方
499 pushParticipantDisconnectedNotification(call, userId); 532 pushParticipantDisconnectedNotification(call, userId);
500 // 关房条件与 leave 一致;群通话仅在「无人在房 + 无人响铃」时关 533 // 关房条件与 leave 一致;群通话仅在「无人在房 + 无人响铃」时关
  534 + // TODO @AI:注释风格;感觉可以抽个 endSessionIfXXX;收口更统一;
501 if (ImConversationTypeEnum.isPrivate(call.getConversationType()) 535 if (ImConversationTypeEnum.isPrivate(call.getConversationType())
502 || shouldCloseGroupRoom(call.getCallId())) { 536 || shouldCloseGroupRoom(call.getCallId())) {
503 endSession(call, userId, ImRtcCallEndReasonEnum.HANGUP); 537 endSession(call, userId, ImRtcCallEndReasonEnum.HANGUP);
@@ -505,17 +539,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -505,17 +539,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
505 } 539 }
506 540
507 /** 541 /**
508 - * 处理 LiveKit 「房间结束」事件;兜底把 call 推到 ENDED 542 + * 处理 LiveKit 「房间结束」事件:兜底把 call 推到 ENDED
509 */ 543 */
510 private void handleRoomFinished(LiveKitWebhookEventDTO event) { 544 private void handleRoomFinished(LiveKitWebhookEventDTO event) {
511 if (event.getRoom() == null) { 545 if (event.getRoom() == null) {
512 return; 546 return;
513 } 547 }
  548 + // 前置检查
514 String roomName = event.getRoom().getName(); 549 String roomName = event.getRoom().getName();
515 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName); 550 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
516 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 551 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
517 return; 552 return;
518 } 553 }
  554 +
  555 + // TODO @AI:注释。带序号的;
519 log.info("[handleRoomFinished][roomName={} 由 LiveKit Webhook 兜底]", roomName); 556 log.info("[handleRoomFinished][roomName={} 由 LiveKit Webhook 兜底]", roomName);
520 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP); 557 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP);
521 } 558 }
@@ -554,6 +591,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -554,6 +591,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
554 591
555 // ========== 内部辅助 ========== 592 // ========== 内部辅助 ==========
556 593
  594 + // TODO @AI:目前是叫 ensureEnabled,还是 validateXXX;
557 /** 595 /**
558 * 校验通话功能开关 596 * 校验通话功能开关
559 */ 597 */
@@ -563,11 +601,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -563,11 +601,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
563 } 601 }
564 } 602 }
565 603
566 - // TODO @AI:validateXXX  
567 /** 604 /**
568 - * 取活跃通话;不存在 / 已 ENDED 直接抛 605 + * 校验通话存在且活跃:不存在 / 已 ENDED 直接抛
569 */ 606 */
570 - private ImRtcCallDO requireActiveCall(String roomName) { 607 + private ImRtcCallDO validateCallActive(String roomName) {
571 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName); 608 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
572 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) { 609 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
573 throw exception(RTC_SESSION_NOT_EXISTS); 610 throw exception(RTC_SESSION_NOT_EXISTS);
@@ -576,7 +613,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -576,7 +613,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
576 } 613 }
577 614
578 /** 615 /**
579 - * 校验 invite 入参;按场景区分必填字段;私聊补好友 / 黑名单校验,群聊补群成员校验 616 + * 校验 invite 入参
  617 + * <p>
  618 + * 1. 按场景区分必填字段;
  619 + * 2. 私聊补好友 / 黑名单校验,群聊补群成员校验
580 */ 620 */
581 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) { 621 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) {
582 Integer scene = reqVO.getScene(); 622 Integer scene = reqVO.getScene();
@@ -587,7 +627,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -587,7 +627,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
587 if (Objects.equals(userId, reqVO.getPeerUserId())) { 627 if (Objects.equals(userId, reqVO.getPeerUserId())) {
588 throw exception(RTC_INVITE_SELF); 628 throw exception(RTC_INVITE_SELF);
589 } 629 }
590 - // TODO DONE @AI:抽到 ImFriendService#validateFriend;  
591 friendService.validateFriend(userId, reqVO.getPeerUserId()); 630 friendService.validateFriend(userId, reqVO.getPeerUserId());
592 return; 631 return;
593 } 632 }
@@ -595,16 +634,18 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -595,16 +634,18 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
595 if (reqVO.getGroupId() == null) { 634 if (reqVO.getGroupId() == null) {
596 throw exception(RTC_GROUP_REQUIRED); 635 throw exception(RTC_GROUP_REQUIRED);
597 } 636 }
598 - List<Long> activeMemberIds = groupMemberService  
599 - .getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId()); 637 + // TODO @AI:是不是 db 里,提供校验的方法,完事了。groupId + userid;
  638 + List<Long> activeMemberIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId());
600 if (!activeMemberIds.contains(userId)) { 639 if (!activeMemberIds.contains(userId)) {
601 throw exception(GROUP_MEMBER_NOT_IN_GROUP); 640 throw exception(GROUP_MEMBER_NOT_IN_GROUP);
602 } 641 }
603 return; 642 return;
604 } 643 }
  644 + // TODO @AI:位置参数,illegalArgumentException;
605 throw exception(RTC_PRIVATE_INVITEE_REQUIRED); 645 throw exception(RTC_PRIVATE_INVITEE_REQUIRED);
606 } 646 }
607 647
  648 + // TODO @AI:前端必须传递成员,不允许过多;
608 /** 649 /**
609 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断 650 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断
610 * <p> 651 * <p>
@@ -631,6 +672,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -631,6 +672,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
631 // 不论来源,发起人本人不进被邀请池 672 // 不论来源,发起人本人不进被邀请池
632 initial.remove(inviterId); 673 initial.remove(inviterId);
633 // 超出最大同时在房成员数;截断为 max-1 个,避免邀请整个 500 人大群 674 // 超出最大同时在房成员数;截断为 max-1 个,避免邀请整个 500 人大群
  675 + // TODO @AI:超过数量,直接抛出异常,不用裁剪,不然很奇怪;
634 int max = imProperties.getRtc().getGroupMaxParticipants(); 676 int max = imProperties.getRtc().getGroupMaxParticipants();
635 if (initial.size() + 1 > max) { 677 if (initial.size() + 1 > max) {
636 return initial.stream().limit(max - 1L) 678 return initial.stream().limit(max - 1L)
@@ -639,6 +681,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -639,6 +681,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
639 return initial; 681 return initial;
640 } 682 }
641 683
  684 + // TODO @AI:按照 updateCallXXX 这种风格?
  685 + // TODO @AI:注释,不用写这么细致。。。通过方法名,可以看的懂的。
642 /** 686 /**
643 * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等 687 * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等
644 * <p> 688 * <p>
@@ -647,30 +691,32 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -647,30 +691,32 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
647 * RUNNING → 已被另一接听者抢先推进,幂等同步内存 691 * RUNNING → 已被另一接听者抢先推进,幂等同步内存
648 */ 692 */
649 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) { 693 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) {
  694 + // TODO @AI:写下注释
650 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) { 695 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
651 return; 696 return;
652 } 697 }
653 if (Objects.equals(call.getInviterUserId(), acceptorId)) { 698 if (Objects.equals(call.getInviterUserId(), acceptorId)) {
654 return; 699 return;
655 } 700 }
656 - int updated = rtcCallMapper.updateByIdAndStatus(call.getId(),  
657 - ImRtcCallStatusEnum.CREATED.getStatus(),  
658 - new ImRtcCallDO()  
659 - .setStatus(ImRtcCallStatusEnum.RUNNING.getStatus())  
660 - .setAcceptTime(now)); 701 +
  702 + // TODO @AI:写下注释
  703 + int updated = rtcCallMapper.updateByIdAndStatus(call.getId(), ImRtcCallStatusEnum.CREATED.getStatus(),
  704 + new ImRtcCallDO().setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now));
  705 + // TODO @AI:是不是 if == 0,然后里面做判断。然后下面统一搞成 call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now);
661 if (updated > 0) { 706 if (updated > 0) {
662 // 内存值同步给后续判断 707 // 内存值同步给后续判断
663 - call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus());  
664 - call.setAcceptTime(now); 708 + call.setStatus(ImRtcCallStatusEnum.RUNNING.getStatus()).setAcceptTime(now);
665 return; 709 return;
666 } 710 }
  711 +
  712 + // TODO @AI:写下注释
667 // updated == 0:与并发路径竞争失败;reload 看真实终态 713 // updated == 0:与并发路径竞争失败;reload 看真实终态
668 ImRtcCallDO latest = rtcCallMapper.selectById(call.getId()); 714 ImRtcCallDO latest = rtcCallMapper.selectById(call.getId());
669 if (latest == null || ImRtcCallStatusEnum.isEnded(latest.getStatus())) { 715 if (latest == null || ImRtcCallStatusEnum.isEnded(latest.getStatus())) {
  716 + // TODO @AI:不用会话把?用 RTC_CALL 吧?
670 throw exception(RTC_SESSION_NOT_EXISTS); 717 throw exception(RTC_SESSION_NOT_EXISTS);
671 } 718 }
672 - call.setStatus(latest.getStatus());  
673 - call.setAcceptTime(latest.getAcceptTime()); 719 + call.setStatus(latest.getStatus()).setAcceptTime(latest.getAcceptTime());
674 } 720 }
675 721
676 /** 722 /**
@@ -708,9 +754,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -708,9 +754,11 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
708 log.info("[endSession][roomName={} operator={} reason={}]", call.getRoomName(), operatorId, reason); 754 log.info("[endSession][roomName={} operator={} reason={}]", call.getRoomName(), operatorId, reason);
709 } 755 }
710 756
711 - 757 + // TODO @AI:getCallAudienceUserIdList?
712 /** 758 /**
713 - * 通话事件的收件人池:私聊为双方参与者,群聊为群活跃成员 759 + * 通话事件的收件人池:1)私聊为双方参与者;2)群聊为群活跃成员
  760 + *
  761 + * TODO @params 补全;
714 */ 762 */
715 private Collection<Long> getCallAudience(ImRtcCallDO call) { 763 private Collection<Long> getCallAudience(ImRtcCallDO call) {
716 if (ImConversationTypeEnum.isGroup(call.getConversationType())) { 764 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
@@ -720,11 +768,14 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -720,11 +768,14 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
720 rtcParticipantMapper.selectListByCallId(call.getCallId()), ImRtcParticipantDO::getUserId); 768 rtcParticipantMapper.selectListByCallId(call.getCallId()), ImRtcParticipantDO::getUserId);
721 } 769 }
722 770
  771 + // TODO @AI:这里的 ========== 太短了;
723 // ========== 推送 ========== 772 // ========== 推送 ==========
  773 + // TODO @AI:@param 注释要补全
724 774
725 /** 775 /**
726 * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人 776 * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人
727 * <p> 777 * <p>
  778 + * TODO @AI:为啥下面多写了个 “不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知”注释;
728 * 不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知 779 * 不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知
729 */ 780 */
730 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) { 781 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) {
@@ -738,6 +789,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -738,6 +789,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
738 /** 789 /**
739 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫 790 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫
740 */ 791 */
  792 + // TODO @AI:有点奇怪,单聊不推送么???
741 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { 793 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) {
742 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null; 794 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
743 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator); 795 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
@@ -779,18 +831,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -779,18 +831,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
779 webSocketService.sendPrivateMessageAsync(receivers, dto); 831 webSocketService.sendPrivateMessageAsync(receivers, dto);
780 } 832 }
781 833
  834 + // TODO @AI:“自增 id 自然保证聊天流顺序”,是不是你不用写这个注释?
782 /** 835 /**
783 * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播 836 * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播
784 * <p> 837 * <p>
785 - * 与 RTC_CALL_END 两段式配对:START 在本接口(invite)事务里 INSERT,END 在 cancel / leave 接口事务里 INSERT,  
786 - * 自增 id 自然保证聊天流顺序 838 + * 与 RTC_CALL_END 两段式配对:START 在本接口(invite)事务里 INSERT,END 在 cancel / leave 接口事务里 INSERT,自增 id 自然保证聊天流顺序
787 */ 839 */
788 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) { 840 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) {
789 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter); 841 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter);
790 - ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()  
791 - .setGroupId(call.getGroupId())  
792 - .setType(ImMessageTypeEnum.RTC_CALL_START.getType())  
793 - .setContent(payload); 842 + ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO().setGroupId(call.getGroupId())
  843 + .setType(ImMessageTypeEnum.RTC_CALL_START.getType()).setContent(payload);
794 groupMessageService.sendGroupMessage(call.getInviterUserId(), dto); 844 groupMessageService.sendGroupMessage(call.getInviterUserId(), dto);
795 } 845 }
796 846
@@ -804,23 +854,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -804,23 +854,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
804 AdminUserRespDTO operator = operatorId != null ? adminUserApi.getUser(operatorId) : null; 854 AdminUserRespDTO operator = operatorId != null ? adminUserApi.getUser(operatorId) : null;
805 ImRtcCallEndNotification payload = ImRtcCallEndNotification.of(call, reason, durationSeconds, operatorId, operator); 855 ImRtcCallEndNotification payload = ImRtcCallEndNotification.of(call, reason, durationSeconds, operatorId, operator);
806 Long senderId = call.getInviterUserId(); 856 Long senderId = call.getInviterUserId();
  857 +
  858 + // 群聊:发起人总是 sender;receiver = 全群;走 imGroupMessageService.send 全群广播
807 if (ImConversationTypeEnum.isGroup(call.getConversationType())) { 859 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
808 - ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()  
809 - .setGroupId(call.getGroupId())  
810 - .setType(ImMessageTypeEnum.RTC_CALL_END.getType())  
811 - .setContent(payload); 860 + ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO().setGroupId(call.getGroupId())
  861 + .setType(ImMessageTypeEnum.RTC_CALL_END.getType()).setContent(payload);
812 groupMessageService.sendGroupMessage(senderId, dto); 862 groupMessageService.sendGroupMessage(senderId, dto);
813 return; 863 return;
814 } 864 }
  865 +
815 // 私聊:发起人总是 sender;receiver = 通话另一方 866 // 私聊:发起人总是 sender;receiver = 通话另一方
816 ImRtcParticipantDO peer = CollUtil.findOne( 867 ImRtcParticipantDO peer = CollUtil.findOne(
817 rtcParticipantMapper.selectListByCallId(call.getCallId()), 868 rtcParticipantMapper.selectListByCallId(call.getCallId()),
818 p -> !Objects.equals(p.getUserId(), senderId)); 869 p -> !Objects.equals(p.getUserId(), senderId));
819 Long receiverId = peer != null ? peer.getUserId() : senderId; 870 Long receiverId = peer != null ? peer.getUserId() : senderId;
820 - ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO()  
821 - .setReceiverId(receiverId)  
822 - .setType(ImMessageTypeEnum.RTC_CALL_END.getType())  
823 - .setContent(payload); 871 + ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO().setReceiverId(receiverId)
  872 + .setType(ImMessageTypeEnum.RTC_CALL_END.getType()).setContent(payload);
824 privateMessageService.sendPrivateMessage(senderId, dto); 873 privateMessageService.sendPrivateMessage(senderId, dto);
825 } 874 }
826 875
@@ -829,7 +878,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -829,7 +878,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
829 /** 878 /**
830 * 签 LiveKit Token 879 * 签 LiveKit Token
831 */ 880 */
  881 + // TODO @AI:sign 方法,是不是传递 adminuserdo?
832 private String signToken(Long userId, String displayName, String roomName) { 882 private String signToken(Long userId, String displayName, String roomName) {
  883 + // TODO @AI:是不是抽一个 signJoinToken(userid, displayName, roomName)做减法
833 return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, roomName); 884 return liveKitClient.signJoinToken(liveKitClient.buildIdentity(userId), displayName, roomName);
834 } 885 }
835 886
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcCallNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  2 +
  3 +import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
  4 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
  5 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  6 +import lombok.Data;
  7 +
  8 +/**
  9 + * RTC_CALL 通话信令通知(通话信令统一入口)
  10 + * <p>
  11 + * 不入库;走 imWebSocketService 仅推参与方
  12 + * <p>
  13 + * status 字段复用 {@link ImRtcParticipantStatusEnum},表达「本次信令对应的参与者状态变迁」
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class ImRtcCallNotification {
  19 +
  20 + /**
  21 + * 信令对应的参与者状态
  22 + *
  23 + * 取值参见 {@link ImRtcParticipantStatusEnum}
  24 + */
  25 + private Integer status;
  26 +
  27 + /**
  28 + * 业务通话编号
  29 + */
  30 + private String callId;
  31 + /**
  32 + * LiveKit 房间名
  33 + */
  34 + private String roomName;
  35 + /**
  36 + * 会话类型
  37 + */
  38 + private Integer conversationType;
  39 + /**
  40 + * 媒体类型
  41 + */
  42 + private Integer mediaType;
  43 + /**
  44 + * 群编号:群通话场景必填
  45 + */
  46 + private Long groupId;
  47 +
  48 + // ========== INVITE 专属字段 ==========
  49 +
  50 + /**
  51 + * LiveKit Server WebSocket 地址;INVITE 专属
  52 + */
  53 + private String livekitUrl;
  54 + /**
  55 + * 该被叫专属的 LiveKit 接听 Token;接通后直接 connect 用;INVITE 专属
  56 + */
  57 + private String token;
  58 + /**
  59 + * 发起人用户编号;INVITE 专属
  60 + */
  61 + private Long inviterUserId;
  62 + /**
  63 + * 发起人昵称;INVITE 专属,前端来电界面展示
  64 + */
  65 + private String inviterNickname;
  66 + /**
  67 + * 发起人头像;INVITE 专属,前端来电界面展示
  68 + */
  69 + private String inviterAvatar;
  70 +
  71 + // ========== 非 INVITE 信令的操作者 ==========
  72 +
  73 + /**
  74 + * 操作者用户编号;ACCEPT / REJECT / CANCEL / HUNGUP 触发本次状态变迁的人
  75 + */
  76 + private Long operatorUserId;
  77 + /**
  78 + * 操作者昵称;前端按需展示(被某某拒接 / 取消 / 挂断),普通文案不依赖
  79 + */
  80 + private String operatorNickname;
  81 + /**
  82 + * 操作者头像;前端按需展示,普通文案不依赖
  83 + */
  84 + private String operatorAvatar;
  85 +
  86 + /**
  87 + * 构造 INVITE 信令;推被邀请人,invitee 状态变为 INVITING
  88 + *
  89 + * @param call 通话主表
  90 + * @param inviter 发起人;可空,缺失时 inviterNickname / inviterAvatar 留空
  91 + * @param livekitUrl LiveKit Server WebSocket 地址
  92 + * @param token 被叫的接听 Token;按收件人单独签发
  93 + * @return INVITE 信令
  94 + */
  95 + public static ImRtcCallNotification ofInvite(ImRtcCallDO call, AdminUserRespDTO inviter,
  96 + String livekitUrl, String token) {
  97 + ImRtcCallNotification notification = baseOf(call, ImRtcParticipantStatusEnum.INVITING.getStatus());
  98 + notification.livekitUrl = livekitUrl;
  99 + notification.token = token;
  100 + notification.inviterUserId = call.getInviterUserId();
  101 + if (inviter != null) {
  102 + notification.inviterNickname = inviter.getNickname();
  103 + notification.inviterAvatar = inviter.getAvatar();
  104 + }
  105 + return notification;
  106 + }
  107 +
  108 + // TODO @AI:为什么没使用?
  109 + /**
  110 + * 构造 ACCEPT 信令;推除接听者外的其它参与方,让主叫尽早结束响铃 UI
  111 + *
  112 + * @param call 通话主表
  113 + * @param operatorUserId 接听者用户编号
  114 + * @param operator 接听者;可空,缺失时 operatorNickname / operatorAvatar 留空
  115 + * @return ACCEPT 信令
  116 + */
  117 + public static ImRtcCallNotification ofAccept(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
  118 + return ofOperatorAction(call, ImRtcParticipantStatusEnum.JOINED.getStatus(), operatorUserId, operator);
  119 + }
  120 +
  121 + /**
  122 + * 构造 REJECT 信令;仅群通话场景;推主叫
  123 + *
  124 + * @param call 通话主表
  125 + * @param operatorUserId 拒接者用户编号
  126 + * @param operator 拒接者;可空,缺失时 operatorNickname / operatorAvatar 留空
  127 + * @return REJECT 信令
  128 + */
  129 + public static ImRtcCallNotification ofReject(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
  130 + return ofOperatorAction(call, ImRtcParticipantStatusEnum.REJECTED.getStatus(), operatorUserId, operator);
  131 + }
  132 +
  133 + // TODO @AI:为什么没使用?
  134 + /**
  135 + * 构造 CANCEL 信令;推被邀请方,invitees 状态变为 NO_ANSWER
  136 + *
  137 + * @param call 通话主表
  138 + * @param operatorUserId 取消者用户编号(主叫)
  139 + * @param operator 取消者;可空,缺失时 operatorNickname / operatorAvatar 留空
  140 + * @return CANCEL 信令
  141 + */
  142 + public static ImRtcCallNotification ofCancel(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
  143 + return ofOperatorAction(call, ImRtcParticipantStatusEnum.NO_ANSWER.getStatus(), operatorUserId, operator);
  144 + }
  145 +
  146 + // TODO @AI:为什么没使用?
  147 + /**
  148 + * 构造 HUNGUP 信令;推其它参与方,操作者状态变为 LEFT
  149 + *
  150 + * @param call 通话主表
  151 + * @param operatorUserId 挂断者用户编号
  152 + * @param operator 挂断者;可空,缺失时 operatorNickname / operatorAvatar 留空
  153 + * @return HUNGUP 信令
  154 + */
  155 + public static ImRtcCallNotification ofHungUp(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
  156 + return ofOperatorAction(call, ImRtcParticipantStatusEnum.LEFT.getStatus(), operatorUserId, operator);
  157 + }
  158 +
  159 + /**
  160 + * ACCEPT / REJECT / CANCEL / HUNGUP 4 个工厂的公共骨架;填充 call 上下文 + status + operator
  161 + */
  162 + private static ImRtcCallNotification ofOperatorAction(ImRtcCallDO call, Integer status,
  163 + Long operatorUserId, AdminUserRespDTO operator) {
  164 + ImRtcCallNotification notification = baseOf(call, status);
  165 + notification.operatorUserId = operatorUserId;
  166 + if (operator != null) {
  167 + notification.operatorNickname = operator.getNickname();
  168 + notification.operatorAvatar = operator.getAvatar();
  169 + }
  170 + return notification;
  171 + }
  172 +
  173 + /**
  174 + * 各 ofXxx 工厂的公共骨架;填充 call 上下文 + status
  175 + */
  176 + private static ImRtcCallNotification baseOf(ImRtcCallDO call, Integer status) {
  177 + ImRtcCallNotification notification = new ImRtcCallNotification();
  178 + notification.status = status;
  179 + notification.callId = call.getCallId();
  180 + notification.roomName = call.getRoomName();
  181 + notification.conversationType = call.getConversationType();
  182 + notification.mediaType = call.getMediaType();
  183 + notification.groupId = call.getGroupId();
  184 + return notification;
  185 + }
  186 +
  187 +}