// Package dao 提供 group 模块的数据库访问操作 package dao import ( "context" "time" "github.com/echochat/backend/app/constants" "github.com/echochat/backend/app/dto" "github.com/echochat/backend/app/group/model" imModel "github.com/echochat/backend/app/im/model" "github.com/echochat/backend/pkg/logs" "go.uber.org/zap" "gorm.io/gorm" ) // GroupDAO 群聊数据访问对象 type GroupDAO struct { db *gorm.DB } // NewGroupDAO 创建 GroupDAO 实例 func NewGroupDAO(db *gorm.DB) *GroupDAO { return &GroupDAO{db: db} } // CreateGroupWithMembers 在事务中创建群聊(会话 + 群信息 + 群成员) // 创建者自动成为群主(role=2) func (d *GroupDAO) CreateGroupWithMembers(ctx context.Context, ownerID int64, name, avatar string, memberIDs []int64) (*model.Group, error) { funcName := "dao.group_dao.CreateGroupWithMembers" logs.Info(ctx, funcName, "创建群聊", zap.Int64("owner_id", ownerID), zap.String("name", name), zap.Int("member_count", len(memberIDs)+1)) var group model.Group err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { conv := &imModel.Conversation{ Type: constants.ConversationTypeGroup, CreatorID: ownerID, } if err := tx.Create(conv).Error; err != nil { logs.Error(ctx, funcName, "创建会话失败", zap.Error(err)) return err } group = model.Group{ ConversationID: conv.ID, Name: name, Avatar: avatar, OwnerID: ownerID, MaxMembers: constants.GroupDefaultMaxMembers, Status: constants.GroupStatusNormal, } if err := tx.Create(&group).Error; err != nil { logs.Error(ctx, funcName, "创建群信息失败", zap.Error(err)) return err } now := time.Now() ownerMember := &imModel.ConversationMember{ ConversationID: conv.ID, UserID: ownerID, Role: constants.GroupRoleOwner, JoinedAt: &now, } if err := tx.Create(ownerMember).Error; err != nil { logs.Error(ctx, funcName, "创建群主成员记录失败", zap.Error(err)) return err } for _, uid := range memberIDs { member := &imModel.ConversationMember{ ConversationID: conv.ID, UserID: uid, Role: constants.GroupRoleNormal, JoinedAt: &now, } if err := tx.Create(member).Error; err != nil { logs.Error(ctx, funcName, "创建群成员记录失败", zap.Int64("user_id", uid), zap.Error(err)) return err } } return nil }) if err != nil { logs.Error(ctx, funcName, "创建群聊失败", zap.Error(err)) return nil, err } return &group, nil } // GetByID 根据群 ID 获取群信息 func (d *GroupDAO) GetByID(ctx context.Context, groupID int64) (*model.Group, error) { funcName := "dao.group_dao.GetByID" logs.Debug(ctx, funcName, "获取群信息", zap.Int64("group_id", groupID)) var group model.Group err := d.db.WithContext(ctx).First(&group, groupID).Error if err != nil { return nil, err } return &group, nil } // GetByConversationID 根据会话 ID 获取群信息 func (d *GroupDAO) GetByConversationID(ctx context.Context, conversationID int64) (*model.Group, error) { funcName := "dao.group_dao.GetByConversationID" logs.Debug(ctx, funcName, "按会话 ID 获取群信息", zap.Int64("conversation_id", conversationID)) var group model.Group err := d.db.WithContext(ctx).Where("conversation_id = ?", conversationID).First(&group).Error if err != nil { return nil, err } return &group, nil } // UpdateGroupInfo 更新群基本信息(名称/头像/公告/可搜索性) func (d *GroupDAO) UpdateGroupInfo(ctx context.Context, groupID int64, updates map[string]interface{}) error { funcName := "dao.group_dao.UpdateGroupInfo" logs.Info(ctx, funcName, "更新群信息", zap.Int64("group_id", groupID)) return d.db.WithContext(ctx). Model(&model.Group{}). Where("id = ?", groupID). Updates(updates).Error } // DissolveGroup 解散群聊(标记状态为已解散) func (d *GroupDAO) DissolveGroup(ctx context.Context, groupID int64) error { funcName := "dao.group_dao.DissolveGroup" logs.Info(ctx, funcName, "解散群聊", zap.Int64("group_id", groupID)) return d.db.WithContext(ctx). Model(&model.Group{}). Where("id = ?", groupID). Update("status", constants.GroupStatusDissolved).Error } // TransferOwner 转让群主 func (d *GroupDAO) TransferOwner(ctx context.Context, groupID, oldOwnerID, newOwnerID int64, conversationID int64) error { funcName := "dao.group_dao.TransferOwner" logs.Info(ctx, funcName, "转让群主", zap.Int64("group_id", groupID), zap.Int64("old_owner", oldOwnerID), zap.Int64("new_owner", newOwnerID)) return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.Group{}). Where("id = ?", groupID). Update("owner_id", newOwnerID).Error; err != nil { return err } if err := tx.Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, oldOwnerID). Update("role", constants.GroupRoleNormal).Error; err != nil { return err } if err := tx.Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, newOwnerID). Update("role", constants.GroupRoleOwner).Error; err != nil { return err } return nil }) } // SetAllMuted 设置/取消全体禁言 func (d *GroupDAO) SetAllMuted(ctx context.Context, groupID int64, isMuted bool) error { funcName := "dao.group_dao.SetAllMuted" logs.Info(ctx, funcName, "设置全体禁言", zap.Int64("group_id", groupID), zap.Bool("is_muted", isMuted)) return d.db.WithContext(ctx). Model(&model.Group{}). Where("id = ?", groupID). Update("is_all_muted", isMuted).Error } // GetGroupBrief 获取群简要信息(满足 im/service.GroupInfoGetter 接口) func (d *GroupDAO) GetGroupBrief(ctx context.Context, conversationID int64) (*dto.GroupBrief, error) { funcName := "dao.group_dao.GetGroupBrief" logs.Debug(ctx, funcName, "获取群简要信息", zap.Int64("conversation_id", conversationID)) var group model.Group err := d.db.WithContext(ctx).Where("conversation_id = ?", conversationID).First(&group).Error if err != nil { return nil, err } return &dto.GroupBrief{ ID: group.ID, Name: group.Name, Avatar: group.Avatar, IsAllMuted: group.IsAllMuted, Status: group.Status, }, nil } // GetMemberCount 获取群成员数量 func (d *GroupDAO) GetMemberCount(ctx context.Context, conversationID int64) (int64, error) { var count int64 err := d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ?", conversationID). Count(&count).Error return count, err } // GetMember 获取指定群成员记录 func (d *GroupDAO) GetMember(ctx context.Context, conversationID, userID int64) (*imModel.ConversationMember, error) { funcName := "dao.group_dao.GetMember" logs.Debug(ctx, funcName, "查询群成员", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) var member imModel.ConversationMember err := d.db.WithContext(ctx). Where("conversation_id = ? AND user_id = ?", conversationID, userID). First(&member).Error if err != nil { return nil, err } return &member, nil } // GetMembers 获取群所有成员列表 func (d *GroupDAO) GetMembers(ctx context.Context, conversationID int64) ([]imModel.ConversationMember, error) { funcName := "dao.group_dao.GetMembers" logs.Debug(ctx, funcName, "获取群成员列表", zap.Int64("conversation_id", conversationID)) var members []imModel.ConversationMember err := d.db.WithContext(ctx). Where("conversation_id = ?", conversationID). Order("role DESC, joined_at ASC"). Find(&members).Error return members, err } // GetMemberIDs 获取群所有成员 ID func (d *GroupDAO) GetMemberIDs(ctx context.Context, conversationID int64) ([]int64, error) { var ids []int64 err := d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ?", conversationID). Pluck("user_id", &ids).Error return ids, err } // GetAdminIDs 获取群主和管理员 ID 列表(用于推送入群申请通知) func (d *GroupDAO) GetAdminIDs(ctx context.Context, conversationID int64) ([]int64, error) { var ids []int64 err := d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND role >= ?", conversationID, constants.GroupRoleAdmin). Pluck("user_id", &ids).Error return ids, err } // AddMember 添加群成员 func (d *GroupDAO) AddMember(ctx context.Context, conversationID, userID int64, role int) error { funcName := "dao.group_dao.AddMember" logs.Info(ctx, funcName, "添加群成员", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) now := time.Now() member := &imModel.ConversationMember{ ConversationID: conversationID, UserID: userID, Role: role, JoinedAt: &now, } return d.db.WithContext(ctx).Create(member).Error } // RemoveMember 移除群成员 func (d *GroupDAO) RemoveMember(ctx context.Context, conversationID, userID int64) error { funcName := "dao.group_dao.RemoveMember" logs.Info(ctx, funcName, "移除群成员", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) return d.db.WithContext(ctx). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Delete(&imModel.ConversationMember{}).Error } // UpdateMemberRole 更新成员角色 func (d *GroupDAO) UpdateMemberRole(ctx context.Context, conversationID, userID int64, role int) error { funcName := "dao.group_dao.UpdateMemberRole" logs.Info(ctx, funcName, "更新成员角色", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID), zap.Int("role", role)) return d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("role", role).Error } // UpdateMemberMuted 更新成员禁言状态 func (d *GroupDAO) UpdateMemberMuted(ctx context.Context, conversationID, userID int64, isMuted bool) error { funcName := "dao.group_dao.UpdateMemberMuted" logs.Info(ctx, funcName, "更新成员禁言状态", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID), zap.Bool("is_muted", isMuted)) return d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("is_muted", isMuted).Error } // UpdateMemberNickname 更新成员群内昵称 func (d *GroupDAO) UpdateMemberNickname(ctx context.Context, conversationID, userID int64, nickname string) error { funcName := "dao.group_dao.UpdateMemberNickname" logs.Info(ctx, funcName, "更新群昵称", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) return d.db.WithContext(ctx). Model(&imModel.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("nickname", nickname).Error } // SearchGroups 搜索可发现的群聊(按群名称全文搜索) func (d *GroupDAO) SearchGroups(ctx context.Context, keyword string, offset, limit int) ([]model.Group, int64, error) { funcName := "dao.group_dao.SearchGroups" logs.Debug(ctx, funcName, "搜索群聊", zap.String("keyword", keyword)) var total int64 query := d.db.WithContext(ctx). Model(&model.Group{}). Where("status = ? AND is_searchable = true AND to_tsvector('simple', name) @@ plainto_tsquery('simple', ?)", constants.GroupStatusNormal, keyword) if err := query.Count(&total).Error; err != nil { logs.Error(ctx, funcName, "搜索计数失败", zap.Error(err)) return nil, 0, err } var groups []model.Group err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&groups).Error if err != nil { logs.Error(ctx, funcName, "搜索查询失败", zap.Error(err)) return nil, 0, err } return groups, total, nil }