Каким образом возникают сбои в транзакциях?

Всех с наступающим!

Мне под Новый год преподнесли подарок. В проекте есть возможность переводить средства от одного пользователя к другому. Дело в том, что произошла такая ситуация:
Нашелся один хитрожопый пользователь, который волшебным образом кидал средства на другой аккаунт так, что с его счета они не снимались.

Сайт весь написан на Yii2. Есть модель формы, унаследованная от base/model, валидация каждого чиха, включая время последнего перевода, тоесть нельзя переводить средства чаще чем раз в 30 секунд.

Сама логика перевода средств:
/**
     * Создать новую заявку на перевод средств
     * @throws \Exception
     * @throws \yii\db\Exception
     * @return bool
     */
    public function create()
    {
        $transaction = Yii::$app->db->beginTransaction();
        try {

            $transfer = new Transfer();
            $transfer->user_sender = Yii::$app->user->id;
            $transfer->user_recipient = $this->_user_recipient;
            $transfer->time_create = time();
            $transfer->funds = $this->amount;
            $transfer->status = Transfer::STATUS_PENDING;
            $transfer->comm = $this->comm;
            $transfer->save(false);

            Yii::$app->balance
                ->setModule('transfer')
                ->setUser(Yii::$app->user->identity)
                ->setEntityId($transfer->id)
                ->costs($transfer->funds, Transfer::TYPE_SUCCESS);

            // Перевод без подтверждения
            if (!Yii::$app->config->get('transferModeration')) {
                $transfer->time_process = time();
                $transfer->status = Transfer::STATUS_SUCCESS;

                Yii::$app->balance
                    ->setModule('transfer')
                    ->setUser(User::findOne($this->_user_recipient))
                    ->setEntityId($transfer->id)
                    ->billing($transfer->funds, Transfer::TYPE_SUCCESS);
            }

            $transfer->save(false);

            $transaction->commit();
            return true;
        } catch(\Exception $e) {
            $transaction->rollBack();
            return false;
        }
    }


Логика компонента баланс (основной смысл в том, что мы либо прибавляем либо вычитаем средства со счета и создаем запись в истории):
...
        // Было средств на счету
        $was = $this->_user->$account;

        $model = $this->_model;

        if ($type == $model::DEPOSIT || $type == $model::BILLING) {
            $this->_user->$account += $amount;
        }
        else {
            $this->_user->$account -= $amount;
        }

        // Обновляем счет пользователя
        $this->_user->save(false);

        // Запись в историю денежного оборота
        $result = call_user_func_array(
            [$model, $this->_method], [
                $this->_module,
                $type,
                $system,
                $this->_user->id,
                $was,
                $amount,
                $this->_entityId,
                $this->_data,
        ]);

        return $result;


Как видно по вышепредставленной форме все выполняется в транзакции, что должно гарантировать целостность данных.

Однако если посмотреть в базу данных:
cc07a0a8499f4e30a981dfba8e89140c.png

То можно заметить, что время создания заявки на перевод одинаковое. Как такое возможно, если стоит валидация на время (тоесть достается последняя заявка и сверяется время).

В результате, походу один из запросов проходит корректно, а второй упускает часть логики и не списывает средства. Как такое возможно ?
Повторить такое в качестве дебага мне не удалось никак. Зажатие ф5, разлогинивание во время процесса оплаты и тп.
  • Вопрос задан
  • 766 просмотров
Решения вопроса 1
Demetriy
@Demetriy
веб и мобильная разработка
Если я ничего не путаю, то:

1) $transfer->save(false); - у вас отключена валидация при сохранении моделей.
2) При коммите вы не проверяете, что результат сохранения всех моделей положительный (true), а вить у вас тупо может не пройти валидация где-либо.
3) При тестировании вы пробовали отправить отрицательное число? Возможно стоит проверка на то, что значение число, но вот, что оно больше 0 не стоит, такое может приводить к проблемам.

Все это разумеется не решения вашей проблемы, а скорее то, на что можно обратить внимание.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@pantsarny
if ($type == $model::DEPOSIT || $type == $model::BILLING) {
            $this->_user->$account += $amount;
        }
        else {
            $this->_user->$account -= $amount;
        }

        // Обновляем счет пользователя
        $this->_user->save(false);


Вот тут подвох. Предыдущий баланс сохраняется в переменную, затем инкремент/декремент переменной и запись нового значения.
Yii делает запрос вида
UPDATE user SET balance = $new_balance...
А вам необходимо делать запрос вида
UPDATE user SET balance = balance - $amount WHERE balance >= $amount AND user_id = 1
И смотрите, если баланс пользователя больше суммы, которую он хочет провести, то запрос вам вернет единицу, т.е. отредактирована одна запись. Тогда можно делать добавление баланса второму пользователю, иначе есть подвох и не одобряем перевод.
UPDATE user SET balance = balance + $amount WHERE user_id = 2
И затем сохраняем саму запись перечеслия в таблицу. Все это дело оборачиваем в транзакцию и счастливы.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы