← Назад к вопросам

Как бы реализовал добавление метки клиента в базу данных при погашении кредита

2.2 Middle🔥 31 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Добавление метки клиента при погашении кредита

Краткий ответ

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

Архитектурный подход

Слой представления (Controller)
        ↓
Слой приложения (Service)
        ↓
Слой домена (Use Case)
        ↓
Слой инфраструктуры (Repository, Event Bus)
        ↓
База данных

Реализация с использованием транзакций

1. Модель данных

// Сущность кредита
@Entity
@Table(name = "credits")
public class Credit {
    @Id
    private UUID id;
    
    @ManyToOne
    private Customer customer;
    
    @Enumerated(EnumType.STRING)
    private CreditStatus status; // ACTIVE, PAID_OFF, DEFAULTED
    
    private BigDecimal amount;
    private LocalDateTime createdAt;
    private LocalDateTime paidOffAt;
}

// Сущность метки клиента
@Entity
@Table(name = "customer_marks")
public class CustomerMark {
    @Id
    private UUID id;
    
    @ManyToOne
    private Customer customer;
    
    @Enumerated(EnumType.STRING)
    private MarkType type; // GOOD_PAYER, BAD_PAYER, etc.
    
    private LocalDateTime createdAt;
    private String reason;
}

// Enum для типов меток
public enum MarkType {
    GOOD_PAYER("Хороший плательщик"),
    EXCELLENT_PAYER("Отличный плательщик"),
    BAD_PAYER("Плохой плательщик"),
    DEFAULTED("Просрочка платежа");
    
    private final String description;
}

2. Repository

@Repository
public interface CreditRepository extends JpaRepository<Credit, UUID> {
    List<Credit> findByCustomerAndStatus(Customer customer, CreditStatus status);
}

@Repository
public interface CustomerMarkRepository extends JpaRepository<CustomerMark, UUID> {
    Optional<CustomerMark> findLatestByCustomer(Customer customer);
    List<CustomerMark> findByCustomer(Customer customer);
}

3. Use Case / Service

@Service
public class CreditPaymentService {
    
    @Autowired
    private CreditRepository creditRepository;
    
    @Autowired
    private CustomerMarkRepository customerMarkRepository;
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    @Autowired
    private CreditPaymentValidator validator;
    
    // Основной метод с транзакцией
    @Transactional
    public CreditPaymentResponse payOffCredit(UUID creditId, BigDecimal paymentAmount) {
        // 1. Получение и валидация кредита
        Credit credit = creditRepository.findById(creditId)
            .orElseThrow(() -> new CreditNotFoundException(creditId));
        
        validator.validateCreditExists(credit);
        validator.validateCreditIsActive(credit);
        validator.validatePaymentAmount(credit, paymentAmount);
        
        // 2. Обновление статуса кредита
        credit.setStatus(CreditStatus.PAID_OFF);
        credit.setPaidOffAt(LocalDateTime.now(UTC));
        creditRepository.save(credit);
        
        // 3. Добавление метки клиенту
        addCustomerMark(credit.getCustomer(), credit);
        
        // 4. Отправка события (асинхронно обрабатывается)
        CreditPaidOffEvent event = new CreditPaidOffEvent(
            creditId,
            credit.getCustomer().getId(),
            paymentAmount,
            LocalDateTime.now(UTC)
        );
        eventPublisher.publishEvent(event);
        
        return new CreditPaymentResponse(credit);
    }
    
    private void addCustomerMark(Customer customer, Credit credit) {
        // Определение типа метки на основе истории платежей
        MarkType markType = determineMarkType(customer);
        
        CustomerMark mark = new CustomerMark();
        mark.setId(UUID.randomUUID());
        mark.setCustomer(customer);
        mark.setType(markType);
        mark.setCreatedAt(LocalDateTime.now(UTC));
        mark.setReason("Кредит ID: " + credit.getId() + " погашен вовремя");
        
        customerMarkRepository.save(mark);
    }
    
    private MarkType determineMarkType(Customer customer) {
        // Анализ истории платежей
        List<Credit> paidCredits = creditRepository
            .findByCustomerAndStatus(customer, CreditStatus.PAID_OFF);
        
        List<Credit> defaultedCredits = creditRepository
            .findByCustomerAndStatus(customer, CreditStatus.DEFAULTED);
        
        // Бизнес-правила
        if (defaultedCredits.size() > 0) {
            return MarkType.BAD_PAYER;
        }
        
        if (paidCredits.size() >= 5) {
            return MarkType.EXCELLENT_PAYER;
        }
        
        if (paidCredits.size() >= 2) {
            return MarkType.GOOD_PAYER;
        }
        
        return MarkType.GOOD_PAYER; // По умолчанию
    }
}

4. Валидатор

@Component
public class CreditPaymentValidator {
    
    public void validateCreditExists(Credit credit) {
        if (credit == null) {
            throw new CreditNotFoundException("Кредит не найден");
        }
    }
    
    public void validateCreditIsActive(Credit credit) {
        if (credit.getStatus() != CreditStatus.ACTIVE) {
            throw new InvalidCreditStatusException(
                "Кредит должен быть в статусе ACTIVE, текущий: " + credit.getStatus()
            );
        }
    }
    
    public void validatePaymentAmount(Credit credit, BigDecimal paymentAmount) {
        if (paymentAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidPaymentAmountException("Сумма платежа должна быть больше нуля");
        }
        
        if (paymentAmount.compareTo(credit.getAmount()) > 0) {
            throw new InvalidPaymentAmountException("Сумма платежа превышает остаток кредита");
        }
    }
}

5. Event Listener (асинхронная обработка)

@Component
public class CreditPaidOffEventListener {
    
    @Autowired
    private NotificationService notificationService;
    
    @Autowired
    private ReportingService reportingService;
    
    @EventListener
    @Async // Выполняется в отдельном потоке
    public void handleCreditPaidOff(CreditPaidOffEvent event) {
        // Асинхронная отправка уведомления
        notificationService.sendCreditPaidOffEmail(event.getCustomerId());
        
        // Логирование в систему отчетов
        reportingService.logCreditPayment(
            event.getCreditId(),
            event.getCustomerId(),
            event.getPaymentAmount()
        );
    }
}

Альтернативные подходы

Подход 1: Saga паттерн (для микросервисов)

@Service
public class CreditPaymentSaga {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Transactional
    public void orchestrateCreditPayment(UUID creditId) {
        try {
            // Шаг 1: Обновление статуса кредита
            updateCreditStatus(creditId);
            
            // Шаг 2: Добавление метки в сервис клиентов
            addCustomerMark(creditId);
            
            // Шаг 3: Отправка уведомления
            notifyCustomer(creditId);
            
        } catch (Exception e) {
            // Rollback всех операций
            compensateCreditPayment(creditId);
        }
    }
}

Подход 2: Outbox паттерн (гарантированная доставка событий)

@Entity
@Table(name = "outbox")
public class OutboxEvent {
    @Id
    private UUID id;
    
    private String eventType;
    private String payload;
    private LocalDateTime createdAt;
    private Boolean published = false;
}

@Service
public class CreditPaymentServiceWithOutbox {
    
    @Transactional
    public void payOffCredit(UUID creditId) {
        // 1. Обновление кредита и добавление метки в одной транзакции
        Credit credit = updateCreditAndAddMark(creditId);
        
        // 2. Создание события в Outbox
        OutboxEvent event = new OutboxEvent();
        event.setEventType("CreditPaidOff");
        event.setPayload(serializeEvent(credit));
        event.setCreatedAt(LocalDateTime.now(UTC));
        outboxRepository.save(event);
        
        // Отправка события гарантирована благодаря транзакции
    }
}

// Отдельный сервис опрашивает Outbox и публикует события
@Component
public class OutboxPoller {
    
    @Scheduled(fixedRate = 1000) // Каждую секунду
    public void pollAndPublish() {
        List<OutboxEvent> unpublished = outboxRepository.findByPublishedFalse();
        
        for (OutboxEvent event : unpublished) {
            try {
                eventPublisher.publish(event);
                event.setPublished(true);
                outboxRepository.save(event);
            } catch (Exception e) {
                logger.error("Failed to publish event", e);
            }
        }
    }
}

Обработка ошибок и откат

@Service
public class CreditPaymentServiceWithRollback {
    
    @Transactional
    public CreditPaymentResponse payOffCredit(UUID creditId) {
        try {
            Credit credit = creditRepository.findById(creditId).orElseThrow();
            
            // Проверка бизнес-правил
            if (credit.getStatus() != CreditStatus.ACTIVE) {
                throw new InvalidCreditStatusException("Кредит уже погашен");
            }
            
            // Все изменения в одной транзакции
            credit.setStatus(CreditStatus.PAID_OFF);
            creditRepository.save(credit);
            
            addCustomerMark(credit.getCustomer());
            
            return new CreditPaymentResponse(credit);
            
        } catch (Exception e) {
            // Spring автоматически откатит все изменения
            logger.error("Credit payment failed: {}", e.getMessage());
            throw new CreditPaymentException("Не удалось погасить кредит", e);
        }
    }
}

Контроллер

@RestController
@RequestMapping("/api/v1/credits")
public class CreditController {
    
    @Autowired
    private CreditPaymentService creditPaymentService;
    
    @PostMapping("/{creditId}/pay-off")
    public ResponseEntity<CreditPaymentResponse> payOffCredit(
            @PathVariable UUID creditId,
            @RequestBody PaymentRequest request) {
        
        CreditPaymentResponse response = creditPaymentService
            .payOffCredit(creditId, request.getAmount());
        
        return ResponseEntity.ok(response);
    }
}

Best Practices

  1. Транзакции: все операции в одной транзакции
  2. Валидация: проверка бизнес-правил до изменения данных
  3. События: асинхронная обработка побочных эффектов
  4. Идемпотентность: операция должна быть безопасна при повторе
  5. Логирование: все критические операции должны логироваться
  6. Откат: автоматический откат при ошибке

Вывод

Правильная реализация требует:

  • Транзакций для консистентности (atomicity)
  • Валидации для корректности (integrity)
  • События для отделения побочных эффектов (separation of concerns)
  • Обработки ошибок для надежности (reliability)

Это обеспечивает надежную и масштабируемую систему управления кредитами.