2026-06-30 24681c81c09022f584a57006f2534b5f74723414
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package cn.iocoder.yudao.module.im.controller.admin.rtc;
 
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallCreateReqVO;
import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO;
import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallRespVO;
import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcGroupCallRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO;
import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
import cn.iocoder.yudao.module.im.framework.config.ImProperties;
import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
 
import java.util.List;
import java.util.Objects;
import java.util.Set;
 
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@Tag(name = "管理后台 - IM 实时通话")
@RestController
@RequestMapping("/im/rtc")
@Validated
public class ImRtcCallController {
 
    @Resource
    private ImRtcCallService rtcCallService;
    @Resource
    private ImProperties imProperties;
 
    @PostMapping("/create")
    @Operation(summary = "创建新通话;按 conversationType 区分私聊 / 群聊")
    public CommonResult<ImRtcCallRespVO> createCall(@Valid @RequestBody ImRtcCallCreateReqVO reqVO) {
        Long userId = getLoginUserId();
        ImRtcCallDO call = rtcCallService.createCall(userId, reqVO);
        return success(buildCallRespVO(call, userId));
    }
 
    @PostMapping("/invite")
    @Operation(summary = "通话中追加邀请;仅群通话可用")
    public CommonResult<Boolean> inviteCall(@Valid @RequestBody ImRtcCallInviteReqVO reqVO) {
        rtcCallService.inviteCall(getLoginUserId(), reqVO);
        return success(true);
    }
 
    @PostMapping("/join")
    @Operation(summary = "加入已有群通话;用于胶囊条「加入」按钮")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<ImRtcCallRespVO> joinCall(@RequestParam("room") String room) {
        Long userId = getLoginUserId();
        return success(buildCallRespVO(rtcCallService.joinCall(userId, room), userId));
    }
 
    @PostMapping("/accept")
    @Operation(summary = "接听通话")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<ImRtcCallRespVO> accept(@RequestParam("room") String room) {
        Long userId = getLoginUserId();
        return success(buildCallRespVO(rtcCallService.acceptCall(userId, room), userId));
    }
 
    @PostMapping("/reject")
    @Operation(summary = "拒绝通话")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<Boolean> reject(@RequestParam("room") String room) {
        rtcCallService.rejectCall(getLoginUserId(), room);
        return success(true);
    }
 
    @PostMapping("/cancel")
    @Operation(summary = "取消邀请;主叫接通前调用")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<Boolean> cancel(@RequestParam("room") String room) {
        rtcCallService.cancelCall(getLoginUserId(), room);
        return success(true);
    }
 
    @PostMapping("/leave")
    @Operation(summary = "离开通话;接通后调用")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<Boolean> leave(@RequestParam("room") String room) {
        rtcCallService.leaveCall(getLoginUserId(), room);
        return success(true);
    }
 
    @PostMapping("/no-answer-call-check")
    @Operation(summary = "前端 RUNNING 端 timer 兜底;触发后端立即扫描该 room 的振铃超时(接口静默)")
    @Parameter(name = "room", description = "业务通话编号", required = true, example = "f47ac10b58cc4372a567")
    public CommonResult<Boolean> noAnswerCallCheck(@RequestParam("room") String room) {
        rtcCallService.noAnswerCallCheck(getLoginUserId(), room);
        return success(true);
    }
 
    @GetMapping("/get-active-call")
    @Operation(summary = "查询当前进行中的通话;用于群聊顶部「N 人正在通话」胶囊条")
    @Parameter(name = "groupId", description = "群编号", required = true, example = "2048")
    public CommonResult<ImRtcGroupCallRespVO> getActiveCall(@RequestParam("groupId") Long groupId) {
        ImRtcCallDO call = rtcCallService.getActiveCall(getLoginUserId(), groupId);
        return success(buildGroupActiveRespVO(call));
    }
 
    // ========== VO 拼装 ==========
 
    /**
     * 拼装 invite / join / accept / refresh-token 的响应 VO;含 token + 参与者分桶
     *
     * @param call   通话主表
     * @param userId 当前用户编号;token 按该用户签发
     * @return 响应 VO;call 为空返回 null
     */
    private ImRtcCallRespVO buildCallRespVO(ImRtcCallDO call, Long userId) {
        if (call == null) {
            return null;
        }
        List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
        boolean ended = ImRtcCallStatusEnum.isEnded(call.getStatus());
        return new ImRtcCallRespVO()
                .setRoom(call.getRoom())
                .setLivekitUrl(imProperties.getRtc().getLivekitUrl())
                // 仅非 ENDED 场景才签 token,ENDED 场景不签,前端根据 token 是否存在,来判断是否展示「通话已结束」的提示
                .setToken(ended ? null : rtcCallService.signCallToken(userId, call.getRoom()))
                .setConversationType(call.getConversationType()).setMediaType(call.getMediaType())
                .setStatus(call.getStatus()).setEndReason(call.getEndReason())
                .setInviterId(call.getInviterUserId()).setGroupId(call.getGroupId())
                .setInviteeIds(filterUserIds(participants, ImRtcParticipantStatusEnum.INVITING))
                .setJoinedUserIds(filterUserIds(participants, ImRtcParticipantStatusEnum.JOINED));
    }
 
    /**
     * 拼装 get-active-call 的响应 VO
     *
     * @param call 通话主表
     * @return 响应 VO:只用于群聊胶囊条,不含 token
     */
    private ImRtcGroupCallRespVO buildGroupActiveRespVO(ImRtcCallDO call) {
        if (call == null) {
            return null;
        }
        List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantList(call.getRoom());
        return new ImRtcGroupCallRespVO().setRoom(call.getRoom()).setMediaType(call.getMediaType())
                .setGroupId(call.getGroupId()).setInviterId(call.getInviterUserId())
                .setJoinedUserIds(filterUserIds(participants, ImRtcParticipantStatusEnum.JOINED))
                .setInviteeIds(filterUserIds(participants, ImRtcParticipantStatusEnum.INVITING));
    }
 
    /**
     * 按状态过滤参与者 userId;用 LinkedHashSet 保留前端展示顺序
     *
     * @param participants 参与者列表
     * @param status       目标状态
     * @return userId 集合
     */
    private static Set<Long> filterUserIds(List<ImRtcParticipantDO> participants,
                                           ImRtcParticipantStatusEnum status) {
        return CollectionUtils.convertLinkedSet(participants, ImRtcParticipantDO::getUserId,
                participant -> Objects.equals(participant.getStatus(), status.getStatus()));
    }
 
}