StatsService.java
package com.archiweb.service;
import com.archiweb.dto.StatsResponse;
import com.archiweb.model.Code;
import com.archiweb.model.Participation;
import com.archiweb.model.PrizeType;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.CodeRepository;
import com.archiweb.repository.ParticipationRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.repository.WinRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.Period;
import java.util.*;
import java.util.stream.Collectors;
import java.util.List;
@Service
public class StatsService {
private final UserRepository userRepository;
private final WinRepository winRepository;
private final CodeRepository codeRepository;
private final ParticipationRepository participationRepository;
public StatsService(UserRepository userRepository,
WinRepository winRepository,
CodeRepository codeRepository,
ParticipationRepository participationRepository) {
this.userRepository = userRepository;
this.winRepository = winRepository;
this.codeRepository = codeRepository;
this.participationRepository = participationRepository;
}
// =============================
// 1️⃣ - Statistiques globales sans filtres
// =============================
@Cacheable(value = "globalStats", unless = "#result == null")
public StatsResponse getStats() {
try {
// Optimisation : utiliser des requêtes SQL natives pour réduire le nombre d'appels
// Une seule requête pour les stats des codes
Object[] codeStats = codeRepository.getCodeStatsNative();
if (codeStats == null || codeStats.length < 4) {
// Fallback si la requête native échoue
return getStatsFallback();
}
long totalCodes = convertToLong(codeStats[0]);
long usedCodes = convertToLong(codeStats[1]);
long unusedCodes = convertToLong(codeStats[2]);
long expiredCodes = convertToLong(codeStats[3]);
// Une seule requête pour les stats des participations
Object[] participationStats = participationRepository.getParticipationStatsNative();
if (participationStats == null || participationStats.length < 3) {
// Fallback si la requête native échoue
return getStatsFallback();
}
long totalParticipations = convertToLong(participationStats[0]);
long claimed = convertToLong(participationStats[1]);
long unclaimed = convertToLong(participationStats[2]);
// Optimisation : utiliser une requête SQL GROUP BY au lieu de charger tous les codes
Map<PrizeType, Long> repartition = new HashMap<>();
List<Object[]> results = codeRepository.countByPrizeTypeGrouped();
for (Object[] result : results) {
PrizeType prizeType = (PrizeType) result[0];
Long count = convertToLong(result[1]);
repartition.put(prizeType, count);
}
return StatsResponse.builder()
.totalCodes(totalCodes)
.codesUtilises(usedCodes)
.codesNonUtilises(unusedCodes)
.codesExpires(expiredCodes)
.totalParticipations(totalParticipations)
.participationsReclamees(claimed)
.participationsNonReclamees(unclaimed)
.repartitionLots(repartition)
.build();
} catch (Exception e) {
// En cas d'erreur avec les requêtes natives, utiliser le fallback
System.err.println("Erreur lors de la récupération des stats natives: " + e.getMessage());
e.printStackTrace();
return getStatsFallback();
}
}
// =============================
// 2️⃣ - Statistiques globales filtrées
// =============================
public StatsResponse getStats(String lot, String boutique, String periode) {
StatsResponse base = getStats();
base.setLotFiltre(lot);
base.setBoutiqueFiltre(boutique);
base.setPeriodeFiltre(periode);
return base;
}
// =============================
// 3️⃣ - Statistiques démographiques
// =============================
@Cacheable(value = "demographicsStats", unless = "#result == null")
public Map<String, Object> getDemographicStats() {
Map<String, Object> stats = new HashMap<>();
// Optimisation : utiliser count() au lieu de findAll().size()
long totalUsers = userRepository.count();
LocalDate today = LocalDate.now();
LocalDate minValidDate = today.minusYears(120); // Maximum 120 ans
LocalDate maxValidDate = today.minusYears(5); // Minimum 5 ans (pour éviter les dates de test)
// Optimisation : calculer l'âge moyen directement en SQL
Double avgAgeResult = userRepository.calculateAverageAge(minValidDate, maxValidDate);
double avgAge = avgAgeResult != null ? avgAgeResult : 0.0;
// Optimisation : utiliser countByConsentGiven au lieu de stream().filter().count()
long consentGiven = userRepository.countByConsentGiven(true);
stats.put("totalUsers", totalUsers);
stats.put("averageAge", Math.round(avgAge * 10.0) / 10.0); // Arrondir à 1 décimale
stats.put("consentGiven", consentGiven);
return stats;
}
// =============================
// 4️⃣ - Statistiques des gains
// =============================
@Cacheable(value = "winsStats", unless = "#result == null")
public Map<String, Long> getWinsStats() {
// Optimisation : utiliser une requête SQL GROUP BY au lieu de charger tous les wins
Map<String, Long> stats = new HashMap<>();
List<Object[]> results = winRepository.countByPrizeNameGrouped();
for (Object[] result : results) {
String prizeName = (String) result[0];
Long count = (Long) result[1];
stats.put(prizeName, count);
}
return stats;
}
// =============================
// 5️⃣ - Statistiques des codes
// =============================
public Map<String, Long> getCodesStats() {
Map<String, Long> stats = new HashMap<>();
stats.put("totalCodes", codeRepository.count());
stats.put("usedCodes", codeRepository.countByUsed(true));
stats.put("unusedCodes", codeRepository.countByUsed(false));
stats.put("expiredCodes", codeRepository.countExpiredCodes());
return stats;
}
/**
* Convertit un objet en long, en gérant tous les types numériques possibles
* (BigInteger, Long, Integer, BigDecimal, etc.)
* Gère aussi le cas où la valeur est un tableau d'objets (problème avec certaines requêtes SQL natives)
*/
private long convertToLong(Object value) {
if (value == null) {
return 0L;
}
// Si c'est un tableau, prendre le premier élément et le convertir récursivement
if (value.getClass().isArray()) {
Object[] array = (Object[]) value;
if (array.length > 0) {
return convertToLong(array[0]);
}
return 0L;
}
// Si c'est un Number, convertir directement
if (value instanceof Number) {
return ((Number) value).longValue();
}
// Si c'est une String, essayer de la parser
if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
return 0L;
}
}
// Par défaut, retourner 0
return 0L;
}
/**
* Méthode de fallback si les requêtes SQL natives échouent
* Utilise les méthodes de repository standard
*/
private StatsResponse getStatsFallback() {
long totalCodes = codeRepository.count();
long usedCodes = codeRepository.countByUsed(true);
long unusedCodes = codeRepository.countByUsed(false);
long expiredCodes = codeRepository.countExpiredCodes();
long totalParticipations = participationRepository.count();
long claimed = participationRepository.countByClaimed(true);
long unclaimed = participationRepository.countByClaimed(false);
Map<PrizeType, Long> repartition = new HashMap<>();
List<Object[]> results = codeRepository.countByPrizeTypeGrouped();
for (Object[] result : results) {
PrizeType prizeType = (PrizeType) result[0];
Long count = convertToLong(result[1]);
repartition.put(prizeType, count);
}
return StatsResponse.builder()
.totalCodes(totalCodes)
.codesUtilises(usedCodes)
.codesNonUtilises(unusedCodes)
.codesExpires(expiredCodes)
.totalParticipations(totalParticipations)
.participationsReclamees(claimed)
.participationsNonReclamees(unclaimed)
.repartitionLots(repartition)
.build();
}
}