package user import ( "context" "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" "gorm.io/gorm/clause" ) 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) { ctxUser, 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") } // Validate payment method fields switch req.Method { case user.WithdrawalMethodAlipay, user.WithdrawalMethodWechat: if req.QrCodeUrl == "" { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "qr_code_url is required for method %d", req.Method) } case user.WithdrawalMethodBank: if req.Account == "" { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account is required for bank transfer") } default: // WithdrawalMethodOther if req.Account == "" { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account is required for other methods") } } // HIF-139: read commission and pending total directly from DB inside a // transaction, FOR UPDATE on the user row. The ctxUser snapshot may be // served from cache and can be stale (the original bug allowed a user // with cached commission=996900 to submit a withdrawal while DB said 0, // which then failed admin approval with 20010). Approve already locks // the user row this way; aligning submission closes the gap. var w user.Withdrawal err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error { var dbUser user.User if txErr := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ?", ctxUser.Id).First(&dbUser).Error; txErr != nil { l.Errorf("Failed to lock user %d for withdrawal: %v", ctxUser.Id, txErr) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to lock user %d: %v", ctxUser.Id, txErr) } var pendingTotal int64 if txErr := tx.Model(&user.Withdrawal{}). Where("user_id = ? AND status = ?", ctxUser.Id, user.WithdrawalStatusPending). Select("COALESCE(SUM(amount), 0)"). Scan(&pendingTotal).Error; txErr != nil { l.Errorf("Failed to query pending withdrawals for user %d: %v", ctxUser.Id, txErr) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to query pending withdrawals for user %d", ctxUser.Id) } if dbUser.Commission < req.Amount+pendingTotal { logger.Errorf("User %d insufficient available commission: db_commission=%d pending=%d requested=%d", ctxUser.Id, dbUser.Commission, pendingTotal, req.Amount) return errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", ctxUser.Id) } w = user.Withdrawal{ UserId: ctxUser.Id, Amount: req.Amount, Content: req.Content, Status: user.WithdrawalStatusPending, Reason: "", Method: req.Method, Account: req.Account, QrCodeUrl: req.QrCodeUrl, } if txErr := tx.Create(&w).Error; txErr != nil { l.Errorf("Failed to create withdrawal for user %d: %v", ctxUser.Id, txErr) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal for user %d: %v", ctxUser.Id, txErr) } return nil }) if err != nil { return nil, err } return &types.WithdrawalLog{ Id: w.Id, UserId: ctxUser.Id, Amount: req.Amount, Content: req.Content, Status: user.WithdrawalStatusPending, Reason: "", Method: req.Method, Account: req.Account, QrCodeUrl: req.QrCodeUrl, CreatedAt: w.CreatedAt.UnixMilli(), UpdatedAt: w.UpdatedAt.UnixMilli(), }, nil }