Files
EchoChat/backend/go-service/app/meeting/dao/meeting_chat_dao.go
bujinyuan ea2bf96c2f 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
2026-04-23 16:53:30 +08:00

112 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dao
import (
"context"
"github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/pkg/logs"
"go.uber.org/zap"
"gorm.io/gorm"
)
// MeetingChatDAO 会议内文字聊天数据访问对象
type MeetingChatDAO struct {
db *gorm.DB
}
// NewMeetingChatDAO 创建实例
func NewMeetingChatDAO(db *gorm.DB) *MeetingChatDAO {
return &MeetingChatDAO{db: db}
}
// Create 写入一条会议聊天消息
// ID / CreatedAt 由数据库自增 + autoCreateTime 填充
func (d *MeetingChatDAO) Create(ctx context.Context, chat *model.MeetingChat) error {
funcName := "dao.meeting_chat_dao.Create"
err := d.db.WithContext(ctx).Create(chat).Error
if err != nil {
logs.Error(ctx, funcName, "写入会议聊天消息失败",
zap.Int64("room_id", chat.RoomID),
zap.Int64("user_id", chat.UserID),
zap.Error(err))
}
return err
}
// ListByRoom 按房间正序返回聊天历史(供会议室打开时加载)
// afterID 为游标id > afterID 的记录0 表示从头开始
// limit 默认 50上限 200
func (d *MeetingChatDAO) ListByRoom(ctx context.Context, roomID int64, afterID int64, limit int) ([]model.MeetingChat, error) {
funcName := "dao.meeting_chat_dao.ListByRoom"
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
q := d.db.WithContext(ctx).Where("room_id = ?", roomID)
if afterID > 0 {
q = q.Where("id > ?", afterID)
}
var list []model.MeetingChat
err := q.Order("id ASC").Limit(limit).Find(&list).Error
if err != nil {
logs.Error(ctx, funcName, "查询会议聊天失败",
zap.Int64("room_id", roomID), zap.Error(err))
}
return list, err
}
// ListByRoomBefore 按房间反向游标分页查询聊天历史id < beforeID按 created_at DESC, id DESC 排序
// Task 16 P1-2ListChatMessages 回归 DAO原 service 层直接拼 SQL 破坏分层)
// beforeID 为 0 表示从最新的一条开始limit <= 0 时返回空列表
// 由调用方传 limit+1 自行判断 has_more
func (d *MeetingChatDAO) ListByRoomBefore(ctx context.Context, roomID int64, beforeID int64, limit int) ([]model.MeetingChat, error) {
if limit <= 0 {
return []model.MeetingChat{}, nil
}
funcName := "dao.meeting_chat_dao.ListByRoomBefore"
q := d.db.WithContext(ctx).
Model(&model.MeetingChat{}).
Where("room_id = ?", roomID)
if beforeID > 0 {
q = q.Where("id < ?", beforeID)
}
var list []model.MeetingChat
if err := q.Order("created_at DESC, id DESC").Limit(limit).Find(&list).Error; err != nil {
logs.Error(ctx, funcName, "反向游标查询会议聊天失败",
zap.Int64("room_id", roomID), zap.Int64("before_id", beforeID), zap.Error(err))
return nil, err
}
return list, nil
}
// DeleteByRoomIDs 按房间 ID 批量删除聊天消息(会议结束 24 小时后清理)
// 外层已有 meeting_rooms ON DELETE CASCADE此方法用于主动定时清理不删除 room 本身)
func (d *MeetingChatDAO) DeleteByRoomIDs(ctx context.Context, roomIDs []int64) (int64, error) {
funcName := "dao.meeting_chat_dao.DeleteByRoomIDs"
if len(roomIDs) == 0 {
return 0, nil
}
res := d.db.WithContext(ctx).
Where("room_id IN ?", roomIDs).
Delete(&model.MeetingChat{})
if res.Error != nil {
logs.Error(ctx, funcName, "批量清理聊天消息失败",
zap.Int("room_count", len(roomIDs)), zap.Error(res.Error))
}
return res.RowsAffected, res.Error
}
// CountByRoom 统计房间内聊天条数(供管理端观测)
func (d *MeetingChatDAO) CountByRoom(ctx context.Context, roomID int64) (int64, error) {
var count int64
err := d.db.WithContext(ctx).
Model(&model.MeetingChat{}).
Where("room_id = ?", roomID).
Count(&count).Error
return count, err
}