Commit 70271c1cbfbabe8656f0639b361cfda2af218cf4

Authored by YunaiV
1 parent c69e20d6

✨ feat(im): 对齐微信的通话,模拟手机拨打电话。每次都一定有记录

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/ImRtcCallController.java
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallRespVO; @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallRespVO;
8 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcGroupCallRespVO; 8 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcGroupCallRespVO;
9 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
10 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO;
  11 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
11 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum; 12 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
12 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 13 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
13 import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService; 14 import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService;
@@ -123,13 +124,15 @@ public class ImRtcCallController { @@ -123,13 +124,15 @@ public class ImRtcCallController {
123 return null; 124 return null;
124 } 125 }
125 List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom()); 126 List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
  127 + boolean ended = ImRtcCallStatusEnum.isEnded(call.getStatus());
126 return new ImRtcCallRespVO() 128 return new ImRtcCallRespVO()
127 .setRoom(call.getRoom()) 129 .setRoom(call.getRoom())
128 .setLivekitUrl(imProperties.getRtc().getLivekitUrl()) 130 .setLivekitUrl(imProperties.getRtc().getLivekitUrl())
129 - .setToken(rtcCallService.signCallToken(userId, call.getRoom())) 131 + // 仅非 ENDED 场景才签 token,ENDED 场景不签,前端根据 token 是否存在,来判断是否展示「通话已结束」的提示
  132 + .setToken(ended ? null : rtcCallService.signCallToken(userId, call.getRoom()))
130 .setConversationType(call.getConversationType()).setMediaType(call.getMediaType()) 133 .setConversationType(call.getConversationType()).setMediaType(call.getMediaType())
131 - .setStatus(call.getStatus()).setInviterId(call.getInviterUserId())  
132 - .setGroupId(call.getGroupId()) 134 + .setStatus(call.getStatus()).setEndReason(call.getEndReason())
  135 + .setInviterId(call.getInviterUserId()).setGroupId(call.getGroupId())
133 .setInviteeIds(filterUserIds(participants, ImRtcParticipantStatusEnum.INVITING)) 136 .setInviteeIds(filterUserIds(participants, ImRtcParticipantStatusEnum.INVITING))
134 .setJoinedUserIds(filterUserIds(participants, ImRtcParticipantStatusEnum.JOINED)); 137 .setJoinedUserIds(filterUserIds(participants, ImRtcParticipantStatusEnum.JOINED));
135 } 138 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/vo/ImRtcCallRespVO.java
@@ -27,6 +27,9 @@ public class ImRtcCallRespVO { @@ -27,6 +27,9 @@ public class ImRtcCallRespVO {
27 @Schema(description = "状态") 27 @Schema(description = "状态")
28 private Integer status; // 参见 ImCallStatusEnum 枚举类 28 private Integer status; // 参见 ImCallStatusEnum 枚举类
29 29
  30 + @Schema(description = "结束原因;仅 status=ENDED 时有值")
  31 + private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类
  32 +
30 @Schema(description = "发起人编号") 33 @Schema(description = "发起人编号")
31 private Long inviterId; 34 private Long inviterId;
32 35
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcCallEndReasonEnum.java
@@ -20,7 +20,6 @@ public enum ImRtcCallEndReasonEnum implements ArrayValuable&lt;Integer&gt; { @@ -20,7 +20,6 @@ public enum ImRtcCallEndReasonEnum implements ArrayValuable&lt;Integer&gt; {
20 HANGUP(1, "通话结束"), // 接通后任一方主动挂断 20 HANGUP(1, "通话结束"), // 接通后任一方主动挂断
21 REJECT(2, "已拒绝"), // 被叫接通前点拒接 21 REJECT(2, "已拒绝"), // 被叫接通前点拒接
22 CANCEL(3, "已取消"), // 主叫接通前主动取消 22 CANCEL(3, "已取消"), // 主叫接通前主动取消
23 - // TODO @AI:思考时,会看到一条,【对方正忙】;目的是:私聊后续会看到这个通话,从而去回去联系。类似未接电话一样的味道。  
24 BUSY(5, "对方正忙"), // 私聊呼叫时对方在另一通话 23 BUSY(5, "对方正忙"), // 私聊呼叫时对方在另一通话
25 ERROR(9, "通话异常"); // 网络中断、设备失败等 24 ERROR(9, "通话异常"); // 网络中断、设备失败等
26 25
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
@@ -112,55 +112,64 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -112,55 +112,64 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
112 } 112 }
113 113
114 /** 114 /**
115 - * 群通话创建锁内主体:群有活跃通话引导走 inviteCall / joinCall;无则兜底自身忙线后新建 115 + * 群通话创建锁内主体:群有活跃通话直接抛(引导走 inviteCall / joinCall);
  116 + * 否则走完整生命周期,若发起人自身忙线立即 end(BUSY) 留下通话记录
116 * 117 *
117 * @param userId 发起人编号 118 * @param userId 发起人编号
118 * @param reqVO 创建请求 119 * @param reqVO 创建请求
119 - * @return 通话主表 120 + * @return 通话主表(可能 status=ENDED 表示自身忙线)
120 */ 121 */
121 private ImRtcCallDO createGroupCall(Long userId, ImRtcCallCreateReqVO reqVO) { 122 private ImRtcCallDO createGroupCall(Long userId, ImRtcCallCreateReqVO reqVO) {
  123 + // 1.1 同群有活跃通话 → 直接抛异常(UI 应已拦截),引导用户走 inviteCall / joinCall;避免重复开通
122 ImRtcCallDO active = rtcCallMapper.selectLastOneByGroupIdAndStatusIn( 124 ImRtcCallDO active = rtcCallMapper.selectLastOneByGroupIdAndStatusIn(
123 reqVO.getGroupId(), ImRtcCallStatusEnum.ACTIVE_STATUSES); 125 reqVO.getGroupId(), ImRtcCallStatusEnum.ACTIVE_STATUSES);
124 if (active != null) { 126 if (active != null) {
125 throw exception(RTC_GROUP_CALL_ACTIVE); 127 throw exception(RTC_GROUP_CALL_ACTIVE);
126 } 128 }
127 - // 兜底校验发起人不在另一通通话中(跨群 / 跨好友)  
128 - ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(  
129 - userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);  
130 - if (selfActive != null) {  
131 - throw exception(RTC_SELF_BUSY); 129 + // 1.2 先检测发起人忙线状态;不抛,留给下方 end 决定
  130 + boolean selfBusy = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
  131 + userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES) != null;
  132 +
  133 + // 2. 完整生命周期:INSERT + INVITE × N + START 全推
  134 + ImRtcCallDO call = createCall0(userId, reqVO);
  135 +
  136 + // 3. 自身忙线立即 end(BUSY);END 推送给群,群成员看到完整 START + END
  137 + if (selfBusy) {
  138 + endSession(call, userId, ImRtcCallEndReasonEnum.BUSY);
132 } 139 }
133 - return createCall0(userId, reqVO); 140 + return call;
134 } 141 }
135 142
136 /** 143 /**
137 - * 私聊创建锁内主体:检查双方忙线后新建 144 + * 私聊创建锁内主体:双方忙线时仍走完整生命周期(create + 立即 end(BUSY)),
  145 + * 同一对正在通话视作数据异常直接抛
138 * 146 *
139 * @param userId 发起人编号 147 * @param userId 发起人编号
140 * @param reqVO 创建请求 148 * @param reqVO 创建请求
141 * @param peerUserId 对端编号;来自 reqVO.inviteeIds 的唯一元素 149 * @param peerUserId 对端编号;来自 reqVO.inviteeIds 的唯一元素
142 - * @return 通话主表 150 + * @return 通话主表(可能 status=ENDED 表示忙线)
143 */ 151 */
144 private ImRtcCallDO createPrivateCall(Long userId, ImRtcCallCreateReqVO reqVO, Long peerUserId) { 152 private ImRtcCallDO createPrivateCall(Long userId, ImRtcCallCreateReqVO reqVO, Long peerUserId) {
145 - // 1.1 发起人 & 对端已在同一通话 → 视为自身忙线  
146 - ImRtcCallDO active = getActivePrivateCallByPair(userId, peerUserId);  
147 - if (active != null) { 153 + // 1.1 双方已在同一通话 → 数据异常(UI 应已拦截),直接抛
  154 + if (getActivePrivateCallByPair(userId, peerUserId) != null) {
148 throw exception(RTC_SELF_BUSY); 155 throw exception(RTC_SELF_BUSY);
149 } 156 }
150 - // 1.2 对端忙线  
151 - ImRtcParticipantDO peerActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(  
152 - peerUserId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);  
153 - if (peerActive != null) {  
154 - throw exception(RTC_PEER_BUSY);  
155 - }  
156 - // 1.3 发起人在另一通通话(跨好友 / 跨群兜底)  
157 - ImRtcParticipantDO selfActive = rtcParticipantMapper.selectLastOneByUserIdAndStatus(  
158 - userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES);  
159 - if (selfActive != null) {  
160 - throw exception(RTC_SELF_BUSY); 157 + // 1.2 忙线检测:self 优先(更可执行的提示);不抛,留给下方 end 决定
  158 + boolean selfBusy = rtcParticipantMapper.selectLastOneByUserIdAndStatus(
  159 + userId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES) != null;
  160 + boolean peerBusy = !selfBusy && rtcParticipantMapper.selectLastOneByUserIdAndStatus(
  161 + peerUserId, ImRtcParticipantStatusEnum.ACTIVE_STATUSES) != null;
  162 +
  163 + // 2. 完整生命周期:INSERT + INVITE 全推
  164 + ImRtcCallDO call = createCall0(userId, reqVO);
  165 +
  166 + // 3. 忙线立即 end(BUSY);operator 决定两端看到的文案(self busy → operator=自己;peer busy → operator=对端)
  167 + if (selfBusy) {
  168 + endSession(call, userId, ImRtcCallEndReasonEnum.BUSY);
  169 + } else if (peerBusy) {
  170 + endSession(call, peerUserId, ImRtcCallEndReasonEnum.BUSY);
161 } 171 }
162 - // 2. 新建通话  
163 - return createCall0(userId, reqVO); 172 + return call;
164 } 173 }
165 174
166 @Override 175 @Override
@@ -769,6 +778,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { @@ -769,6 +778,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
769 call.getRoom(), operatorId, reason); 778 call.getRoom(), operatorId, reason);
770 return; 779 return;
771 } 780 }
  781 + // 【特殊】同步内存 call:让 createCall 这类调用方拿到的 DO 立即反映 ENDED 终态,Controller 拼 RespVO 能直接用
  782 + call.setStatus(ImRtcCallStatusEnum.ENDED.getStatus()).setEndReason(reason.getReason()).setEndTime(now);
772 // 1.2 更新参与表为已结束:残留 INVITING 改 NO_ANSWER;残留 JOINED 改 LEFT 并写 leaveTime 783 // 1.2 更新参与表为已结束:残留 INVITING 改 NO_ANSWER;残留 JOINED 改 LEFT 并写 leaveTime
773 rtcParticipantMapper.updateByRoomAndStatus(call.getRoom(), ImRtcParticipantStatusEnum.INVITING.getStatus(), 784 rtcParticipantMapper.updateByRoomAndStatus(call.getRoom(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
774 new ImRtcParticipantDO().setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus())); 785 new ImRtcParticipantDO().setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus()));