// Copyright © 2023 OpenIM open source community. 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 chat import ( "context" "errors" "regexp" "strconv" "strings" "time" "git.imall.cloud/openim/protocol/wrapperspb" "github.com/openimsdk/tools/utils/stringutil" "git.imall.cloud/openim/chat/pkg/common/db/dbutil" chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat" constantpb "git.imall.cloud/openim/protocol/constant" "github.com/openimsdk/tools/mcontext" "go.mongodb.org/mongo-driver/mongo" "git.imall.cloud/openim/chat/pkg/common/constant" "git.imall.cloud/openim/chat/pkg/common/mctx" "git.imall.cloud/openim/chat/pkg/eerrs" "git.imall.cloud/openim/chat/pkg/protocol/chat" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" ) func (o *chatSvr) checkUpdateInfo(ctx context.Context, req *chat.UpdateUserInfoReq) error { if req.AreaCode != nil || req.PhoneNumber != nil { if !(req.AreaCode != nil && req.PhoneNumber != nil) { return errs.ErrArgs.WrapMsg("areaCode and phoneNumber must be set together") } if req.AreaCode.Value == "" || req.PhoneNumber.Value == "" { if req.AreaCode.Value != req.PhoneNumber.Value { return errs.ErrArgs.WrapMsg("areaCode and phoneNumber must be set together") } } } if req.UserID == "" { return errs.ErrArgs.WrapMsg("user id is empty") } credentials, err := o.Database.TakeCredentialsByUserID(ctx, req.UserID) if err != nil { return err } else if len(credentials) == 0 { return errs.ErrArgs.WrapMsg("user not found") } var ( credNum, delNum, addNum = len(credentials), 0, 0 ) addFunc := func(s *wrapperspb.StringValue) { if s != nil { if s.Value != "" { addNum++ } } } for _, s := range []*wrapperspb.StringValue{req.Account, req.PhoneNumber, req.Email} { addFunc(s) } for _, credential := range credentials { switch credential.Type { case constant.CredentialAccount: if req.Account != nil { if req.Account.Value == credential.Account { req.Account = nil } else if req.Account.Value == "" { delNum += 1 } } case constant.CredentialPhone: if req.PhoneNumber != nil { phoneAccount := BuildCredentialPhone(req.AreaCode.Value, req.PhoneNumber.Value) if phoneAccount == credential.Account { req.AreaCode = nil req.PhoneNumber = nil } else if req.PhoneNumber.Value == "" { delNum += 1 } } case constant.CredentialEmail: if req.Email != nil { if req.Email.Value == credential.Account { req.Email = nil } else if req.Email.Value == "" { delNum += 1 } } } } if addNum+credNum-delNum <= 0 { return errs.ErrArgs.WrapMsg("a login method must exist") } if req.PhoneNumber.GetValue() != "" { if !strings.HasPrefix(req.AreaCode.GetValue(), "+") { req.AreaCode.Value = "+" + req.AreaCode.Value } if _, err := strconv.ParseUint(req.AreaCode.Value[1:], 10, 64); err != nil { return errs.ErrArgs.WrapMsg("area code must be number") } if _, err := strconv.ParseUint(req.PhoneNumber.GetValue(), 10, 64); err != nil { return errs.ErrArgs.WrapMsg("phone number must be number") } phoneAccount := BuildCredentialPhone(req.AreaCode.GetValue(), req.PhoneNumber.GetValue()) existingCredential, err := o.Database.TakeCredentialByAccount(ctx, phoneAccount) if err == nil { // 如果手机号已存在,检查是否是当前用户的手机号 if existingCredential.UserID == req.UserID { // 是当前用户的手机号,允许更新(实际上是相同值,不需要更新) req.AreaCode = nil req.PhoneNumber = nil } else { // 是其他用户的手机号,返回错误 return eerrs.ErrPhoneAlreadyRegister.Wrap() } } else if !dbutil.IsDBNotFound(err) { return err } } if req.Account.GetValue() != "" { accountValue := req.Account.GetValue() // 验证长度:6到20位 if len(accountValue) < 6 || len(accountValue) > 20 { return errs.ErrArgs.WrapMsg("account must be between 6 and 20 characters") } // 验证格式:只能包含数字、字母、下划线(_)、横线(-) pattern := `^[a-zA-Z0-9_-]+$` matched, err := regexp.MatchString(pattern, accountValue) if err != nil || !matched { return errs.ErrArgs.WrapMsg("account must contain only letters, numbers, underscores, and hyphens") } existingCredential, err := o.Database.TakeCredentialByAccount(ctx, accountValue) if err == nil { // 如果账户已存在,检查是否是当前用户的账户 if existingCredential.UserID == req.UserID { // 是当前用户的账户,允许更新(实际上是相同值,不需要更新) req.Account = nil } else { // 是其他用户的账户,返回错误 return eerrs.ErrAccountAlreadyRegister.Wrap() } } else if !dbutil.IsDBNotFound(err) { return err } } if req.Email.GetValue() != "" { if !stringutil.IsValidEmail(req.Email.GetValue()) { return errs.ErrArgs.WrapMsg("invalid email") } existingCredential, err := o.Database.TakeCredentialByAccount(ctx, req.Email.GetValue()) if err == nil { // 如果邮箱已存在,检查是否是当前用户的邮箱 if existingCredential.UserID == req.UserID { // 是当前用户的邮箱,允许更新(实际上是相同值,不需要更新) req.Email = nil } else { // 是其他用户的邮箱,返回错误 return eerrs.ErrEmailAlreadyRegister.Wrap() } } else if !dbutil.IsDBNotFound(err) { return err } } return nil } func (o *chatSvr) UpdateUserInfo(ctx context.Context, req *chat.UpdateUserInfoReq) (*chat.UpdateUserInfoResp, error) { opUserID, userType, err := mctx.Check(ctx) if err != nil { return nil, err } if err = o.checkUpdateInfo(ctx, req); err != nil { return nil, err } switch userType { case constant.NormalUser: if req.RegisterType != nil { return nil, errs.ErrNoPermission.WrapMsg("registerType can not be updated") } if req.UserID != opUserID { return nil, errs.ErrNoPermission.WrapMsg("only admin can update other user info") } // 普通用户不能修改自己的用户类型 if req.UserType != 0 { return nil, errs.ErrNoPermission.WrapMsg("normal user can not update userType") } case constant.AdminUser: // 管理员可以修改用户类型,但需要验证值 if req.UserType < 0 || req.UserType > 3 { return nil, errs.ErrArgs.WrapMsg("invalid userType: must be 0-3") } default: return nil, errs.ErrNoPermission.WrapMsg("user type error") } update, err := ToDBAttributeUpdate(req) if err != nil { return nil, err } if userType == constant.NormalUser { delete(update, "user_flag") delete(update, "user_type") } credUpdate, credDel, err := ToDBCredentialUpdate(req, true) if err != nil { return nil, err } if len(update) > 0 { if err := o.Database.UpdateUseInfo(ctx, req.UserID, update, credUpdate, credDel); err != nil { return nil, err } } return &chat.UpdateUserInfoResp{}, nil } func (o *chatSvr) FindUserPublicInfo(ctx context.Context, req *chat.FindUserPublicInfoReq) (*chat.FindUserPublicInfoResp, error) { if len(req.UserIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("UserIDs is empty") } attributes, err := o.Database.FindAttribute(ctx, req.UserIDs) if err != nil { return nil, err } return &chat.FindUserPublicInfoResp{ Users: DbToPbAttributes(attributes), }, nil } func (o *chatSvr) AddUserAccount(ctx context.Context, req *chat.AddUserAccountReq) (*chat.AddUserAccountResp, error) { if _, _, err := mctx.Check(ctx); err != nil { return nil, err } if err := o.checkRegisterInfo(ctx, req.User, true); err != nil { return nil, err } if req.User.UserID == "" { for i := 0; i < 20; i++ { userID := o.genUserID() _, err := o.Database.GetUser(ctx, userID) if err == nil { continue } else if dbutil.IsDBNotFound(err) { req.User.UserID = userID break } else { return nil, err } } if req.User.UserID == "" { return nil, errs.ErrInternalServer.WrapMsg("gen user id failed") } } else { _, err := o.Database.GetUser(ctx, req.User.UserID) if err == nil { return nil, errs.ErrArgs.WrapMsg("appoint user id already register") } else if !dbutil.IsDBNotFound(err) { return nil, err } } var ( credentials []*chatdb.Credential ) if req.User.PhoneNumber != "" { credentials = append(credentials, &chatdb.Credential{ UserID: req.User.UserID, Account: BuildCredentialPhone(req.User.AreaCode, req.User.PhoneNumber), Type: constant.CredentialPhone, AllowChange: true, }) } if req.User.Account != "" { credentials = append(credentials, &chatdb.Credential{ UserID: req.User.UserID, Account: req.User.Account, Type: constant.CredentialAccount, AllowChange: true, }) } if req.User.Email != "" { credentials = append(credentials, &chatdb.Credential{ UserID: req.User.UserID, Account: req.User.Email, Type: constant.CredentialEmail, AllowChange: true, }) } register := &chatdb.Register{ UserID: req.User.UserID, DeviceID: req.DeviceID, IP: req.Ip, Platform: constantpb.PlatformID2Name[int(req.Platform)], AccountType: "", Mode: constant.UserMode, CreateTime: time.Now(), } account := &chatdb.Account{ UserID: req.User.UserID, Password: req.User.Password, OperatorUserID: mcontext.GetOpUserID(ctx), ChangeTime: register.CreateTime, CreateTime: register.CreateTime, } attribute := &chatdb.Attribute{ UserID: req.User.UserID, Account: req.User.Account, PhoneNumber: req.User.PhoneNumber, AreaCode: req.User.AreaCode, Email: req.User.Email, Nickname: req.User.Nickname, FaceURL: req.User.FaceURL, Gender: req.User.Gender, BirthTime: time.UnixMilli(req.User.Birth), ChangeTime: register.CreateTime, CreateTime: register.CreateTime, AllowVibration: constant.DefaultAllowVibration, AllowBeep: constant.DefaultAllowBeep, AllowAddFriend: constant.DefaultAllowAddFriend, } if err := o.Database.RegisterUser(ctx, register, account, attribute, credentials); err != nil { return nil, err } return &chat.AddUserAccountResp{}, nil } func (o *chatSvr) SearchUserPublicInfo(ctx context.Context, req *chat.SearchUserPublicInfoReq) (*chat.SearchUserPublicInfoResp, error) { if _, _, err := mctx.Check(ctx); err != nil { return nil, err } total, list, err := o.Database.Search(ctx, constant.FinDAllUser, req.Keyword, req.Genders, nil, nil, req.Pagination) if err != nil { return nil, err } return &chat.SearchUserPublicInfoResp{ Total: uint32(total), Users: DbToPbAttributes(list), }, nil } func (o *chatSvr) FindUserFullInfo(ctx context.Context, req *chat.FindUserFullInfoReq) (*chat.FindUserFullInfoResp, error) { if _, _, err := mctx.Check(ctx); err != nil { return nil, err } if len(req.UserIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("UserIDs is empty") } attributes, err := o.Database.FindAttribute(ctx, req.UserIDs) if err != nil { return nil, err } // 获取每个用户的最新登录IP userIPMap := make(map[string]string) for _, attr := range attributes { ip, err := o.Database.GetLatestLoginIP(ctx, attr.UserID) if err != nil { // 如果获取IP失败,记录错误但继续处理其他用户 continue } userIPMap[attr.UserID] = ip } return &chat.FindUserFullInfoResp{Users: DbToPbUserFullInfosWithIP(attributes, userIPMap)}, nil } func (o *chatSvr) SearchUserFullInfo(ctx context.Context, req *chat.SearchUserFullInfoReq) (*chat.SearchUserFullInfoResp, error) { if _, _, err := mctx.Check(ctx); err != nil { return nil, err } // 解析时间戳为 time.Time(毫秒时间戳) var startTime, endTime *time.Time if req.StartTime > 0 { st := time.UnixMilli(req.StartTime) startTime = &st } if req.EndTime > 0 { // 将endTime加1000毫秒,确保包含到当天的最后一毫秒 // 例如:endTime=1727740799000 (2025-11-01 23:59:59) 会被转换为 1727740800000 (2025-11-02 00:00:00) // 这样使用 $lt 查询时,会包含 2025-11-01 23:59:59.999 但不包含 2025-11-02 00:00:00 et := time.UnixMilli(req.EndTime + 1000) endTime = &et } // 使用支持实名信息搜索的方法 total, list, err := o.Database.SearchWithRealNameAuth(ctx, req.Normal, req.Keyword, req.Genders, startTime, endTime, req.RealNameKeyword, req.IdCardKeyword, req.Pagination) if err != nil { return nil, err } // 批量获取钱包信息(用于填充实名信息) userIDs := make([]string, 0, len(list)) for _, attr := range list { userIDs = append(userIDs, attr.UserID) } walletMap := make(map[string]*chatdb.Wallet) if len(userIDs) > 0 { wallets, err := o.Database.GetWalletsByUserIDs(ctx, userIDs) if err != nil { log.ZWarn(ctx, "Failed to get wallets for user search", err, "userIDs", userIDs) } else { for _, wallet := range wallets { walletMap[wallet.UserID] = wallet } } } // 获取每个用户的最新登录IP userIPMap := make(map[string]string) for _, attr := range list { ip, err := o.Database.GetLatestLoginIP(ctx, attr.UserID) if err != nil { // 如果获取IP失败,记录错误但继续处理其他用户 log.ZWarn(ctx, "Failed to get latest login IP for user", err, "userID", attr.UserID) // 即使出错也设置空字符串,确保map中有该用户的记录 userIPMap[attr.UserID] = "" continue } // 记录获取到的IP(用于调试) if ip != "" { log.ZDebug(ctx, "Got latest login IP for user", "userID", attr.UserID, "ip", ip) } else { log.ZDebug(ctx, "No login IP found for user (empty string)", "userID", attr.UserID) } userIPMap[attr.UserID] = ip } // 统计有IP的用户数量 usersWithIP := 0 for _, ip := range userIPMap { if ip != "" { usersWithIP++ } } log.ZInfo(ctx, "User IP map summary", "totalUsers", len(list), "ipMapSize", len(userIPMap), "usersWithIP", usersWithIP) return &chat.SearchUserFullInfoResp{ Total: uint32(total), Users: DbToPbUserFullInfosWithRealNameAuthAndIP(list, walletMap, userIPMap), }, nil } // GetUserLoginRecords 查询用户登录记录 func (o *chatSvr) GetUserLoginRecords(ctx context.Context, req *chat.GetUserLoginRecordsReq) (*chat.GetUserLoginRecordsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 查询登录记录 total, records, err := o.Database.SearchUserLoginRecords(ctx, req.UserId, req.Ip, req.Pagination) if err != nil { return nil, err } // 收集所有用户ID userIDs := make([]string, 0, len(records)) userIDSet := make(map[string]bool) for _, record := range records { if !userIDSet[record.UserID] { userIDs = append(userIDs, record.UserID) userIDSet[record.UserID] = true } } // 批量获取用户属性(头像和昵称) userAttrMap := make(map[string]*chatdb.Attribute) if len(userIDs) > 0 { attributes, err := o.Database.FindAttribute(ctx, userIDs) if err != nil { log.ZWarn(ctx, "Failed to get user attributes for login records", err, "userIDs", userIDs) } else { for _, attr := range attributes { userAttrMap[attr.UserID] = attr } } } // 转换结果 var recordInfos []*chat.UserLoginRecordInfo for _, record := range records { recordInfo := &chat.UserLoginRecordInfo{ UserId: record.UserID, LoginTime: record.LoginTime.UnixMilli(), Ip: record.IP, DeviceId: record.DeviceID, Platform: record.Platform, } // 填充用户头像和昵称 if attr, ok := userAttrMap[record.UserID]; ok { recordInfo.FaceUrl = attr.FaceURL recordInfo.Nickname = attr.Nickname } recordInfos = append(recordInfos, recordInfo) } return &chat.GetUserLoginRecordsResp{ Total: uint32(total), Records: recordInfos, }, nil } func (o *chatSvr) FindUserAccount(ctx context.Context, req *chat.FindUserAccountReq) (*chat.FindUserAccountResp, error) { if len(req.UserIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("user id list must be set") } if _, _, err := mctx.CheckAdminOrUser(ctx); err != nil { return nil, err } attributes, err := o.Database.FindAttribute(ctx, req.UserIDs) if err != nil { return nil, err } userAccountMap := make(map[string]string) for _, attribute := range attributes { userAccountMap[attribute.UserID] = attribute.Account } return &chat.FindUserAccountResp{UserAccountMap: userAccountMap}, nil } func (o *chatSvr) FindAccountUser(ctx context.Context, req *chat.FindAccountUserReq) (*chat.FindAccountUserResp, error) { if len(req.Accounts) == 0 { return nil, errs.ErrArgs.WrapMsg("account list must be set") } if _, _, err := mctx.CheckAdminOrUser(ctx); err != nil { return nil, err } attributes, err := o.Database.FindAttribute(ctx, req.Accounts) if err != nil { return nil, err } accountUserMap := make(map[string]string) for _, attribute := range attributes { accountUserMap[attribute.Account] = attribute.UserID } return &chat.FindAccountUserResp{AccountUserMap: accountUserMap}, nil } func (o *chatSvr) SearchUserInfo(ctx context.Context, req *chat.SearchUserInfoReq) (*chat.SearchUserInfoResp, error) { if _, _, err := mctx.Check(ctx); err != nil { return nil, err } total, list, err := o.Database.SearchUser(ctx, req.Keyword, req.UserIDs, req.Genders, req.Pagination) if err != nil { return nil, err } return &chat.SearchUserInfoResp{ Total: uint32(total), Users: DbToPbUserFullInfos(list), }, nil } func (o *chatSvr) CheckUserExist(ctx context.Context, req *chat.CheckUserExistReq) (resp *chat.CheckUserExistResp, err error) { if req.User == nil { return nil, errs.ErrArgs.WrapMsg("user is nil") } if req.User.PhoneNumber != "" { account, err := o.Database.TakeCredentialByAccount(ctx, BuildCredentialPhone(req.User.AreaCode, req.User.PhoneNumber)) // err != nil is not found User if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { return nil, err } if account != nil { return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil } } if req.User.Email != "" { account, err := o.Database.TakeCredentialByAccount(ctx, req.User.AreaCode) if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { return nil, err } if account != nil { return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil } } if req.User.Account != "" { account, err := o.Database.TakeCredentialByAccount(ctx, req.User.Account) if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { return nil, err } if account != nil { return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil } } return nil, nil } func (o *chatSvr) DelUserAccount(ctx context.Context, req *chat.DelUserAccountReq) (resp *chat.DelUserAccountResp, err error) { if err := o.Database.DelUserAccount(ctx, req.UserIDs); err != nil && errs.Unwrap(err) != mongo.ErrNoDocuments { return nil, err } return nil, nil }