Systeme d'alarme IoT evenementiel : hexagonal, CQRS, Kafka
Backend d'un systeme de video-surveillance dont le capteur est un iPhone recycle. Le probleme est simple, la structure est le sujet : - domain/ sans aucun import de framework (verifie par 7 regles ArchUnit qui tournent aussi pendant le docker build) - CQRS strict : write et read ne partagent ni modele ni port - AlarmDecision en sealed interface -> switch exhaustif verifie a la compilation, plutot qu'une chaine de if - Kafka en KRaft (Zookeeper est supprime depuis Kafka 4.0), cle de partition = deviceId pour garantir l'ordre par capteur - ErrorHandlingDeserializer + DLQ : un message malforme ne bloque pas le flux des evenements suivants - notification WebSocket differee apres commit, pour ne jamais afficher une intrusion qui partirait en rollback 31 tests au vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
63ead8a966
81 changed files with 5051 additions and 0 deletions
|
|
@ -0,0 +1,86 @@
|
|||
package me.belkacemi.iotalarm.infrastructure.persistence;
|
||||
|
||||
import me.belkacemi.iotalarm.application.query.port.AlarmReadModelPort;
|
||||
import me.belkacemi.iotalarm.application.query.view.AlarmStatusView;
|
||||
import me.belkacemi.iotalarm.application.query.view.AlertView;
|
||||
import me.belkacemi.iotalarm.domain.model.AlarmPolicy;
|
||||
import me.belkacemi.iotalarm.domain.model.AlarmState;
|
||||
import me.belkacemi.iotalarm.infrastructure.persistence.jpa.AlarmStateJpaRepository;
|
||||
import me.belkacemi.iotalarm.infrastructure.persistence.jpa.AlertJpaRepository;
|
||||
import me.belkacemi.iotalarm.infrastructure.persistence.mapper.AlertMapper;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* ADAPTER SORTANT du cote LECTURE : implementation du {@link AlarmReadModelPort}.
|
||||
*
|
||||
* <p>Il ne charge jamais l'agregat. Il assemble une vue a partir de requetes
|
||||
* ciblees - c'est la raison d'etre du CQRS : afficher un compteur d'alertes non
|
||||
* acquittees via un {@code COUNT(*)} plutot qu'en rehydratant un objet metier
|
||||
* complet dont on n'utiliserait aucune methode.</p>
|
||||
*/
|
||||
@Component
|
||||
public class AlarmReadModelAdapter implements AlarmReadModelPort {
|
||||
|
||||
private final AlertJpaRepository alertRepository;
|
||||
private final AlarmStateJpaRepository alarmStateRepository;
|
||||
private final AlarmPolicy policy;
|
||||
private final Clock clock;
|
||||
|
||||
public AlarmReadModelAdapter(AlertJpaRepository alertRepository,
|
||||
AlarmStateJpaRepository alarmStateRepository,
|
||||
AlarmPolicy policy,
|
||||
Clock clock) {
|
||||
this.alertRepository = alertRepository;
|
||||
this.alarmStateRepository = alarmStateRepository;
|
||||
this.policy = policy;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AlarmStatusView> alarmStatus(String zoneId) {
|
||||
long unacknowledged = alertRepository.countByZoneIdAndAcknowledgedFalse(zoneId);
|
||||
AlertView lastAlert = alertRepository.findFirstByZoneIdOrderByDetectedAtDesc(zoneId)
|
||||
.map(AlertMapper::toView)
|
||||
.orElse(null);
|
||||
|
||||
return Optional.of(alarmStateRepository.findById(zoneId)
|
||||
.map(entity -> new AlarmStatusView(
|
||||
entity.getZoneId(),
|
||||
entity.getState().name(),
|
||||
entity.getStateChangedAt(),
|
||||
policy.exitDelay().toSeconds(),
|
||||
entity.getLastAlertAt(),
|
||||
unacknowledged,
|
||||
lastAlert))
|
||||
// Base neuve : l'alarme n'a simplement jamais ete armee. La
|
||||
// console doit afficher DESARMEE, pas une erreur 404.
|
||||
.orElseGet(() -> new AlarmStatusView(
|
||||
zoneId,
|
||||
AlarmState.DISARMED.name(),
|
||||
clock.instant(),
|
||||
policy.exitDelay().toSeconds(),
|
||||
null,
|
||||
unacknowledged,
|
||||
lastAlert)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AlertView> recentAlerts(String zoneId, int limit) {
|
||||
return alertRepository
|
||||
.findByZoneIdOrderByDetectedAtDesc(zoneId, PageRequest.of(0, limit))
|
||||
.stream()
|
||||
.map(AlertMapper::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AlertView> findAlert(UUID alertId) {
|
||||
return alertRepository.findById(alertId).map(AlertMapper::toView);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue