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>
86 lines
3.5 KiB
Java
86 lines
3.5 KiB
Java
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);
|
|
}
|
|
}
|