复制项目

This commit is contained in:
kim.dev.6789
2026-01-14 22:35:45 +08:00
parent 305d526110
commit b7f8db7d08
297 changed files with 81784 additions and 0 deletions

623
internal/rpc/chat/user.go Normal file
View File

@@ -0,0 +1,623 @@
// 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
}