mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-07-16 02:51:10 +08:00
search phone
This commit is contained in:
parent
e9d75d8e78
commit
d787d49d48
@ -15,3 +15,6 @@ prometheus:
|
|||||||
# Prometheus listening ports, must be consistent with the number of rpc.ports
|
# Prometheus listening ports, must be consistent with the number of rpc.ports
|
||||||
# It will only take effect when autoSetPorts is set to false.
|
# It will only take effect when autoSetPorts is set to false.
|
||||||
ports: [ 12320 ]
|
ports: [ 12320 ]
|
||||||
|
|
||||||
|
# GetUserByPhone: false = ignore phone_visibility when searching by phone; true = enforce phone_visibility (hidden / friends-only).
|
||||||
|
phoneSearchVisibility: false
|
||||||
|
|||||||
@ -163,6 +163,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
userRouterGroup.POST("/set_phone_visibility", u.SetPhoneVisibility)
|
userRouterGroup.POST("/set_phone_visibility", u.SetPhoneVisibility)
|
||||||
userRouterGroup.POST("/set_call_accept_setting", u.SetCallAcceptSetting)
|
userRouterGroup.POST("/set_call_accept_setting", u.SetCallAcceptSetting)
|
||||||
userRouterGroup.POST("/set_msg_receive_setting", u.SetMsgReceiveSetting)
|
userRouterGroup.POST("/set_msg_receive_setting", u.SetMsgReceiveSetting)
|
||||||
|
// 根据手机号精确查找用户(phoneSearchVisibility=true 时遵守 phone_visibility 设置)
|
||||||
|
userRouterGroup.POST("/get_user_by_phone", u.GetUserByPhone)
|
||||||
|
|
||||||
// 全局黑名单管理(仅管理员)
|
// 全局黑名单管理(仅管理员)
|
||||||
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
||||||
|
|||||||
@ -317,3 +317,7 @@ func (u *UserApi) SetCallAcceptSetting(c *gin.Context) {
|
|||||||
func (u *UserApi) SetMsgReceiveSetting(c *gin.Context) {
|
func (u *UserApi) SetMsgReceiveSetting(c *gin.Context) {
|
||||||
a2r.Call(c, user.UserClient.SetMsgReceiveSetting, u.Client)
|
a2r.Call(c, user.UserClient.SetMsgReceiveSetting, u.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) GetUserByPhone(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.GetUserByPhone, u.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -53,6 +54,10 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// phoneRe 仅校验手机号的基本数字格式,不强制区号/国家码前缀。
|
||||||
|
// 规则:纯数字,长度 5-20 位,允许可选的 + 前缀(如 +86...)。
|
||||||
|
var phoneRe = regexp.MustCompile(`^\+?\d{5,20}$`)
|
||||||
|
|
||||||
type userServer struct {
|
type userServer struct {
|
||||||
pbuser.UnimplementedUserServer
|
pbuser.UnimplementedUserServer
|
||||||
online cache.OnlineCache
|
online cache.OnlineCache
|
||||||
@ -296,6 +301,9 @@ func (s *userServer) SetPhoneVisibility(ctx context.Context, req *pbuser.SetPhon
|
|||||||
if req.PhoneVisibility < 0 || req.PhoneVisibility > 2 {
|
if req.PhoneVisibility < 0 || req.PhoneVisibility > 2 {
|
||||||
return nil, errs.ErrArgs.WrapMsg("phoneVisibility must be 0, 1 or 2")
|
return nil, errs.ErrArgs.WrapMsg("phoneVisibility must be 0, 1 or 2")
|
||||||
}
|
}
|
||||||
|
if req.Phone != "" && !phoneRe.MatchString(req.Phone) {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone must contain digits only (5-20 digits), optionally prefixed with +")
|
||||||
|
}
|
||||||
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
|
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
log.ZWarn(ctx, "SetPhoneVisibility: access denied", err,
|
log.ZWarn(ctx, "SetPhoneVisibility: access denied", err,
|
||||||
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
|
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
|
||||||
@ -384,6 +392,65 @@ func (s *userServer) SetMsgReceiveSetting(ctx context.Context, req *pbuser.SetMs
|
|||||||
return &pbuser.SetMsgReceiveSettingResp{}, nil
|
return &pbuser.SetMsgReceiveSettingResp{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByPhone 根据精确手机号查询用户。
|
||||||
|
//
|
||||||
|
// phoneSearchVisibility=false(默认)时忽略 phone_visibility,任何人均可搜到。
|
||||||
|
// phoneSearchVisibility=true 时按 phone_visibility 过滤:
|
||||||
|
// - Hidden(2) → 非管理员不可搜到
|
||||||
|
// - Friends(1) → 仅好友/管理员可搜到
|
||||||
|
// - Public(0) → 任何人均可搜到
|
||||||
|
//
|
||||||
|
// 返回空 userInfo 并不代表错误,调用方应以 nil userInfo 判断"未找到"。
|
||||||
|
func (s *userServer) GetUserByPhone(ctx context.Context, req *pbuser.GetUserByPhoneReq) (*pbuser.GetUserByPhoneResp, error) {
|
||||||
|
if req.Phone == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone is required")
|
||||||
|
}
|
||||||
|
if !phoneRe.MatchString(req.Phone) {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone must contain digits only (5-20 digits), optionally prefixed with +")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser, err := s.db.FindByPhone(ctx, req.Phone)
|
||||||
|
if err != nil {
|
||||||
|
if errs.ErrRecordNotFound.Is(err) {
|
||||||
|
// 手机号未注册,返回空响应而非错误,避免枚举攻击
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
log.ZError(ctx, "GetUserByPhone: FindByPhone failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "phone", req.Phone)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅在 phoneSearchVisibility=true 时才按 phone_visibility 过滤,默认跳过
|
||||||
|
if s.config.RpcConfig.PhoneSearchVisibility {
|
||||||
|
callerID := mcontext.GetOpUserID(ctx)
|
||||||
|
isAdmin := datautil.Contain(callerID, s.config.Share.IMAdminUserID...)
|
||||||
|
|
||||||
|
switch dbUser.PhoneVisibility {
|
||||||
|
case tablerelation.PhoneVisibilityHidden:
|
||||||
|
// 完全隐藏:非管理员无法通过手机号搜到该用户
|
||||||
|
if !isAdmin {
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
case tablerelation.PhoneVisibilityFriends:
|
||||||
|
// 仅好友可搜索
|
||||||
|
if !isAdmin && callerID != dbUser.UserID {
|
||||||
|
isFriend, err := s.relationClient.IsFriend(ctx, callerID, dbUser.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "GetUserByPhone: IsFriend failed", err,
|
||||||
|
"callerID", callerID, "targetUserID", dbUser.UserID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isFriend {
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUser := convert.UserDB2Pb(dbUser)
|
||||||
|
return &pbuser.GetUserByPhoneResp{UserInfo: pbUser}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {
|
func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {
|
||||||
resp = &pbuser.AccountCheckResp{}
|
resp = &pbuser.AccountCheckResp{}
|
||||||
if datautil.Duplicate(req.CheckUserIDs) {
|
if datautil.Duplicate(req.CheckUserIDs) {
|
||||||
|
|||||||
@ -364,6 +364,10 @@ type User struct {
|
|||||||
Ports []int `mapstructure:"ports"`
|
Ports []int `mapstructure:"ports"`
|
||||||
} `mapstructure:"rpc"`
|
} `mapstructure:"rpc"`
|
||||||
Prometheus Prometheus `mapstructure:"prometheus"`
|
Prometheus Prometheus `mapstructure:"prometheus"`
|
||||||
|
// PhoneSearchVisibility 控制 GetUserByPhone 是否尊重 phone_visibility 设置。
|
||||||
|
// false(默认):任何人均可通过手机号搜到用户,忽略 phone_visibility;
|
||||||
|
// true:按 phone_visibility 过滤(Hidden 不可搜,Friends 仅好友可搜)。
|
||||||
|
PhoneSearchVisibility bool `mapstructure:"phoneSearchVisibility"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Redis struct {
|
type Redis struct {
|
||||||
|
|||||||
@ -37,6 +37,9 @@ type UserDatabase interface {
|
|||||||
Find(ctx context.Context, userIDs []string) (users []*model.User, err error)
|
Find(ctx context.Context, userIDs []string) (users []*model.User, err error)
|
||||||
// Find userInfo By Nickname
|
// Find userInfo By Nickname
|
||||||
FindByNickname(ctx context.Context, nickname string) (users []*model.User, err error)
|
FindByNickname(ctx context.Context, 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)
|
||||||
// Find notificationAccounts
|
// Find notificationAccounts
|
||||||
FindNotification(ctx context.Context, level int64) (users []*model.User, err error)
|
FindNotification(ctx context.Context, level int64) (users []*model.User, err error)
|
||||||
// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage
|
// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage
|
||||||
@ -135,6 +138,10 @@ func (u *userDatabase) FindByNickname(ctx context.Context, nickname string) (use
|
|||||||
return u.userDB.TakeByNickname(ctx, nickname)
|
return u.userDB.TakeByNickname(ctx, nickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *userDatabase) FindByPhone(ctx context.Context, phone string) (*model.User, error) {
|
||||||
|
return u.userDB.FindByPhone(ctx, phone)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *userDatabase) FindNotification(ctx context.Context, level int64) (users []*model.User, err error) {
|
func (u *userDatabase) FindNotification(ctx context.Context, level int64) (users []*model.User, err error) {
|
||||||
return u.userDB.TakeNotification(ctx, level)
|
return u.userDB.TakeNotification(ctx, level)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,18 @@ import (
|
|||||||
|
|
||||||
func NewUserMongo(db *mongo.Database) (database.User, error) {
|
func NewUserMongo(db *mongo.Database) (database.User, error) {
|
||||||
coll := db.Collection(database.UserName)
|
coll := db.Collection(database.UserName)
|
||||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
indexes := []mongo.IndexModel{
|
||||||
Keys: bson.D{
|
{
|
||||||
{Key: "user_id", Value: 1},
|
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
},
|
},
|
||||||
Options: options.Index().SetUnique(true),
|
{
|
||||||
})
|
// 支持按手机号快速查找,phone 为空串的文档不参与索引
|
||||||
if err != nil {
|
Keys: bson.D{{Key: "phone", Value: 1}},
|
||||||
|
Options: options.Index().SetSparse(true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := coll.Indexes().CreateMany(context.Background(), indexes); err != nil {
|
||||||
return nil, errs.Wrap(err)
|
return nil, errs.Wrap(err)
|
||||||
}
|
}
|
||||||
return &UserMgo{coll: coll}, nil
|
return &UserMgo{coll: coll}, nil
|
||||||
@ -75,6 +80,10 @@ func (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []*
|
|||||||
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname})
|
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserMgo) FindByPhone(ctx context.Context, phone string) (*model.User, error) {
|
||||||
|
return mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{"phone": phone})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {
|
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)
|
return mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type User interface {
|
|||||||
Take(ctx context.Context, userID string) (user *model.User, err error)
|
Take(ctx context.Context, userID string) (user *model.User, err error)
|
||||||
TakeNotification(ctx context.Context, level int64) (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)
|
TakeByNickname(ctx context.Context, nickname string) (user []*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)
|
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)
|
PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
||||||
PageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
PageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user