feat(im): 将后端的 roomName 和 callId 融合,简化字段和逻辑(一致性更好、概念更简洁)

im
YunaiV 2026-05-12 20:29:08 +08:00
parent 38cb980ce4
commit 18e5c97bf3
4 changed files with 146 additions and 103 deletions

View File

@ -1,33 +1,8 @@
import request from '@/config/axios' import request from '@/config/axios'
import type {
/** 会话场景;对齐后端 ImConversationTypeEnum */ ImCallEndReasonValue,
export const ImCallScene = { ImCallParticipantStatusValue
PRIVATE: 1, } from '@/views/im/utils/constants'
GROUP: 2
} as const
/** 媒体类型;对齐后端 ImCallMediaTypeEnum */
export const ImCallMediaType = {
VOICE: 1,
VIDEO: 2
} as const
/** 通话状态;对齐后端 ImCallStatusEnum */
export const ImCallStatus = {
INVITING: 10,
ONGOING: 20,
ENDED: 30
} as const
/** 通话结束原因;对齐后端 ImCallEndReasonEnum */
export const ImCallEndReason = {
HANGUP: 1,
REJECT: 2,
CANCEL: 3,
TIMEOUT: 4,
BUSY: 5,
ERROR: 9
} as const
/** 发起通话请求 VO */ /** 发起通话请求 VO */
export interface ImRtcCallInviteReqVO { export interface ImRtcCallInviteReqVO {
@ -41,14 +16,14 @@ export interface ImRtcCallInviteReqVO {
/** 通话中添加成员请求 VO */ /** 通话中添加成员请求 VO */
export interface ImRtcCallInviteMoreReqVO { export interface ImRtcCallInviteMoreReqVO {
roomName: string room: string
inviteeIds: number[] inviteeIds: number[]
} }
/** 通话会话 VOinvite / accept / refreshToken / getActiveSessions 共用 */ /** 通话会话 VOinvite / join / accept / refreshToken 共用 */
export interface ImRtcCallRespVO { export interface ImRtcCallRespVO {
callId: string /** 业务通话编号(同时作为 LiveKit 房间名) */
roomName: string room: string
livekitUrl: string livekitUrl: string
token?: string token?: string
scene: number scene: number
@ -58,54 +33,81 @@ export interface ImRtcCallRespVO {
groupId?: number groupId?: number
inviteeIds?: number[] inviteeIds?: number[]
joinedUserIds?: number[] joinedUserIds?: number[]
newCreated?: boolean
} }
/** RTC_INVITE 信令载荷payload 走 ImPrivateMessageDTO.contentJSON 字符串) */ /** RTC_CALL 通话信令载荷通话信令统一入口status 区分子类型(复用参与者状态枚举);走 ImPrivateMessageDTO.content 仅推参与方 */
export interface ImRtcInviteNotification { export interface ImRtcCallNotification {
callId: string /** 信令对应的参与者状态变迁;取值参见 ImCallParticipantStatus */
roomName: string status: ImCallParticipantStatusValue
livekitUrl: string /** 业务通话编号(同时作为 LiveKit 房间名) */
token: string room: string
scene: number conversationType: number
mediaType: number mediaType: number
inviterId: number groupId?: number
/** INVITE 专属 */
livekitUrl?: string
/** INVITE 专属 */
token?: string
/** INVITE 专属 */
inviterUserId?: number
/** INVITE 专属 */
inviterNickname?: string inviterNickname?: string
/** INVITE 专属 */
inviterAvatar?: string inviterAvatar?: string
/** ACCEPT / REJECT / CANCEL / HUNGUP 专属 */
operatorUserId?: number
/** 操作者昵称;按需展示,普通文案不依赖 */
operatorNickname?: string
/** 操作者头像;按需展示,普通文案不依赖 */
operatorAvatar?: string
}
/** RTC_PARTICIPANT_CONNECTED 通话参与者加入载荷LiveKit webhook participant_joined 转推callStore +userId 进 joinedUserIds群聊场景非邀请成员靠 mediaType/inviterUserId 字段首次填充胶囊条 */
export interface ImRtcParticipantConnectedNotification {
room: string
userId: number
conversationType: number
groupId?: number
mediaType?: number
inviterUserId?: number
}
/** RTC_PARTICIPANT_DISCONNECTED 通话参与者离开载荷LiveKit webhook participant_left 转推callStore -userId 出 joinedUserIds */
export interface ImRtcParticipantDisconnectedNotification {
room: string
userId: number
conversationType: number
groupId?: number groupId?: number
} }
/** RTC_ACCEPT 信令载荷 */ /** RTC_CALL_START 通话开始载荷;仅群聊;入消息流;前端渲染聊天 tip「{inviterNickname} 发起了{voice/video}通话」;与 END 两段式配对 */
export interface ImRtcAcceptNotification { export interface ImRtcCallStartNotification {
callId: string room: string
roomName: string conversationType: number
acceptorId: number
}
/** RTC_END 信令载荷 */
export interface ImRtcEndNotification {
callId: string
roomName: string
operatorId?: number
reason: number
durationSeconds?: number
}
/** RTC_GROUP_STARTED / RTC_GROUP_UPDATED / RTC_GROUP_ENDED 共用载荷 */
export interface ImRtcGroupNotification {
callId: string
roomName: string
groupId: number
mediaType: number mediaType: number
inviterId: number inviterUserId: number
joinedUserIds?: number[] inviterNickname?: string
inviteeIds?: number[] inviterAvatar?: string
}
/** RTC_CALL_END 通话结束载荷;入消息流;私聊渲染准气泡,群聊渲染 tip「{voice/video}通话已结束 [时长 X]」 */
export interface ImRtcCallEndNotification {
room: string
conversationType: number
mediaType: number
endReason: ImCallEndReasonValue
durationSeconds?: number
/** 操作者用户编号HANGUP/CANCEL/REJECT 触发人webhook 兜底为 null */
operatorUserId?: number
/** 操作者昵称;按需展示,普通文案不依赖 */
operatorNickname?: string
/** 操作者头像;按需展示,普通文案不依赖 */
operatorAvatar?: string
} }
/** 群活跃通话查询响应;不含 token */ /** 群活跃通话查询响应;不含 token */
export interface ImRtcGroupCallRespVO { export interface ImRtcGroupCallRespVO {
callId: string room: string
roomName: string
groupId: number groupId: number
mediaType: number mediaType: number
inviterId: number inviterId: number
@ -113,50 +115,50 @@ export interface ImRtcGroupCallRespVO {
inviteeIds?: number[] inviteeIds?: number[]
} }
/** 发起通话;同好友对 / 群已有进行中通话则返回该会话并标记 newCreated=false */ /** 发起新通话;同好友对 / 同群已有进行中通话直接抛错(群场景应改走 joinCall */
export const inviteCall = (data: ImRtcCallInviteReqVO) => { export const inviteCall = (data: ImRtcCallInviteReqVO) => {
return request.post<ImRtcCallRespVO>({ url: '/im/rtc/invite', data }) return request.post<ImRtcCallRespVO>({ url: '/im/rtc/invite', data })
} }
/** 加入已有群通话;用于胶囊条「加入」按钮 */
export const joinCall = (room: string) => {
return request.post<ImRtcCallRespVO>({ url: '/im/rtc/join', params: { room } })
}
/** 通话中添加成员;仅群通话可用 */ /** 通话中添加成员;仅群通话可用 */
export const inviteMoreCall = (data: ImRtcCallInviteMoreReqVO) => { export const inviteMoreCall = (data: ImRtcCallInviteMoreReqVO) => {
return request.post<boolean>({ url: '/im/rtc/invite-more', data }) return request.post<boolean>({ url: '/im/rtc/invite-more', data })
} }
/** 接听通话 */ /** 接听通话 */
export const acceptCall = (roomName: string) => { export const acceptCall = (room: string) => {
return request.post<ImRtcCallRespVO>({ url: '/im/rtc/accept', params: { roomName } }) return request.post<ImRtcCallRespVO>({ url: '/im/rtc/accept', params: { room } })
} }
/** 拒绝通话 */ /** 拒绝通话 */
export const rejectCall = (roomName: string) => { export const rejectCall = (room: string) => {
return request.post<boolean>({ url: '/im/rtc/reject', params: { roomName } }) return request.post<boolean>({ url: '/im/rtc/reject', params: { room } })
} }
/** 取消邀请;主叫接通前调用 */ /** 取消邀请;主叫接通前调用 */
export const cancelCall = (roomName: string) => { export const cancelCall = (room: string) => {
return request.post<boolean>({ url: '/im/rtc/cancel', params: { roomName } }) return request.post<boolean>({ url: '/im/rtc/cancel', params: { room } })
} }
/** 离开通话;接通后调用 */ /** 离开通话;接通后调用 */
export const leaveCall = (roomName: string) => { export const leaveCall = (room: string) => {
return request.post<boolean>({ url: '/im/rtc/leave', params: { roomName } }) return request.post<boolean>({ url: '/im/rtc/leave', params: { room } })
} }
/** 重新签发 Token客户端重连或 Token 过期续期 */ /** 重新签发 Token客户端重连或 Token 过期续期 */
export const refreshCallToken = (roomName: string) => { export const refreshCallToken = (room: string) => {
return request.get<ImRtcCallRespVO>({ url: '/im/rtc/refresh-token', params: { roomName } }) return request.get<ImRtcCallRespVO>({ url: '/im/rtc/refresh-token', params: { room } })
} }
/** 查询当前用户活跃通话;冷启动 / 推送点开恢复 */ /** 查询当前进行中的通话;目前仅群聊场景(胶囊条),后端 API 已留扩展点;返回 null 表示无活跃通话 */
export const getActiveCallSessions = () => { export const getActiveCall = (groupId: number) => {
return request.get<ImRtcCallRespVO[]>({ url: '/im/rtc/active-sessions' })
}
/** 查询群当前进行中的通话;用于群聊顶部胶囊条;返回 null 表示无活跃通话 */
export const getGroupActiveCall = (groupId: number) => {
return request.get<ImRtcGroupCallRespVO | null>({ return request.get<ImRtcGroupCallRespVO | null>({
url: '/im/rtc/group-active-call', url: '/im/rtc/get-active-call',
params: { groupId } params: { groupId }
}) })
} }

View File

@ -183,7 +183,7 @@ export const useMessagePuller = () => {
* pull true isConnected watch pull * pull true isConnected watch pull
* socket onopen friendStore/groupStore watcher * socket onopen friendStore/groupStore watcher
*/ */
let bootstrapped = false let initialPulled = false
/** 执行一次全量增量拉取(重入安全:进行中再次调用复用同一个 promise */ /** 执行一次全量增量拉取(重入安全:进行中再次调用复用同一个 promise */
const pullOnce = (): Promise<void> => { const pullOnce = (): Promise<void> => {
@ -246,7 +246,7 @@ export const useMessagePuller = () => {
} finally { } finally {
// 整个 IIFE 全部完成(含已读位置补齐)后才允许下一次 pullOnce 重入 // 整个 IIFE 全部完成(含已读位置补齐)后才允许下一次 pullOnce 重入
pullPromise = null pullPromise = null
bootstrapped = true initialPulled = true
} }
})() })()
return pullPromise return pullPromise
@ -254,12 +254,12 @@ export const useMessagePuller = () => {
/** /**
* WS minId * WS minId
* Index.vue pullOnce bootstrap * Index.vue pullOnce
*/ */
watch( watch(
() => wsStore.isConnected, () => wsStore.isConnected,
(isConnected) => { (isConnected) => {
if (isConnected && bootstrapped) { if (isConnected && initialPulled) {
void pullOnce() void pullOnce()
} }
} }

View File

@ -585,7 +585,7 @@ export const useGroupStore = defineStore('imGroupStore', {
} }
}, },
/** 创建群广播:创建者多端同步 + 初始成员 bootstrappayload.memberUserIds 含自己 → 拉群详情 / 成员;本端发起者已经 upsert 过本群,跳过避免双拉 */ /** 创建群广播:创建者多端同步 + 初始成员首次拉取payload.memberUserIds 含自己 → 拉群详情 / 成员;本端发起者已经 upsert 过本群,跳过避免双拉 */
async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) { async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) {
if (!isSelfInPayloadMembers(payload)) { if (!isSelfInPayloadMembers(payload)) {
return return
@ -623,7 +623,7 @@ export const useGroupStore = defineStore('imGroupStore', {
} }
}, },
/** 成员加入:被邀请者本端 group 未就位先 fetchGroupInfo bootstrap;所有人都刷成员列表(新成员 nickname / avatar 不在 payload */ /** 成员加入:被邀请者本端 group 未就位先 fetchGroupInfo 初次拉取;所有人都刷成员列表(新成员 nickname / avatar 不在 payload */
async applyGroupMemberInviteNotification(groupId: number, payload: GroupNotificationPayload) { async applyGroupMemberInviteNotification(groupId: number, payload: GroupNotificationPayload) {
// 自己刚被拉进来:必须 await fetchGroupInfo 让群入 state.groups否则 fetchGroupMembers 的 guard 会兜空 // 自己刚被拉进来:必须 await fetchGroupInfo 让群入 state.groups否则 fetchGroupMembers 的 guard 会兜空
if (isSelfInPayloadMembers(payload) && !this.getGroup(groupId)) { if (isSelfInPayloadMembers(payload) && !this.getGroup(groupId)) {
@ -632,7 +632,7 @@ export const useGroupStore = defineStore('imGroupStore', {
this.fetchGroupMembers(groupId, true).catch(() => undefined) this.fetchGroupMembers(groupId, true).catch(() => undefined)
}, },
/** 自由进群:进群者本端 group 未就位先 fetchGroupInfo bootstrap;所有人都刷成员列表 */ /** 自由进群:进群者本端 group 未就位先 fetchGroupInfo 初次拉取;所有人都刷成员列表 */
async applyGroupMemberEnterNotification(groupId: number, payload: GroupNotificationPayload) { async applyGroupMemberEnterNotification(groupId: number, payload: GroupNotificationPayload) {
const selfUserId = getCurrentUserId() const selfUserId = getCurrentUserId()
// 自己自由进群:必须 await fetchGroupInfo 让群入 state.groups否则 fetchGroupMembers 的 guard 会兜空 // 自己自由进群:必须 await fetchGroupInfo 让群入 state.groups否则 fetchGroupMembers 的 guard 会兜空

View File

@ -13,15 +13,13 @@ export const ImMessageType = {
RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101 RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101
RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200 RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200
READ: 2201, // 已读多端同步OpenIM 无对应;自有扩展) READ: 2201, // 已读多端同步OpenIM 无对应;自有扩展)
// TODO @AI是不是要把单聊、群聊的信令融合 // ========== 实时通话信令1601-1605 段位与 OpenIM 对齐1610+ 自有扩展) ==========
// ========== 实时通话信令2300-2302 ========== RTC_CALL: 1601, // 通话信令统一入口(对应 OpenIM SignalingNotification=1601
RTC_INVITE: 2300, // 通话邀请(推给被叫弹来电) RTC_PARTICIPANT_CONNECTED: 1602, // 通话参与者加入(对应 OpenIM RoomParticipantsConnectedNotification=1602
RTC_ACCEPT: 2301, // 通话接通(推给主叫切到通话中 UI RTC_PARTICIPANT_DISCONNECTED: 1603, // 通话参与者离开(对应 OpenIM RoomParticipantsDisconnectedNotification=1603
RTC_END: 2302, // 通话结束(拒绝/取消/挂断/超时统一) // 1604-1609 OpenIM 已用 / 留作扩展,本系统暂不使用
// ========== 群通话广播2310-2312让所有群成员能看胶囊条 / 主动加入 ========== RTC_CALL_START: 1610, // 通话开始自有扩展OpenIM 无;仅群聊;与 END 两段式配对)
RTC_GROUP_STARTED: 2310, // 群通话开始(全群广播) RTC_CALL_END: 1611, // 通话结束自有扩展OpenIM 无;私聊 / 群聊)
RTC_GROUP_ENDED: 2311, // 群通话结束(全群广播;胶囊条移除)
RTC_GROUP_UPDATED: 2312, // 群通话成员变更(全群广播;胶囊条人数刷新)
// ========== 好友通知1201-1210 直接复用 OpenIM 段位编号) ========== // ========== 好友通知1201-1210 直接复用 OpenIM 段位编号) ==========
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意 FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝 FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝
@ -89,6 +87,11 @@ export function isFriendChatTip(type: number): boolean {
return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE
} }
/** 判断是否「会话内的通话事件气泡」RTC_CALL_START / RTC_CALL_END 渲染成灰色提示 */
export function isRtcCallTip(type: number): boolean {
return type === ImMessageType.RTC_CALL_START || type === ImMessageType.RTC_CALL_END
}
/** /**
* IM normal vs event ImMessageTypeEnum.normal * IM normal vs event ImMessageTypeEnum.normal
* *
@ -161,6 +164,44 @@ export function isGroupConversation(type: number | undefined): boolean {
return type === ImConversationType.GROUP return type === ImConversationType.GROUP
} }
/** IM 通话媒体类型(对齐后端 ImRtcCallMediaTypeEnum */
export const ImCallMediaType = {
VOICE: 1,
VIDEO: 2
} as const
/** IM 通话状态(对齐后端 ImRtcCallStatusEnum */
export const ImCallStatus = {
CREATED: 10, // 创建:私聊等被叫接听;群聊发起人已进房等其他人加入
RUNNING: 20, // 进行中:第一个非发起人接通后进入
ENDED: 30 // 已结束
} as const
/** IM 通话结束原因(对齐后端 ImRtcCallEndReasonEnum */
export const ImCallEndReason = {
HANGUP: 1, // 接通后任一方主动挂断
REJECT: 2, // 被叫接通前点拒接
CANCEL: 3, // 主叫接通前主动取消
BUSY: 5, // 私聊呼叫时对方正忙
ERROR: 9 // 网络中断 / 设备失败
} as const
/** ImCallEndReason 取值类型 */
export type ImCallEndReasonValue = (typeof ImCallEndReason)[keyof typeof ImCallEndReason]
/** IM 通话参与者状态(对齐后端 ImRtcParticipantStatusEnum同时作为 RTC_CALL 信令 status 字段取值 */
export const ImCallParticipantStatus = {
INVITING: 10, // 来电邀请
JOINED: 20, // 接听 / 已加入
REJECTED: 30, // 拒接
NO_ANSWER: 40, // 主叫取消,被邀请方未应答
LEFT: 50 // 挂断离开
} as const
/** ImCallParticipantStatus 取值类型 */
export type ImCallParticipantStatusValue =
(typeof ImCallParticipantStatus)[keyof typeof ImCallParticipantStatus]
/** IM WebSocket 外层帧类型(对齐后端 ImPrivateMessageDTO.TYPE / ImGroupMessageDTO.TYPE */ /** IM WebSocket 外层帧类型(对齐后端 ImPrivateMessageDTO.TYPE / ImGroupMessageDTO.TYPE */
export const ImWebSocketMessageType = { export const ImWebSocketMessageType = {
PRIVATE_MESSAGE: 'im-private-message', // 私聊通道 PRIVATE_MESSAGE: 'im-private-message', // 私聊通道