fix: 修复 IM 申请与 RTC 边界问题

- 复用好友申请、群申请和群邀请唯一键冲突后的旧记录,并补充测试
- 收敛 RTC 旁观者加入、忙线校验、追加邀请超员和群通话通知逻辑
- 为 RTC 参与者补充房间用户唯一约束与 MySQL 迁移
- 统一群本体管理请求的 id 字段,并同步前端调用
- 修复前端来电活跃态守卫和 LiveKit 重连前断开旧房间
- 清理群成员通知基类命名和相关注释
im
YunaiV 2026-05-25 20:54:11 +08:00
parent a4dfb717aa
commit e1b8370267
9 changed files with 56 additions and 21 deletions

View File

@ -20,7 +20,7 @@ export interface ImGroupRespVO {
// 群消息置顶 / 取消置顶 Request VO
export interface ImGroupMessagePinReqVO {
groupId: number // 群编号
id: number // 群编号
messageId: number // 消息编号
}
@ -42,16 +42,35 @@ export interface ImGroupUpdateReqVO {
// 添加 / 撤销群管理员 Request VO
export interface ImGroupAdminReqVO {
groupId: number // 群编号
id: number // 群编号
userIds: number[] // 目标用户编号列表
}
// 群主转让 Request VO
export interface ImGroupTransferOwnerReqVO {
groupId: number // 群编号
id: number // 群编号
newOwnerUserId: number // 新群主用户编号
}
// 全群禁言 / 取消 Request VO
export interface ImGroupMuteAllReqVO {
id: number // 群编号
mutedAll: boolean // 是否全群禁言
}
// 成员禁言 Request VO
export interface ImGroupMuteMemberReqVO {
id: number // 群编号
userId: number // 被禁言的用户编号
mutedSeconds: number // 禁言时长0 表示永久禁言
}
// 取消成员禁言 Request VO
export interface ImGroupCancelMuteMemberReqVO {
id: number // 群编号
userId: number // 被取消禁言的用户编号
}
// 获得当前登录用户的群列表
export const getMyGroupList = () => {
return request.get<ImGroupRespVO[]>({ url: '/im/group/list' })
@ -103,16 +122,16 @@ export const unpinGroupMessage = (data: ImGroupMessagePinReqVO) => {
}
// 全群禁言 / 取消(仅群主 / 管理员可调)
export const muteAll = (data: { groupId: number; mutedAll: boolean }) => {
export const muteAll = (data: ImGroupMuteAllReqVO) => {
return request.put<boolean>({ url: '/im/group/mute-all', data })
}
// 禁言成员
export const muteMember = (data: { groupId: number; userId: number; mutedSeconds: number }) => {
export const muteMember = (data: ImGroupMuteMemberReqVO) => {
return request.put<boolean>({ url: '/im/group/mute-member', data })
}
// 取消成员禁言
export const cancelMuteMember = (data: { groupId: number; userId: number }) => {
export const cancelMuteMember = (data: ImGroupCancelMuteMemberReqVO) => {
return request.put<boolean>({ url: '/im/group/cancel-mute-member', data })
}

View File

@ -98,10 +98,10 @@ async function handleOk() {
submitting.value = true
try {
if (addedIds.length > 0) {
await addGroupAdmin({ groupId: groupId.value, userIds: addedIds })
await addGroupAdmin({ id: groupId.value, userIds: addedIds })
}
if (removedIds.length > 0) {
await removeGroupAdmin({ groupId: groupId.value, userIds: removedIds })
await removeGroupAdmin({ id: groupId.value, userIds: removedIds })
}
message.success(`已更新群管理员(新增 ${addedIds.length} 位,撤销 ${removedIds.length} 位)`)
emit('reload')

View File

@ -81,7 +81,7 @@ async function handleConfirm() {
loading.value = true
try {
await muteMember({
groupId: groupId.value,
id: groupId.value,
userId: userId.value,
mutedSeconds: selected.value
})

View File

@ -101,7 +101,7 @@ async function handleOk() {
submitting.value = true
try {
await transferGroupOwner({
groupId: groupId.value,
id: groupId.value,
newOwnerUserId: newOwner.value.userId
})
message.success('群主转让成功')
@ -121,4 +121,3 @@ async function handleOk() {
@include picker.styles;
}
</style>

View File

@ -50,6 +50,10 @@ export function useLiveKitRoom() {
/** 连接 LiveKit Serveraudio / video 控制初始默认开关 */
async function connect(url: string, token: string, opts: { audio?: boolean; video?: boolean }) {
// 新连接前先断开旧 Room保留本次注册的事件回调
if (_room.value) {
await disconnectRoom(false)
}
const r = new Room({
// 按格子尺寸自动选 simulcast 层
adaptiveStream: true,
@ -267,18 +271,23 @@ export function useLiveKitRoom() {
return stream
}
/** 主动断开;通话结束统一调 */
async function disconnect() {
disconnectedHandlers.clear()
participantConnectedHandlers.clear()
participantDisconnectedHandlers.clear()
/** 断开当前 RoomclearHandlers 为 true 时同步清理外部注册的事件回调 */
async function disconnectRoom(clearHandlers: boolean) {
// 清理通话结束后不再复用的订阅回调
if (clearHandlers) {
disconnectedHandlers.clear()
participantConnectedHandlers.clear()
participantDisconnectedHandlers.clear()
}
// 清理音视频轨道缓存
streamCache.clear()
if (_room.value) {
// 断开前先卸事件,避免 disconnect 期间 ParticipantDisconnected / TrackUnsubscribed 仍触发 syncRemotes
// 卸载 Room 事件并断开连接
_room.value.removeAllListeners()
await _room.value.disconnect()
_room.value = null
}
// 重置连接和设备状态
localParticipant.value = null
remoteParticipants.value = []
isConnected.value = false
@ -289,6 +298,11 @@ export function useLiveKitRoom() {
screenShareEnabled.value = false
}
/** 主动断开;通话结束统一调 */
async function disconnect() {
await disconnectRoom(true)
}
return {
room,
localParticipant,

View File

@ -649,7 +649,7 @@ async function onMuteAllChange(value: boolean | string | number) {
return
}
const newValue = !!value
await muteAll({ groupId: props.group.id, mutedAll: newValue })
await muteAll({ id: props.group.id, mutedAll: newValue })
message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
emit('reload')
}

View File

@ -147,7 +147,7 @@ async function handleRemove(msg: Message) {
}
removingId.value = msg.id
try {
await apiUnpinGroupMessage({ groupId: group.value.id, messageId: msg.id })
await apiUnpinGroupMessage({ id: group.value.id, messageId: msg.id })
message.success('已取消置顶')
} finally {
removingId.value = null

View File

@ -875,7 +875,7 @@ async function handlePin() {
cancelButtonText: '取消',
type: 'warning'
})
await apiPinGroupMessage({ groupId: group.id, messageId: props.message.id })
await apiPinGroupMessage({ id: group.id, messageId: props.message.id })
successMessage('已置顶')
} catch {}
}
@ -1007,7 +1007,7 @@ async function handleUnmute() {
}
try {
await confirmDialog('确定解除该成员的禁言吗?', '解除禁言')
await cancelMuteMember({ groupId: group.id, userId: props.message.senderId })
await cancelMuteMember({ id: group.id, userId: props.message.senderId })
successMessage('已解除禁言')
emit('reload')
} catch {}

View File

@ -163,6 +163,9 @@ export const useRtcStore = defineStore('imRtc', () => {
/** 被叫收到来电;切到 INCOMING接收 RTC_CALL(INVITE) payload */
function showIncoming(payload: ImRtcCallNotification) {
if (isActive.value) {
return
}
incomingPayload.value = payload
stage.value = ImRtcCallStage.INCOMING
// 按 inviter 兜底首次填充胶囊条