按 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
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
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-2:ListChatMessages 回归 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
|
||
}
|