Files
kim.dev.6789 b7f8db7d08 复制项目
2026-01-14 22:35:45 +08:00

983 lines
32 KiB
Go
Raw Permalink 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 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
}