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 153 * @param queryWrapper 查询条件
154 154 * @return 最新一条;不存在返回 null
155 155 */
156   - default T selectLast(LambdaQueryWrapper<T> queryWrapper) {
  156 + default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
157 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 26 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
27 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 30 @Tag(name = "管理后台 - IM 实时通话")
30 31 @RestController
31 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 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 31 .eq(ImRtcCallDO::getGroupId, groupId)
32 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 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 38 .eq(ImRtcParticipantDO::getUserId, userId)
39 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 17  
18 18 INVITER(1, "发起人"),
19 19 INVITEE(2, "被邀请者"),
  20 + // TODO @AI:改成 joiner?还是 joinee?看看这样拼写是不是对的。
20 21 ACTIVE_JOIN(3, "主动加入者"); // 仅群通话场景:旁观者点胶囊条加入已有通话
21 22  
22 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 13 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
14 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
15 15 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallEndReasonEnum;
  16 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
16 17 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum;
17 18 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
18   -import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
19 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
20 20 import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitClient;
  21 +import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO;
21 22 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
22 23 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
23 24 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
24 25 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
25 26 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
26 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 28 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
30 29 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  30 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc.*;
31 31 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
32 32 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
33 33 import jakarta.annotation.Resource;
... ... @@ -68,7 +68,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
68 68  
69 69 @Resource
70 70 private ImRtcCallMapper rtcCallMapper;
71   - // TODO @AI:可以搞个 ImRtcParticipantService,从而简化这个类的大小么?
  71 + // TODO DONE @AI:可以搞个 ImRtcParticipantService 简化本类么 —— 暂不抽:参与表 CRUD 与通话状态机紧耦合(INVITING→JOINED→LEFT 等转换都跟主表 status 联动),独立 service 会沦为薄包装且需双方互调,反而增加复杂度
  72 + // TODO @AI:要不试着拆出来,看看 ImRtcParticipantService 能不能稍微简化一些些;
72 73 @Resource
73 74 private ImRtcParticipantMapper rtcParticipantMapper;
74 75 @Resource
... ... @@ -102,8 +103,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
102 103 ensureEnabled();
103 104 // 1. 校验入参与场景
104 105 validateInviteScene(userId, reqVO);
105   - // TODO @AI:2 这个注释,有点复杂。略微重复了。应该就是加锁 + 所内逻辑。为什么加锁:xxxx
106   - // 2. 包锁:同好友 / 同群串行;锁内 SELECT 命中已有活跃通话改走加入分支,否则 INSERT 新通话
  106 +
  107 + // 2. 加锁后跑业务主体;同好友对 / 同群串行,避免并发各开一通的竞态
107 108 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) {
108 109 return rtcCallLockRedisDAO.lockGroup(reqVO.getGroupId(), () -> inviteCall0(userId, reqVO));
109 110 }
... ... @@ -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 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 135 if (peerActive != null) {
135 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 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 160 String callId = IdUtil.fastSimpleUUID();
148 161 String roomName = ImRtcCallDO.ROOM_NAME_PREFIX + callId;
149 162 LocalDateTime now = LocalDateTime.now();
150   - // 2. 构建被邀请池(私聊为 peerUserId 单元素;群聊取 invitee 子集 / 全员)
151 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 167 .setConversationType(reqVO.getScene()).setMediaType(reqVO.getMediaType())
157 168 .setInviterUserId(inviterId).setGroupId(reqVO.getGroupId())
158   - .setStatus(ImRtcCallStatusEnum.CREATED.getStatus())
159   - .setStartTime(now);
  169 + .setStatus(ImRtcCallStatusEnum.CREATED.getStatus()).setStartTime(now);
160 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 174 .setRole(ImRtcParticipantRoleEnum.INVITER.getRole())
166 175 .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus())
167 176 .setInviteTime(now).setAcceptTime(now));
168   - // 5. INSERT 被邀请人 participants
169 177 for (Long inviteeId : invitees) {
170   - rtcParticipantMapper.insert(new ImRtcParticipantDO()
  178 + participants.add(new ImRtcParticipantDO()
171 179 .setCallId(callId).setUserId(inviteeId)
172 180 .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole())
173 181 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
174 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 188 AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId);
178 189 for (Long inviteeId : invitees) {
179 190 pushCallInviteNotification(call, inviterUser, inviteeId);
180 191 }
  192 + // 3.2 群通话再向全群广播 RTC_CALL_START 入聊天流
181 193 if (ImConversationTypeEnum.isGroup(reqVO.getScene())) {
182 194 pushCallStartNotification(call, inviterUser);
183 195 }
... ... @@ -188,47 +200,38 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
188 200 public ImRtcCallDO joinCall(Long userId, String roomName) {
189 201 ensureEnabled();
190 202 // 1.1 校验通话存在且活跃
191   - ImRtcCallDO call = requireActiveCall(roomName);
  203 + ImRtcCallDO call = validateCallActive(roomName);
192 204 // 1.2 仅群通话支持「旁观者加入」
193 205 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
194 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 210 LocalDateTime now = LocalDateTime.now();
  211 + ImRtcParticipantDO existing = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
207 212 if (existing != null) {
208 213 // 非 JOINED 状态切回 JOINED + accept_time;INVITER / INVITEE 重连 / 旁观点胶囊条都走这条
209 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 218 } else {
216 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 230 @Override
229 231 public void inviteMoreCall(Long userId, ImRtcCallInviteMoreReqVO reqVO) {
230 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 236 if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {
234 237 throw exception(RTC_GROUP_REQUIRED);
... ... @@ -238,19 +241,24 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
238 241 if (operator == null || !ImRtcParticipantStatusEnum.isJoined(operator.getStatus())) {
239 242 throw exception(RTC_NOT_PARTICIPANT);
240 243 }
  244 + // TODO @AI:如果存在非本群里,直接抛出异常。
241 245 // 校验被邀请人必须是该群活跃成员;且未在通话池
242 246 Set<Long> validMemberIds = new LinkedHashSet<>(
243 247 groupMemberService.getActiveGroupMemberUserIdsByGroupId(call.getGroupId()));
  248 + // TODO @AI:convertSet;
244 249 Set<Long> existingUserIds = rtcParticipantMapper.selectListByCallId(call.getCallId()).stream()
245 250 .map(ImRtcParticipantDO::getUserId)
246 251 .collect(Collectors.toSet());
  252 + // TODO @AI:incomingUserIds;直接 remove 原始的元素;没必要;单独的;
247 253 List<Long> incoming = reqVO.getInviteeIds().stream()
248 254 .filter(id -> validMemberIds.contains(id) && !existingUserIds.contains(id))
249 255 .toList();
250 256 if (CollUtil.isEmpty(incoming)) {
251 257 return;
252 258 }
  259 +
253 260 // INSERT 新邀请人 + 推 INVITE
  261 + // TODO @AI:batch 插入;
254 262 LocalDateTime now = LocalDateTime.now();
255 263 for (Long inviteeId : incoming) {
256 264 rtcParticipantMapper.insert(new ImRtcParticipantDO()
... ... @@ -259,6 +267,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
259 267 .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus())
260 268 .setInviteTime(now));
261 269 }
  270 +
  271 + // TODO @AI:这里在写推送的;
262 272 AdminUserRespDTO inviter = adminUserApi.getUser(userId);
263 273 for (Long inviteeId : incoming) {
264 274 pushCallInviteNotification(call, inviter, inviteeId);
... ... @@ -268,28 +278,27 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
268 278 @Override
269 279 public ImRtcCallDO acceptCall(Long userId, String roomName) {
270 280 ensureEnabled();
271   - ImRtcCallDO call = requireActiveCall(roomName);
  281 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  282 + ImRtcCallDO call = validateCallActive(roomName);
272 283 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
273 284 if (participant == null) {
274 285 throw exception(RTC_NOT_PARTICIPANT);
275 286 }
276 287 // 已 JOINED 视为重复 accept,幂等返回
277 288 LocalDateTime now = LocalDateTime.now();
  289 + // TODO @AI:如果已经是,就 if return,简化层级。
278 290 if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
279 291 // 仅 INVITING → JOINED;其它状态拒
280 292 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
281 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 297 if (updated == 0) {
290 298 throw exception(RTC_SESSION_NOT_EXISTS);
291 299 }
292 300 }
  301 +
293 302 // 主表 CREATED → RUNNING(首次有非发起人接通)
294 303 maybeMarkOngoing(call, userId, now);
295 304 return call;
... ... @@ -298,7 +307,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
298 307 @Override
299 308 public void rejectCall(Long userId, String roomName) {
300 309 ensureEnabled();
301   - ImRtcCallDO call = requireActiveCall(roomName);
  310 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  311 + ImRtcCallDO call = validateCallActive(roomName);
302 312 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
303 313 if (participant == null) {
304 314 throw exception(RTC_NOT_PARTICIPANT);
... ... @@ -307,15 +317,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
307 317 if (!ImRtcParticipantStatusEnum.isInviting(participant.getStatus())) {
308 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 324 if (updated == 0) {
317 325 return;
318 326 }
  327 +
  328 + // TODO @AI:3.1 3.2 这样;
  329 + // TODO @AI:群聊如果拒绝,需要做类似的 endSession 逻辑么?
319 330 // 群通话拒绝:仅推 RTC_CALL(REJECT) 给主叫;不影响通话整体
320 331 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
321 332 pushCallRejectNotification(call, userId);
... ... @@ -328,7 +339,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
328 339 @Override
329 340 public void cancelCall(Long userId, String roomName) {
330 341 ensureEnabled();
331   - ImRtcCallDO call = requireActiveCall(roomName);
  342 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  343 + ImRtcCallDO call = validateCallActive(roomName);
  344 + // TODO @AI:notEquals;
332 345 if (!Objects.equals(call.getInviterUserId(), userId)) {
333 346 throw exception(RTC_NOT_PARTICIPANT);
334 347 }
... ... @@ -336,23 +349,28 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
336 349 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
337 350 throw exception(RTC_SESSION_NOT_EXISTS);
338 351 }
  352 +
  353 + // TODO @AI:这里写下注释;
339 354 endSession(call, userId, ImRtcCallEndReasonEnum.CANCEL);
340 355 }
341 356  
342 357 @Override
343 358 public void leaveCall(Long userId, String roomName) {
344 359 ensureEnabled();
345   - ImRtcCallDO call = requireActiveCall(roomName);
  360 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  361 + ImRtcCallDO call = validateCallActive(roomName);
346 362 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
347 363 if (participant == null) {
348 364 throw exception(RTC_NOT_PARTICIPANT);
349 365 }
  366 +
  367 + // TODO @AI:这里要写下注释。
350 368 LocalDateTime now = LocalDateTime.now();
351 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 375 if (ImConversationTypeEnum.isPrivate(call.getConversationType())
358 376 || shouldCloseGroupRoom(call.getCallId())) {
... ... @@ -369,18 +387,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
369 387 * 关房后 endSession 会把残留 INVITING 批量改 NO_ANSWER 并推 RTC_CALL_END,响铃端 UI 自动收敛
370 388 */
371 389 private boolean shouldCloseGroupRoom(String callId) {
  390 + // TODO @AI:通过查询,计算情况?;感觉所有群聊的关闭条件:都是只有 1 个 join,剩余都处于结束,说白了没人可以和他电话了。。。。
372 391 List<ImRtcParticipantDO> joined = rtcParticipantMapper.selectListByCallIdAndStatus(callId,
373 392 ImRtcParticipantStatusEnum.JOINED.getStatus());
374 393 return CollUtil.isEmpty(joined);
375 394 }
376 395  
  396 + // TODO @AI:把“拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃”;写到方法注释里,更好理解点。
  397 + // TODO @AI:不用说 “由应用层 Redisson 锁串行化,最多一个”;感觉意义不大呀。
377 398 /**
378 399 * 查询两人共同所在的活跃私聊通话;由应用层 Redisson 锁串行化,最多一个
379 400 * <p>
380 401 * 拆成 3 段简单查询:拿 A 的活跃 participant → 拿主表判私聊未结束 → 看 B 是否在同 call 活跃
381 402 */
382 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 406 if (aPart == null) {
385 407 return null;
386 408 }
... ... @@ -398,10 +420,12 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
398 420 return call;
399 421 }
400 422  
  423 + // TODO @AI:怎么感觉这个要改下?是不是根据 userid + callid 更合理?
401 424 @Override
402 425 public ImRtcCallDO refreshCallToken(Long userId, String roomName) {
403 426 ensureEnabled();
404   - ImRtcCallDO call = requireActiveCall(roomName);
  427 + // TODO @AI:按照 inviteMoreCall 风格,优化下注释;
  428 + ImRtcCallDO call = validateCallActive(roomName);
405 429 ImRtcParticipantDO participant = rtcParticipantMapper.selectByCallIdAndUserId(call.getCallId(), userId);
406 430 if (participant == null) {
407 431 throw exception(RTC_NOT_PARTICIPANT);
... ... @@ -415,9 +439,10 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
415 439 // 1. 鉴权:仅群活跃成员能查(走单行 SQL,不依赖成员列表缓存)
416 440 groupMemberService.validateMemberInGroup(groupId, userId);
417 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 446 @Override
422 447 public List<ImRtcParticipantDO> getCallParticipants(String callId) {
423 448 return rtcParticipantMapper.selectListByCallId(callId);
... ... @@ -426,12 +451,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
426 451 @Override
427 452 public String signCallToken(Long userId, String roomName) {
428 453 AdminUserRespDTO user = adminUserApi.getUser(userId);
  454 + // TODO @AI:不能为空。。。nickname 为空,则使用 userid;
429 455 String displayName = user == null ? null : user.getNickname();
430 456 return signToken(userId, displayName, roomName);
431 457 }
432 458  
  459 + // TODO DONE @AI:兼容 JDK8 —— 项目 Spring Boot 3 已要求 JDK17+,switch arrow case 可直接用
433 460 @Override
434   - // TODO @AI:这个方法的编写,需要兼容 JDK8
435 461 public void handleLiveKitEvent(LiveKitWebhookEventDTO event) {
436 462 if (event == null || event.getEvent() == null) {
437 463 return;
... ... @@ -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 480 private void handleParticipantJoined(LiveKitWebhookEventDTO event) {
  481 + // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
453 482 if (event.getRoom() == null || event.getParticipant() == null) {
454 483 return;
455 484 }
  485 + // 前置检查
456 486 String roomName = event.getRoom().getName();
457 487 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
458 488 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
... ... @@ -462,16 +492,20 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
462 492 if (userId == null) {
463 493 return;
464 494 }
  495 +
  496 + // 推 1602 通知参与方 / 全群参与方
465 497 pushParticipantConnectedNotification(call, userId);
466 498 }
467 499  
468 500 /**
469   - * 处理 LiveKit 「成员离开」事件;正常 leave 接口已经清理过的话条件 UPDATE 自动幂等
  501 + * 处理 LiveKit 「成员离开」事件:正常 {@link #leaveCall(Long, String)} 接口已经清理过的话,条件 UPDATE 自动幂等
470 502 */
471 503 private void handleParticipantLeft(LiveKitWebhookEventDTO event) {
  504 + // TODO @AI:ObjUtil.isAllEmpty(event.getRoom(), event.getParticipant()); 类似这种判断,简化下?
472 505 if (event.getRoom() == null || event.getParticipant() == null) {
473 506 return;
474 507 }
  508 + // 前置检查
475 509 String roomName = event.getRoom().getName();
476 510 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
477 511 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
... ... @@ -485,19 +519,19 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
485 519 if (participant == null || !ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
486 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 526 if (updated == 0) {
495 527 return;
496 528 }
497 529 log.info("[handleParticipantLeft][roomName={} userId={} 由 LiveKit Webhook 兜底]", roomName, userId);
  530 +
498 531 // 推 1603 通知参与方 / 全群参与方
499 532 pushParticipantDisconnectedNotification(call, userId);
500 533 // 关房条件与 leave 一致;群通话仅在「无人在房 + 无人响铃」时关
  534 + // TODO @AI:注释风格;感觉可以抽个 endSessionIfXXX;收口更统一;
501 535 if (ImConversationTypeEnum.isPrivate(call.getConversationType())
502 536 || shouldCloseGroupRoom(call.getCallId())) {
503 537 endSession(call, userId, ImRtcCallEndReasonEnum.HANGUP);
... ... @@ -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 544 private void handleRoomFinished(LiveKitWebhookEventDTO event) {
511 545 if (event.getRoom() == null) {
512 546 return;
513 547 }
  548 + // 前置检查
514 549 String roomName = event.getRoom().getName();
515 550 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
516 551 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
517 552 return;
518 553 }
  554 +
  555 + // TODO @AI:注释。带序号的;
519 556 log.info("[handleRoomFinished][roomName={} 由 LiveKit Webhook 兜底]", roomName);
520 557 endSession(call, null, ImRtcCallEndReasonEnum.HANGUP);
521 558 }
... ... @@ -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 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 608 ImRtcCallDO call = rtcCallMapper.selectByRoomName(roomName);
572 609 if (call == null || ImRtcCallStatusEnum.isEnded(call.getStatus())) {
573 610 throw exception(RTC_SESSION_NOT_EXISTS);
... ... @@ -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 621 private void validateInviteScene(Long userId, ImRtcCallInviteReqVO reqVO) {
582 622 Integer scene = reqVO.getScene();
... ... @@ -587,7 +627,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
587 627 if (Objects.equals(userId, reqVO.getPeerUserId())) {
588 628 throw exception(RTC_INVITE_SELF);
589 629 }
590   - // TODO DONE @AI:抽到 ImFriendService#validateFriend;
591 630 friendService.validateFriend(userId, reqVO.getPeerUserId());
592 631 return;
593 632 }
... ... @@ -595,16 +634,18 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
595 634 if (reqVO.getGroupId() == null) {
596 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 639 if (!activeMemberIds.contains(userId)) {
601 640 throw exception(GROUP_MEMBER_NOT_IN_GROUP);
602 641 }
603 642 return;
604 643 }
  644 + // TODO @AI:位置参数,illegalArgumentException;
605 645 throw exception(RTC_PRIVATE_INVITEE_REQUIRED);
606 646 }
607 647  
  648 + // TODO @AI:前端必须传递成员,不允许过多;
608 649 /**
609 650 * 解析被邀请池:私聊为 peerUserId 单元素;群聊优先用前端选中的子集,否则取群活跃成员;超量截断
610 651 * <p>
... ... @@ -631,6 +672,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
631 672 // 不论来源,发起人本人不进被邀请池
632 673 initial.remove(inviterId);
633 674 // 超出最大同时在房成员数;截断为 max-1 个,避免邀请整个 500 人大群
  675 + // TODO @AI:超过数量,直接抛出异常,不用裁剪,不然很奇怪;
634 676 int max = imProperties.getRtc().getGroupMaxParticipants();
635 677 if (initial.size() + 1 > max) {
636 678 return initial.stream().limit(max - 1L)
... ... @@ -639,6 +681,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
639 681 return initial;
640 682 }
641 683  
  684 + // TODO @AI:按照 updateCallXXX 这种风格?
  685 + // TODO @AI:注释,不用写这么细致。。。通过方法名,可以看的懂的。
642 686 /**
643 687 * 主表 CREATED → RUNNING;仅当首个非发起人加入时推进;条件 UPDATE 保幂等
644 688 * <p>
... ... @@ -647,30 +691,32 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
647 691 * RUNNING → 已被另一接听者抢先推进,幂等同步内存
648 692 */
649 693 private void maybeMarkOngoing(ImRtcCallDO call, Long acceptorId, LocalDateTime now) {
  694 + // TODO @AI:写下注释
650 695 if (!ImRtcCallStatusEnum.isCreated(call.getStatus())) {
651 696 return;
652 697 }
653 698 if (Objects.equals(call.getInviterUserId(), acceptorId)) {
654 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 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 709 return;
666 710 }
  711 +
  712 + // TODO @AI:写下注释
667 713 // updated == 0:与并发路径竞争失败;reload 看真实终态
668 714 ImRtcCallDO latest = rtcCallMapper.selectById(call.getId());
669 715 if (latest == null || ImRtcCallStatusEnum.isEnded(latest.getStatus())) {
  716 + // TODO @AI:不用会话把?用 RTC_CALL 吧?
670 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 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 763 private Collection<Long> getCallAudience(ImRtcCallDO call) {
716 764 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
... ... @@ -720,11 +768,14 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
720 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 776 * RTC_CALL(INVITE):走 webSocketService 直推到被邀请人
727 777 * <p>
  778 + * TODO @AI:为啥下面多写了个 “不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知”注释;
728 779 * 不能走 imPrivateMessageService.send:persistent=false 时 send 仅推 sender 多端,receiver 不感知
729 780 */
730 781 private void pushCallInviteNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Long inviteeId) {
... ... @@ -738,6 +789,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
738 789 /**
739 790 * RTC_CALL(REJECT):仅群通话场景;走 webSocketService 直推主叫
740 791 */
  792 + // TODO @AI:有点奇怪,单聊不推送么???
741 793 private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) {
742 794 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
743 795 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
... ... @@ -779,18 +831,16 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
779 831 webSocketService.sendPrivateMessageAsync(receivers, dto);
780 832 }
781 833  
  834 + // TODO @AI:“自增 id 自然保证聊天流顺序”,是不是你不用写这个注释?
782 835 /**
783 836 * RTC_CALL_START:仅群通话场景;走 imGroupMessageService.send,入 im_group_message 全群广播
784 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 840 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter) {
789 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 844 groupMessageService.sendGroupMessage(call.getInviterUserId(), dto);
795 845 }
796 846  
... ... @@ -804,23 +854,22 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
804 854 AdminUserRespDTO operator = operatorId != null ? adminUserApi.getUser(operatorId) : null;
805 855 ImRtcCallEndNotification payload = ImRtcCallEndNotification.of(call, reason, durationSeconds, operatorId, operator);
806 856 Long senderId = call.getInviterUserId();
  857 +
  858 + // 群聊:发起人总是 sender;receiver = 全群;走 imGroupMessageService.send 全群广播
807 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 862 groupMessageService.sendGroupMessage(senderId, dto);
813 863 return;
814 864 }
  865 +
815 866 // 私聊:发起人总是 sender;receiver = 通话另一方
816 867 ImRtcParticipantDO peer = CollUtil.findOne(
817 868 rtcParticipantMapper.selectListByCallId(call.getCallId()),
818 869 p -> !Objects.equals(p.getUserId(), senderId));
819 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 873 privateMessageService.sendPrivateMessage(senderId, dto);
825 874 }
826 875  
... ... @@ -829,7 +878,9 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
829 878 /**
830 879 * 签 LiveKit Token
831 880 */
  881 + // TODO @AI:sign 方法,是不是传递 adminuserdo?
832 882 private String signToken(Long userId, String displayName, String roomName) {
  883 + // TODO @AI:是不是抽一个 signJoinToken(userid, displayName, roomName)做减法
833 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 +}
... ...