Всех с наступающим!
Мне под Новый год преподнесли подарок. В проекте есть возможность переводить средства от одного пользователя к другому. Дело в том, что произошла такая ситуация:
Нашелся один хитрожопый пользователь, который волшебным образом кидал средства на другой аккаунт так, что с его счета они не снимались.
Сайт весь написан на 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;
Как видно по вышепредставленной форме все выполняется в транзакции, что должно гарантировать целостность данных.
Однако если посмотреть в базу данных:
То можно заметить, что время создания заявки на перевод одинаковое. Как такое возможно, если стоит валидация на время (тоесть достается последняя заявка и сверяется время).
В результате, походу один из запросов проходит корректно, а второй упускает часть логики и не списывает средства.
Как такое возможно ?
Повторить такое в качестве дебага мне не удалось никак. Зажатие ф5, разлогинивание во время процесса оплаты и тп.