// Package dao 提供 contact 模块的数据库访问操作 package dao import ( "context" "time" authModel "github.com/echochat/backend/app/auth/model" "github.com/echochat/backend/app/constants" "github.com/echochat/backend/app/contact/model" "github.com/echochat/backend/pkg/logs" "go.uber.org/zap" "gorm.io/gorm" ) // FriendshipDAO 好友关系数据访问对象 type FriendshipDAO struct { db *gorm.DB } // NewFriendshipDAO 创建 FriendshipDAO 实例 func NewFriendshipDAO(db *gorm.DB) *FriendshipDAO { return &FriendshipDAO{db: db} } // CreateRequest 创建好友申请(单向 A→B,status=0) func (d *FriendshipDAO) CreateRequest(ctx context.Context, userID, friendID int64, message string) (*model.Friendship, error) { funcName := "dao.friendship_dao.CreateRequest" logs.Info(ctx, funcName, "创建好友申请", zap.Int64("user_id", userID), zap.Int64("friend_id", friendID)) f := &model.Friendship{ UserID: userID, FriendID: friendID, Status: constants.FriendshipStatusPending, Message: message, } err := d.db.WithContext(ctx).Create(f).Error if err != nil { logs.Error(ctx, funcName, "创建好友申请失败", zap.Error(err)) } return f, err } // AcceptRequest 接受好友申请(事务内:更新 A→B 为 accepted + 创建或更新 B→A 为 accepted) func (d *FriendshipDAO) AcceptRequest(ctx context.Context, requestID, userID int64) error { funcName := "dao.friendship_dao.AcceptRequest" logs.Info(ctx, funcName, "接受好友申请", zap.Int64("request_id", requestID), zap.Int64("user_id", userID)) return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var req model.Friendship if err := tx.First(&req, requestID).Error; err != nil { return err } if req.FriendID != userID { return gorm.ErrRecordNotFound } now := time.Now() if err := tx.Model(&req).Updates(map[string]interface{}{ "status": constants.FriendshipStatusAccepted, "updated_at": now, }).Error; err != nil { return err } var existing model.Friendship err := tx.Where("user_id = ? AND friend_id = ?", userID, req.UserID).First(&existing).Error if err == nil { return tx.Model(&existing).Updates(map[string]interface{}{ "status": constants.FriendshipStatusAccepted, "updated_at": now, }).Error } reverse := &model.Friendship{ UserID: userID, FriendID: req.UserID, Status: constants.FriendshipStatusAccepted, } return tx.Create(reverse).Error }) } // RejectRequest 拒绝好友申请 func (d *FriendshipDAO) RejectRequest(ctx context.Context, requestID, userID int64) error { funcName := "dao.friendship_dao.RejectRequest" logs.Info(ctx, funcName, "拒绝好友申请", zap.Int64("request_id", requestID), zap.Int64("user_id", userID)) result := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("id = ? AND friend_id = ? AND status = ?", requestID, userID, constants.FriendshipStatusPending). Update("status", constants.FriendshipStatusRejected) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil } // GetFriendList 获取好友列表(status=1 的记录,JOIN 用户表获取详情) func (d *FriendshipDAO) GetFriendList(ctx context.Context, userID int64, groupID *int64) ([]FriendWithUser, error) { funcName := "dao.friendship_dao.GetFriendList" logs.Debug(ctx, funcName, "查询好友列表", zap.Int64("user_id", userID)) var results []FriendWithUser query := d.db.WithContext(ctx). Table("contact_friendships f"). Select("f.id, f.friend_id as user_id, u.username, u.nickname, u.avatar, f.remark, f.group_id, f.created_at"). Joins("JOIN auth_users u ON u.id = f.friend_id"). Where("f.user_id = ? AND f.status = ?", userID, constants.FriendshipStatusAccepted) if groupID != nil { query = query.Where("f.group_id = ?", *groupID) } err := query.Order("u.nickname ASC").Scan(&results).Error if err != nil { logs.Error(ctx, funcName, "查询好友列表失败", zap.Error(err)) } return results, err } // FriendWithUser 好友列表查询结果(JOIN 用户表) type FriendWithUser struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` Username string `json:"username"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` Remark string `json:"remark"` GroupID *int64 `json:"group_id"` CreatedAt time.Time `json:"created_at"` } // GetPendingRequests 获取收到的待处理好友申请 func (d *FriendshipDAO) GetPendingRequests(ctx context.Context, userID int64) ([]RequestWithUser, error) { funcName := "dao.friendship_dao.GetPendingRequests" logs.Debug(ctx, funcName, "查询待处理申请", zap.Int64("user_id", userID)) var results []RequestWithUser err := d.db.WithContext(ctx). Table("contact_friendships f"). Select("f.id, f.user_id, u.username, u.nickname, u.avatar, f.message, f.status, f.created_at"). Joins("JOIN auth_users u ON u.id = f.user_id"). Where("f.friend_id = ? AND f.status = ?", userID, constants.FriendshipStatusPending). Order("f.created_at DESC"). Scan(&results).Error if err != nil { logs.Error(ctx, funcName, "查询待处理申请失败", zap.Error(err)) } return results, err } // RequestWithUser 好友申请查询结果(JOIN 申请方用户表) type RequestWithUser struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` Username string `json:"username"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` Message string `json:"message"` Status int `json:"status"` CreatedAt time.Time `json:"created_at"` } // DeleteFriend 删除好友(事务内双向删除) func (d *FriendshipDAO) DeleteFriend(ctx context.Context, userID, friendID int64) error { funcName := "dao.friendship_dao.DeleteFriend" logs.Info(ctx, funcName, "删除好友", zap.Int64("user_id", userID), zap.Int64("friend_id", friendID)) return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("user_id = ? AND friend_id = ?", userID, friendID). Delete(&model.Friendship{}).Error; err != nil { return err } return tx.Where("user_id = ? AND friend_id = ?", friendID, userID). Delete(&model.Friendship{}).Error }) } // UpdateRemark 设置好友备注 func (d *FriendshipDAO) UpdateRemark(ctx context.Context, userID, friendID int64, remark string) error { funcName := "dao.friendship_dao.UpdateRemark" logs.Info(ctx, funcName, "更新好友备注", zap.Int64("user_id", userID), zap.Int64("friend_id", friendID)) return d.db.WithContext(ctx). Model(&model.Friendship{}). Where("user_id = ? AND friend_id = ? AND status = ?", userID, friendID, constants.FriendshipStatusAccepted). Update("remark", remark).Error } // BlockUser 拉黑用户(事务内:删除双向好友 + 创建单向拉黑记录) func (d *FriendshipDAO) BlockUser(ctx context.Context, userID, targetID int64) error { funcName := "dao.friendship_dao.BlockUser" logs.Info(ctx, funcName, "拉黑用户", zap.Int64("user_id", userID), zap.Int64("target_id", targetID)) return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)", userID, targetID, targetID, userID). Delete(&model.Friendship{}).Error; err != nil { return err } block := &model.Friendship{ UserID: userID, FriendID: targetID, Status: constants.FriendshipStatusBlocked, } return tx.Create(block).Error }) } // UnblockUser 取消拉黑 func (d *FriendshipDAO) UnblockUser(ctx context.Context, userID, targetID int64) error { funcName := "dao.friendship_dao.UnblockUser" logs.Info(ctx, funcName, "取消拉黑", zap.Int64("user_id", userID), zap.Int64("target_id", targetID)) return d.db.WithContext(ctx). Where("user_id = ? AND friend_id = ? AND status = ?", userID, targetID, constants.FriendshipStatusBlocked). Delete(&model.Friendship{}).Error } // GetBlockList 获取黑名单列表 func (d *FriendshipDAO) GetBlockList(ctx context.Context, userID int64) ([]FriendWithUser, error) { funcName := "dao.friendship_dao.GetBlockList" logs.Debug(ctx, funcName, "查询黑名单", zap.Int64("user_id", userID)) var results []FriendWithUser err := d.db.WithContext(ctx). Table("contact_friendships f"). Select("f.id, f.friend_id as user_id, u.username, u.nickname, u.avatar, f.remark, f.group_id, f.created_at"). Joins("JOIN auth_users u ON u.id = f.friend_id"). Where("f.user_id = ? AND f.status = ?", userID, constants.FriendshipStatusBlocked). Scan(&results).Error return results, err } // IsBlocked 检查 userID 是否被 targetID 拉黑 func (d *FriendshipDAO) IsBlocked(ctx context.Context, userID, targetID int64) (bool, error) { var count int64 err := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("user_id = ? AND friend_id = ? AND status = ?", targetID, userID, constants.FriendshipStatusBlocked). Count(&count).Error return count > 0, err } // IsFriend 检查是否互为好友 func (d *FriendshipDAO) IsFriend(ctx context.Context, userID, friendID int64) (bool, error) { var count int64 err := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("user_id = ? AND friend_id = ? AND status = ?", userID, friendID, constants.FriendshipStatusAccepted). Count(&count).Error return count > 0, err } // HasPendingRequest 检查是否已有待处理的申请(A→B 或 B→A) func (d *FriendshipDAO) HasPendingRequest(ctx context.Context, userID, friendID int64) (bool, error) { var count int64 err := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)) AND status = ?", userID, friendID, friendID, userID, constants.FriendshipStatusPending). Count(&count).Error return count > 0, err } // ReactivateRejectedRequest 将已拒绝的申请重新激活为待处理状态 // 返回是否找到并更新了已拒绝的记录 func (d *FriendshipDAO) ReactivateRejectedRequest(ctx context.Context, userID, friendID int64, message string) (bool, error) { funcName := "dao.friendship_dao.ReactivateRejectedRequest" logs.Info(ctx, funcName, "重新激活已拒绝的好友申请", zap.Int64("user_id", userID), zap.Int64("friend_id", friendID)) result := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("user_id = ? AND friend_id = ? AND status = ?", userID, friendID, constants.FriendshipStatusRejected). Updates(map[string]interface{}{ "status": constants.FriendshipStatusPending, "message": message, }) if result.Error != nil { logs.Error(ctx, funcName, "重新激活好友申请失败", zap.Error(result.Error)) return false, result.Error } return result.RowsAffected > 0, nil } // GetRequestByID 根据 ID 获取好友申请 func (d *FriendshipDAO) GetRequestByID(ctx context.Context, id int64) (*model.Friendship, error) { var f model.Friendship err := d.db.WithContext(ctx).First(&f, id).Error return &f, err } // GetCommonFriends 获取共同好友 ID 列表 func (d *FriendshipDAO) GetCommonFriends(ctx context.Context, userID, targetID int64) ([]int64, error) { funcName := "dao.friendship_dao.GetCommonFriends" logs.Debug(ctx, funcName, "查询共同好友", zap.Int64("user_id", userID), zap.Int64("target_id", targetID)) var ids []int64 err := d.db.WithContext(ctx). Raw(`SELECT a.friend_id FROM contact_friendships a JOIN contact_friendships b ON a.friend_id = b.friend_id WHERE a.user_id = ? AND a.status = ? AND b.user_id = ? AND b.status = ?`, userID, constants.FriendshipStatusAccepted, targetID, constants.FriendshipStatusAccepted). Scan(&ids).Error return ids, err } // SearchUsers 搜索用户(按用户名或昵称模糊匹配,排除自己) func (d *FriendshipDAO) SearchUsers(ctx context.Context, keyword string, excludeUserID int64, page, pageSize int) ([]authModel.User, int64, error) { funcName := "dao.friendship_dao.SearchUsers" logs.Debug(ctx, funcName, "搜索用户", zap.String("keyword", keyword)) var users []authModel.User var total int64 query := d.db.WithContext(ctx). Model(&authModel.User{}). Where("id != ? AND status = 1 AND (username LIKE ? OR nickname LIKE ?)", excludeUserID, "%"+keyword+"%", "%"+keyword+"%") if err := query.Count(&total).Error; err != nil { return nil, 0, err } offset := (page - 1) * pageSize err := query.Select("id, username, nickname, avatar"). Offset(offset).Limit(pageSize). Find(&users).Error return users, total, err } // GetFriendIDs 获取用户的所有好友 ID 列表 func (d *FriendshipDAO) GetFriendIDs(ctx context.Context, userID int64) ([]int64, error) { var ids []int64 err := d.db.WithContext(ctx). Model(&model.Friendship{}). Where("user_id = ? AND status = ?", userID, constants.FriendshipStatusAccepted). Pluck("friend_id", &ids).Error return ids, err } // GetUsersByIDs 批量查询用户信息(仅返回 id/username/nickname/avatar) func (d *FriendshipDAO) GetUsersByIDs(ctx context.Context, userIDs []int64) ([]authModel.User, error) { funcName := "dao.friendship_dao.GetUsersByIDs" logs.Debug(ctx, funcName, "批量查询用户信息", zap.Int("count", len(userIDs))) var users []authModel.User err := d.db.WithContext(ctx). Model(&authModel.User{}). Select("id, username, nickname, avatar"). Where("id IN ?", userIDs). Find(&users).Error if err != nil { logs.Error(ctx, funcName, "批量查询用户信息失败", zap.Error(err)) } return users, err } // CountFriendsByGroup 统计每个分组的好友数 func (d *FriendshipDAO) CountFriendsByGroup(ctx context.Context, userID int64) (map[int64]int, error) { type GroupCount struct { GroupID int64 `gorm:"column:group_id"` Count int `gorm:"column:cnt"` } var counts []GroupCount err := d.db.WithContext(ctx). Model(&model.Friendship{}). Select("group_id, COUNT(*) as cnt"). Where("user_id = ? AND status = ? AND group_id IS NOT NULL", userID, constants.FriendshipStatusAccepted). Group("group_id"). Scan(&counts).Error result := make(map[int64]int) for _, c := range counts { result[c.GroupID] = c.Count } return result, err }