diff --git a/internal/api/router.go b/internal/api/router.go index 5eae5b8ac..f689bec03 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -165,6 +165,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co userRouterGroup.POST("/set_msg_receive_setting", u.SetMsgReceiveSetting) // 根据手机号精确查找用户(phoneSearchVisibility=true 时遵守 phone_visibility 设置) userRouterGroup.POST("/get_user_by_phone", u.GetUserByPhone) + // 根据昵称精确查询用户(可多结果,与 getPaginationUsers 模糊搜索不同) + userRouterGroup.POST("/get_users_by_nickname", u.GetUsersByNickname) // 全局黑名单管理(仅管理员) userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist) diff --git a/internal/api/user.go b/internal/api/user.go index 5bb154481..994575fa6 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -321,3 +321,7 @@ func (u *UserApi) SetMsgReceiveSetting(c *gin.Context) { func (u *UserApi) GetUserByPhone(c *gin.Context) { a2r.Call(c, user.UserClient.GetUserByPhone, u.Client) } + +func (u *UserApi) GetUsersByNickname(c *gin.Context) { + a2r.Call(c, user.UserClient.GetUsersByNickname, u.Client) +} diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index cf9f55528..66bd1d682 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -22,6 +22,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/openimsdk/open-im-server/v3/internal/rpc/relation" "github.com/openimsdk/open-im-server/v3/pkg/common/config" @@ -451,6 +452,61 @@ func (s *userServer) GetUserByPhone(ctx context.Context, req *pbuser.GetUserByPh return &pbuser.GetUserByPhoneResp{UserInfo: pbUser}, nil } +// GetUsersByNickname 按昵称精确匹配查询普通用户(app_manger_level 与分页拉取用户一致)。 +// 全局黑名单用户会被过滤;手机号字段按 phone_visibility 与 getDesignateUsers 相同规则处理。 +func (s *userServer) GetUsersByNickname(ctx context.Context, req *pbuser.GetUsersByNicknameReq) (*pbuser.GetUsersByNicknameResp, error) { + nickname := strings.TrimSpace(req.Nickname) + if nickname == "" { + return nil, errs.ErrArgs.WrapMsg("nickname is required") + } + if n := utf8.RuneCountInString(nickname); n < 1 || n > 64 { + return nil, errs.ErrArgs.WrapMsg("nickname length must be 1-64 characters") + } + + users, err := s.db.FindOrdinaryUsersByNickname(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, nickname) + if err != nil { + log.ZError(ctx, "GetUsersByNickname: FindOrdinaryUsersByNickname failed", err, + "opUserID", mcontext.GetOpUserID(ctx), "nickname", nickname) + return nil, err + } + if len(users) == 0 { + return &pbuser.GetUsersByNicknameResp{}, nil + } + + userIDs := datautil.Slice(users, func(u *tablerelation.User) string { return u.UserID }) + blocked, err := s.globalBlackDB.FindBlocked(ctx, userIDs) + if err != nil { + log.ZError(ctx, "GetUsersByNickname: FindBlocked failed", err, + "opUserID", mcontext.GetOpUserID(ctx), "count", len(userIDs)) + return nil, err + } + if len(blocked) > 0 { + banned := make(map[string]struct{}, len(blocked)) + for _, b := range blocked { + banned[b.UserID] = struct{}{} + } + filtered := make([]*tablerelation.User, 0, len(users)) + for _, u := range users { + if _, ok := banned[u.UserID]; !ok { + filtered = append(filtered, u) + } + } + users = filtered + } + if len(users) == 0 { + return &pbuser.GetUsersByNicknameResp{}, nil + } + + pbUsers := convert.UsersDB2Pb(users) + viewerID := mcontext.GetOpUserID(ctx) + if err := s.applyPhoneVisibility(ctx, viewerID, pbUsers, users); err != nil { + log.ZError(ctx, "GetUsersByNickname: applyPhoneVisibility failed", err, + "opUserID", viewerID, "count", len(users)) + return nil, err + } + return &pbuser.GetUsersByNicknameResp{UsersInfo: pbUsers}, nil +} + func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) { resp = &pbuser.AccountCheckResp{} if datautil.Duplicate(req.CheckUserIDs) { diff --git a/pkg/common/storage/controller/user.go b/pkg/common/storage/controller/user.go index db056ae9a..d1ff44101 100644 --- a/pkg/common/storage/controller/user.go +++ b/pkg/common/storage/controller/user.go @@ -37,6 +37,8 @@ type UserDatabase interface { Find(ctx context.Context, userIDs []string) (users []*model.User, err error) // Find userInfo By Nickname FindByNickname(ctx context.Context, nickname string) (users []*model.User, err error) + // FindOrdinaryUsersByNickname 昵称精确匹配,仅普通用户(与分页拉取用户 level 一致) + FindOrdinaryUsersByNickname(ctx context.Context, level1 int64, level2 int64, nickname string) (users []*model.User, err error) // FindByPhone looks up a single user by exact phone number. // Returns errs.ErrRecordNotFound if no user has the given phone. FindByPhone(ctx context.Context, phone string) (user *model.User, err error) @@ -138,6 +140,10 @@ func (u *userDatabase) FindByNickname(ctx context.Context, nickname string) (use return u.userDB.TakeByNickname(ctx, nickname) } +func (u *userDatabase) FindOrdinaryUsersByNickname(ctx context.Context, level1, level2 int64, nickname string) ([]*model.User, error) { + return u.userDB.FindOrdinaryUsersByNickname(ctx, level1, level2, nickname) +} + func (u *userDatabase) FindByPhone(ctx context.Context, phone string) (*model.User, error) { return u.userDB.FindByPhone(ctx, phone) } diff --git a/pkg/common/storage/database/mgo/user.go b/pkg/common/storage/database/mgo/user.go index b2e437f14..5a2dc7e34 100644 --- a/pkg/common/storage/database/mgo/user.go +++ b/pkg/common/storage/database/mgo/user.go @@ -38,10 +38,12 @@ func NewUserMongo(db *mongo.Database) (database.User, error) { Options: options.Index().SetUnique(true), }, { - // 支持按手机号快速查找,phone 为空串的文档不参与索引 Keys: bson.D{{Key: "phone", Value: 1}}, Options: options.Index().SetSparse(true), }, + { + Keys: bson.D{{Key: "nickname", Value: 1}}, + }, } if _, err := coll.Indexes().CreateMany(context.Background(), indexes); err != nil { return nil, errs.Wrap(err) @@ -80,6 +82,14 @@ func (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []* return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname}) } +func (u *UserMgo) FindOrdinaryUsersByNickname(ctx context.Context, level1, level2 int64, nickname string) ([]*model.User, error) { + query := bson.M{ + "nickname": nickname, + "app_manger_level": bson.M{"$in": []int64{level1, level2}}, + } + return mongoutil.Find[*model.User](ctx, u.coll, query, options.Find().SetLimit(100)) +} + func (u *UserMgo) FindByPhone(ctx context.Context, phone string) (*model.User, error) { return mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{"phone": phone}) } diff --git a/pkg/common/storage/database/user.go b/pkg/common/storage/database/user.go index 7e7df0836..2682bc780 100644 --- a/pkg/common/storage/database/user.go +++ b/pkg/common/storage/database/user.go @@ -29,6 +29,8 @@ type User interface { Take(ctx context.Context, userID string) (user *model.User, err error) TakeNotification(ctx context.Context, level int64) (user []*model.User, err error) TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error) + // FindOrdinaryUsersByNickname 按昵称精确匹配,且 app_manger_level 为普通用户范围(与分页拉取用户一致) + FindOrdinaryUsersByNickname(ctx context.Context, level1 int64, level2 int64, nickname string) (users []*model.User, err error) FindByPhone(ctx context.Context, phone string) (user *model.User, err error) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)