package services import ( "app/commons/constant" "app/commons/core" "app/commons/core/exchange" "app/commons/core/redisclient" "app/commons/model/entity" "app/commons/utils" "errors" "fmt" "github.com/shopspring/decimal" ) // 提现检查 func (s *CommonService) WithdrawCheck(userId int64, symbol string, amount decimal.Decimal) (*entity.User, error) { if amount.LessThanOrEqual(decimal.Zero) { return nil, fmt.Errorf(constant.ErrorResponseTransferAmountInvalid) } conf, err := s.AssetConfigFromCache() if err != nil { return nil, err } if !conf.WithdrawStatus { return nil, fmt.Errorf(constant.ErrorResponseTransferWithdrawSystemTending) } if conf.MaxWithdrawAmount.Equal(decimal.Zero) { return nil, fmt.Errorf(constant.ErrorResponseTransferWithdrawSystemTending) } if conf.MinWithdrawAmount.GreaterThan(amount) { return nil, fmt.Errorf(constant.ErrorResponseUnderMinAmount) } user, err := s.GetUserByUserId(userId) if err != nil { return nil, err } if !user.LockWithdraw { return nil, fmt.Errorf(constant.ErrorResponseExceedAmountLimit) } asset, err := s.GetAssetBySymbol(userId, symbol) if err != nil { return nil, fmt.Errorf(constant.ErrorResponseExceedAmountLimit) } if asset.Balance.LessThan(amount) { return nil, fmt.Errorf(constant.ErrorResponseBalanceNotEnough) } return user, nil } // 提现申请 func (s *CommonService) WithdrawApply(user *entity.User, symbol string, amount decimal.Decimal) error { //检查当前是否有提现中订单 如果有则不允许提现 // 避免出现同时多订单提现 导致被交易所封控 row, err := s.FirstAssetRwRecord(s.DB(). Where("user_id", user.Id). Where("direction", constant.RwDirectionWithdraw). Where("status in (?,?)", constant.RwStateWaiting, constant.RwStatePass)) if err == nil && row.Id > 0 { return fmt.Errorf("the withdrawal is currently being processed") } userAsset, err := s.CheckUserAssetByUserIdSymbol(s.DB(), user.Id, symbol) if err != nil { return fmt.Errorf("asset not found") } if userAsset.Balance.LessThan(amount) { return fmt.Errorf("balance inadequate") } // 获取分布式锁 存在用户购买商品 发放奖励等情况 可能会影响资产变化 lockKey := fmt.Sprintf("%s:%d", constant.UserAssetLocker, user.Id) lock, err := redisclient.Lock(lockKey) if err != nil { return fmt.Errorf("failed to acquire lock: %v", err) } core.Log.Infof("用户ID:%s 资产锁获取", lockKey) defer func() { err = redisclient.UnlockSafe(lock) if err != nil { core.Log.Error(err) } core.Log.Infof("用户ID:%s 资产锁释放", lockKey) }() conf, err := s.AssetConfigFromCache() if err != nil { core.Log.Error(err) return err } feeRatio := decimal.NewFromFloat(0) // 默认0%手续费 if conf.WithdrawRatio.GreaterThan(decimal.Zero) && conf.WithdrawRatio.LessThan(decimal.NewFromFloat(1)) { feeRatio = conf.WithdrawRatio } feeBaseAmount := conf.WithdrawBaseAmount // 单笔提现基础手续费 fee := amount.Mul(feeRatio).Add(feeBaseAmount) // 手续费 = 手续费比例 + 单次手续费 realAmount := amount.Sub(fee) // 实际转账 isPass := conf.WithdrawNoauditLimit.LessThanOrEqual(amount) if realAmount.LessThan(decimal.Zero) { return fmt.Errorf(constant.ErrorResponseBalanceNotEnough) } // 构建转账订单 withdrawOrder := s.buildRwRecord( symbol, user.Id, user.OpenId, utils.UuidPrefix("W"), constant.RwDirectionWithdraw, amount, feeRatio, feeBaseAmount, fee, realAmount, ) dbTx := s.DB().Begin() if isPass { // 修改订单状态 withdrawOrder.Status = constant.RwStatePass // 审核完成 转账中 } withdrawOrder.WithdrawState = constant.WithdrawStatePending // 待转账 // 修改订单状态 err = dbTx.Create(&withdrawOrder).Error if err != nil { dbTx.Rollback() return err } // 创建流水 冻结可用余额 err = s.GenBillAndActionAsset(dbTx, userAsset.UserId, userAsset.Symbol, decimal.Zero.Sub(amount), amount, constant.BsById(constant.BsAssetWithdrawApply)) if err != nil { dbTx.Rollback() return err } dbTx.Commit() // 发送转账处理信号 go s.handleAllWithdraw() return nil } // 处理提现 var withdrawIsRun = false func (s *CommonService) handleAllWithdraw() { // 使用内部锁 if withdrawIsRun { return } withdrawIsRun = true defer func() { withdrawIsRun = false }() // 处理审核通过转账 if err := s.handWithdrawPassOrder(); err != nil { core.Log.Errorf("处理审核通过转账 error:%s", err.Error()) } // 处理驳回与系统失败转账 if err := s.handWithdrawFailOrder(); err != nil { core.Log.Errorf("处理驳回与系统失败转账 error:%s", err.Error()) } } // 处理提现订单 -- 状态为审核通过的产品 func (s *CommonService) handWithdrawPassOrder() error { // 收到信号即处理全部订单 items, err := s.BatchTransferRecord(s.DB(). Where("status", constant.RwStatePass). Where("direction", constant.RwDirectionWithdraw). Where("transfer_state", constant.WithdrawStatePending)) if err != nil { return err } for _, item := range items { item.WithdrawState = constant.WithdrawStateFinish _, err = s.withdrawToEx(item) if err != nil { // 资金退回处理 item.Describe = fmt.Sprintf("%s\n%s", item.Describe, err.Error()) core.Log.Errorf("系统 to 交易所 转账失败:%s", err.Error()) item.Status = constant.RwStateSystemFail } if err = s.DB().Model(&entity.AssetRwRecord{}).Where("id", item.Id).Updates( map[string]interface{}{ "status": item.Status, "describe": item.Describe, }).Error; err != nil { core.Log.Errorf("转出订单处理失败:%s 存储失败", err.Error()) } } return nil } // 向交易所发起转账申请 func (s *CommonService) withdrawToEx(req *entity.AssetRwRecord) (int, error) { // 这里仅做发起交易 resp, err := exchange.TransferOut(req.OpenId, &exchange.TransferOutReq{ Currency: req.Symbol, Amount: req.RealAmount.String(), OrderId: req.OrderId, }) if err != nil { return 0, err } if resp.Code == 10000 { return resp.Code, errors.New(resp.Msg) } return resp.Code, nil } // 驳回订单处理 func (s *CommonService) handWithdrawFailOrder() error { // 收到信号即处理全部订单 itemsReject, err := s.BatchTransferRecord(s.DB(). Where("status in (?,?)", constant.RwStateReject, constant.RwStateSystemFail). Where("direction", constant.RwDirectionWithdraw)) if err != nil { return err } for _, item := range itemsReject { err = s.returnWithdraw(item) if err != nil { core.Log.Errorf("转账失败订单 资金退回错误:%s", err.Error()) } } return nil } // 解冻资产 提现退回 func (s *CommonService) returnWithdraw(item *entity.AssetRwRecord) error { var err error dbTx := s.DB().Begin() userAsset, err := s.CheckUserAssetByUserIdSymbol(dbTx, item.UserId, item.Symbol) if err != nil { err = fmt.Errorf("asset error") dbTx.Rollback() return err } if userAsset.Frozen.LessThan(item.Amount) { err = fmt.Errorf("OrderId:%s资金错误:冻结金额:%s小于提现金额:%s 转账 ", item.OrderId, userAsset.Frozen, item.Amount) dbTx.Rollback() return err } if err = dbTx.Model(&entity.AssetRwRecord{}).Where("id", item.Id). Update("Status", constant.RwStateReturnSuccess).Error; err != nil { core.Log.Errorf("转账失败订单 资金退回错误:%s", err.Error()) dbTx.Rollback() return err } // 修改用户余额 可用->冻结 // 创建流水 冻结可用余额 err = s.GenBillAndActionAsset(dbTx, userAsset.UserId, userAsset.Symbol, decimal.Zero, decimal.Zero.Sub(item.Amount), constant.BsById(constant.BsAssetWithdrawApply)) if err != nil { dbTx.Rollback() return err } dbTx.Commit() return nil }