Sobes.tech
Senior

Zaimplementuj przelew środków między dwoma kontami. Pamiętaj, że jest to rozwiązanie produkcyjne Parametry wejściowe: @param fromAccountId unikalny identyfikator użytkownika, który dokonuje przelewu @param toAccountId unikalny identyfikator użytkownika, na którego konto są przelewane środki @param amount kwota przelewu. Liczba dodatnia public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("Kwota musi być > 0"); } if (fromAccountId.equals(toAccountId)) { throw new RuntimeException("Konto nie może być takie samo"); } Long firstId = Math.min(fromAccountId, toAccountId); Long secondId = Math.max(fromAccountId, toAccountId); Account first = accountRepository.findByIdForUpdate(firstId) .orElseThrow(() -> new RuntimeException("Konto nie znalezione")); Account second = accountRepository.findByIdForUpdate(secondId) .orElseThrow(() -> new RuntimeException("Konto nie znalezione")); Account from = fromAccountId.equals(firstId) ? first : second; Account to = fromAccountId.equals(firstId) ? second : first; if (from.getBalance().compareTo(amount) < 0) { throw new RuntimeException("Niewystarczające saldo"); } from.setBalance(from.getBalance().subtract(amount)); to.setBalance(to.getBalance().add(amount)); accountRepository.save(to); accountRepository.save(from); }

sobes.tech AI

Odpowiedź od AI

В представленном коде есть несколько важных моментов, но также и критическая ошибка, которая приведёт к неправильному обновлению баланса.

  1. Проверка входных параметров и логики:
  • Проверяется, что сумма перевода положительна.
  • Проверяется, что счета разные.
  • Для предотвращения дедлоков блокируем счета в порядке возрастания ID.
  1. Ошибка в обновлении балансов:
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(from.getBalance().add(amount));

Здесь после вычитания суммы из from баланс from.getBalance() уже уменьшен, и при добавлении к to используется уже уменьшенный баланс from, что неверно. Нужно использовать старый баланс to.

Правильный вариант:

from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
  1. Транзакционность:

Для production решения обязательно обернуть операцию в транзакцию, чтобы обеспечить атомарность и консистентность данных.

  1. Исключения:

Лучше использовать специализированные исключения, а не RuntimeException.

  1. Пример исправленного кода с транзакцией (например, с Spring):
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
    if (amount.compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("Amount must be > 0");
    }
    if (fromAccountId.equals(toAccountId)) {
        throw new IllegalArgumentException("Account can't be the same");
    }

    Long firstId = Math.min(fromAccountId, toAccountId);
    Long secondId = Math.max(fromAccountId, toAccountId);

    Account first = accountRepository.findByIdForUpdate(firstId)
        .orElseThrow(() -> new EntityNotFoundException("Account not found"));
    Account second = accountRepository.findByIdForUpdate(secondId)
        .orElseThrow(() -> new EntityNotFoundException("Account not found"));

    Account from = fromAccountId.equals(firstId) ? first : second;
    Account to = fromAccountId.equals(firstId) ? second : first;

    if (from.getBalance().compareTo(amount) < 0) {
        throw new IllegalStateException("Insufficient funds");
    }

    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));

    accountRepository.save(from);
    accountRepository.save(to);
}

Таким образом, ключевые моменты — правильное обновление балансов, блокировка записей для предотвращения гонок, транзакционность и корректная обработка ошибок.