Commit 70271c1cbfbabe8656f0639b361cfda2af218cf4
1 parent
c69e20d6
✨ feat(im): 对齐微信的通话,模拟手机拨打电话。每次都一定有记录
Showing
4 changed files
with
46 additions
and
30 deletions
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<Integer> { |
| 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())); | ... | ... |