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 8 import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcGroupCallRespVO;
9 9 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
10 10 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO;
  11 +import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
11 12 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
12 13 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
13 14 import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService;
... ... @@ -123,13 +124,15 @@ public class ImRtcCallController {
123 124 return null;
124 125 }
125 126 List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
  127 + boolean ended = ImRtcCallStatusEnum.isEnded(call.getStatus());
126 128 return new ImRtcCallRespVO()
127 129 .setRoom(call.getRoom())
128 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 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 136 .setInviteeIds(filterUserIds(participants, ImRtcParticipantStatusEnum.INVITING))
134 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 27 @Schema(description = "状态")
28 28 private Integer status; // 参见 ImCallStatusEnum 枚举类
29 29  
  30 + @Schema(description = "结束原因;仅 status=ENDED 时有值")
  31 + private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类
  32 +
30 33 @Schema(description = "发起人编号")
31 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 20 HANGUP(1, "通话结束"), // 接通后任一方主动挂断
21 21 REJECT(2, "已拒绝"), // 被叫接通前点拒接
22 22 CANCEL(3, "已取消"), // 主叫接通前主动取消
23   - // TODO @AI:思考时,会看到一条,【对方正忙】;目的是:私聊后续会看到这个通话,从而去回去联系。类似未接电话一样的味道。
24 23 BUSY(5, "对方正忙"), // 私聊呼叫时对方在另一通话
25 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 112 }
113 113  
114 114 /**
115   - * 群通话创建锁内主体:群有活跃通话引导走 inviteCall / joinCall;无则兜底自身忙线后新建
  115 + * 群通话创建锁内主体:群有活跃通话直接抛(引导走 inviteCall / joinCall);
  116 + * 否则走完整生命周期,若发起人自身忙线立即 end(BUSY) 留下通话记录
116 117 *
117 118 * @param userId 发起人编号
118 119 * @param reqVO 创建请求
119   - * @return 通话主表
  120 + * @return 通话主表(可能 status=ENDED 表示自身忙线)
120 121 */
121 122 private ImRtcCallDO createGroupCall(Long userId, ImRtcCallCreateReqVO reqVO) {
  123 + // 1.1 同群有活跃通话 → 直接抛异常(UI 应已拦截),引导用户走 inviteCall / joinCall;避免重复开通
122 124 ImRtcCallDO active = rtcCallMapper.selectLastOneByGroupIdAndStatusIn(
123 125 reqVO.getGroupId(), ImRtcCallStatusEnum.ACTIVE_STATUSES);
124 126 if (active != null) {
125 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 147 * @param userId 发起人编号
140 148 * @param reqVO 创建请求
141 149 * @param peerUserId 对端编号;来自 reqVO.inviteeIds 的唯一元素
142   - * @return 通话主表
  150 + * @return 通话主表(可能 status=ENDED 表示忙线)
143 151 */
144 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 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 175 @Override
... ... @@ -769,6 +778,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
769 778 call.getRoom(), operatorId, reason);
770 779 return;
771 780 }
  781 + // 【特殊】同步内存 call:让 createCall 这类调用方拿到的 DO 立即反映 ENDED 终态,Controller 拼 RespVO 能直接用
  782 + call.setStatus(ImRtcCallStatusEnum.ENDED.getStatus()).setEndReason(reason.getReason()).setEndTime(now);
772 783 // 1.2 更新参与表为已结束:残留 INVITING 改 NO_ANSWER;残留 JOINED 改 LEFT 并写 leaveTime
773 784 rtcParticipantMapper.updateByRoomAndStatus(call.getRoom(), ImRtcParticipantStatusEnum.INVITING.getStatus(),
774 785 new ImRtcParticipantDO().setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus()));
... ...