// Copyright © 2023 OpenIM. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mgo import ( "context" "time" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model" "git.imall.cloud/openim/protocol/user" "github.com/openimsdk/tools/db/mongoutil" "github.com/openimsdk/tools/db/pagination" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func NewUserMongo(db *mongo.Database) (database.User, error) { coll := db.Collection(database.UserName) _, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{ Keys: bson.D{ {Key: "user_id", Value: 1}, }, Options: options.Index().SetUnique(true), }) if err != nil { return nil, errs.Wrap(err) } return &UserMgo{coll: coll}, nil } type UserMgo struct { coll *mongo.Collection } func (u *UserMgo) Create(ctx context.Context, users []*model.User) error { return mongoutil.InsertMany(ctx, u.coll, users) } func (u *UserMgo) UpdateByMap(ctx context.Context, userID string, args map[string]any) (err error) { if len(args) == 0 { return nil } return mongoutil.UpdateOne(ctx, u.coll, bson.M{"user_id": userID}, bson.M{"$set": args}, true) } func (u *UserMgo) Find(ctx context.Context, userIDs []string) (users []*model.User, err error) { query := bson.M{"user_id": bson.M{"$in": userIDs}} log.ZInfo(ctx, "UserMongo Find query", "collection", u.coll.Name(), "query", query) users, err = mongoutil.Find[*model.User](ctx, u.coll, query) log.ZInfo(ctx, "UserMongo Find result", "userCount", len(users), "err", err) return users, err } func (u *UserMgo) Take(ctx context.Context, userID string) (user *model.User, err error) { return mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{"user_id": userID}) } func (u *UserMgo) TakeNotification(ctx context.Context, level int64) (user []*model.User, err error) { return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"app_manger_level": level}) } func (u *UserMgo) TakeGTEAppManagerLevel(ctx context.Context, level int64) (user []*model.User, err error) { return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"app_manger_level": bson.M{"$gte": level}}) } func (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error) { return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname}) } func (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) { return mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination) } func (u *UserMgo) PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error) { query := bson.M{ "$or": []bson.M{ {"app_manger_level": level1}, {"app_manger_level": level2}, }, } return mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination) } func (u *UserMgo) PageFindUserWithKeyword( ctx context.Context, level1 int64, level2 int64, userID string, nickName string, pagination pagination.Pagination, ) (count int64, users []*model.User, err error) { // Initialize the base query with level conditions query := bson.M{ "$and": []bson.M{ {"app_manger_level": bson.M{"$in": []int64{level1, level2}}}, }, } // Add userID and userName conditions to the query if they are provided if userID != "" || nickName != "" { userConditions := []bson.M{} if userID != "" { // Use regex for userID regexPattern := primitive.Regex{Pattern: userID, Options: "i"} // 'i' for case-insensitive matching userConditions = append(userConditions, bson.M{"user_id": regexPattern}) } if nickName != "" { // Use regex for userName regexPattern := primitive.Regex{Pattern: nickName, Options: "i"} // 'i' for case-insensitive matching userConditions = append(userConditions, bson.M{"nickname": regexPattern}) } query["$and"] = append(query["$and"].([]bson.M), bson.M{"$or": userConditions}) } // Perform the paginated search return mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination) } func (u *UserMgo) GetAllUserID(ctx context.Context, pagination pagination.Pagination) (int64, []string, error) { return mongoutil.FindPage[string](ctx, u.coll, bson.M{}, pagination, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1})) } func (u *UserMgo) Exist(ctx context.Context, userID string) (exist bool, err error) { return mongoutil.Exist(ctx, u.coll, bson.M{"user_id": userID}) } func (u *UserMgo) GetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error) { return mongoutil.FindOne[int](ctx, u.coll, bson.M{"user_id": userID}, options.FindOne().SetProjection(bson.M{"_id": 0, "global_recv_msg_opt": 1})) } // SearchUsersByFields 根据多个字段搜索用户:account(userID)、phone、nickname // 返回匹配的用户ID列表 // 使用 MongoDB $lookup 实现连表查询:attribute 集合和 user 集合 func (u *UserMgo) SearchUsersByFields(ctx context.Context, account, phone, nickname string) (userIDs []string, err error) { log.ZInfo(ctx, "SearchUsersByFields START", "account", account, "phone", phone, "nickname", nickname) // 获取 attribute 集合 attributeColl := u.coll.Database().Collection("attribute") log.ZInfo(ctx, "SearchUsersByFields collections", "attributeCollection", "attribute", "userCollection", u.coll.Name(), "database", u.coll.Database().Name()) // 构建聚合管道,使用 $lookup 实现连表查询 pipeline := bson.A{} // 第一步:从 attribute 集合开始,构建匹配条件 attributeMatch := bson.M{} attributeOrConditions := []bson.M{} if account != "" { attributeOrConditions = append(attributeOrConditions, bson.M{"account": bson.M{"$regex": account, "$options": "i"}}) log.ZInfo(ctx, "SearchUsersByFields add account condition", "account", account) } if phone != "" { // phone_number 在 attribute 集合中搜索(注意:字段名是 phone_number,不是 phone) attributeOrConditions = append(attributeOrConditions, bson.M{"phone_number": bson.M{"$regex": phone, "$options": "i"}}) log.ZInfo(ctx, "SearchUsersByFields add phone condition", "phone", phone, "field", "phone_number") } // 判断查询策略:如果有 account 或 phone,从 attribute 开始;如果只有 nickname,从 user 开始 hasAttributeSearch := len(attributeOrConditions) > 0 hasUserSearch := nickname != "" var startCollection *mongo.Collection // 记录起始集合 if hasAttributeSearch { // 从 attribute 集合开始查询 startCollection = attributeColl // 先直接查询 attribute 集合,看看实际的数据结构 type FullAttributeDoc map[string]interface{} allDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, bson.M{}, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0})) if err == nil && len(allDocs) > 0 { log.ZInfo(ctx, "SearchUsersByFields attribute sample documents (all fields)", "sampleCount", len(allDocs), "samples", allDocs) } // 尝试精确匹配手机号(使用正确的字段名 phone_number) exactPhoneMatch := bson.M{"phone_number": phone} exactDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, exactPhoneMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0})) if err == nil { log.ZInfo(ctx, "SearchUsersByFields exact phone_number match", "phone", phone, "matchCount", len(exactDocs), "docs", exactDocs) } // 尝试正则匹配(不区分大小写,使用正确的字段名 phone_number) regexPhoneMatch := bson.M{"phone_number": bson.M{"$regex": phone, "$options": "i"}} regexDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, regexPhoneMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0})) if err == nil { log.ZInfo(ctx, "SearchUsersByFields regex phone_number match", "phone", phone, "matchCount", len(regexDocs), "docs", regexDocs) } // 尝试查询包含 phone_number 字段的所有记录 hasPhoneFieldMatch := bson.M{"phone_number": bson.M{"$exists": true, "$ne": ""}} hasPhoneDocs, err := mongoutil.Find[*FullAttributeDoc](ctx, attributeColl, hasPhoneFieldMatch, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0})) if err == nil { log.ZInfo(ctx, "SearchUsersByFields documents with phone_number field", "matchCount", len(hasPhoneDocs), "docs", hasPhoneDocs) } attributeMatch["$or"] = attributeOrConditions pipeline = append(pipeline, bson.M{"$match": attributeMatch}) log.ZInfo(ctx, "SearchUsersByFields attribute match stage", "match", attributeMatch) // 使用 $lookup 关联 user 集合 lookupStage := bson.M{ "$lookup": bson.M{ "from": u.coll.Name(), // user 集合名称 "localField": "user_id", // attribute 集合的字段 "foreignField": "user_id", // user 集合的字段 "as": "userInfo", // 关联后的字段名 }, } pipeline = append(pipeline, lookupStage) log.ZInfo(ctx, "SearchUsersByFields add lookup stage", "from", u.coll.Name(), "localField", "user_id", "foreignField", "user_id") // 展开 userInfo 数组 pipeline = append(pipeline, bson.M{"$unwind": bson.M{ "path": "$userInfo", "preserveNullAndEmptyArrays": true, }}) // 如果有 nickname 条件,需要匹配 user 集合的 nickname if hasUserSearch { userMatch := bson.M{ "$or": []bson.M{ {"userInfo.nickname": bson.M{"$regex": nickname, "$options": "i"}}, {"userInfo": bson.M{"$exists": false}}, // 如果没有关联到 user,也保留 }, } pipeline = append(pipeline, bson.M{"$match": userMatch}) log.ZInfo(ctx, "SearchUsersByFields add nickname match", "nickname", nickname) } // 从 attribute 集合开始,user_id 字段在根级别 pipeline = append(pipeline, bson.M{ "$project": bson.M{ "_id": 0, "user_id": 1, }, }) } else if hasUserSearch { // 只有 nickname 条件,从 user 集合开始查询 startCollection = u.coll userMatch := bson.M{"nickname": bson.M{"$regex": nickname, "$options": "i"}} pipeline = append(pipeline, bson.M{"$match": userMatch}) log.ZInfo(ctx, "SearchUsersByFields user match stage", "match", userMatch) // 从 user 集合开始,user_id 字段在根级别 pipeline = append(pipeline, bson.M{ "$project": bson.M{ "_id": 0, "user_id": 1, }, }) } else { // 没有任何搜索条件,返回空 log.ZInfo(ctx, "SearchUsersByFields no search conditions", "returning empty") return []string{}, nil } // 第五步:去重 pipeline = append(pipeline, bson.M{ "$group": bson.M{ "_id": "$user_id", "user_id": bson.M{"$first": "$user_id"}, }, }) // 第六步:只返回 user_id pipeline = append(pipeline, bson.M{ "$project": bson.M{ "_id": 0, "user_id": 1, }, }) log.ZInfo(ctx, "SearchUsersByFields pipeline", "pipeline", pipeline, "startCollection", startCollection.Name()) // 执行聚合查询 type ResultDoc struct { UserID string `bson:"user_id"` } results, err := mongoutil.Aggregate[*ResultDoc](ctx, startCollection, pipeline) if err != nil { log.ZError(ctx, "SearchUsersByFields Aggregate failed", err, "pipeline", pipeline) return nil, err } log.ZInfo(ctx, "SearchUsersByFields Aggregate result", "resultCount", len(results), "results", results) // 提取 user_id 列表 userIDs = make([]string, 0, len(results)) for _, result := range results { if result.UserID != "" { userIDs = append(userIDs, result.UserID) } } log.ZInfo(ctx, "SearchUsersByFields FINAL result", "totalUserIDs", len(userIDs), "userIDs", userIDs) return userIDs, nil } // 旧版本的实现(保留作为备份) func (u *UserMgo) SearchUsersByFields_old(ctx context.Context, account, phone, nickname string) (userIDs []string, err error) { log.ZInfo(ctx, "SearchUsersByFields START", "account", account, "phone", phone, "nickname", nickname) userIDMap := make(map[string]bool) // 用于去重 // 获取 attribute 集合 attributeColl := u.coll.Database().Collection("attribute") log.ZInfo(ctx, "SearchUsersByFields attribute collection", "collectionName", "attribute", "database", u.coll.Database().Name()) // 从 attribute 集合查询 account 和 phone if account != "" || phone != "" { attributeFilter := bson.M{} attributeConditions := []bson.M{} if account != "" { // account 在 attribute 集合中搜索 attributeConditions = append(attributeConditions, bson.M{"account": bson.M{"$regex": account, "$options": "i"}}) log.ZInfo(ctx, "SearchUsersByFields add account condition", "account", account) } if phone != "" { // phone 在 attribute 集合中搜索 attributeConditions = append(attributeConditions, bson.M{"phone": bson.M{"$regex": phone, "$options": "i"}}) log.ZInfo(ctx, "SearchUsersByFields add phone condition", "phone", phone) } if len(attributeConditions) > 0 { attributeFilter["$or"] = attributeConditions log.ZInfo(ctx, "SearchUsersByFields query attribute", "filter", attributeFilter, "account", account, "phone", phone, "conditionsCount", len(attributeConditions)) // attribute 集合的结构包含:user_id, account, phone 等字段 type AttributeDoc struct { UserID string `bson:"user_id"` } // 先尝试查询,看看集合是否存在数据 count, err := mongoutil.Count(ctx, attributeColl, bson.M{}) log.ZInfo(ctx, "SearchUsersByFields attribute collection total count", "count", count, "err", err) // 尝试查询多条记录看看结构,特别是包含 phone 数据的记录 type SampleDoc struct { UserID string `bson:"user_id"` Account string `bson:"account"` Phone string `bson:"phone"` } // 查询所有记录,看看实际的数据结构 samples, err := mongoutil.Find[*SampleDoc](ctx, attributeColl, bson.M{}, options.Find().SetLimit(10).SetProjection(bson.M{"_id": 0, "user_id": 1, "account": 1, "phone": 1})) if err == nil && len(samples) > 0 { log.ZInfo(ctx, "SearchUsersByFields attribute sample documents", "sampleCount", len(samples), "samples", samples) // 尝试查询包含 phone 字段不为空的记录 phoneFilter := bson.M{"phone": bson.M{"$exists": true, "$ne": ""}} phoneSamples, err := mongoutil.Find[*SampleDoc](ctx, attributeColl, phoneFilter, options.Find().SetLimit(5).SetProjection(bson.M{"_id": 0, "user_id": 1, "account": 1, "phone": 1})) if err == nil { log.ZInfo(ctx, "SearchUsersByFields attribute documents with phone", "phoneSampleCount", len(phoneSamples), "phoneSamples", phoneSamples) } else { log.ZWarn(ctx, "SearchUsersByFields cannot find documents with phone", err) } // 尝试查询所有字段,看看实际的数据结构(只查一条) type FullDoc map[string]interface{} fullSample, err := mongoutil.FindOne[*FullDoc](ctx, attributeColl, bson.M{}, options.FindOne().SetProjection(bson.M{"_id": 0})) if err == nil && fullSample != nil { log.ZInfo(ctx, "SearchUsersByFields attribute full document structure", "fullSample", fullSample) } } else { log.ZWarn(ctx, "SearchUsersByFields cannot get samples from attribute", err, "sampleCount", len(samples)) } // 尝试精确匹配手机号,看看是否有数据 exactPhoneFilter := bson.M{"phone": phone} exactCount, err := mongoutil.Count(ctx, attributeColl, exactPhoneFilter) log.ZInfo(ctx, "SearchUsersByFields exact phone match count", "phone", phone, "count", exactCount, "err", err) attributeDocs, err := mongoutil.Find[*AttributeDoc](ctx, attributeColl, attributeFilter, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1})) if err != nil { log.ZError(ctx, "SearchUsersByFields Find failed in attribute collection", err, "filter", attributeFilter) return nil, err } log.ZInfo(ctx, "SearchUsersByFields Find result from attribute", "userCount", len(attributeDocs), "userIDs", attributeDocs) for i, doc := range attributeDocs { log.ZDebug(ctx, "SearchUsersByFields processing attribute doc", "index", i, "userID", doc.UserID) if doc.UserID != "" && !userIDMap[doc.UserID] { userIDMap[doc.UserID] = true log.ZDebug(ctx, "SearchUsersByFields added userID from attribute", "userID", doc.UserID) } } } } // 从 user 集合查询 nickname if nickname != "" { userFilter := bson.M{"nickname": bson.M{"$regex": nickname, "$options": "i"}} log.ZInfo(ctx, "SearchUsersByFields query user", "filter", userFilter, "nickname", nickname) users, err := mongoutil.Find[*model.User](ctx, u.coll, userFilter, options.Find().SetProjection(bson.M{"_id": 0, "user_id": 1})) if err != nil { log.ZError(ctx, "SearchUsersByFields Find failed in user collection", err, "filter", userFilter) return nil, err } log.ZInfo(ctx, "SearchUsersByFields Find result from user", "userCount", len(users)) for i, user := range users { log.ZDebug(ctx, "SearchUsersByFields processing user doc", "index", i, "userID", user.UserID) if user.UserID != "" && !userIDMap[user.UserID] { userIDMap[user.UserID] = true log.ZDebug(ctx, "SearchUsersByFields added userID from user", "userID", user.UserID) } } } // 将 map 转换为 slice userIDs = make([]string, 0, len(userIDMap)) for userID := range userIDMap { userIDs = append(userIDs, userID) } log.ZInfo(ctx, "SearchUsersByFields FINAL result", "totalUserIDs", len(userIDs), "userIDs", userIDs) return userIDs, nil } func (u *UserMgo) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) { if before == nil { return mongoutil.Count(ctx, u.coll, bson.M{}) } return mongoutil.Count(ctx, u.coll, bson.M{"create_time": bson.M{"$lt": before}}) } func (u *UserMgo) AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error { collection := u.coll.Database().Collection("userCommands") // Create a new document instead of updating an existing one doc := bson.M{ "userID": userID, "type": Type, "uuid": UUID, "createTime": time.Now().Unix(), // assuming you want the creation time in Unix timestamp "value": value, "ex": ex, } _, err := collection.InsertOne(ctx, doc) return errs.Wrap(err) } func (u *UserMgo) DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error { collection := u.coll.Database().Collection("userCommands") filter := bson.M{"userID": userID, "type": Type, "uuid": UUID} result, err := collection.DeleteOne(ctx, filter) // when err is not nil, result might be nil if err != nil { return errs.Wrap(err) } if result.DeletedCount == 0 { // No records found to update return errs.Wrap(errs.ErrRecordNotFound) } return errs.Wrap(err) } func (u *UserMgo) UpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error { if len(val) == 0 { return nil } collection := u.coll.Database().Collection("userCommands") filter := bson.M{"userID": userID, "type": Type, "uuid": UUID} update := bson.M{"$set": val} result, err := collection.UpdateOne(ctx, filter, update) if err != nil { return errs.Wrap(err) } if result.MatchedCount == 0 { // No records found to update return errs.Wrap(errs.ErrRecordNotFound) } return nil } func (u *UserMgo) GetUserCommand(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error) { collection := u.coll.Database().Collection("userCommands") filter := bson.M{"userID": userID, "type": Type} cursor, err := collection.Find(ctx, filter) if err != nil { return nil, err } defer cursor.Close(ctx) // Initialize commands as a slice of pointers commands := []*user.CommandInfoResp{} for cursor.Next(ctx) { var document struct { Type int32 `bson:"type"` UUID string `bson:"uuid"` Value string `bson:"value"` CreateTime int64 `bson:"createTime"` Ex string `bson:"ex"` } if err := cursor.Decode(&document); err != nil { return nil, err } commandInfo := &user.CommandInfoResp{ Type: document.Type, Uuid: document.UUID, Value: document.Value, CreateTime: document.CreateTime, Ex: document.Ex, } commands = append(commands, commandInfo) } if err := cursor.Err(); err != nil { return nil, errs.Wrap(err) } return commands, nil } func (u *UserMgo) GetAllUserCommand(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error) { collection := u.coll.Database().Collection("userCommands") filter := bson.M{"userID": userID} cursor, err := collection.Find(ctx, filter) if err != nil { return nil, errs.Wrap(err) } defer cursor.Close(ctx) // Initialize commands as a slice of pointers commands := []*user.AllCommandInfoResp{} for cursor.Next(ctx) { var document struct { Type int32 `bson:"type"` UUID string `bson:"uuid"` Value string `bson:"value"` CreateTime int64 `bson:"createTime"` Ex string `bson:"ex"` } if err := cursor.Decode(&document); err != nil { return nil, errs.Wrap(err) } commandInfo := &user.AllCommandInfoResp{ Type: document.Type, Uuid: document.UUID, Value: document.Value, CreateTime: document.CreateTime, Ex: document.Ex, } commands = append(commands, commandInfo) } if err := cursor.Err(); err != nil { return nil, errs.Wrap(err) } return commands, nil } func (u *UserMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) { pipeline := bson.A{ bson.M{ "$match": bson.M{ "create_time": bson.M{ "$gte": start, "$lt": end, }, }, }, bson.M{ "$group": bson.M{ "_id": bson.M{ "$dateToString": bson.M{ "format": "%Y-%m-%d", "date": "$create_time", }, }, "count": bson.M{ "$sum": 1, }, }, }, } type Item struct { Date string `bson:"_id"` Count int64 `bson:"count"` } items, err := mongoutil.Aggregate[Item](ctx, u.coll, pipeline) if err != nil { return nil, err } res := make(map[string]int64, len(items)) for _, item := range items { res[item.Date] = item.Count } return res, nil } func (u *UserMgo) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) { if len(userIDName) == 0 { return nil, nil } userIDs := make([]string, 0, len(userIDName)) attached := make(map[string]string) for userID, name := range userIDName { userIDs = append(userIDs, userID) if name == "" { continue } attached[userID] = name } var sortValue int if asc { sortValue = 1 } else { sortValue = -1 } if len(attached) == 0 { filter := bson.M{"user_id": bson.M{"$in": userIDs}} opt := options.Find().SetSort(bson.M{"nickname": sortValue}) return mongoutil.Find[*model.User](ctx, u.coll, filter, opt) } pipeline := []bson.M{ { "$match": bson.M{ "user_id": bson.M{"$in": userIDs}, }, }, { "$addFields": bson.M{ "_query_sort_name": bson.M{ "$arrayElemAt": []any{ bson.M{ "$filter": bson.M{ "input": bson.M{ "$objectToArray": attached, }, "as": "item", "cond": bson.M{ "$eq": []any{"$$item.k", "$user_id"}, }, }, }, 0, }, }, }, }, { "$addFields": bson.M{ "_query_sort_name": bson.M{ "$ifNull": []any{"$_query_sort_name.v", "$nickname"}, }, }, }, { "$sort": bson.M{ "_query_sort_name": sortValue, }, }, } return mongoutil.Aggregate[*model.User](ctx, u.coll, pipeline) }