// 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 admin import ( "context" "errors" "time" "git.imall.cloud/openim/chat/pkg/common/constant" chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat" "git.imall.cloud/openim/chat/pkg/common/mctx" "git.imall.cloud/openim/chat/pkg/eerrs" adminpb "git.imall.cloud/openim/chat/pkg/protocol/admin" "git.imall.cloud/openim/protocol/sdkws" "github.com/google/uuid" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "go.mongodb.org/mongo-driver/mongo" ) // ==================== 钱包管理相关 RPC ==================== // GetUserWallet 获取用户钱包信息 func (o *adminServer) GetUserWallet(ctx context.Context, req *adminpb.GetUserWalletReq) (*adminpb.GetUserWalletResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } // 获取钱包信息 wallet, err := o.ChatDatabase.GetWallet(ctx, req.UserID) if err != nil { // 如果钱包不存在,返回默认值 if errors.Is(err, mongo.ErrNoDocuments) { return &adminpb.GetUserWalletResp{ Wallet: &adminpb.WalletInfo{ UserID: req.UserID, Balance: 0, WithdrawAccount: "", RealNameAuth: nil, WithdrawReceiveAccount: "", HasPaymentPassword: false, CreateTime: 0, UpdateTime: 0, }, }, nil } return nil, err } // 转换实名认证信息 var realNameAuth *adminpb.RealNameAuthInfo if wallet.RealNameAuth.IDCard != "" { realNameAuth = &adminpb.RealNameAuthInfo{ IdCard: wallet.RealNameAuth.IDCard, Name: wallet.RealNameAuth.Name, IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront, IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack, AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态:0-未审核,1-审核通过,2-审核拒绝 } } return &adminpb.GetUserWalletResp{ Wallet: &adminpb.WalletInfo{ UserID: wallet.UserID, Balance: wallet.Balance, WithdrawAccount: wallet.WithdrawAccount, RealNameAuth: realNameAuth, WithdrawReceiveAccount: wallet.WithdrawReceiveAccount, HasPaymentPassword: wallet.PaymentPassword != "", CreateTime: wallet.CreateTime.UnixMilli(), UpdateTime: wallet.UpdateTime.UnixMilli(), }, }, nil } // UpdateUserWalletBalance 更新用户余额(后台充值/扣款) // 使用原子操作防止并发问题 func (o *adminServer) UpdateUserWalletBalance(ctx context.Context, req *adminpb.UpdateUserWalletBalanceReq) (*adminpb.UpdateUserWalletBalanceResp, error) { // 检查管理员权限 userID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 获取管理员信息 adminUser, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return nil, err } // 检查是否为超级管理员(level:100) if adminUser.Level != constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can update wallet balance") } // 检查是否设置了操作密码 if adminUser.OperationPassword == "" { return nil, errs.ErrNoPermission.WrapMsg("operation password must be set before updating wallet balance") } // 验证操作密码 if req.OperationPassword == "" { return nil, eerrs.ErrPassword.WrapMsg("operation password is required") } if adminUser.OperationPassword != req.OperationPassword { return nil, eerrs.ErrPassword.WrapMsg("operation password is incorrect") } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } if req.Amount == 0 { return nil, errs.ErrArgs.WrapMsg("amount cannot be zero") } // 使用原子操作更新余额(防止并发问题) // IncrementBalance 方法已经处理了钱包不存在的情况(通过 upsert) // 如果是扣款且余额不足,会返回明确的错误信息 beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, req.UserID, req.Amount) if err != nil { // 所有错误都直接返回,IncrementBalance 已经处理了各种情况: // 1. 余额不足(扣款时):返回明确的错误信息 // 2. 钱包不存在且充值:upsert 会自动创建 // 3. 钱包不存在且扣款:返回余额不足错误 return nil, err } // 创建余额变动记录 record := &chatdb.WalletBalanceRecord{ ID: uuid.New().String(), UserID: req.UserID, Amount: req.Amount, Type: req.Type, BeforeBalance: beforeBalance, AfterBalance: afterBalance, Remark: req.Remark, CreateTime: time.Now(), } if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil { // 余额变动记录创建失败,记录错误日志 // 注意:余额已经更新,但记录创建失败,这是一个严重的数据不一致问题 // 不返回错误,因为余额已经更新,返回错误会让调用方误以为余额未更新 log.ZError(ctx, "Failed to create wallet balance record", err, "userID", req.UserID, "amount", req.Amount, "beforeBalance", beforeBalance, "afterBalance", afterBalance, "type", req.Type, "remark", req.Remark) } return &adminpb.UpdateUserWalletBalanceResp{ Balance: afterBalance, }, nil } // GetUserWalletBalanceRecords 获取用户余额变动记录列表 func (o *adminServer) GetUserWalletBalanceRecords(ctx context.Context, req *adminpb.GetUserWalletBalanceRecordsReq) (*adminpb.GetUserWalletBalanceRecordsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } // 获取余额变动记录列表 total, records, err := o.ChatDatabase.GetWalletBalanceRecords(ctx, req.UserID, req.Pagination) if err != nil { return nil, err } // 转换为响应格式 recordInfos := make([]*adminpb.WalletBalanceRecordInfo, 0, len(records)) for _, record := range records { recordInfos = append(recordInfos, &adminpb.WalletBalanceRecordInfo{ Id: record.ID, UserID: record.UserID, Amount: record.Amount, Type: record.Type, BeforeBalance: record.BeforeBalance, AfterBalance: record.AfterBalance, OrderID: record.OrderID, TransactionID: record.TransactionID, RedPacketID: record.RedPacketID, Remark: record.Remark, CreateTime: record.CreateTime.UnixMilli(), }) } return &adminpb.GetUserWalletBalanceRecordsResp{ Total: uint32(total), List: recordInfos, }, nil } // UpdateUserPaymentPassword 修改用户支付密码(后台) func (o *adminServer) UpdateUserPaymentPassword(ctx context.Context, req *adminpb.UpdateUserPaymentPasswordReq) (*adminpb.UpdateUserPaymentPasswordResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } if req.PaymentPassword == "" { return nil, errs.ErrArgs.WrapMsg("paymentPassword is required") } // 更新支付密码 if err := o.ChatDatabase.UpdateWalletPaymentPassword(ctx, req.UserID, req.PaymentPassword); err != nil { return nil, err } return &adminpb.UpdateUserPaymentPasswordResp{}, nil } // SetUserWithdrawAccount 设置用户提款账号(后台) func (o *adminServer) SetUserWithdrawAccount(ctx context.Context, req *adminpb.SetUserWithdrawAccountReq) (*adminpb.SetUserWithdrawAccountResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } if req.WithdrawAccount == "" { return nil, errs.ErrArgs.WrapMsg("withdrawAccount is required") } // 更新提款账号 if err := o.ChatDatabase.UpdateWalletWithdrawAccount(ctx, req.UserID, req.WithdrawAccount); err != nil { return nil, err } return &adminpb.SetUserWithdrawAccountResp{}, nil } // ==================== 提现管理相关 RPC(操作 withdraw_applications)==================== // GetWithdraw 获取提现申请详情 func (o *adminServer) GetWithdraw(ctx context.Context, req *adminpb.GetWithdrawReq) (*adminpb.GetWithdrawResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.WithdrawID == "" { return nil, errs.ErrArgs.WrapMsg("applicationID is required") } // 获取提现申请 application, err := o.ChatDatabase.GetWithdrawApplication(ctx, req.WithdrawID) if err != nil { return nil, err } // 转换为响应格式 withdrawInfo := &adminpb.WithdrawInfo{ Id: application.ID, UserID: application.UserID, Amount: application.Amount, WithdrawAccount: application.WithdrawAccount, Status: application.Status, AuditorID: application.AuditorID, AuditTime: application.AuditTime.UnixMilli(), AuditRemark: application.AuditRemark, Ip: application.IP, DeviceID: application.DeviceID, Platform: application.Platform, DeviceModel: application.DeviceModel, DeviceBrand: application.DeviceBrand, OsVersion: application.OSVersion, AppVersion: application.AppVersion, CreateTime: application.CreateTime.UnixMilli(), UpdateTime: application.UpdateTime.UnixMilli(), } return &adminpb.GetWithdrawResp{ Withdraw: withdrawInfo, }, nil } // GetUserWithdraws 获取用户的提现申请列表 func (o *adminServer) GetUserWithdraws(ctx context.Context, req *adminpb.GetUserWithdrawsReq) (*adminpb.GetUserWithdrawsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } // 获取用户的提现申请列表 total, applications, err := o.ChatDatabase.GetWithdrawApplicationsByUserID(ctx, req.UserID, req.Pagination) if err != nil { return nil, err } // 查询用户钱包信息(用于获取实名信息) var wallet *chatdb.Wallet wallet, err = o.ChatDatabase.GetWallet(ctx, req.UserID) if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { log.ZWarn(ctx, "Failed to get wallet for user withdraws", err, "userID", req.UserID) } // 转换为响应格式 withdrawInfos := make([]*adminpb.WithdrawInfo, 0, len(applications)) for _, application := range applications { withdrawInfo := &adminpb.WithdrawInfo{ Id: application.ID, UserID: application.UserID, Amount: application.Amount, WithdrawAccount: application.WithdrawAccount, Status: application.Status, AuditorID: application.AuditorID, AuditTime: application.AuditTime.UnixMilli(), AuditRemark: application.AuditRemark, Ip: application.IP, DeviceID: application.DeviceID, Platform: application.Platform, DeviceModel: application.DeviceModel, DeviceBrand: application.DeviceBrand, OsVersion: application.OSVersion, AppVersion: application.AppVersion, CreateTime: application.CreateTime.UnixMilli(), UpdateTime: application.UpdateTime.UnixMilli(), } // 填充用户实名认证信息 if wallet != nil && wallet.RealNameAuth.IDCard != "" { withdrawInfo.RealNameAuth = &adminpb.RealNameAuthInfo{ IdCard: wallet.RealNameAuth.IDCard, Name: wallet.RealNameAuth.Name, IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront, IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack, AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态:0-未审核,1-审核通过,2-审核拒绝 } } withdrawInfos = append(withdrawInfos, withdrawInfo) } return &adminpb.GetUserWithdrawsResp{ Total: uint32(total), List: withdrawInfos, }, nil } // GetWithdraws 获取提现申请列表(后台,支持按状态筛选) func (o *adminServer) GetWithdraws(ctx context.Context, req *adminpb.GetWithdrawsReq) (*adminpb.GetWithdrawsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } var total int64 var applications []*chatdb.WithdrawApplication var err error // 如果指定了状态,按状态筛选;否则获取全部 if req.Status > 0 { total, applications, err = o.ChatDatabase.GetWithdrawApplicationsByStatus(ctx, req.Status, req.Pagination) } else { total, applications, err = o.ChatDatabase.GetWithdrawApplicationsPage(ctx, req.Pagination) } if err != nil { return nil, err } // 收集所有用户ID,批量查询钱包信息(用于获取实名信息) userIDs := make([]string, 0, len(applications)) userIDSet := make(map[string]bool) for _, application := range applications { if !userIDSet[application.UserID] { userIDs = append(userIDs, application.UserID) userIDSet[application.UserID] = true } } // 批量查询钱包信息 walletMap := make(map[string]*chatdb.Wallet) if len(userIDs) > 0 { wallets, err := o.ChatDatabase.GetWalletsByUserIDs(ctx, userIDs) if err != nil { log.ZWarn(ctx, "Failed to get wallets for withdraw list", err, "userIDs", userIDs) } else { for _, wallet := range wallets { walletMap[wallet.UserID] = wallet } } } // 转换为响应格式 withdrawInfos := make([]*adminpb.WithdrawInfo, 0, len(applications)) for _, application := range applications { withdrawInfo := &adminpb.WithdrawInfo{ Id: application.ID, UserID: application.UserID, Amount: application.Amount, WithdrawAccount: application.WithdrawAccount, Status: application.Status, AuditorID: application.AuditorID, AuditTime: application.AuditTime.UnixMilli(), AuditRemark: application.AuditRemark, Ip: application.IP, DeviceID: application.DeviceID, Platform: application.Platform, DeviceModel: application.DeviceModel, DeviceBrand: application.DeviceBrand, OsVersion: application.OSVersion, AppVersion: application.AppVersion, CreateTime: application.CreateTime.UnixMilli(), UpdateTime: application.UpdateTime.UnixMilli(), } // 填充用户实名认证信息 if wallet, ok := walletMap[application.UserID]; ok && wallet.RealNameAuth.IDCard != "" { withdrawInfo.RealNameAuth = &adminpb.RealNameAuthInfo{ IdCard: wallet.RealNameAuth.IDCard, Name: wallet.RealNameAuth.Name, IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront, IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack, AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态:0-未审核,1-审核通过,2-审核拒绝 } } withdrawInfos = append(withdrawInfos, withdrawInfo) } return &adminpb.GetWithdrawsResp{ Total: uint32(total), List: withdrawInfos, }, nil } // AuditWithdraw 批量审核提现申请 func (o *adminServer) AuditWithdraw(ctx context.Context, req *adminpb.AuditWithdrawReq) (*adminpb.AuditWithdrawResp, error) { // 检查管理员权限 auditorID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 验证必填字段 if len(req.WithdrawIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("withdrawIDs is required and cannot be empty") } if req.Status != chatdb.WithdrawApplicationStatusApproved && req.Status != chatdb.WithdrawApplicationStatusRejected { return nil, errs.ErrArgs.WrapMsg("status must be 2 (approved) or 3 (rejected)") } var successCount uint32 var failCount uint32 var failedIDs []string // 批量处理每个提现申请 for _, withdrawID := range req.WithdrawIDs { // 获取提现申请 application, err := o.ChatDatabase.GetWithdrawApplication(ctx, withdrawID) if err != nil { failCount++ failedIDs = append(failedIDs, withdrawID) log.ZWarn(ctx, "Get withdraw application failed", err, "withdrawID", withdrawID) continue } // 检查提现申请状态:允许"待审核"和"已通过"状态的提现申请可以被审核 // 已通过的提现申请可以重新审核为拒绝 if application.Status != chatdb.WithdrawApplicationStatusPending && application.Status != chatdb.WithdrawApplicationStatusApproved { failCount++ failedIDs = append(failedIDs, withdrawID) log.ZWarn(ctx, "Withdraw application status is not pending or approved", nil, "withdrawID", withdrawID, "status", application.Status) continue } // 更新提现申请状态 if err := o.ChatDatabase.UpdateWithdrawApplicationStatus(ctx, withdrawID, req.Status, auditorID, req.AuditRemark); err != nil { failCount++ failedIDs = append(failedIDs, withdrawID) log.ZWarn(ctx, "Update withdraw application status failed", err, "withdrawID", withdrawID) continue } // 如果审核通过,不需要额外操作(因为用户申请时已经扣除了余额) // 如果审核拒绝(包括从"待审核"改为"已拒绝",或从"已通过"改为"已拒绝"),需要将余额退回给用户 if req.Status == chatdb.WithdrawApplicationStatusRejected { // 退回余额 beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, application.UserID, application.Amount) if err != nil { // 记录错误但不影响审核状态更新 log.ZError(ctx, "Refund balance failed", err, "withdrawID", withdrawID, "userID", application.UserID, "amount", application.Amount) } else { // 创建余额变动记录(退款) record := &chatdb.WalletBalanceRecord{ ID: uuid.New().String(), UserID: application.UserID, Amount: application.Amount, Type: 4, // 4-退款 BeforeBalance: beforeBalance, AfterBalance: afterBalance, Remark: "提现审核拒绝,退回余额", CreateTime: time.Now(), } if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil { log.ZWarn(ctx, "Create wallet balance record failed", err, "withdrawID", withdrawID) } } } successCount++ } return &adminpb.AuditWithdrawResp{ SuccessCount: successCount, FailCount: failCount, FailedIDs: failedIDs, }, nil } // BatchUpdateWalletBalance 批量更新用户余额 func (o *adminServer) BatchUpdateWalletBalance(ctx context.Context, req *adminpb.BatchUpdateWalletBalanceReq) (*adminpb.BatchUpdateWalletBalanceResp, error) { // 检查管理员权限 adminID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 获取管理员信息 adminUser, err := o.Database.GetAdminUserID(ctx, adminID) if err != nil { return nil, err } // 检查是否为超级管理员(level:100) if adminUser.Level != constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can batch update wallet balance") } // 检查是否设置了操作密码 if adminUser.OperationPassword == "" { return nil, errs.ErrNoPermission.WrapMsg("operation password must be set before batch updating wallet balance") } // 验证操作密码 if req.OperationPassword == "" { return nil, eerrs.ErrPassword.WrapMsg("operation password is required") } if adminUser.OperationPassword != req.OperationPassword { return nil, eerrs.ErrPassword.WrapMsg("operation password is incorrect") } // 验证必填字段 if len(req.Users) == 0 { return nil, errs.ErrArgs.WrapMsg("users list cannot be empty") } // 验证默认操作类型 defaultOperation := req.Operation if defaultOperation == "" { defaultOperation = "add" // 默认为增加 } if defaultOperation != "set" && defaultOperation != "add" && defaultOperation != "subtract" { return nil, errs.ErrArgs.WrapMsg("default operation must be one of: set, add, subtract") } var results []*adminpb.BatchUpdateResultItem var successCount uint32 var failedCount uint32 // 批量处理每个用户 for _, userItem := range req.Users { result := &adminpb.BatchUpdateResultItem{ UserID: userItem.UserID, PhoneNumber: userItem.PhoneNumber, Account: userItem.Account, Remark: userItem.Remark, } // 1. 根据提供的标识符查找用户 var targetUserID string if userItem.UserID != "" { // 直接使用 userID targetUserID = userItem.UserID } else if userItem.PhoneNumber != "" { // 通过手机号查找用户(假设区号为空或默认) attr, err := o.ChatDatabase.TakeAttributeByPhone(ctx, "", userItem.PhoneNumber) if err != nil { result.Success = false result.Message = "user not found by phone number" results = append(results, result) failedCount++ continue } targetUserID = attr.UserID result.UserID = targetUserID } else if userItem.Account != "" { // 通过账号查找用户 attr, err := o.ChatDatabase.TakeAttributeByAccount(ctx, userItem.Account) if err != nil { result.Success = false result.Message = "user not found by account" results = append(results, result) failedCount++ continue } targetUserID = attr.UserID result.UserID = targetUserID } else { result.Success = false result.Message = "at least one of userID, phoneNumber, or account must be provided" results = append(results, result) failedCount++ continue } // 2. 确定使用的金额和操作类型 amount := userItem.Amount if amount == 0 { amount = req.Amount } operation := userItem.Operation if operation == "" { operation = defaultOperation } if operation != "set" && operation != "add" && operation != "subtract" { result.Success = false result.Message = "operation must be one of: set, add, subtract" results = append(results, result) failedCount++ continue } // 3. 获取当前余额 wallet, err := o.ChatDatabase.GetWallet(ctx, targetUserID) var oldBalance int64 if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { oldBalance = 0 } else { result.Success = false result.Message = "failed to get wallet: " + err.Error() results = append(results, result) failedCount++ continue } } else { oldBalance = wallet.Balance } result.OldBalance = oldBalance // 4. 根据操作类型计算新余额和变动金额 var newBalance int64 var incrementAmount int64 var balanceChangeType int32 = 99 // 99-其他(后台批量操作) switch operation { case "set": newBalance = amount incrementAmount = amount - oldBalance case "add": incrementAmount = amount newBalance = oldBalance + amount case "subtract": incrementAmount = -amount newBalance = oldBalance - amount } // 5. 检查余额是否会变为负数 if newBalance < 0 { result.Success = false result.Message = "insufficient balance: cannot be negative" result.NewBalance = oldBalance results = append(results, result) failedCount++ continue } // 6. 更新余额 beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, targetUserID, incrementAmount) if err != nil { result.Success = false result.Message = "failed to update balance: " + err.Error() result.NewBalance = oldBalance results = append(results, result) failedCount++ continue } result.OldBalance = beforeBalance result.NewBalance = afterBalance // 7. 创建余额变动记录 record := &chatdb.WalletBalanceRecord{ ID: uuid.New().String(), UserID: targetUserID, Amount: incrementAmount, Type: balanceChangeType, BeforeBalance: beforeBalance, AfterBalance: afterBalance, Remark: userItem.Remark, CreateTime: time.Now(), } if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil { log.ZWarn(ctx, "Create wallet balance record failed", err, "userID", targetUserID) } result.Success = true result.Message = "success" results = append(results, result) successCount++ } return &adminpb.BatchUpdateWalletBalanceResp{ Total: uint32(len(req.Users)), Success: successCount, Failed: failedCount, Results: results, }, nil } // GetWallets 获取钱包列表 func (o *adminServer) GetWallets(ctx context.Context, req *adminpb.GetWalletsReq) (*adminpb.GetWalletsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } var total int64 var wallets []*chatdb.Wallet var err error // 如果提供了查询条件,先查找用户 if req.UserID != "" || req.PhoneNumber != "" || req.Account != "" { var userIDs []string if req.UserID != "" { // 直接使用 userID userIDs = []string{req.UserID} } else if req.PhoneNumber != "" { // 通过手机号模糊查询用户 _, attributes, err := o.ChatDatabase.SearchUser(ctx, req.PhoneNumber, nil, nil, &sdkws.RequestPagination{PageNumber: 1, ShowNumber: 1000}) if err != nil { return nil, err } for _, attr := range attributes { userIDs = append(userIDs, attr.UserID) } } else if req.Account != "" { // 通过账号模糊查询用户 _, attributes, err := o.ChatDatabase.SearchUser(ctx, req.Account, nil, nil, &sdkws.RequestPagination{PageNumber: 1, ShowNumber: 1000}) if err != nil { return nil, err } for _, attr := range attributes { userIDs = append(userIDs, attr.UserID) } } // 根据 userIDs 查询钱包 if len(userIDs) > 0 { wallets, err = o.ChatDatabase.GetWalletsByUserIDs(ctx, userIDs) if err != nil { return nil, err } total = int64(len(wallets)) } else { total = 0 wallets = []*chatdb.Wallet{} } } else { // 没有查询条件,获取所有钱包(分页) total, wallets, err = o.ChatDatabase.GetWalletsPage(ctx, req.Pagination) if err != nil { return nil, err } } // 提取所有 userIDs userIDs := make([]string, 0, len(wallets)) for _, wallet := range wallets { userIDs = append(userIDs, wallet.UserID) } // 批量获取用户属性(昵称、头像等) userAttrMap := make(map[string]*chatdb.Attribute) if len(userIDs) > 0 { attributes, err := o.ChatDatabase.FindAttribute(ctx, userIDs) if err != nil { log.ZWarn(ctx, "Find user attributes failed", err, "userIDs", userIDs) } else { for _, attr := range attributes { userAttrMap[attr.UserID] = attr } } } // 转换为响应格式 walletInfos := make([]*adminpb.WalletListItemInfo, 0, len(wallets)) for _, wallet := range wallets { info := &adminpb.WalletListItemInfo{ UserID: wallet.UserID, Balance: wallet.Balance, CreateTime: wallet.CreateTime.UnixMilli(), UpdateTime: wallet.UpdateTime.UnixMilli(), } // 填充用户昵称和头像 if attr, ok := userAttrMap[wallet.UserID]; ok { info.Nickname = attr.Nickname info.FaceURL = attr.FaceURL } walletInfos = append(walletInfos, info) } return &adminpb.GetWalletsResp{ Total: uint32(total), Wallets: walletInfos, }, nil } // GetRealNameAuths 获取实名认证列表(支持按审核状态筛选) func (o *adminServer) GetRealNameAuths(ctx context.Context, req *adminpb.GetRealNameAuthsReq) (*adminpb.GetRealNameAuthsResp, error) { // 检查管理员权限 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } var total int64 var wallets []*chatdb.Wallet var err error // 查询逻辑:过滤身份证号不为空的(已完成实名认证) // auditStatus: 0-待审核,1-审核通过,2-审核拒绝,<0 表示不过滤状态(全部) // userID: 用户ID搜索(可选,为空时不过滤) total, wallets, err = o.ChatDatabase.GetWalletsPageByRealNameAuthAuditStatus(ctx, req.AuditStatus, req.UserID, req.Pagination) if err != nil { return nil, err } // 提取所有 userIDs userIDs := make([]string, 0, len(wallets)) for _, wallet := range wallets { userIDs = append(userIDs, wallet.UserID) } // 批量获取用户属性(昵称、头像等) userAttrMap := make(map[string]*chatdb.Attribute) if len(userIDs) > 0 { attributes, err := o.ChatDatabase.FindAttribute(ctx, userIDs) if err != nil { log.ZWarn(ctx, "Find user attributes failed", err, "userIDs", userIDs) } else { for _, attr := range attributes { userAttrMap[attr.UserID] = attr } } } // 转换为响应格式 authInfos := make([]*adminpb.RealNameAuthListItemInfo, 0, len(wallets)) for _, wallet := range wallets { // 注意:数据库查询已经过滤了身份证号不为空的记录,这里不需要再次检查 // 如果在这里再次过滤,会导致返回数量不一致 // 处理创建时间:如果为零值(负数时间戳),使用更新时间或当前时间 createTime := wallet.CreateTime if createTime.IsZero() || createTime.UnixMilli() < 0 { if !wallet.UpdateTime.IsZero() { createTime = wallet.UpdateTime } else { createTime = time.Now() } } // 处理更新时间:如果为零值(负数时间戳),使用当前时间 updateTime := wallet.UpdateTime if updateTime.IsZero() || updateTime.UnixMilli() < 0 { updateTime = time.Now() } info := &adminpb.RealNameAuthListItemInfo{ UserID: wallet.UserID, IdCard: wallet.RealNameAuth.IDCard, Name: wallet.RealNameAuth.Name, IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront, IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack, AuditStatus: wallet.RealNameAuth.AuditStatus, CreateTime: createTime.UnixMilli(), UpdateTime: updateTime.UnixMilli(), } // 填充用户昵称和头像 if attr, ok := userAttrMap[wallet.UserID]; ok { info.Nickname = attr.Nickname info.FaceURL = attr.FaceURL } authInfos = append(authInfos, info) } // 注意:不在应用层排序,因为数据库查询时已经按 create_time 倒序排序并分页 // 如果在应用层重新排序,会导致分页结果不准确 return &adminpb.GetRealNameAuthsResp{ Total: uint32(total), List: authInfos, }, nil } // AuditRealNameAuth 审核实名认证(通过/拒绝) func (o *adminServer) AuditRealNameAuth(ctx context.Context, req *adminpb.AuditRealNameAuthReq) (*adminpb.AuditRealNameAuthResp, error) { // 检查管理员权限 auditorID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 验证必填字段 if req.UserID == "" { return nil, errs.ErrArgs.WrapMsg("userID is required") } if req.AuditStatus != 1 && req.AuditStatus != 2 { return nil, errs.ErrArgs.WrapMsg("auditStatus must be 1 (approved) or 2 (rejected)") } // 获取钱包信息 wallet, err := o.ChatDatabase.GetWallet(ctx, req.UserID) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return nil, errs.ErrArgs.WrapMsg("wallet not found") } return nil, err } // 检查是否已完成实名认证 if wallet.RealNameAuth.IDCard == "" || wallet.RealNameAuth.Name == "" { return nil, errs.ErrArgs.WrapMsg("user has not completed real name authentication") } // 更新审核状态 wallet.RealNameAuth.AuditStatus = req.AuditStatus if err := o.ChatDatabase.UpdateWalletRealNameAuth(ctx, req.UserID, wallet.RealNameAuth); err != nil { return nil, errs.WrapMsg(err, "failed to update real name auth audit status") } log.ZInfo(ctx, "Real name auth audited", "userID", req.UserID, "auditorID", auditorID, "auditStatus", req.AuditStatus, "auditRemark", req.AuditRemark) return &adminpb.AuditRealNameAuthResp{}, nil }