Files
chat-deploy/internal/rpc/chat/user.go
kim.dev.6789 b7f8db7d08 复制项目
2026-01-14 22:35:45 +08:00

624 lines
19 KiB
Go
Raw 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 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
}