diff --git a/cmd/server/main.go b/cmd/server/main.go index 6def122..be3c93e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -224,6 +224,7 @@ func main() { service.InitAttachmentService(db) // 附件服务(文件上传、管理) service.InitLoginLogService(db) // 登录日志服务(记录登录历史) service.InitMomentService(db) // 朋友圈服务(动态、点赞、评论) + service.InitSearchService(db) // 搜索服务(聚合搜索) // 步骤5: 启动TURN服务器(用于WebRTC音视频通话) go turnserver.Start() @@ -329,6 +330,9 @@ func main() { authGroup := apiGroup.Group("") authGroup.Use(middleware.JWTAuthMiddleware()) // 使用JWT认证中间件 { + // 聚合搜索 + authGroup.GET("/search", api.GlobalSearchHandler) + // 用户管理 authGroup.GET("/user/my-info", api.GetMyInfoHandler) authGroup.GET("/user/list", api.GetUserListHandler) diff --git a/internal/api/search_handler.go b/internal/api/search_handler.go new file mode 100644 index 0000000..4da6685 --- /dev/null +++ b/internal/api/search_handler.go @@ -0,0 +1,67 @@ +/** + * package api + * 作用:全局搜索API处理器 + */ +package api + +import ( + "strconv" + "xk-websocket-v2/internal/service" + "xk-websocket-v2/internal/utils" + + "github.com/gin-gonic/gin" +) + +/** + * GlobalSearchHandler + * 功能:聚合搜索(联系人、群聊、聊天记录) + * 路径:GET /api/search?keyword=xxx&type=all|contacts|groups|messages&limit=20 + * + * 参数说明: + * - keyword: 搜索关键词(必填) + * - type: 搜索类型,可选值:all(默认)、contacts、groups、messages + * - limit: 每类结果的最大数量,默认20 + * + * 返回结构: + * { + * "contacts": [{ "user": {...}, "remark_name": "xxx", "room_id": "xxx" }], + * "groups": [{ "room_id": "xxx", "room_name": "xxx", ... }], + * "messages": [{ "id": 1, "room_id": "xxx", "content": "xxx", "sender": {...} }] + * } + */ +func GlobalSearchHandler(c *gin.Context) { + // 获取当前用户ID + userID, exists := c.Get("user_id") + if !exists { + utils.Unauthorized(c, "未登录") + return + } + + // 获取搜索参数 + keyword := c.Query("keyword") + if keyword == "" { + utils.BadRequest(c, "搜索关键词不能为空") + return + } + + searchType := c.DefaultQuery("type", "all") + if searchType != "all" && searchType != "contacts" && searchType != "groups" && searchType != "messages" { + searchType = "all" + } + + limitStr := c.DefaultQuery("limit", "20") + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 || limit > 100 { + limit = 20 + } + + // 执行搜索 + result, err := service.SearchSvc.GlobalSearch(userID.(string), keyword, searchType, limit) + if err != nil { + utils.InternalError(c, "搜索失败") + return + } + + utils.SuccessWithData(c, result, "搜索成功") +} + diff --git a/internal/service/search_service.go b/internal/service/search_service.go new file mode 100644 index 0000000..fff74b1 --- /dev/null +++ b/internal/service/search_service.go @@ -0,0 +1,352 @@ +/** + * package service + * 作用:聚合搜索服务 + */ +package service + +import ( + "strings" + "xk-websocket-v2/internal/model" + + "gorm.io/gorm" +) + +// SearchService 搜索服务结构体 +type SearchService struct { + DB *gorm.DB +} + +// SearchSvc 全局单例 +var SearchSvc *SearchService + +/** + * InitSearchService + * 功能:初始化搜索服务 + */ +func InitSearchService(db *gorm.DB) { + SearchSvc = &SearchService{DB: db} +} + +// SearchResult 搜索结果结构 +type SearchResult struct { + Contacts []ContactSearchResult `json:"contacts"` + Groups []GroupSearchResult `json:"groups"` + Messages []MessageSearchResult `json:"messages"` +} + +// ContactSearchResult 联系人搜索结果 +type ContactSearchResult struct { + User *model.User `json:"user"` + RemarkName string `json:"remark_name"` + RoomID string `json:"room_id"` +} + +// GroupSearchResult 群聊搜索结果 +type GroupSearchResult struct { + RoomID string `json:"room_id"` + RoomName string `json:"room_name"` + RoomAvatar string `json:"room_avatar"` + OwnerID string `json:"owner_id"` + MemberCount int `json:"member_count"` +} + +// MessageSearchResult 消息搜索结果 +type MessageSearchResult struct { + ID uint `json:"id"` + RoomID string `json:"room_id"` + RoomName string `json:"room_name"` + Content string `json:"content"` + MessageType int `json:"message_type"` + CreatedAt string `json:"created_at"` + Sender *model.User `json:"sender"` + IsGroupChat bool `json:"is_group_chat"` + MatchContent string `json:"match_content"` +} + +/** + * GlobalSearch + * 功能:全局聚合搜索(联系人、群聊、聊天记录) + * @param userID 当前用户ID + * @param keyword 搜索关键词 + * @param searchType 搜索类型:all, contacts, groups, messages + * @param limit 每类结果的最大数量 + */ +func (s *SearchService) GlobalSearch(userID, keyword, searchType string, limit int) (*SearchResult, error) { + result := &SearchResult{ + Contacts: []ContactSearchResult{}, + Groups: []GroupSearchResult{}, + Messages: []MessageSearchResult{}, + } + + if keyword == "" { + return result, nil + } + + keyword = strings.TrimSpace(keyword) + likeKeyword := "%" + keyword + "%" + + // 搜索联系人 + if searchType == "all" || searchType == "contacts" { + contacts, err := s.searchContacts(userID, likeKeyword, limit) + if err == nil { + result.Contacts = contacts + } + } + + // 搜索群聊 + if searchType == "all" || searchType == "groups" { + groups, err := s.searchGroups(userID, likeKeyword, limit) + if err == nil { + result.Groups = groups + } + } + + // 搜索聊天记录 + if searchType == "all" || searchType == "messages" { + messages, err := s.searchMessages(userID, likeKeyword, limit) + if err == nil { + result.Messages = messages + } + } + + return result, nil +} + +/** + * searchContacts + * 功能:搜索联系人(好友) + * 搜索字段:用户名、备注名、邮箱、手机号 + */ +func (s *SearchService) searchContacts(userID, likeKeyword string, limit int) ([]ContactSearchResult, error) { + var contacts []model.UserContact + var results []ContactSearchResult + + // 查询当前用户的好友 + err := s.DB.Where("user_id = ?", userID).Find(&contacts).Error + if err != nil { + return results, err + } + + // 获取所有好友的用户ID + contactIDs := make([]string, 0, len(contacts)) + contactMap := make(map[string]model.UserContact) + for _, c := range contacts { + contactIDs = append(contactIDs, c.ContactID) + contactMap[c.ContactID] = c + } + + if len(contactIDs) == 0 { + return results, nil + } + + // 查询匹配的用户 + var users []model.User + err = s.DB.Where("id IN ?", contactIDs). + Where("name LIKE ? OR email LIKE ? OR phone LIKE ?", likeKeyword, likeKeyword, likeKeyword). + Limit(limit). + Find(&users).Error + if err != nil { + return results, err + } + + // 组装结果 + for _, user := range users { + contact := contactMap[user.ID] + userCopy := user + results = append(results, ContactSearchResult{ + User: &userCopy, + RemarkName: contact.RemarkName, + RoomID: contact.RoomID, + }) + } + + // 同时搜索备注名匹配的联系人 + var remarkContacts []model.UserContact + err = s.DB.Where("user_id = ? AND remark_name LIKE ?", userID, likeKeyword). + Limit(limit). + Find(&remarkContacts).Error + if err == nil { + remarkIDs := make([]string, 0) + existingIDs := make(map[string]bool) + for _, r := range results { + existingIDs[r.User.ID] = true + } + for _, c := range remarkContacts { + if !existingIDs[c.ContactID] { + remarkIDs = append(remarkIDs, c.ContactID) + } + } + if len(remarkIDs) > 0 { + var remarkUsers []model.User + s.DB.Where("id IN ?", remarkIDs).Find(&remarkUsers) + userMap := make(map[string]model.User) + for _, u := range remarkUsers { + userMap[u.ID] = u + } + for _, c := range remarkContacts { + if u, ok := userMap[c.ContactID]; ok && !existingIDs[c.ContactID] { + userCopy := u + results = append(results, ContactSearchResult{ + User: &userCopy, + RemarkName: c.RemarkName, + RoomID: c.RoomID, + }) + } + } + } + } + + return results, nil +} + +/** + * searchGroups + * 功能:搜索群聊 + * 搜索字段:群名称 + */ +func (s *SearchService) searchGroups(userID, likeKeyword string, limit int) ([]GroupSearchResult, error) { + var results []GroupSearchResult + + // 获取用户所在的群聊 + var members []model.RoomMember + err := s.DB.Where("user_id = ?", userID).Find(&members).Error + if err != nil { + return results, err + } + + roomIDs := make([]string, 0, len(members)) + for _, m := range members { + if strings.HasPrefix(m.RoomID, "group_") { + roomIDs = append(roomIDs, m.RoomID) + } + } + + if len(roomIDs) == 0 { + return results, nil + } + + // 查询匹配的群聊 + var rooms []model.ChatRoom + err = s.DB.Where("room_id IN ? AND room_type = ? AND room_name LIKE ?", roomIDs, "group", likeKeyword). + Limit(limit). + Find(&rooms).Error + if err != nil { + return results, err + } + + // 获取每个群的成员数量 + for _, room := range rooms { + var memberCount int64 + s.DB.Model(&model.RoomMember{}).Where("room_id = ?", room.RoomID).Count(&memberCount) + + results = append(results, GroupSearchResult{ + RoomID: room.RoomID, + RoomName: room.RoomName, + RoomAvatar: room.RoomAvatar, + OwnerID: room.OwnerID, + MemberCount: int(memberCount), + }) + } + + return results, nil +} + +/** + * searchMessages + * 功能:搜索聊天记录(仅文本消息) + * 搜索字段:消息内容 + */ +func (s *SearchService) searchMessages(userID, likeKeyword string, limit int) ([]MessageSearchResult, error) { + var results []MessageSearchResult + + // 获取用户有权限查看的房间 + var contacts []model.UserContact + s.DB.Where("user_id = ?", userID).Find(&contacts) + + var members []model.RoomMember + s.DB.Where("user_id = ?", userID).Find(&members) + + roomIDs := make([]string, 0) + for _, c := range contacts { + if c.RoomID != "" { + roomIDs = append(roomIDs, c.RoomID) + } + } + for _, m := range members { + roomIDs = append(roomIDs, m.RoomID) + } + + if len(roomIDs) == 0 { + return results, nil + } + + // 查询匹配的消息(仅文本消息) + var messages []model.ChatMessage + err := s.DB.Where("room_id IN ? AND message_type = ? AND content LIKE ?", roomIDs, 0, likeKeyword). + Order("created_at DESC"). + Limit(limit). + Find(&messages).Error + if err != nil { + return results, err + } + + // 获取发送者信息和房间信息 + senderIDs := make([]string, 0) + roomIDSet := make(map[string]bool) + for _, m := range messages { + senderIDs = append(senderIDs, m.SenderUserID) + roomIDSet[m.RoomID] = true + } + + var users []model.User + if len(senderIDs) > 0 { + s.DB.Where("id IN ?", senderIDs).Find(&users) + } + userMap := make(map[string]*model.User) + for i := range users { + userMap[users[i].ID] = &users[i] + } + + uniqueRoomIDs := make([]string, 0, len(roomIDSet)) + for rid := range roomIDSet { + uniqueRoomIDs = append(uniqueRoomIDs, rid) + } + var rooms []model.ChatRoom + if len(uniqueRoomIDs) > 0 { + s.DB.Where("room_id IN ?", uniqueRoomIDs).Find(&rooms) + } + roomMap := make(map[string]*model.ChatRoom) + for i := range rooms { + roomMap[rooms[i].RoomID] = &rooms[i] + } + + for _, msg := range messages { + room := roomMap[msg.RoomID] + roomName := "" + isGroupChat := false + if room != nil { + roomName = room.RoomName + isGroupChat = room.RoomType == "group" + } + + matchContent := msg.Content + if len(matchContent) > 100 { + matchContent = matchContent[:100] + "..." + } + + results = append(results, MessageSearchResult{ + ID: msg.ID, + RoomID: msg.RoomID, + RoomName: roomName, + Content: msg.Content, + MessageType: msg.MessageType, + CreatedAt: msg.CreatedAt.Format("2006-01-02 15:04:05"), + Sender: userMap[msg.SenderUserID], + IsGroupChat: isGroupChat, + MatchContent: matchContent, + }) + } + + return results, nil +} +