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:
@@ -22,6 +22,13 @@ func NewMeetingParticipantDAO(db *gorm.DB) *MeetingParticipantDAO {
|
||||
return &MeetingParticipantDAO{db: db}
|
||||
}
|
||||
|
||||
// WithTx 返回绑定到指定事务句柄的新 DAO 实例
|
||||
// 所有现有方法复用 db 字段,因此替换 db 为 tx 可让 DAO 方法参与上游事务。
|
||||
// Task 16 P1-4:EndRoom 行锁 + 事务化 新增
|
||||
func (d *MeetingParticipantDAO) WithTx(tx *gorm.DB) *MeetingParticipantDAO {
|
||||
return &MeetingParticipantDAO{db: tx}
|
||||
}
|
||||
|
||||
// JoinRoom 记录用户加入会议
|
||||
// 语义:
|
||||
// - 若 (room_id, user_id) 不存在 → 创建新行,role 取参数值,joined_at = NOW()
|
||||
@@ -207,11 +214,16 @@ func (d *MeetingParticipantDAO) FindActiveByUser(ctx context.Context, userID int
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// TransferHost 事务内完成主持人转让
|
||||
// 1) 旧 host 的 role 置为 Participant(0)
|
||||
// 2) 新 host 的 role 置为 Host(1)
|
||||
// 调用方应在同一上游事务中同时调 MeetingRoomDAO.UpdateHost 修改 meeting_rooms.host_id
|
||||
// 本方法内部已自带事务,上游无需再包裹
|
||||
// TransferHost 事务内完成主持人转让(Task 16 P1-1 强化为跨表原子事务)
|
||||
// 同一事务内完成三步:
|
||||
// 1) 旧 host 的 meeting_participants.role 置为 Participant(0)
|
||||
// 2) 新 host 的 meeting_participants.role 置为 Host(1),且必须仍在会议中(left_at IS NULL)
|
||||
// 3) meeting_rooms.host_id 原子更新为新 host
|
||||
//
|
||||
// 背景(审计 P1-1):旧实现把 1+2 放在 DAO 事务,3 由 service 层另起 SQL 执行;
|
||||
// 两步非原子 → 第 3 步失败会造成 participant.role 与 room.host_id 不一致,
|
||||
// 进而 assertIsHost(以 room.host_id 为准)永久拒绝老新 host 的所有主持人操作。
|
||||
// 现合并到单一事务,任一子步骤失败自动回滚,跨表一致性得到保证。
|
||||
func (d *MeetingParticipantDAO) TransferHost(ctx context.Context, roomID, oldHostID, newHostID int64) error {
|
||||
funcName := "dao.meeting_participant_dao.TransferHost"
|
||||
logs.Info(ctx, funcName, "转让主持人",
|
||||
@@ -232,6 +244,11 @@ func (d *MeetingParticipantDAO) TransferHost(ctx context.Context, roomID, oldHos
|
||||
Update("role", constants.MeetingRoleHost).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.MeetingRoom{}).
|
||||
Where("id = ?", roomID).
|
||||
Update("host_id", newHostID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -244,6 +261,44 @@ func (d *MeetingParticipantDAO) UpdateRole(ctx context.Context, roomID, userID i
|
||||
Update("role", role).Error
|
||||
}
|
||||
|
||||
// ListJoinedRoomsByUser 用户参与过的会议列表(JOIN meeting_rooms,支持 status 过滤 + 游标分页)
|
||||
// Task 16 P1-3:替代 "ListByUser + 内存合并 + ListByIDs" 的 N+1 查询路径
|
||||
// 返回按 meeting_rooms.created_at DESC, meeting_rooms.id DESC 排序;
|
||||
// 使用 DISTINCT 避免参与者多次 join/leave 同一会议导致的行重复。
|
||||
// 由调用方自行传 limit+1 判断 has_more
|
||||
func (d *MeetingParticipantDAO) ListJoinedRoomsByUser(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
statusFilter *int,
|
||||
beforeID int64,
|
||||
limit int,
|
||||
) ([]model.MeetingRoom, error) {
|
||||
if limit <= 0 {
|
||||
return []model.MeetingRoom{}, nil
|
||||
}
|
||||
funcName := "dao.meeting_participant_dao.ListJoinedRoomsByUser"
|
||||
|
||||
q := d.db.WithContext(ctx).
|
||||
Table("meeting_rooms AS r").
|
||||
Select("DISTINCT r.*").
|
||||
Joins("INNER JOIN meeting_participants AS p ON p.room_id = r.id").
|
||||
Where("p.user_id = ?", userID)
|
||||
|
||||
if statusFilter != nil {
|
||||
q = q.Where("r.status = ?", *statusFilter)
|
||||
}
|
||||
if beforeID > 0 {
|
||||
q = q.Where("r.id < ?", beforeID)
|
||||
}
|
||||
|
||||
var rooms []model.MeetingRoom
|
||||
if err := q.Order("r.created_at DESC, r.id DESC").Limit(limit).Find(&rooms).Error; err != nil {
|
||||
logs.Error(ctx, funcName, "JOIN 查询用户会议失败", zap.Int64("user_id", userID), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// ListByUser 用户历史会议列表(分页,按加入时间倒序)
|
||||
func (d *MeetingParticipantDAO) ListByUser(ctx context.Context, userID int64, offset, limit int) ([]model.MeetingParticipant, int64, error) {
|
||||
q := d.db.WithContext(ctx).
|
||||
|
||||
Reference in New Issue
Block a user