fix(meeting): Task 16 修复 code-reviewer 审计 P1 八项
按 docs/reviews/2026-04-23-phase2e-2-code-review.md 清单落地 P1 批次: - P1-1 主持人转让原子性:MeetingParticipantDAO.TransferHost 内部事务 合并 meeting_rooms.host_id 更新,service/lifecycle 移除冗余 UpdateHost - P1-2 ListChatMessages / ListMyMeetings 回归 DAO:新增 MeetingChatDAO.ListByRoomBefore 反向游标,service 移除 s.db 直查 - P1-3 ListMyMeetings N+1 放大优化:新增 MeetingParticipantDAO.ListJoinedRoomsByUser JOIN + DISTINCT 单次 SQL - P1-4 EndRoom 行锁 + 事务:s.db.Transaction 包裹 SELECT FOR UPDATE → 快照 → MarkEnded → LeaveAllActive;DAO 补 WithTx(*gorm.DB) 和 GetByIDForUpdate - P1-5 后台 goroutine trace_id 保留:新增 logs.DetachContext(ctx); 替换 meeting_service / meeting_signal_service 共 10 处 context.Background(); meeting_ws_handler 每条 WS 消息分配独立 trace_id - P1-6 _broadcastSelfState 重试:最多 2 次指数退避(700ms→2100ms), 检测 localAudioEnabled/localVideoEnabled 已被后续动作覆盖时放弃旧 patch - P1-7 HandleHostGraceExpired / HandleEmptyRoomExpired 处理锁: 独立 host_grace_handling:<code> / empty_ttl_handling:<code> SETNX + 60s TTL, 消除 Redis 自然过期 + 本地 timer 到点 + DEL 返回 0 的盲区 - P1-8 pushExistingRoomState 同步化:OnRoomJoin 返回前完成补推, 消除与 REST member.joined 并行造成的前端状态闪烁 并附带修复 .gitignore 中 logs/ 规则误伤 pkg/logs/ 代码目录的历史遗留问题, 将 logger.go / trace.go 正式纳入版本控制。 验证:backend go build + go vet 通过;frontend npm run build:h5 通过。 Made-with: Cursor
This commit is contained in:
@@ -1011,7 +1011,13 @@ export const useMeetingStore = defineStore('meeting', () => {
|
||||
* Task 15:向房间其他成员广播自身音视频状态
|
||||
* - 后端 meeting.member.state.changed 语义:操作自己无需 target_user_id
|
||||
* - 后端会广播给房间其他成员(不回显给本人)
|
||||
* - 容错:WS 未连接或 ACK 失败仅 warn 日志,不影响本地状态机
|
||||
*
|
||||
* Task 16 P1-6:增加 sendWithAck 重试与 member_state Hash 最终一致性保障
|
||||
* - 旧版失败仅 warn,WS 抖动期其他成员的"图标灰色"问题会重现
|
||||
* - 现实现最多 2 次指数退避重试(700ms → 2100ms);所有重试失败再输出 error 日志
|
||||
* - 设计约束:总耗时上限 ~6s,避免用户切换过快时累积旧状态覆盖新状态
|
||||
* (每次调用前先记录 targetSnapshot,重试前若 currentRoom 已变则放弃;
|
||||
* 若 patch 同字段已被后续 broadcast 覆盖,也放弃旧 patch 的重试)
|
||||
*/
|
||||
const _broadcastSelfState = (patch) => {
|
||||
if (!currentRoom.value) return
|
||||
@@ -1020,10 +1026,36 @@ export const useMeetingStore = defineStore('meeting', () => {
|
||||
if (typeof patch.audio_enabled === 'boolean') payload.audio_enabled = patch.audio_enabled
|
||||
if (typeof patch.video_enabled === 'boolean') payload.video_enabled = patch.video_enabled
|
||||
if (Object.keys(payload).length <= 1) return
|
||||
wsService.sendWithAck(MEETING_WS_MEMBER_STATE_CHANGED, payload, 3000)
|
||||
.catch((err) => {
|
||||
_log('warn', '[Meeting] 上报本地音视频状态失败', err)
|
||||
})
|
||||
|
||||
const targetRoomCode = currentRoom.value.room_code
|
||||
const maxAttempts = 3
|
||||
const backoffMs = [0, 700, 2100]
|
||||
|
||||
const attemptSend = (attempt) => {
|
||||
if (!currentRoom.value || currentRoom.value.room_code !== targetRoomCode) {
|
||||
_log('info', '[Meeting] 切换会议/已离会,放弃 state 重试', payload)
|
||||
return
|
||||
}
|
||||
if (typeof patch.audio_enabled === 'boolean' && localAudioEnabled.value !== patch.audio_enabled) {
|
||||
_log('info', '[Meeting] audio_enabled 已被后续动作覆盖,放弃旧 patch 重试', patch)
|
||||
return
|
||||
}
|
||||
if (typeof patch.video_enabled === 'boolean' && localVideoEnabled.value !== patch.video_enabled) {
|
||||
_log('info', '[Meeting] video_enabled 已被后续动作覆盖,放弃旧 patch 重试', patch)
|
||||
return
|
||||
}
|
||||
wsService.sendWithAck(MEETING_WS_MEMBER_STATE_CHANGED, payload, 3000)
|
||||
.catch((err) => {
|
||||
if (attempt + 1 >= maxAttempts) {
|
||||
_log('error', '[Meeting] 上报本地音视频状态失败(已用尽重试)', err, payload)
|
||||
return
|
||||
}
|
||||
const delay = backoffMs[attempt + 1] || 2100
|
||||
_log('warn', `[Meeting] 上报本地音视频状态失败,${delay}ms 后重试 (${attempt + 2}/${maxAttempts})`, err)
|
||||
setTimeout(() => attemptSend(attempt + 1), delay)
|
||||
})
|
||||
}
|
||||
attemptSend(0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user