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