// Copyright © 2023 OpenIM. 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 api import ( "strconv" "time" "github.com/gin-gonic/gin" "git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model" "git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli" "github.com/openimsdk/tools/apiresp" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/utils/idutil" "github.com/openimsdk/tools/utils/timeutil" ) type WalletApi struct { walletDB database.Wallet walletBalanceRecordDB database.WalletBalanceRecord userDB database.User userClient *rpcli.UserClient } func NewWalletApi(walletDB database.Wallet, walletBalanceRecordDB database.WalletBalanceRecord, userDB database.User, userClient *rpcli.UserClient) *WalletApi { return &WalletApi{ walletDB: walletDB, walletBalanceRecordDB: walletBalanceRecordDB, userDB: userDB, userClient: userClient, } } // updateBalanceWithRecord 统一的余额更新方法,包含余额记录创建和并发控制 // operation: 操作类型(set/add/subtract) // amount: 金额(分) // oldBalance: 旧余额 // oldVersion: 旧版本号 // remark: 备注信息 // 返回新余额、新版本号和错误 func (w *WalletApi) updateBalanceWithRecord(ctx *gin.Context, userID string, operation string, amount int64, oldBalance int64, oldVersion int64, remark string) (newBalance int64, newVersion int64, err error) { // 使用版本号更新余额(防止并发覆盖) params := &database.WalletUpdateParams{ UserID: userID, Operation: operation, Amount: amount, OldBalance: oldBalance, OldVersion: oldVersion, } result, err := w.walletDB.UpdateBalanceWithVersion(ctx, params) if err != nil { return 0, 0, err } // 计算变动金额:用于余额记录 var recordAmount int64 switch operation { case "set": recordAmount = result.NewBalance - oldBalance case "add": recordAmount = amount case "subtract": recordAmount = -amount } // 创建余额记录(新字段) recordID := idutil.GetMsgIDByMD5(userID + timeutil.GetCurrentTimeFormatted() + operation + strconv.FormatInt(amount, 10)) var recordType int32 = 99 // 默认其他 switch operation { case "add": recordType = 99 // 通用“加钱”,具体业务可在上层传入 case "subtract": recordType = 99 // 通用“减钱” case "set": recordType = 99 // 设置,归为其他 } balanceRecord := &model.WalletBalanceRecord{ ID: recordID, UserID: userID, Amount: recordAmount, Type: recordType, BeforeBalance: oldBalance, AfterBalance: result.NewBalance, OrderID: "", TransactionID: "", RedPacketID: "", Remark: remark, CreateTime: time.Now(), } if err := w.walletBalanceRecordDB.Create(ctx, balanceRecord); err != nil { // 余额记录创建失败不影响主流程,只记录警告日志 log.ZWarn(ctx, "updateBalanceWithRecord: failed to create balance record", err, "userID", userID, "operation", operation, "amount", amount, "oldBalance", oldBalance, "newBalance", result.NewBalance) } return result.NewBalance, result.NewVersion, nil } // paginationWrapper 实现 pagination.Pagination 接口 type walletPaginationWrapper struct { pageNumber int32 showNumber int32 } func (p *walletPaginationWrapper) GetPageNumber() int32 { if p.pageNumber <= 0 { return 1 } return p.pageNumber } func (p *walletPaginationWrapper) GetShowNumber() int32 { if p.showNumber <= 0 { return 20 } return p.showNumber } // GetWallets 查询用户钱包列表(后台管理接口) func (w *WalletApi) GetWallets(c *gin.Context) { var ( req apistruct.GetWalletsReq resp apistruct.GetWalletsResp ) if err := c.BindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) return } // 设置默认分页参数 if req.Pagination.PageNumber <= 0 { req.Pagination.PageNumber = 1 } if req.Pagination.ShowNumber <= 0 { req.Pagination.ShowNumber = 20 } // 创建分页对象 pagination := &walletPaginationWrapper{ pageNumber: req.Pagination.PageNumber, showNumber: req.Pagination.ShowNumber, } // 查询钱包列表 var total int64 var wallets []*apistruct.WalletInfo var err error // 判断是否有查询条件(用户ID、手机号、账号) hasQueryCondition := req.UserID != "" || req.PhoneNumber != "" || req.Account != "" if hasQueryCondition { // 如果有查询条件,先通过条件查询用户ID var userIDs []string if req.UserID != "" { // 如果直接提供了用户ID,直接使用 userIDs = []string{req.UserID} } else { // 通过手机号或账号查询用户ID searchUserIDs, err := w.userDB.SearchUsersByFields(c, req.Account, req.PhoneNumber, "") if err != nil { log.ZError(c, "GetWallets: failed to search users", err, "account", req.Account, "phoneNumber", req.PhoneNumber) apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to search users")) return } if len(searchUserIDs) == 0 { // 没有找到匹配的用户,返回空列表 resp.Total = 0 resp.Wallets = []*apistruct.WalletInfo{} apiresp.GinSuccess(c, resp) return } userIDs = searchUserIDs } // 根据查询到的用户ID列表查询钱包 walletModels, err := w.walletDB.FindWalletsByUserIDs(c, userIDs) if err != nil { log.ZError(c, "GetWallets: failed to find wallets by userIDs", err, "userIDs", userIDs) apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find wallets")) return } // 转换为响应格式 wallets = make([]*apistruct.WalletInfo, 0, len(walletModels)) for _, wallet := range walletModels { wallets = append(wallets, &apistruct.WalletInfo{ UserID: wallet.UserID, Balance: wallet.Balance, CreateTime: wallet.CreateTime.UnixMilli(), UpdateTime: wallet.UpdateTime.UnixMilli(), }) } total = int64(len(wallets)) } else { // 如果没有任何查询条件,查询所有钱包(带分页) var walletModels []*model.Wallet total, walletModels, err = w.walletDB.FindAllWallets(c, pagination) if err != nil { log.ZError(c, "GetWallets: failed to find wallets", err) apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find wallets")) return } // 转换为响应格式 wallets = make([]*apistruct.WalletInfo, 0, len(walletModels)) for _, wallet := range walletModels { wallets = append(wallets, &apistruct.WalletInfo{ UserID: wallet.UserID, Balance: wallet.Balance, CreateTime: wallet.CreateTime.UnixMilli(), UpdateTime: wallet.UpdateTime.UnixMilli(), }) } } // 收集所有用户ID userIDMap := make(map[string]bool) for _, wallet := range wallets { if wallet.UserID != "" { userIDMap[wallet.UserID] = true } } // 批量查询用户信息 userIDList := make([]string, 0, len(userIDMap)) for userID := range userIDMap { userIDList = append(userIDList, userID) } userInfoMap := make(map[string]*apistruct.WalletInfo) // userID -> WalletInfo (with nickname and faceURL) if len(userIDList) > 0 { userInfos, err := w.userClient.GetUsersInfo(c, userIDList) if err == nil && len(userInfos) > 0 { for _, userInfo := range userInfos { if userInfo != nil { userInfoMap[userInfo.UserID] = &apistruct.WalletInfo{ Nickname: userInfo.Nickname, FaceURL: userInfo.FaceURL, } } } } else { log.ZWarn(c, "GetWallets: failed to get users info", err, "userIDs", userIDList) } } // 填充用户信息 for _, wallet := range wallets { if userInfo, ok := userInfoMap[wallet.UserID]; ok { wallet.Nickname = userInfo.Nickname wallet.FaceURL = userInfo.FaceURL } } // 填充响应 resp.Total = total resp.Wallets = wallets log.ZInfo(c, "GetWallets: success", "userID", req.UserID, "phoneNumber", req.PhoneNumber, "account", req.Account, "total", total, "count", len(resp.Wallets)) apiresp.GinSuccess(c, resp) } // BatchUpdateWalletBalance 批量修改用户余额(后台管理接口) func (w *WalletApi) BatchUpdateWalletBalance(c *gin.Context) { var ( req apistruct.BatchUpdateWalletBalanceReq resp apistruct.BatchUpdateWalletBalanceResp ) if err := c.BindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) return } // 验证请求参数 if len(req.Users) == 0 { apiresp.GinError(c, errs.ErrArgs.WrapMsg("users list cannot be empty")) return } // 设置默认操作类型 defaultOperation := req.Operation if defaultOperation == "" { defaultOperation = "add" // 默认为增加 } if defaultOperation != "" && defaultOperation != "set" && defaultOperation != "add" && defaultOperation != "subtract" { apiresp.GinError(c, errs.ErrArgs.WrapMsg("operation must be 'set', 'add', or 'subtract'")) return } // 处理每个用户 resp.Total = int32(len(req.Users)) resp.Results = make([]apistruct.WalletUpdateResult, 0, len(req.Users)) for _, user := range req.Users { result := apistruct.WalletUpdateResult{ UserID: user.UserID.String(), PhoneNumber: user.PhoneNumber, Account: user.Account, Success: false, Remark: user.Remark, // 保存备注信息 } // 确定用户ID var userID string if user.UserID != "" { userID = user.UserID.String() } else if user.PhoneNumber != "" || user.Account != "" { // 通过手机号或账号查询用户ID searchUserIDs, err := w.userDB.SearchUsersByFields(c, user.Account, user.PhoneNumber, "") if err != nil { result.Message = "failed to search user: " + err.Error() resp.Results = append(resp.Results, result) resp.Failed++ continue } if len(searchUserIDs) == 0 { result.Message = "user not found" resp.Results = append(resp.Results, result) resp.Failed++ continue } if len(searchUserIDs) > 1 { result.Message = "multiple users found, please use userID" resp.Results = append(resp.Results, result) resp.Failed++ continue } userID = searchUserIDs[0] } else { result.Message = "user identifier is required (userID, phoneNumber, or account)" resp.Results = append(resp.Results, result) resp.Failed++ continue } // 先验证用户是否存在 userExists, err := w.userDB.Exist(c, userID) if err != nil { result.Message = "failed to check user existence: " + err.Error() resp.Results = append(resp.Results, result) resp.Failed++ continue } if !userExists { result.Message = "user not found" resp.Results = append(resp.Results, result) resp.Failed++ continue } // 查询当前钱包 wallet, err := w.walletDB.Take(c, userID) if err != nil { // 如果钱包不存在,但用户存在,创建新钱包 if errs.ErrRecordNotFound.Is(err) { wallet = &model.Wallet{ UserID: userID, Balance: 0, CreateTime: time.Now(), UpdateTime: time.Now(), } if err := w.walletDB.Create(c, wallet); err != nil { result.Message = "failed to create wallet: " + err.Error() resp.Results = append(resp.Results, result) resp.Failed++ continue } } else { result.Message = "failed to get wallet: " + err.Error() resp.Results = append(resp.Results, result) resp.Failed++ continue } } // 记录旧余额和版本号(旧数据可能没有 version,保持 0 以兼容) result.OldBalance = wallet.Balance oldVersion := wallet.Version // 确定该用户使用的金额和操作类型 userAmount := user.Amount if userAmount == 0 { // 如果用户没有指定金额,使用默认金额 userAmount = req.Amount } if userAmount == 0 { result.Message = "amount is required (either in user object or in request)" resp.Results = append(resp.Results, result) resp.Failed++ continue } userOperation := user.Operation if userOperation == "" { // 如果用户没有指定操作类型,使用默认操作类型 userOperation = defaultOperation } if userOperation != "set" && userOperation != "add" && userOperation != "subtract" { result.Message = "operation must be 'set', 'add', or 'subtract'" resp.Results = append(resp.Results, result) resp.Failed++ continue } // 预检查:计算新余额并检查不能为负数 var expectedNewBalance int64 switch userOperation { case "set": expectedNewBalance = userAmount case "add": expectedNewBalance = wallet.Balance + userAmount case "subtract": expectedNewBalance = wallet.Balance - userAmount } if expectedNewBalance < 0 { result.Message = "balance cannot be negative" resp.Results = append(resp.Results, result) resp.Failed++ continue } // 使用统一的余额更新方法(包含并发控制和余额记录创建) // 如果发生并发冲突,重试一次(重新获取最新余额和版本号) var newBalance int64 maxRetries := 2 for retry := 0; retry < maxRetries; retry++ { if retry > 0 { // 重试时重新获取钱包信息 wallet, err = w.walletDB.Take(c, userID) if err != nil { result.Message = "failed to get wallet for retry: " + err.Error() break } result.OldBalance = wallet.Balance oldVersion = wallet.Version // 重新计算预期新余额 switch userOperation { case "set": expectedNewBalance = userAmount case "add": expectedNewBalance = wallet.Balance + userAmount case "subtract": expectedNewBalance = wallet.Balance - userAmount } if expectedNewBalance < 0 { result.Message = "balance cannot be negative after retry" break } } newBalance, _, err = w.updateBalanceWithRecord(c, userID, userOperation, userAmount, result.OldBalance, oldVersion, user.Remark) if err == nil { // 更新成功 break } // 如果是并发冲突且还有重试机会,继续重试 if retry < maxRetries-1 && errs.ErrInternalServer.Is(err) { log.ZWarn(c, "BatchUpdateWalletBalance: concurrent modification detected, retrying", err, "userID", userID, "retry", retry+1, "maxRetries", maxRetries) continue } // 其他错误或重试次数用完,返回错误 result.Message = "failed to update balance: " + err.Error() resp.Results = append(resp.Results, result) resp.Failed++ continue } if err != nil { // 更新失败 resp.Results = append(resp.Results, result) resp.Failed++ continue } // 更新成功 result.Success = true result.NewBalance = newBalance result.Message = "success" // 备注信息已在初始化时设置 resp.Results = append(resp.Results, result) resp.Success++ // 记录日志,包含备注信息 if user.Remark != "" { log.ZInfo(c, "BatchUpdateWalletBalance: user balance updated", "userID", userID, "operation", userOperation, "amount", userAmount, "oldBalance", result.OldBalance, "newBalance", newBalance, "remark", user.Remark) } } log.ZInfo(c, "BatchUpdateWalletBalance: success", "total", resp.Total, "success", resp.Success, "failed", resp.Failed) apiresp.GinSuccess(c, resp) }