Files
hi-server/internal/logic/public/user/commissionWithdrawLogic.go
T
shanshanzhong147 878d5006f0 fix: move withdrawal commission deduction from application to approval
- commissionWithdrawLogic: remove upfront commission deduction;
  balance check now includes sum of all pending withdrawals to prevent
  double-spending; transaction only creates the withdrawal record (status=0)
- approveWithdrawal: add FOR UPDATE lock on user row, balance check before
  deducting, atomic commission decrement and commission log inside one
  transaction; clear user cache after commit
- rejectWithdrawal: remove commission refund and log — commission was never
  deducted on application under the new flow
- add migration 02150: refund commission for existing status=0 withdrawals
  that were deducted under the old logic; includes rollback script

Co-authored-by: multica-agent <github@multica.ai>
2026-05-24 19:38:59 -07:00

84 lines
2.6 KiB
Go

package user
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type CommissionWithdrawLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Commission Withdraw
func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic {
return &CommissionWithdrawLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// Sum all pending (status=0) withdrawals to compute available balance.
// Available = commission - pendingTotal; commission is only deducted on approval.
var pendingTotal int64
if err = l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Withdrawal{}).
Where("user_id = ? AND status = 0", u.Id).
Select("COALESCE(SUM(amount), 0)").
Scan(&pendingTotal).Error; err != nil {
l.Errorf("Failed to query pending withdrawals for user %d: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to query pending withdrawals for user %d", u.Id)
}
if u.Commission < req.Amount+pendingTotal {
logger.Errorf("User %d insufficient available commission: total=%d pending=%d requested=%d",
u.Id, u.Commission, pendingTotal, req.Amount)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id)
}
now := time.Now()
var w user.Withdrawal
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
w = user.Withdrawal{
UserId: u.Id,
Amount: req.Amount,
Content: req.Content,
Status: 0,
Reason: "",
}
return tx.Create(&w).Error
})
if err != nil {
l.Errorf("Failed to create withdrawal for user %d: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal for user %d: %v", u.Id, err)
}
return &types.WithdrawalLog{
UserId: u.Id,
Amount: req.Amount,
Content: req.Content,
Status: 0,
Reason: "",
CreatedAt: now.UnixMilli(),
UpdatedAt: now.UnixMilli(),
}, nil
}