复制项目
This commit is contained in:
982
internal/rpc/admin/wallet.go
Normal file
982
internal/rpc/admin/wallet.go
Normal file
@@ -0,0 +1,982 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user