Files
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

700 lines
24 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 根据多个字段搜索用户accountuserID、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)
}