fix(meeting): Task 16 会议销毁路径资源清理专项

前端修复:
- _onRoomEnded 保留结束页数据,新增 exitEndedRoom() 动作
  room.vue onUnload 在 ENDED 状态调用,释放 currentRoom / participants / chatMessages 等 pinia state
- endMeeting() API 成功后立即触发 _onRoomEnded 本地兜底,不再依赖 room.ended 广播
  避免 API 成功但 WS 抖动导致 engine / timer / AudioContext 泄漏
- 新增 _pendingBroadcastTimers 集合收纳 _broadcastSelfState 的 setTimeout 句柄
  _cleanupMedia 统一 clearTimeout,避免会议结束后 stale 重试逻辑排队

后端修复:
- MeetingLifecycleService.OnRoomEnded 统一生命周期收尾
  取消 graceTimers / emptyTTLTimers + Pipeline DEL host_grace / empty_ttl / 两个 handling 锁 key
- EndRoom 事务提交后遍历 activesBefore 调 cleanupRoomRedisResidual
  批量 DEL 每个前任活跃成员的 resourceTrackKey + memberStateKey
  补调 lifecycleSvc.OnRoomEnded 清理自身 timer + keys
- HandleEmptyRoomExpired MarkEnded 后补调 cleanupRoomRedisResidual + 清 host_grace 残留

验证:go vet + go build + npm run build:h5 全部通过

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-23 17:12:38 +08:00
parent ea2bf96c2f
commit f5ae095033
5 changed files with 123 additions and 3 deletions

View File

@@ -602,6 +602,11 @@ onUnload(() => {
if (meetingStore.isInMeeting && meetingStore.localState !== MEETING_LOCAL_STATE_LEAVING) {
meetingStore.leave().catch(() => {})
}
// Task 16 资源清理:若停留在"会议已结束"页面后物理返回,清空 pinia state
// 避免 currentRoom / participants / chatMessages 等大对象常驻内存
if (meetingStore.localState === MEETING_LOCAL_STATE_ENDED) {
meetingStore.exitEndedRoom()
}
})
</script>

View File

@@ -177,6 +177,12 @@ export const useMeetingStore = defineStore('meeting', () => {
/** { [userId]: { inCount: number, outCount: number } },用于防抖 1-in / 2-out */
const _speakingDebounce = {}
/**
* Task 16 资源清理专项_broadcastSelfState 重试期间的 setTimeout 句柄集合
* 离会 / 结束会议时统一 clearTimeout避免会议结束后仍有 stale 重试逻辑在 event loop 排队
*/
const _pendingBroadcastTimers = new Set()
// ==================== Getters ====================
const isInMeeting = computed(() => {
@@ -583,6 +589,12 @@ export const useMeetingStore = defineStore('meeting', () => {
const _cleanupMedia = () => {
_stopSpeakingDetection()
// Task 16 资源清理:清理 _broadcastSelfState 挂起的 setTimeout 重试
// 防止会议结束后重试逻辑仍在 event loop 排队造成 memory pin
_pendingBroadcastTimers.forEach((handle) => {
try { clearTimeout(handle) } catch {}
})
_pendingBroadcastTimers.clear()
if (_engine) {
try { _engine.close() } catch (e) { _log('warn', '_cleanupMedia engine.close', e) }
_engine = null
@@ -927,7 +939,13 @@ export const useMeetingStore = defineStore('meeting', () => {
}
}
/** 主持人结束会议(会广播 room.ended 给所有人) */
/**
* 主持人结束会议(后端会广播 room.ended 给所有人)
* Task 16 资源清理API 成功后立即本地兜底 _cleanupMedia
* - 避免"API 成功但 WS 断线导致 room.ended 广播丢失 → 本地 engine/timer/AudioContext 泄漏"
* - 同时触发 _onRoomEnded 走 ENDED 状态UI 展示"会议已结束"提示页
* - 若之后广播到达也是幂等_engine 已 null + closed 判定)
*/
const endMeeting = async () => {
if (!currentRoom.value) return
const code = currentRoom.value.room_code
@@ -937,7 +955,20 @@ export const useMeetingStore = defineStore('meeting', () => {
_log('warn', '[Meeting] end API 失败', err)
throw err
}
// room.ended WS 广播会触发 _onRoomEnded 清理
_onRoomEnded({ data: { room_code: code, reason: 'host_ended' } })
}
/**
* Task 16 资源清理:退出"会议已结束"提示页时的清理
* - _onRoomEnded 刻意保留了 currentRoom / participants / chatMessages 供结束页渲染
* - 但若用户长时间停留在结束页(或直接 reLaunch 去别处pinia state 会一直占内存
* - 调用方room.vue 的 onUnload / 结束页"返回首页"按钮
* - 仅在 ENDED 状态下执行 _reset其他状态用 leave() 通道
*/
const exitEndedRoom = () => {
if (localState.value === MEETING_LOCAL_STATE_ENDED) {
_reset()
}
}
// ==================== Actions本地推流 ====================
@@ -1052,7 +1083,13 @@ export const useMeetingStore = defineStore('meeting', () => {
}
const delay = backoffMs[attempt + 1] || 2100
_log('warn', `[Meeting] 上报本地音视频状态失败,${delay}ms 后重试 (${attempt + 2}/${maxAttempts})`, err)
setTimeout(() => attemptSend(attempt + 1), delay)
// Task 16 资源清理:把 setTimeout 句柄登记到全局 Set
// 离会/结束会议时 _cleanupMedia() 统一 clearTimeout避免 stale 重试排队
const handle = setTimeout(() => {
_pendingBroadcastTimers.delete(handle)
attemptSend(attempt + 1)
}, delay)
_pendingBroadcastTimers.add(handle)
})
}
attemptSend(0)
@@ -1224,6 +1261,7 @@ export const useMeetingStore = defineStore('meeting', () => {
joinAndEnter,
leave,
endMeeting,
exitEndedRoom,
// media
startLocalAudio,