feat: migration complète vers WebSockets Next + Kafka pour temps réel

- Migration de Jakarta WebSocket vers Quarkus WebSockets Next
- Implémentation de l'architecture Kafka pour événements temps réel
- Ajout des DTOs d'événements (NotificationEvent, ChatMessageEvent, ReactionEvent, PresenceEvent)
- Création des bridges Kafka → WebSocket (NotificationKafkaBridge, ChatKafkaBridge, ReactionKafkaBridge)
- Mise à jour des services pour publier dans Kafka au lieu d'appeler directement WebSocket
- Suppression des classes obsolètes (ChatWebSocket, NotificationWebSocket)
- Correction de l'injection des paramètres path dans WebSockets Next (utilisation de connection.pathParam)
- Ajout des migrations DB pour bookings, promotions, business hours, amenities, reviews
- Mise à jour de la configuration application.properties pour Kafka et WebSockets Next
- Mise à jour .gitignore pour ignorer les fichiers de logs
This commit is contained in:
dahoud
2026-01-21 13:46:16 +00:00
parent 7dd0969799
commit 93c63fd600
78 changed files with 5019 additions and 1113 deletions

View File

@@ -0,0 +1,75 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* Événement de message chat publié dans Kafka.
*
* Utilisé pour garantir la livraison des messages même si le destinataire
* est temporairement déconnecté. Le message est persisté dans Kafka et
* délivré dès la reconnexion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessageEvent {
/**
* ID de la conversation (utilisé comme clé Kafka pour garantir l'ordre).
*/
private String conversationId;
/**
* ID de l'expéditeur.
*/
private String senderId;
/**
* ID du destinataire.
*/
private String recipientId;
/**
* Contenu du message.
*/
private String content;
/**
* ID unique du message.
*/
private String messageId;
/**
* Timestamp de création.
*/
private Long timestamp;
/**
* Type d'événement (message, typing, read_receipt, delivery_confirmation).
*/
private String eventType;
/**
* Données additionnelles (pour typing indicators, read receipts, etc.).
*/
private java.util.Map<String, Object> metadata;
/**
* Constructeur pour un message standard.
*/
public ChatMessageEvent(String conversationId, String senderId, String recipientId,
String content, String messageId) {
this.conversationId = conversationId;
this.senderId = senderId;
this.recipientId = recipientId;
this.content = content;
this.messageId = messageId;
this.eventType = "message";
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,52 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
/**
* Événement de notification publié dans Kafka.
*
* Utilisé pour découpler les services métier des WebSockets.
* Les services publient dans Kafka, et un bridge consomme depuis Kafka
* pour envoyer via WebSocket aux clients connectés.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NotificationEvent {
/**
* ID de l'utilisateur destinataire (utilisé comme clé Kafka pour routing).
*/
private String userId;
/**
* Type de notification (friend_request, friend_request_accepted, event_reminder, etc.).
*/
private String type;
/**
* Données de la notification (contenu spécifique au type).
*/
private Map<String, Object> data;
/**
* Timestamp de création de l'événement.
*/
private Long timestamp;
/**
* Constructeur simplifié (timestamp auto-généré).
*/
public NotificationEvent(String userId, String type, Map<String, Object> data) {
this.userId = userId;
this.type = type;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,48 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Événement de présence (online/offline) publié dans Kafka.
*
* Utilisé pour notifier les amis quand un utilisateur se connecte/déconnecte.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PresenceEvent {
/**
* ID de l'utilisateur concerné.
*/
private String userId;
/**
* Statut (online, offline).
*/
private String status;
/**
* Timestamp de dernière activité.
*/
private Long lastSeen;
/**
* Timestamp de l'événement.
*/
private Long timestamp;
/**
* Constructeur simplifié.
*/
public PresenceEvent(String userId, String status, Long lastSeen) {
this.userId = userId;
this.status = status;
this.lastSeen = lastSeen;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,63 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
/**
* Événement de réaction (like, comment, share) publié dans Kafka.
*
* Utilisé pour notifier en temps réel les réactions sur les posts,
* stories et événements.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReactionEvent {
/**
* ID du post/story/event concerné (utilisé comme clé Kafka).
*/
private String targetId;
/**
* Type de cible (post, story, event).
*/
private String targetType;
/**
* ID de l'utilisateur qui réagit.
*/
private String userId;
/**
* Type de réaction (like, comment, share).
*/
private String reactionType;
/**
* Données additionnelles (contenu du commentaire, etc.).
*/
private Map<String, Object> data;
/**
* Timestamp de création.
*/
private Long timestamp;
/**
* Constructeur simplifié.
*/
public ReactionEvent(String targetId, String targetType, String userId,
String reactionType, Map<String, Object> data) {
this.targetId = targetId;
this.targetType = targetType;
this.userId = userId;
this.reactionType = reactionType;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -8,6 +8,10 @@ import java.util.UUID;
/**
* DTO pour la création d'un établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Seuls les responsables d'établissement peuvent créer des établissements.
*/
@Getter
@@ -32,18 +36,50 @@ public class EstablishmentCreateRequestDTO {
private String description;
private String phoneNumber;
private String email;
private String website;
private String imageUrl;
private Double rating;
private String priceRange;
private Integer capacity;
private String amenities;
private String openingHours;
private String verificationStatus = "PENDING"; // v2.0 - Par défaut PENDING
private Double latitude;
private Double longitude;
@NotNull(message = "L'identifiant du responsable est obligatoire.")
private UUID managerId;
// Champs dépréciés (v1.0) - conservés pour compatibilité mais ignorés
/**
* @deprecated Supprimé en v2.0 (utiliser manager.email à la place).
*/
@Deprecated
private String email;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_media à la place).
*/
@Deprecated
private String imageUrl;
/**
* @deprecated Utiliser averageRating calculé depuis reviews à la place.
*/
@Deprecated
private Double rating;
/**
* @deprecated Supprimé en v2.0.
*/
@Deprecated
private Integer capacity;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_amenities à la place).
*/
@Deprecated
private String amenities;
/**
* @deprecated Supprimé en v2.0 (utiliser business_hours à la place).
*/
@Deprecated
private String openingHours;
}

View File

@@ -0,0 +1,31 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la requête d'upload d'un média d'établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
*/
@Getter
@Setter
public class EstablishmentMediaRequestDTO {
@NotBlank(message = "L'URL du média est obligatoire")
private String mediaUrl;
@NotBlank(message = "Le type de média est obligatoire")
private String mediaType; // PHOTO ou VIDEO
private String name; // Nom du fichier (fileName) - optionnel, peut être extrait de mediaUrl si non fourni
private String thumbnailUrl; // Optionnel, pour les vidéos
private Integer displayOrder = 0; // Ordre d'affichage (par défaut 0)
private String uploadedByUserId; // ID de l'utilisateur qui upload (optionnel, peut être extrait du contexte)
}

View File

@@ -9,6 +9,10 @@ import java.time.LocalDateTime;
/**
* DTO pour la création d'un événement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé dans les requêtes de création d'événements, envoyant les informations
* nécessaires comme le titre, les dates, la description, le créateur, et d'autres attributs.
*/
@@ -28,7 +32,7 @@ public class EventCreateRequestDTO {
@NotNull(message = "La date de fin est obligatoire.")
private LocalDateTime endDate; // Date de fin de l'événement
private String location; // Lieu de l'événement
private UUID establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
private String category; // Catégorie de l'événement
private String link; // Lien d'information supplémentaire
private String imageUrl; // URL de l'image associée à l'événement
@@ -37,6 +41,8 @@ public class EventCreateRequestDTO {
private String tags; // Tags/mots-clés associés à l'événement (séparés par des virgules)
private String organizer; // Nom de l'organisateur de l'événement
private Integer participationFee; // Frais de participation en centimes
private Boolean isPrivate = false; // v2.0 - Indique si l'événement est privé
private Boolean waitlistEnabled = false; // v2.0 - Indique si la liste d'attente est activée
private String privacyRules; // Règles de confidentialité de l'événement
private String transportInfo; // Informations sur les transports disponibles
private String accommodationInfo; // Informations sur l'hébergement
@@ -47,7 +53,24 @@ public class EventCreateRequestDTO {
@NotNull(message = "L'identifiant du créateur est obligatoire.")
private UUID creatorId; // Identifiant du créateur de l'événement
// Champ déprécié (v1.0) - conservé pour compatibilité mais ignoré
/**
* @deprecated Supprimé en v2.0 (utiliser establishmentId à la place).
*/
@Deprecated
private String location;
public EventCreateRequestDTO() {
System.out.println("[LOG] DTO de requête de création d'événement initialisé.");
}
/**
* Méthode pour obtenir le lieu (compatibilité v1.0 et v2.0).
* Retourne null car location est déprécié en v2.0.
*
* @return Le lieu (null en v2.0, utiliser establishmentId à la place).
*/
public String getLocation() {
return location; // Retourne null en v2.0
}
}

View File

@@ -16,7 +16,10 @@ import lombok.Setter;
@AllArgsConstructor
public class EventReadManyByIdRequestDTO {
private UUID userId; // Identifiant de l'utilisateur pour lequel on souhaite obtenir les événements
private UUID id; // v2.0 - Identifiant de l'utilisateur pour lequel on souhaite obtenir les événements
private Integer page = 0; // v2.0 - Numéro de la page (0-indexé)
private Integer size = 10; // v2.0 - Taille de la page
// Ajoutez ici d'autres critères de filtre si besoin, comme une plage de dates, un statut, etc.
}

View File

@@ -23,7 +23,7 @@ public class SocialPostCreateRequestDTO {
private String content; // Le contenu textuel du post
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire.")
private UUID userId; // L'ID de l'utilisateur créateur
private UUID creatorId; // v2.0 - L'ID de l'utilisateur créateur
@Size(max = 500, message = "L'URL de l'image ne peut pas dépasser 500 caractères.")
private String imageUrl; // URL de l'image (optionnel)

View File

@@ -19,7 +19,7 @@ import lombok.Setter;
public class StoryCreateRequestDTO {
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire.")
private UUID userId; // L'ID de l'utilisateur créateur
private UUID creatorId; // v2.0 - L'ID de l'utilisateur créateur
@NotNull(message = "Le type de média est obligatoire.")
private MediaType mediaType; // Type de média (IMAGE ou VIDEO)

View File

@@ -9,6 +9,10 @@ import org.slf4j.LoggerFactory;
/**
* DTO pour la requête d'authentification de l'utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Utilisé pour encapsuler les informations nécessaires lors de l'authentification d'un utilisateur.
*/
@Getter
@@ -25,9 +29,16 @@ public class UserAuthenticateRequestDTO {
private String email;
/**
* Mot de passe de l'utilisateur en texte clair.
* Ce champ sera haché avant d'être utilisé pour l'authentification.
* Mot de passe hashé de l'utilisateur (v2.0).
* Format standardisé pour l'authentification.
*/
private String password_hash; // v2.0
/**
* Mot de passe de l'utilisateur en texte clair (v1.0 - déprécié).
* @deprecated Utiliser {@link #password_hash} à la place.
*/
@Deprecated
private String motDePasse;
/**
@@ -37,6 +48,15 @@ public class UserAuthenticateRequestDTO {
logger.info("UserAuthenticateRequestDTO - DTO pour l'authentification initialisé");
}
/**
* Méthode pour obtenir le mot de passe (compatibilité v1.0 et v2.0).
*
* @return Le mot de passe (password_hash ou motDePasse).
*/
public String getPassword() {
return password_hash != null ? password_hash : motDePasse;
}
// Méthode personnalisée pour loguer les détails de la requête
public void logRequestDetails() {
logger.info("Authentification demandée pour l'email: {}", email);

View File

@@ -7,21 +7,25 @@ import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la création et l'authentification d'un utilisateur.
* Ce DTO est utilisé dans les requêtes pour créer ou authentifier un utilisateur,
* contenant les informations comme le nom, les prénoms, l'email, et le mot de passe.
* DTO pour la création d'un utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé dans les requêtes pour créer un utilisateur,
* contenant les informations comme le prénom, le nom, l'email, et le mot de passe.
*/
@Getter
@Setter
public class UserCreateRequestDTO {
@NotNull(message = "Le nom est obligatoire.")
@Size(min = 1, max = 100, message = "Le nom doit comporter entre 1 et 100 caractères.")
private String nom;
@NotNull(message = "Le prénom est obligatoire.")
@Size(min = 1, max = 100, message = "Le prénom doit comporter entre 1 et 100 caractères.")
private String firstName; // v2.0
@NotNull(message = "Les prénoms sont obligatoires.")
@Size(min = 1, max = 100, message = "Les prénoms doivent comporter entre 1 et 100 caractères.")
private String prenoms;
@NotNull(message = "Le nom de famille est obligatoire.")
@Size(min = 1, max = 100, message = "Le nom de famille doit comporter entre 1 et 100 caractères.")
private String lastName; // v2.0
@NotNull(message = "L'adresse email est obligatoire.")
@Email(message = "Veuillez fournir une adresse email valide.")
@@ -29,11 +33,86 @@ public class UserCreateRequestDTO {
@NotNull(message = "Le mot de passe est obligatoire.")
@Size(min = 6, message = "Le mot de passe doit comporter au moins 6 caractères.")
private String motDePasse;
private String password; // v2.0 - sera hashé en passwordHash
private String profileImageUrl;
private String bio; // v2.0
private Integer loyaltyPoints = 0; // v2.0
/**
* Préférences utilisateur (v2.0).
*
* Structure attendue:
* {
* "preferredCategory": "RESTAURANT" | "BAR" | "CLUB" | "CAFE" | "EVENT" | null,
* "notifications": {
* "email": boolean,
* "push": boolean
* },
* "language": "fr" | "en" | "es"
* }
*
* Exemple:
* {
* "preferredCategory": "RESTAURANT",
* "notifications": {
* "email": true,
* "push": true
* },
* "language": "fr"
* }
*/
private java.util.Map<String, Object> preferences; // v2.0
// Ajout du rôle avec validation
@NotNull(message = "Le rôle est obligatoire.")
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, etc.)
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, MANAGER, etc.)
// Champs de compatibilité v1.0 (dépréciés mais supportés pour migration progressive)
/**
* @deprecated Utiliser {@link #firstName} à la place.
*/
@Deprecated
private String prenoms;
/**
* @deprecated Utiliser {@link #lastName} à la place.
*/
@Deprecated
private String nom;
/**
* @deprecated Utiliser {@link #password} à la place.
*/
@Deprecated
private String motDePasse;
/**
* Méthode pour obtenir le prénom (compatibilité v1.0 et v2.0).
*
* @return Le prénom (firstName ou prenoms).
*/
public String getFirstName() {
return firstName != null ? firstName : prenoms;
}
/**
* Méthode pour obtenir le nom de famille (compatibilité v1.0 et v2.0).
*
* @return Le nom de famille (lastName ou nom).
*/
public String getLastName() {
return lastName != null ? lastName : nom;
}
/**
* Méthode pour obtenir le mot de passe (compatibilité v1.0 et v2.0).
*
* @return Le mot de passe (password ou motDePasse).
*/
public String getPassword() {
return password != null ? password : motDePasse;
}
}

View File

@@ -40,8 +40,9 @@ public class ConversationResponseDTO {
Users otherUser = conversation.getOtherUser(currentUser);
if (otherUser != null) {
this.participantId = otherUser.getId();
this.participantFirstName = otherUser.getPrenoms();
this.participantLastName = otherUser.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
this.participantFirstName = otherUser.getFirstName();
this.participantLastName = otherUser.getLastName();
this.participantProfileImageUrl = otherUser.getProfileImageUrl();
}

View File

@@ -30,14 +30,15 @@ public class MessageResponseDTO {
private LocalDateTime timestamp;
/**
* Constructeur depuis une entité Message.
* Constructeur depuis une entité Message (v2.0).
*/
public MessageResponseDTO(Message message) {
this.id = message.getId();
this.conversationId = message.getConversation().getId();
this.senderId = message.getSender().getId();
this.senderFirstName = message.getSender().getPrenoms();
this.senderLastName = message.getSender().getNom();
// v2.0 - Utiliser les nouveaux noms de champs
this.senderFirstName = message.getSender().getFirstName();
this.senderLastName = message.getSender().getLastName();
this.senderProfileImageUrl = message.getSender().getProfileImageUrl();
this.content = message.getContent();
this.attachmentType = message.getMessageType();

View File

@@ -64,8 +64,9 @@ public class CommentResponseDTO {
this.id = comment.getId(); // Identifiant unique du commentaire
this.texte = comment.getText(); // Texte du commentaire
this.userId = comment.getUser().getId(); // Identifiant de l'utilisateur (auteur du commentaire)
this.userNom = comment.getUser().getNom(); // Nom de l'utilisateur
this.userPrenoms = comment.getUser().getPrenoms(); // Prénom de l'utilisateur
// v2.0 - Utiliser les nouveaux noms de champs
this.userNom = comment.getUser().getLastName(); // Nom de famille de l'utilisateur (v2.0)
this.userPrenoms = comment.getUser().getFirstName(); // Prénom de l'utilisateur (v2.0)
}
}
}

View File

@@ -36,10 +36,11 @@ public class EstablishmentMediaResponseDTO {
this.displayOrder = media.getDisplayOrder();
if (media.getUploadedBy() != null) {
// v2.0 - Utiliser les nouveaux noms de champs
this.uploadedBy = new MediaUploaderDTO(
media.getUploadedBy().getId().toString(),
media.getUploadedBy().getPrenoms(),
media.getUploadedBy().getNom(),
media.getUploadedBy().getFirstName(),
media.getUploadedBy().getLastName(),
media.getUploadedBy().getProfileImageUrl()
);
}

View File

@@ -1,12 +1,17 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.establishment.MediaType;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer les informations d'un établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé pour structurer les données retournées dans les réponses
* après les opérations sur les établissements (création, récupération, mise à jour).
*/
@@ -21,27 +26,66 @@ public class EstablishmentResponseDTO {
private String postalCode;
private String description;
private String phoneNumber;
private String email;
private String website;
private String imageUrl;
private Double rating; // Déprécié, utiliser averageRating
private Double averageRating; // Note moyenne calculée
private Integer totalRatingsCount; // Nombre total de notes
private Integer totalReviewsCount; // v2.0 - renommé depuis totalRatingsCount
private String priceRange;
private Integer capacity;
private String amenities;
private String openingHours;
private String verificationStatus; // v2.0 - PENDING, VERIFIED, REJECTED
private Double latitude;
private Double longitude;
private String managerId;
private String managerEmail;
private String managerFirstName;
private String managerLastName;
private String managerFirstName; // v2.0
private String managerLastName; // v2.0
private String mainImageUrl; // v2.0 - URL de l'image principale (premier média avec displayOrder 0)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Champs dépréciés (v1.0) - conservés pour compatibilité
/**
* Constructeur qui transforme une entité Establishment en DTO.
* @deprecated Utiliser {@link #averageRating} à la place.
*/
@Deprecated
private Double rating;
/**
* @deprecated Supprimé en v2.0 (utiliser manager.email à la place).
*/
@Deprecated
private String email;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_media à la place).
*/
@Deprecated
private String imageUrl;
/**
* @deprecated Supprimé en v2.0.
*/
@Deprecated
private Integer capacity;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_amenities à la place).
*/
@Deprecated
private String amenities;
/**
* @deprecated Supprimé en v2.0 (utiliser business_hours à la place).
*/
@Deprecated
private String openingHours;
/**
* @deprecated Utiliser {@link #totalReviewsCount} à la place.
*/
@Deprecated
private Integer totalRatingsCount;
/**
* Constructeur qui transforme une entité Establishment en DTO (v2.0).
*
* @param establishment L'établissement à convertir en DTO.
*/
@@ -54,28 +98,46 @@ public class EstablishmentResponseDTO {
this.postalCode = establishment.getPostalCode();
this.description = establishment.getDescription();
this.phoneNumber = establishment.getPhoneNumber();
this.email = establishment.getEmail();
this.website = establishment.getWebsite();
this.imageUrl = establishment.getImageUrl();
this.rating = establishment.getRating(); // Déprécié
this.averageRating = establishment.getAverageRating();
this.totalRatingsCount = establishment.getTotalRatingsCount();
this.totalReviewsCount = establishment.getTotalReviewsCount(); // v2.0
this.priceRange = establishment.getPriceRange();
this.capacity = establishment.getCapacity();
this.amenities = establishment.getAmenities();
this.openingHours = establishment.getOpeningHours();
this.verificationStatus = establishment.getVerificationStatus(); // v2.0
this.latitude = establishment.getLatitude();
this.longitude = establishment.getLongitude();
if (establishment.getManager() != null) {
this.managerId = establishment.getManager().getId().toString();
this.managerEmail = establishment.getManager().getEmail();
this.managerFirstName = establishment.getManager().getPrenoms();
this.managerLastName = establishment.getManager().getNom();
this.managerFirstName = establishment.getManager().getFirstName(); // v2.0
this.managerLastName = establishment.getManager().getLastName(); // v2.0
}
// Récupérer l'image principale (premier média photo avec displayOrder 0 ou le premier disponible)
if (establishment.getMedias() != null && !establishment.getMedias().isEmpty()) {
this.mainImageUrl = establishment.getMedias().stream()
.filter(media -> media.getMediaType() == MediaType.PHOTO)
.sorted((a, b) -> Integer.compare(
a.getDisplayOrder() != null ? a.getDisplayOrder() : Integer.MAX_VALUE,
b.getDisplayOrder() != null ? b.getDisplayOrder() : Integer.MAX_VALUE))
.map(media -> media.getMediaUrl())
.findFirst()
.orElse(null);
} else {
this.mainImageUrl = null;
}
this.createdAt = establishment.getCreatedAt();
this.updatedAt = establishment.getUpdatedAt();
// Compatibilité v1.0 - valeurs null pour les champs dépréciés
this.rating = null;
this.email = null;
this.imageUrl = null;
this.capacity = null;
this.amenities = null;
this.openingHours = null;
this.totalRatingsCount = this.totalReviewsCount; // Alias pour compatibilité
}
}

View File

@@ -27,7 +27,7 @@ public class EventReadManyByIdResponseDTO {
private String profileImageUrl; // URL de l'image de profil de l'utilisateur qui a criané l'événement
/**
* Constructeur qui transforme une entité Events en DTO de réponse.
* Constructeur qui transforme une entité Events en DTO de réponse (v2.0).
*
* @param event L'événement à convertir en DTO.
*/
@@ -37,14 +37,16 @@ public class EventReadManyByIdResponseDTO {
this.description = event.getDescription();
this.startDate = event.getStartDate();
this.endDate = event.getEndDate();
// v2.0 - Utiliser getLocation() qui retourne l'adresse de l'établissement
this.location = event.getLocation();
this.category = event.getCategory();
this.link = event.getLink();
this.imageUrl = event.getImageUrl();
this.status = event.getStatus();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getPrenoms();
this.creatorLastName = event.getCreator().getNom();
// v2.0 - Utiliser les nouveaux noms de champs
this.creatorFirstName = event.getCreator().getFirstName();
this.creatorLastName = event.getCreator().getLastName();
this.profileImageUrl = event.getCreator().getProfileImageUrl();
}
}

View File

@@ -36,11 +36,12 @@ public class FriendshipReadStatusResponseDTO {
public FriendshipReadStatusResponseDTO(Friendship friendship) {
this.friendshipId = friendship.getId();
this.userId = friendship.getUser().getId();
this.userNom = friendship.getUser().getNom();
this.userPrenoms = friendship.getUser().getPrenoms();
// v2.0 - Utiliser les nouveaux noms de champs
this.userNom = friendship.getUser().getLastName();
this.userPrenoms = friendship.getUser().getFirstName();
this.friendId = friendship.getFriend().getId();
this.friendNom = friendship.getFriend().getNom();
this.friendPrenoms = friendship.getFriend().getPrenoms();
this.friendNom = friendship.getFriend().getLastName();
this.friendPrenoms = friendship.getFriend().getFirstName();
this.status = friendship.getStatus();
this.createdAt = friendship.getCreatedAt();
}

View File

@@ -33,7 +33,7 @@ public class SocialPostResponseDTO {
private int sharesCount;
/**
* Constructeur à partir d'une entité SocialPost.
* Constructeur à partir d'une entité SocialPost (v2.0).
*
* @param post L'entité SocialPost
*/
@@ -42,8 +42,9 @@ public class SocialPostResponseDTO {
this.id = post.getId();
this.content = post.getContent();
this.userId = post.getUser() != null ? post.getUser().getId() : null;
this.userFirstName = post.getUser() != null ? post.getUser().getPrenoms() : null;
this.userLastName = post.getUser() != null ? post.getUser().getNom() : null;
// v2.0 - Utiliser les nouveaux noms de champs
this.userFirstName = post.getUser() != null ? post.getUser().getFirstName() : null;
this.userLastName = post.getUser() != null ? post.getUser().getLastName() : null;
this.userProfileImageUrl = post.getUser() != null ? post.getUser().getProfileImageUrl() : null;
this.timestamp = post.getCreatedAt();
this.imageUrl = post.getImageUrl();

View File

@@ -46,8 +46,9 @@ public class StoryResponseDTO {
if (story != null) {
this.id = story.getId();
this.userId = story.getUser() != null ? story.getUser().getId() : null;
this.userFirstName = story.getUser() != null ? story.getUser().getPrenoms() : null;
this.userLastName = story.getUser() != null ? story.getUser().getNom() : null;
// v2.0 - Utiliser les nouveaux noms de champs
this.userFirstName = story.getUser() != null ? story.getUser().getFirstName() : null;
this.userLastName = story.getUser() != null ? story.getUser().getLastName() : null;
this.userProfileImageUrl = story.getUser() != null ? story.getUser().getProfileImageUrl() : null;
this.userIsVerified = story.getUser() != null && story.getUser().isVerified();
this.mediaType = story.getMediaType();

View File

@@ -26,7 +26,7 @@ public class FriendSuggestionResponseDTO {
}
/**
* Constructeur à partir d'un utilisateur.
* Constructeur à partir d'un utilisateur (v2.0).
*
* @param user L'utilisateur suggéré
* @param mutualFriendsCount Le nombre d'amis en commun
@@ -34,8 +34,9 @@ public class FriendSuggestionResponseDTO {
*/
public FriendSuggestionResponseDTO(Users user, int mutualFriendsCount, String reason) {
this.userId = user.getId();
this.nom = user.getNom();
this.prenoms = user.getPrenoms();
// v2.0 - Utiliser les nouveaux noms de champs
this.nom = user.getLastName(); // Compatibilité v1.0
this.prenoms = user.getFirstName(); // Compatibilité v1.0
this.email = user.getEmail();
this.profileImageUrl = user.getProfileImageUrl();
this.mutualFriendsCount = mutualFriendsCount;

View File

@@ -10,30 +10,33 @@ import org.slf4j.LoggerFactory;
/**
* DTO pour la réponse d'authentification de l'utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Utilisé pour renvoyer les informations nécessaires après l'authentification réussie d'un utilisateur.
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserAuthenticateResponseDTO {
private static final Logger logger = LoggerFactory.getLogger(UserAuthenticateResponseDTO.class);
/**
* Identifiant unique de l'utilisateur authentifié.
* Identifiant unique de l'utilisateur authentifié (v2.0).
*/
private UUID userId;
private UUID id; // v2.0
/**
* Nom de l'utilisateur.
* Prénom de l'utilisateur (v2.0).
*/
private String nom;
private String firstName; // v2.0
/**
* Prénom de l'utilisateur.
* Nom de famille de l'utilisateur (v2.0).
*/
private String prenoms;
private String lastName; // v2.0
/**
* Adresse email de l'utilisateur.
@@ -45,6 +48,49 @@ public class UserAuthenticateResponseDTO {
*/
private String role;
// Champs de compatibilité v1.0 (dépréciés)
/**
* @deprecated Utiliser {@link #id} à la place.
*/
@Deprecated
private UUID userId;
/**
* @deprecated Utiliser {@link #lastName} à la place.
*/
@Deprecated
private String nom;
/**
* @deprecated Utiliser {@link #firstName} à la place.
*/
@Deprecated
private String prenoms;
/**
* Constructeur v2.0.
*/
public UserAuthenticateResponseDTO(UUID id, String firstName, String lastName, String email, String role) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.role = role;
// Compatibilité v1.0
this.userId = id;
this.nom = lastName;
this.prenoms = firstName;
}
/**
* Constructeur de compatibilité v1.0 (déprécié).
* @deprecated Utiliser le constructeur avec firstName et lastName à la place.
*/
@Deprecated
public UserAuthenticateResponseDTO(UUID userId, String prenoms, String nom, String email, String role, boolean deprecated) {
this(userId, prenoms, nom, email, role);
}
/**
* Log de création de l'objet DTO.
*/
@@ -53,9 +99,10 @@ public class UserAuthenticateResponseDTO {
}
/**
* Méthode personnalisée pour loguer les détails de la réponse.
* Méthode personnalisée pour loguer les détails de la réponse (v2.0).
*/
public void logResponseDetails() {
logger.info("[LOG] Réponse d'authentification - Utilisateur: {}, {}, Email: {}, Rôle: {}, ID: {}", prenoms, nom, email, role, userId);
logger.info("[LOG] Réponse d'authentification - Utilisateur: {} {}, Email: {}, Rôle: {}, ID: {}",
firstName, lastName, email, role, id);
}
}

View File

@@ -6,30 +6,66 @@ import lombok.Getter;
/**
* DTO pour renvoyer les informations d'un utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé pour structurer les données retournées dans les réponses
* après les opérations sur les utilisateurs (création, récupération).
*/
@Getter
public class UserCreateResponseDTO {
private UUID uuid; // Identifiant unique de l'utilisateur
private String nom; // Nom de l'utilisateur
private String prenoms; // Prénoms de l'utilisateur
private UUID id; // v2.0 - Identifiant unique de l'utilisateur
private String firstName; // v2.0 - Prénom de l'utilisateur
private String lastName; // v2.0 - Nom de famille de l'utilisateur
private String email; // Email de l'utilisateur
private String role; // Re de l'utilisateur
private String profileImageUrl; // Url de l'image de profil de l'utilisateur
private String role; // Rôle de l'utilisateur
private String profileImageUrl; // URL de l'image de profil de l'utilisateur
private String bio; // v2.0 - Biographie courte
private Integer loyaltyPoints; // v2.0 - Points de fidélité
private java.util.Map<String, Object> preferences; // v2.0 - Préférences utilisateur
// Champs de compatibilité v1.0 (dépréciés)
/**
* Constructeur qui transforme une entité Users en DTO.
* @deprecated Utiliser {@link #id} à la place.
*/
@Deprecated
private UUID uuid;
/**
* @deprecated Utiliser {@link #lastName} à la place.
*/
@Deprecated
private String nom;
/**
* @deprecated Utiliser {@link #firstName} à la place.
*/
@Deprecated
private String prenoms;
/**
* Constructeur qui transforme une entité Users en DTO (v2.0).
*
* @param user L'utilisateur à convertir en DTO.
*/
public UserCreateResponseDTO(Users user) {
this.uuid = user.getId();
this.nom = user.getNom();
this.prenoms = user.getPrenoms();
this.id = user.getId(); // v2.0
this.firstName = user.getFirstName(); // v2.0
this.lastName = user.getLastName(); // v2.0
this.email = user.getEmail();
this.role = user.getRole();
this.profileImageUrl = user.getProfileImageUrl();
this.bio = user.getBio(); // v2.0
this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
this.preferences = user.getPreferences(); // v2.0
// Compatibilité v1.0
this.uuid = this.id;
this.nom = this.lastName;
this.prenoms = this.firstName;
System.out.println("[LOG] DTO créé pour l'utilisateur : " + this.email);
}
}

View File

@@ -9,6 +9,10 @@ import lombok.Setter;
/**
* DTO (Data Transfer Object) pour l'utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* <p>
* Cette classe sert de représentation simplifiée d'un utilisateur, avec un ensemble d'informations nécessaires à
* la réponse de l'API. Elle est utilisée pour transférer des données entre le backend (serveur) et le frontend (client)
@@ -27,14 +31,14 @@ public class UserResponseDTO {
private UUID id;
/**
* Nom de famille de l'utilisateur. C'est une donnée importante pour l'affichage du profil.
* Prénom de l'utilisateur (v2.0).
*/
private String nom;
private String firstName;
/**
* Prénom(s) de l'utilisateur. Représente le ou les prénoms associés à l'utilisateur.
* Nom de famille de l'utilisateur (v2.0).
*/
private String prenoms;
private String lastName;
/**
* Adresse email de l'utilisateur. C'est une donnée souvent utilisée pour les communications.
@@ -47,13 +51,48 @@ public class UserResponseDTO {
*/
private String profileImageUrl;
/**
* Biographie courte de l'utilisateur (v2.0).
*/
private String bio;
/**
* Points de fidélité accumulés (v2.0).
*/
private Integer loyaltyPoints;
/**
* Préférences utilisateur en JSON (v2.0).
*/
private java.util.Map<String, Object> preferences;
/**
* Rôle de l'utilisateur (ADMIN, USER, MANAGER, etc.).
*/
private String role;
/**
* Indique si l'utilisateur est vérifié (compte officiel).
*/
private boolean isVerified;
/**
* Constructeur de DTO à partir d'une entité Users.
* Indique si l'utilisateur est actuellement en ligne.
*/
private boolean isOnline;
/**
* Dernière fois que l'utilisateur était en ligne.
*/
private java.time.LocalDateTime lastSeen;
/**
* Date de création du compte.
*/
private java.time.LocalDateTime createdAt;
/**
* Constructeur de DTO à partir d'une entité Users (v2.0).
* <p>
* Ce constructeur prend une entité {@link Users} et extrait les données nécessaires pour
* peupler les champs du DTO. Cette transformation permet de transférer des données sans exposer
@@ -64,12 +103,28 @@ public class UserResponseDTO {
*/
public UserResponseDTO(Users user) {
if (user != null) {
this.id = user.getId(); // Identifiant unique de l'utilisateur
this.nom = user.getNom(); // Nom de famille
this.prenoms = user.getPrenoms(); // Prénom(s)
this.email = user.getEmail(); // Email
this.profileImageUrl = user.getProfileImageUrl(); // URL de l'image de profil
this.isVerified = user.isVerified(); // Statut de vérification
this.id = user.getId();
this.firstName = user.getFirstName(); // v2.0
this.lastName = user.getLastName(); // v2.0
this.email = user.getEmail();
this.profileImageUrl = user.getProfileImageUrl();
this.bio = user.getBio(); // v2.0
this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
this.preferences = user.getPreferences(); // v2.0
this.role = user.getRole();
this.isVerified = user.isVerified();
this.isOnline = user.isOnline();
this.lastSeen = user.getLastSeen();
this.createdAt = user.getCreatedAt();
}
}
/**
* Retourne le nom complet de l'utilisateur (v2.0).
*
* @return Le nom complet (firstName + lastName).
*/
public String getFullName() {
return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "").trim();
}
}

View File

@@ -0,0 +1,105 @@
package com.lions.dev.entity.booking;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.users.Users;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* Entité représentant une réservation d'établissement.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité remplace/étend Reservation avec plus de détails et de flexibilité.
*/
@Entity
@Table(name = "bookings")
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Booking extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id", nullable = false)
private Establishment establishment; // L'établissement concerné par la réservation
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Users user; // L'utilisateur qui a fait la réservation
@Column(name = "reservation_time", nullable = false)
private LocalDateTime reservationTime; // Date et heure de la réservation
@Column(name = "guest_count", nullable = false)
private Integer guestCount = 1; // Nombre de convives
@Column(name = "status", nullable = false, length = 20)
private String status = "PENDING"; // Statut: PENDING, CONFIRMED, CANCELLED, COMPLETED
@Column(name = "special_requests", columnDefinition = "TEXT")
private String specialRequests; // Demandes spéciales (allergies, préférences, etc.)
@Column(name = "table_number", length = 20)
private String tableNumber; // Numéro de table assigné (si applicable)
/**
* Constructeur pour créer une réservation.
*
* @param establishment L'établissement concerné
* @param user L'utilisateur qui fait la réservation
* @param reservationTime La date et heure de la réservation
* @param guestCount Le nombre de convives
*/
public Booking(Establishment establishment, Users user, LocalDateTime reservationTime, Integer guestCount) {
this.establishment = establishment;
this.user = user;
this.reservationTime = reservationTime;
this.guestCount = guestCount;
this.status = "PENDING";
}
/**
* Vérifie si la réservation est confirmée.
*
* @return true si la réservation est confirmée, false sinon.
*/
public boolean isConfirmed() {
return "CONFIRMED".equalsIgnoreCase(this.status);
}
/**
* Vérifie si la réservation est en attente.
*
* @return true si la réservation est en attente, false sinon.
*/
public boolean isPending() {
return "PENDING".equalsIgnoreCase(this.status);
}
/**
* Vérifie si la réservation est annulée.
*
* @return true si la réservation est annulée, false sinon.
*/
public boolean isCancelled() {
return "CANCELLED".equalsIgnoreCase(this.status);
}
/**
* Vérifie si la réservation est complétée.
*
* @return true si la réservation est complétée, false sinon.
*/
public boolean isCompleted() {
return "COMPLETED".equalsIgnoreCase(this.status);
}
}

View File

@@ -0,0 +1,90 @@
package com.lions.dev.entity.establishment;
import com.lions.dev.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* Entité représentant les horaires d'ouverture d'un établissement.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité permet de gérer les horaires d'ouverture par jour de la semaine,
* ainsi que les exceptions (fermetures temporaires, jours fériés, etc.).
*/
@Entity
@Table(name = "business_hours")
@Getter
@Setter
@NoArgsConstructor
@ToString
public class BusinessHours extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id", nullable = false)
private Establishment establishment; // L'établissement concerné
@Column(name = "day_of_week", nullable = false, length = 20)
private String dayOfWeek; // Jour de la semaine: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
@Column(name = "open_time", nullable = false, length = 5)
private String openTime; // Heure d'ouverture (format HH:MM, ex: "09:00")
@Column(name = "close_time", nullable = false, length = 5)
private String closeTime; // Heure de fermeture (format HH:MM, ex: "18:00")
@Column(name = "is_closed", nullable = false)
private Boolean isClosed = false; // Indique si l'établissement est fermé ce jour
@Column(name = "is_exception", nullable = false)
private Boolean isException = false; // Indique si c'est un jour exceptionnel (fermeture temporaire, jour férié, etc.)
@Column(name = "exception_date")
private LocalDateTime exceptionDate; // Date de l'exception (si is_exception = true)
/**
* Constructeur pour créer des horaires d'ouverture standard.
*
* @param establishment L'établissement concerné
* @param dayOfWeek Le jour de la semaine
* @param openTime L'heure d'ouverture (format HH:MM)
* @param closeTime L'heure de fermeture (format HH:MM)
*/
public BusinessHours(Establishment establishment, String dayOfWeek, String openTime, String closeTime) {
this.establishment = establishment;
this.dayOfWeek = dayOfWeek;
this.openTime = openTime;
this.closeTime = closeTime;
this.isClosed = false;
this.isException = false;
}
/**
* Constructeur pour créer un jour de fermeture.
*
* @param establishment L'établissement concerné
* @param dayOfWeek Le jour de la semaine
*/
public BusinessHours(Establishment establishment, String dayOfWeek) {
this.establishment = establishment;
this.dayOfWeek = dayOfWeek;
this.isClosed = true;
this.isException = false;
}
/**
* Vérifie si l'établissement est ouvert ce jour.
*
* @return true si l'établissement est ouvert, false sinon.
*/
public boolean isOpen() {
return !isClosed;
}
}

View File

@@ -10,6 +10,10 @@ import lombok.ToString;
/**
* Entité représentant un établissement dans le système AfterWork.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Un établissement est un lieu physique (bar, restaurant, club, etc.)
* où peuvent se dérouler des événements Afterwork.
* Seuls les responsables d'établissement peuvent créer et gérer des établissements.
@@ -43,35 +47,20 @@ public class Establishment extends BaseEntity {
@Column(name = "phone_number")
private String phoneNumber; // Numéro de téléphone
@Column(name = "email")
private String email; // Email de contact
@Column(name = "website")
private String website; // Site web
@Column(name = "image_url")
private String imageUrl; // URL de l'image de l'établissement
@Column(name = "rating")
private Double rating; // Note moyenne sur 5 (déprécié, utiliser averageRating)
@Column(name = "average_rating")
private Double averageRating; // Note moyenne calculée (0.0 à 5.0)
@Column(name = "total_ratings_count")
private Integer totalRatingsCount; // Nombre total de notes
@Column(name = "total_reviews_count")
private Integer totalReviewsCount; // Nombre total d'avis (v2.0 - renommé depuis total_ratings_count)
@Column(name = "price_range")
private String priceRange; // Fourchette de prix (cheap, moderate, expensive, luxury)
private String priceRange; // Fourchette de prix (LOW, MEDIUM, HIGH, PREMIUM)
@Column(name = "capacity")
private Integer capacity; // Capacité maximale
@Column(name = "amenities", length = 1000)
private String amenities; // Équipements (WiFi, Terrasse, Parking, etc.) - JSON ou séparés par virgule
@Column(name = "opening_hours")
private String openingHours; // Heures d'ouverture
@Column(name = "verification_status", nullable = false)
private String verificationStatus = "PENDING"; // Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)
@Column(name = "latitude")
private Double latitude; // Latitude pour la géolocalisation
@@ -88,7 +77,25 @@ public class Establishment extends BaseEntity {
private java.util.List<EstablishmentMedia> medias = new java.util.ArrayList<>(); // Liste des médias de l'établissement
@OneToMany(mappedBy = "establishment", cascade = CascadeType.ALL, orphanRemoval = true)
private java.util.List<EstablishmentRating> ratings = new java.util.ArrayList<>(); // Liste des notes de l'établissement
private java.util.List<EstablishmentRating> ratings = new java.util.ArrayList<>(); // Liste des notes de l'établissement (v1.0 - à migrer vers Review)
/**
* Vérifie si l'établissement est vérifié.
*
* @return true si l'établissement est vérifié, false sinon.
*/
public boolean isVerified() {
return "VERIFIED".equalsIgnoreCase(this.verificationStatus);
}
/**
* Vérifie si l'établissement est en attente de vérification.
*
* @return true si l'établissement est en attente, false sinon.
*/
public boolean isPending() {
return "PENDING".equalsIgnoreCase(this.verificationStatus);
}
/**
* Constructeur pour créer un établissement avec les informations de base.

View File

@@ -0,0 +1,98 @@
package com.lions.dev.entity.establishment;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Entité représentant un équipement d'un établissement.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité fait la liaison entre un établissement et un type d'équipement,
* avec la possibilité d'ajouter des détails supplémentaires.
*/
@Entity
@Table(name = "establishment_amenities")
@Getter
@Setter
@NoArgsConstructor
@ToString
@IdClass(EstablishmentAmenityId.class)
public class EstablishmentAmenity {
@Id
@Column(name = "establishment_id", nullable = false)
private UUID establishmentId; // ID de l'établissement
@Id
@Column(name = "amenity_id", nullable = false)
private UUID amenityId; // ID du type d'équipement (référence à amenity_types)
@Column(name = "details", length = 500)
private String details; // Détails supplémentaires (ex: "Parking gratuit pour 20 voitures")
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now(); // Date de création
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id", insertable = false, updatable = false)
private Establishment establishment; // L'établissement concerné
/**
* Constructeur pour créer une liaison établissement-équipement.
*
* @param establishmentId L'ID de l'établissement
* @param amenityId L'ID du type d'équipement
* @param details Les détails supplémentaires (optionnel)
*/
public EstablishmentAmenity(UUID establishmentId, UUID amenityId, String details) {
this.establishmentId = establishmentId;
this.amenityId = amenityId;
this.details = details;
this.createdAt = LocalDateTime.now();
}
@PrePersist
protected void onCreate() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
}
}
/**
* Classe composite pour la clé primaire de EstablishmentAmenity.
*/
@Getter
@Setter
@NoArgsConstructor
class EstablishmentAmenityId implements java.io.Serializable {
private UUID establishmentId;
private UUID amenityId;
public EstablishmentAmenityId(UUID establishmentId, UUID amenityId) {
this.establishmentId = establishmentId;
this.amenityId = amenityId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EstablishmentAmenityId that = (EstablishmentAmenityId) o;
return establishmentId.equals(that.establishmentId) && amenityId.equals(that.amenityId);
}
@Override
public int hashCode() {
return establishmentId.hashCode() + amenityId.hashCode();
}
}

View File

@@ -0,0 +1,92 @@
package com.lions.dev.entity.establishment;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.users.Users;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.HashMap;
import java.util.Map;
/**
* Entité représentant un avis (review) sur un établissement.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité remplace EstablishmentRating avec des fonctionnalités plus avancées,
* incluant des notes par critères et la vérification des visites.
*/
@Entity
@Table(name = "reviews",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "establishment_id"}))
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Review extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Users user; // L'utilisateur qui a écrit l'avis
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id", nullable = false)
private Establishment establishment; // L'établissement concerné
@Column(name = "overall_rating", nullable = false)
private Integer overallRating; // Note globale (1-5)
@Column(name = "comment", columnDefinition = "TEXT")
private String comment; // Commentaire libre de l'utilisateur
@Column(name = "criteria_ratings", nullable = false)
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.JSON)
private Map<String, Integer> criteriaRatings = new HashMap<>(); // Notes par critères (ex: {"ambiance": 4, "service": 5})
@Column(name = "is_verified_visit", nullable = false)
private Boolean isVerifiedVisit = false; // Indique si l'avis est lié à une visite vérifiée (réservation complétée)
/**
* Constructeur pour créer un avis.
*
* @param user L'utilisateur qui écrit l'avis
* @param establishment L'établissement concerné
* @param overallRating La note globale (1-5)
* @param comment Le commentaire (optionnel)
*/
public Review(Users user, Establishment establishment, Integer overallRating, String comment) {
this.user = user;
this.establishment = establishment;
this.overallRating = overallRating;
this.comment = comment;
this.criteriaRatings = new HashMap<>();
this.isVerifiedVisit = false;
}
/**
* Ajoute une note pour un critère spécifique.
*
* @param criteria Le nom du critère (ex: "ambiance", "service", "qualité")
* @param rating La note (1-5)
*/
public void addCriteriaRating(String criteria, Integer rating) {
if (rating >= 1 && rating <= 5) {
this.criteriaRatings.put(criteria, rating);
}
}
/**
* Retourne la note pour un critère spécifique.
*
* @param criteria Le nom du critère
* @return La note, ou null si le critère n'existe pas
*/
public Integer getCriteriaRating(String criteria) {
return this.criteriaRatings.get(criteria);
}
}

View File

@@ -16,8 +16,12 @@ import java.util.Set;
/**
* Entité représentant un événement dans le système AfterWork.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Chaque événement possède un titre, une description, une date de début, une date de fin,
* un créateur, une catégorie, un lieu, une URL d'image, et des participants.
* un créateur, une catégorie, un établissement, une URL d'image, et des participants.
* Tous les logs et commentaires nécessaires pour la traçabilité et la documentation sont inclus.
*/
@Entity
@@ -40,8 +44,9 @@ public class Events extends BaseEntity {
@Column(name = "end_date", nullable = false)
private LocalDateTime endDate; // La date de fin de l'événement
@Column(name = "location")
private String location; // Le lieu de l'événement
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id")
private com.lions.dev.entity.establishment.Establishment establishment; // L'établissement où se déroule l'événement (v2.0)
@Column(name = "category")
private String category; // La catégorie de l'événement
@@ -53,7 +58,7 @@ public class Events extends BaseEntity {
private String imageUrl; // URL d'une image associée à l'événement
@Column(name = "status", nullable = false)
private String status = "ouvert"; // Le statut de l'événement (en cours, terminé, annulé, etc.)
private String status = "OPEN"; // Le statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED) (v2.0)
@Column(name = "max_participants")
private Integer maxParticipants; // Nombre maximum de participants autorisés
@@ -67,6 +72,12 @@ public class Events extends BaseEntity {
@Column(name = "participation_fee")
private Integer participationFee; // Frais de participation en centimes
@Column(name = "is_private", nullable = false)
private Boolean isPrivate = false; // Indique si l'événement est privé (v2.0)
@Column(name = "waitlist_enabled", nullable = false)
private Boolean waitlistEnabled = false; // Indique si la liste d'attente est activée (v2.0)
@Column(name = "privacy_rules", length = 1000)
private String privacyRules; // Règles de confidentialité de l'événement
@@ -131,13 +142,44 @@ public class Events extends BaseEntity {
}
/**
* Ferme l'événement en changeant son statut.
* Ferme l'événement en changeant son statut (v2.0).
*/
public void setClosed(boolean closed) {
this.status = closed ? "fermé" : "ouvert";
this.status = closed ? "CLOSED" : "OPEN";
System.out.println("[LOG] Statut de l'événement mis à jour : " + this.title + " - " + this.status);
}
/**
* Vérifie si l'événement est ouvert (v2.0).
*
* @return true si l'événement est ouvert, false sinon.
*/
public boolean isOpen() {
return "OPEN".equalsIgnoreCase(this.status);
}
/**
* Vérifie si l'événement est fermé (v2.0).
*
* @return true si l'événement est fermé, false sinon.
*/
public boolean isClosed() {
return "CLOSED".equalsIgnoreCase(this.status);
}
/**
* Retourne le lieu de l'événement depuis l'établissement associé (v2.0).
* Méthode de compatibilité pour remplacer l'ancien champ location.
*
* @return L'adresse de l'établissement, ou null si aucun établissement n'est associé.
*/
public String getLocation() {
if (establishment != null) {
return establishment.getAddress() + ", " + establishment.getCity();
}
return null;
}
@OneToMany(fetch = FetchType.LAZY, mappedBy = "event")
private List<Comment> comments; // Liste des commentaires associés à l'événement

View File

@@ -0,0 +1,119 @@
package com.lions.dev.entity.promotion;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.establishment.Establishment;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* Entité représentant une promotion ou une offre spéciale d'un établissement.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité permet de gérer les promotions, happy hours, et offres spéciales
* avec différents types de réductions et codes promo.
*/
@Entity
@Table(name = "promotions")
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Promotion extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "establishment_id", nullable = false)
private Establishment establishment; // L'établissement qui propose la promotion
@Column(name = "title", nullable = false, length = 200)
private String title; // Titre de la promotion (ex: "Happy Hour", "Menu du jour")
@Column(name = "description", columnDefinition = "TEXT")
private String description; // Description détaillée de la promotion
@Column(name = "promo_code", length = 50, unique = true)
private String promoCode; // Code promo optionnel (ex: "HAPPY2024")
@Column(name = "discount_type", nullable = false, length = 20)
private String discountType; // Type de réduction: PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
@Column(name = "discount_value", nullable = false)
private java.math.BigDecimal discountValue; // Valeur de la réduction (pourcentage, montant fixe, etc.)
@Column(name = "valid_from", nullable = false)
private LocalDateTime validFrom; // Date de début de validité
@Column(name = "valid_until", nullable = false)
private LocalDateTime validUntil; // Date de fin de validité
@Column(name = "is_active", nullable = false)
private Boolean isActive = true; // Indique si la promotion est active
/**
* Constructeur pour créer une promotion.
*
* @param establishment L'établissement qui propose la promotion
* @param title Le titre de la promotion
* @param description La description
* @param discountType Le type de réduction
* @param discountValue La valeur de la réduction
* @param validFrom La date de début
* @param validUntil La date de fin
*/
public Promotion(Establishment establishment, String title, String description,
String discountType, java.math.BigDecimal discountValue,
LocalDateTime validFrom, LocalDateTime validUntil) {
this.establishment = establishment;
this.title = title;
this.description = description;
this.discountType = discountType;
this.discountValue = discountValue;
this.validFrom = validFrom;
this.validUntil = validUntil;
this.isActive = true;
}
/**
* Vérifie si la promotion est actuellement valide.
*
* @return true si la promotion est active et dans sa période de validité, false sinon.
*/
public boolean isValid() {
LocalDateTime now = LocalDateTime.now();
return isActive && now.isAfter(validFrom) && now.isBefore(validUntil);
}
/**
* Vérifie si la promotion est expirée.
*
* @return true si la promotion est expirée, false sinon.
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(validUntil);
}
/**
* Vérifie si la promotion est une réduction en pourcentage.
*
* @return true si c'est un pourcentage, false sinon.
*/
public boolean isPercentage() {
return "PERCENTAGE".equalsIgnoreCase(this.discountType);
}
/**
* Vérifie si la promotion est un montant fixe.
*
* @return true si c'est un montant fixe, false sinon.
*/
public boolean isFixedAmount() {
return "FIXED_AMOUNT".equalsIgnoreCase(this.discountType);
}
}

View File

@@ -2,7 +2,6 @@ package com.lions.dev.entity.reaction;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.users.Users;
import com.lions.dev.entity.events.Events;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -10,58 +9,66 @@ import lombok.Setter;
import lombok.ToString;
/**
* Entité représentant une réaction d'un utilisateur à un événement dans le système AfterWork.
* Une réaction peut être un "like", un "dislike" ou toute autre forme de réaction définie.
*
* Cette entité est liée à l'utilisateur qui réagit et à l'événement auquel la réaction est associée.
* Tous les logs et commentaires sont inclus pour la traçabilité.
* Entité représentant une réaction d'un utilisateur à un contenu.
*
* Version 2.0 - Architecture refactorée.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Cette entité remplace les compteurs de réactions simples avec un système
* plus flexible permettant différents types de réactions sur différents contenus.
*/
@Entity
@Table(name = "reactions")
@Table(name = "reactions",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "target_type", "target_id"}))
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Reaction extends BaseEntity {
// Types de réaction possibles
public enum ReactionType {
LIKE, DISLIKE, LOVE, ANGRY, SAD
}
@Enumerated(EnumType.STRING)
@Column(name = "reaction_type", nullable = false)
private ReactionType reactionType; // Le type de réaction (LIKE, DISLIKE, etc.)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Users user; // L'utilisateur qui a effectué la réaction
private Users user; // L'utilisateur qui a réagi
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id", nullable = false)
private Events event; // L'événement auquel la réaction est associée
@Column(name = "reaction_type", nullable = false, length = 20)
private String reactionType; // Type de réaction: LIKE, LOVE, HAHA, WOW, SAD, ANGRY
@Column(name = "target_type", nullable = false, length = 20)
private String targetType; // Type de contenu ciblé: POST, EVENT, COMMENT, REVIEW
@Column(name = "target_id", nullable = false)
private java.util.UUID targetId; // ID du contenu ciblé (post_id, event_id, comment_id, review_id)
/**
* Associe une réaction à un utilisateur et un événement.
* Constructeur pour créer une réaction.
*
* @param user L'utilisateur qui réagit.
* @param event L'événement auquel la réaction est liée.
* @param reactionType Le type de réaction.
* @param user L'utilisateur qui réagit
* @param reactionType Le type de réaction (LIKE, LOVE, HAHA, WOW, SAD, ANGRY)
* @param targetType Le type de contenu (POST, EVENT, COMMENT, REVIEW)
* @param targetId L'ID du contenu ciblé
*/
public Reaction(Users user, Events event, ReactionType reactionType) {
public Reaction(Users user, String reactionType, String targetType, java.util.UUID targetId) {
this.user = user;
this.event = event;
this.reactionType = reactionType;
System.out.println("[LOG] Nouvelle réaction ajoutée : " + reactionType + " par l'utilisateur : " + user.getEmail() + " à l'événement : " + event.getTitle());
this.targetType = targetType;
this.targetId = targetId;
}
/**
* Modifie le type de réaction de l'utilisateur pour cet événement.
* Vérifie si la réaction est un "like".
*
* @param newReactionType Le nouveau type de réaction.
* @return true si c'est un like, false sinon.
*/
public void updateReaction(ReactionType newReactionType) {
System.out.println("[LOG] Changement de la réaction de " + this.reactionType + " à " + newReactionType + " pour l'utilisateur : " + user.getEmail() + " à l'événement : " + event.getTitle());
this.reactionType = newReactionType;
public boolean isLike() {
return "LIKE".equalsIgnoreCase(this.reactionType);
}
/**
* Vérifie si la réaction est un "love".
*
* @return true si c'est un love, false sinon.
*/
public boolean isLove() {
return "LOVE".equalsIgnoreCase(this.reactionType);
}
}

View File

@@ -17,13 +17,25 @@ public class EstablishmentRatingRepository implements PanacheRepositoryBase<Esta
/**
* Récupère la note d'un utilisateur pour un établissement.
* Utilise un fetch join pour charger les relations establishment et user en une seule requête.
*
* @param establishmentId L'ID de l'établissement
* @param userId L'ID de l'utilisateur
* @return La note de l'utilisateur ou null si pas encore noté
*/
public EstablishmentRating findByEstablishmentIdAndUserId(UUID establishmentId, UUID userId) {
return find("establishment.id = ?1 AND user.id = ?2", establishmentId, userId).firstResult();
List<EstablishmentRating> results = getEntityManager()
.createQuery(
"SELECT r FROM EstablishmentRating r " +
"JOIN FETCH r.establishment " +
"JOIN FETCH r.user " +
"WHERE r.establishment.id = :establishmentId AND r.user.id = :userId",
EstablishmentRating.class
)
.setParameter("establishmentId", establishmentId)
.setParameter("userId", userId)
.getResultList();
return results.isEmpty() ? null : results.get(0);
}
/**

View File

@@ -25,10 +25,23 @@ public class StoryRepository implements PanacheRepositoryBase<Story, UUID> {
* @return Liste des stories actives
*/
public List<Story> findAllActive() {
System.out.println("[LOG] Récupération de toutes les stories actives");
return findAllActive(0, Integer.MAX_VALUE);
}
/**
* Récupère toutes les stories actives (non expirées) avec pagination, triées par date de création décroissante.
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des stories actives
*/
public List<Story> findAllActive(int page, int size) {
System.out.println("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
List<Story> stories = find("isActive = true AND expiresAt > ?1",
Sort.by("createdAt", Sort.Direction.Descending),
LocalDateTime.now()).list();
LocalDateTime.now())
.page(page, size)
.list();
System.out.println("[LOG] " + stories.size() + " story(ies) active(s) récupérée(s)");
return stories;
}
@@ -40,11 +53,25 @@ public class StoryRepository implements PanacheRepositoryBase<Story, UUID> {
* @return Liste des stories actives de l'utilisateur
*/
public List<Story> findActiveByUserId(UUID userId) {
System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId);
return findActiveByUserId(userId, 0, Integer.MAX_VALUE);
}
/**
* Récupère toutes les stories actives d'un utilisateur avec pagination.
*
* @param userId L'ID de l'utilisateur
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des stories actives de l'utilisateur
*/
public List<Story> findActiveByUserId(UUID userId, int page, int size) {
System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
List<Story> stories = find("user.id = ?1 AND isActive = true AND expiresAt > ?2",
Sort.by("createdAt", Sort.Direction.Descending),
userId,
LocalDateTime.now()).list();
LocalDateTime.now())
.page(page, size)
.list();
System.out.println("[LOG] " + stories.size() + " story(ies) active(s) trouvée(s) pour l'utilisateur ID : " + userId);
return stories;
}

View File

@@ -1,11 +1,13 @@
package com.lions.dev.resource;
import com.lions.dev.dto.request.establishment.EstablishmentMediaRequestDTO;
import com.lions.dev.dto.response.establishment.EstablishmentMediaResponseDTO;
import com.lions.dev.entity.establishment.EstablishmentMedia;
import com.lions.dev.entity.establishment.MediaType;
import com.lions.dev.service.EstablishmentMediaService;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -62,6 +64,7 @@ public class EstablishmentMediaResource {
/**
* Upload un nouveau média pour un établissement.
* Accepte un body JSON avec les informations du média.
*/
@POST
@Transactional
@@ -69,36 +72,56 @@ public class EstablishmentMediaResource {
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
public Response uploadMedia(
@PathParam("establishmentId") String establishmentId,
@QueryParam("mediaUrl") String mediaUrl,
@QueryParam("mediaType") String mediaTypeStr,
@QueryParam("uploadedByUserId") String uploadedByUserIdStr,
@QueryParam("thumbnailUrl") String thumbnailUrl) {
@Valid EstablishmentMediaRequestDTO requestDTO,
@QueryParam("uploadedByUserId") String uploadedByUserIdStr) {
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
try {
UUID id = UUID.fromString(establishmentId);
UUID uploadedByUserId = UUID.fromString(uploadedByUserIdStr);
// Utiliser uploadedByUserId du query param ou du DTO
String userIdStr = uploadedByUserIdStr != null && !uploadedByUserIdStr.isEmpty()
? uploadedByUserIdStr
: requestDTO.getUploadedByUserId();
if (userIdStr == null || userIdStr.isEmpty()) {
LOG.error("uploadedByUserId est obligatoire");
return Response.status(Response.Status.BAD_REQUEST)
.entity("L'ID de l'utilisateur (uploadedByUserId) est obligatoire")
.build();
}
UUID uploadedByUserId = UUID.fromString(userIdStr);
// Valider le type de média
MediaType mediaType;
try {
mediaType = MediaType.valueOf(mediaTypeStr.toUpperCase());
mediaType = MediaType.valueOf(requestDTO.getMediaType().toUpperCase());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de média invalide. Utilisez PHOTO ou VIDEO")
.build();
}
EstablishmentMedia media = mediaService.uploadMedia(id, mediaUrl, mediaType, uploadedByUserId, thumbnailUrl);
// Appeler le service avec displayOrder
EstablishmentMedia media = mediaService.uploadMedia(
id,
requestDTO.getMediaUrl(),
mediaType,
uploadedByUserId,
requestDTO.getThumbnailUrl(),
requestDTO.getDisplayOrder() != null ? requestDTO.getDisplayOrder() : 0
);
EstablishmentMediaResponseDTO responseDTO = new EstablishmentMediaResponseDTO(media);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.error("Paramètres invalides : " + e.getMessage());
LOG.error("Paramètres invalides : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity("Paramètres invalides : " + e.getMessage())
.build();
} catch (RuntimeException e) {
LOG.error("Erreur lors de l'upload du média : " + e.getMessage());
LOG.error("Erreur lors de l'upload du média : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage())
.build();

View File

@@ -109,46 +109,9 @@ public class EstablishmentRatingResource {
}
}
/**
* Récupère la note d'un utilisateur pour un établissement.
*/
@GET
@Path("/users/{userId}")
@Operation(summary = "Récupérer la note d'un utilisateur",
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement")
public Response getUserRating(
@PathParam("establishmentId") String establishmentId,
@PathParam("userId") String userIdStr) {
LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
try {
UUID id = UUID.fromString(establishmentId);
UUID userId = UUID.fromString(userIdStr);
EstablishmentRating rating = ratingService.getUserRating(id, userId);
if (rating == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Note non trouvée")
.build();
}
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.error("ID invalide : " + e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide : " + e.getMessage())
.build();
} catch (Exception e) {
LOG.error("Erreur inattendue lors de la récupération de la note", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la note")
.build();
}
}
/**
* Récupère les statistiques de notation d'un établissement.
* Doit être déclaré avant les endpoints génériques GET pour la résolution correcte par JAX-RS.
*/
@GET
@Path("/stats")
@@ -179,5 +142,92 @@ public class EstablishmentRatingResource {
.build();
}
}
/**
* Récupère la note d'un utilisateur pour un établissement (via path parameter).
* Endpoint alternatif pour compatibilité.
*/
@GET
@Path("/users/{userId}")
@Operation(summary = "Récupérer la note d'un utilisateur (path parameter)",
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via path parameter)")
public Response getUserRatingByPath(
@PathParam("establishmentId") String establishmentId,
@PathParam("userId") String userIdStr) {
LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
try {
UUID id = UUID.fromString(establishmentId);
UUID userId = UUID.fromString(userIdStr);
EstablishmentRating rating = ratingService.getUserRating(id, userId);
if (rating == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Note non trouvée")
.build();
}
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.error("ID invalide : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide : " + e.getMessage())
.build();
} catch (Exception e) {
LOG.error("Erreur inattendue lors de la récupération de la note", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la note")
.build();
}
}
/**
* Récupère la note d'un utilisateur pour un établissement (via query parameter).
* Endpoint utilisé par le frontend Flutter.
* Doit être déclaré en dernier car c'est l'endpoint le plus générique.
*/
@GET
@Operation(summary = "Récupérer la note d'un utilisateur",
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via query parameter userId)")
public Response getUserRatingByQuery(
@PathParam("establishmentId") String establishmentId,
@QueryParam("userId") String userIdStr) {
LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
// Si userId n'est pas fourni, retourner une erreur
if (userIdStr == null || userIdStr.isEmpty()) {
LOG.warn("userId manquant dans la requête");
return Response.status(Response.Status.BAD_REQUEST)
.entity("Le paramètre userId est requis")
.build();
}
try {
UUID id = UUID.fromString(establishmentId);
UUID userId = UUID.fromString(userIdStr);
EstablishmentRating rating = ratingService.getUserRating(id, userId);
if (rating == null) {
// Retourner 404 si la note n'existe pas
return Response.status(Response.Status.NOT_FOUND)
.entity("Note non trouvée")
.build();
}
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
LOG.error("ID invalide : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity("ID invalide : " + e.getMessage())
.build();
} catch (Exception e) {
LOG.error("Erreur inattendue lors de la récupération de la note", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération de la note")
.build();
}
}
}

View File

@@ -76,14 +76,8 @@ public class EstablishmentResource {
establishment.setPostalCode(requestDTO.getPostalCode());
establishment.setDescription(requestDTO.getDescription());
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
establishment.setEmail(requestDTO.getEmail());
establishment.setWebsite(requestDTO.getWebsite());
establishment.setImageUrl(requestDTO.getImageUrl());
establishment.setRating(requestDTO.getRating());
establishment.setPriceRange(requestDTO.getPriceRange());
establishment.setCapacity(requestDTO.getCapacity());
establishment.setAmenities(requestDTO.getAmenities());
establishment.setOpeningHours(requestDTO.getOpeningHours());
establishment.setLatitude(requestDTO.getLatitude());
establishment.setLongitude(requestDTO.getLongitude());
@@ -245,14 +239,8 @@ public class EstablishmentResource {
establishment.setPostalCode(requestDTO.getPostalCode());
establishment.setDescription(requestDTO.getDescription());
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
establishment.setEmail(requestDTO.getEmail());
establishment.setWebsite(requestDTO.getWebsite());
establishment.setImageUrl(requestDTO.getImageUrl());
establishment.setRating(requestDTO.getRating());
establishment.setPriceRange(requestDTO.getPriceRange());
establishment.setCapacity(requestDTO.getCapacity());
establishment.setAmenities(requestDTO.getAmenities());
establishment.setOpeningHours(requestDTO.getOpeningHours());
establishment.setLatitude(requestDTO.getLatitude());
establishment.setLongitude(requestDTO.getLongitude());

View File

@@ -229,10 +229,12 @@ public class EventsResource {
@Path("/created-by-user-and-friends")
@Consumes("application/json")
@Produces("application/json")
@Operation(summary = "Récupérer les événements créés par un utilisateur et ses amis", description = "Retourne la liste des événements créés par un utilisateur spécifique et ses amis")
@Operation(summary = "Récupérer les événements créés par un utilisateur et ses amis", description = "Retourne la liste paginée des événements créés par un utilisateur spécifique et ses amis")
public Response getEventsCreatedByUserAndFriends(EventReadManyByIdRequestDTO requestDTO) {
UUID userId = requestDTO.getUserId();
LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " et ses amis");
UUID userId = requestDTO.getId();
int page = requestDTO.getPage() != null ? requestDTO.getPage() : 0;
int size = requestDTO.getSize() != null ? requestDTO.getSize() : 10;
LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " et ses amis (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
@@ -247,8 +249,10 @@ public class EventsResource {
LOG.info("[LOG] IDs d'amis + utilisateur (taille: " + friendIds.size() + ") : " + friendIds);
// Rechercher les événements créés par l'utilisateur et ses amis
List<Events> events = eventsRepository.find("creator.id IN ?1", friendIds).list();
// Rechercher les événements créés par l'utilisateur et ses amis avec pagination
List<Events> events = eventsRepository.find("creator.id IN ?1 ORDER BY startDate DESC", friendIds)
.page(page, size)
.list();
LOG.info("[LOG] Nombre d'événements récupérés dans la requête : " + events.size());
// ✅ Retourner avec reactionsCount et isFavorite pour l'utilisateur actuel
@@ -443,14 +447,17 @@ public class EventsResource {
@Operation(
summary = "Récupérer les événements auxquels un utilisateur est inscrit",
description = "Retourne la liste des événements auxquels un utilisateur spécifique est inscrit")
public Response getEventsByUser(@PathParam("userId") UUID userId) {
LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId);
public Response getEventsByUser(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
return Response.status(Response.Status.NOT_FOUND).entity("Utilisateur non trouvé.").build();
}
List<Events> events = eventService.findEventsByUser(user);
List<Events> events = eventService.findEventsByUser(user, page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement trouvé pour l'utilisateur avec l'ID : " + userId);
return Response.status(Response.Status.NOT_FOUND).entity("Aucun événement trouvé.").build();
@@ -1011,7 +1018,7 @@ public class EventsResource {
public Response getEventsByFriends(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements des amis pour l'utilisateur ID : " + userId);
try {

View File

@@ -91,6 +91,7 @@ public class FileUploadResource {
// Construire la réponse JSON
Map<String, Object> response = new HashMap<>();
response.put("url", fileUrl);
response.put("fileName", finalFileName); // ✅ Ajout du fileName dans la réponse (DRY/WOU)
if (thumbnailUrl != null) {
response.put("thumbnailUrl", thumbnailUrl);
}
@@ -105,7 +106,7 @@ public class FileUploadResource {
}
}
LOG.infof("Upload réussi, URL: %s", fileUrl);
LOG.infof("Upload réussi, URL: %s, FileName: %s", fileUrl, finalFileName);
return Response.status(Response.Status.CREATED)
.entity(response)

View File

@@ -80,7 +80,7 @@ public class NotificationResource {
public Response getNotificationsByUserIdWithPagination(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération paginée des notifications pour l'utilisateur ID : " + userId);
try {

View File

@@ -11,6 +11,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -49,7 +50,7 @@ public class SocialPostResource {
description = "Retourne une liste paginée de tous les posts sociaux, triés par date de création décroissante")
public Response getAllPosts(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération de tous les posts (page: " + page + ", size: " + size + ")");
try {
@@ -110,12 +111,12 @@ public class SocialPostResource {
summary = "Créer un nouveau post",
description = "Crée un nouveau post social et retourne ses détails")
public Response createPost(@Valid SocialPostCreateRequestDTO requestDTO) {
LOG.info("[LOG] Création d'un nouveau post par l'utilisateur ID : " + requestDTO.getUserId());
LOG.info("[LOG] Création d'un nouveau post par l'utilisateur ID : " + requestDTO.getCreatorId());
try {
SocialPost post = socialPostService.createPost(
requestDTO.getContent(),
requestDTO.getUserId(),
requestDTO.getCreatorId(),
requestDTO.getImageUrl()
);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
@@ -244,11 +245,19 @@ public class SocialPostResource {
@Operation(
summary = "Liker un post",
description = "Incrémente le compteur de likes d'un post")
public Response likePost(@PathParam("id") UUID postId) {
LOG.info("[LOG] Like du post ID : " + postId);
public Response likePost(
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId) {
LOG.info("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
try {
SocialPost post = socialPostService.likePost(postId);
SocialPost post = socialPostService.likePost(postId, userId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -273,14 +282,30 @@ public class SocialPostResource {
@POST
@Path("/{id}/comment")
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
@Operation(
summary = "Commenter un post",
description = "Incrémente le compteur de commentaires d'un post")
public Response addComment(@PathParam("id") UUID postId) {
LOG.info("[LOG] Ajout de commentaire au post ID : " + postId);
description = "Ajoute un commentaire à un post")
public Response addComment(
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId,
String requestBody) {
LOG.info("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
try {
SocialPost post = socialPostService.addComment(postId);
// Parser le body pour obtenir le contenu du commentaire
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> body = mapper.readValue(requestBody, Map.class);
String commentContent = (String) body.getOrDefault("content", "");
SocialPost post = socialPostService.addComment(postId, userId, commentContent);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -308,11 +333,19 @@ public class SocialPostResource {
@Operation(
summary = "Partager un post",
description = "Incrémente le compteur de partages d'un post")
public Response sharePost(@PathParam("id") UUID postId) {
LOG.info("[LOG] Partage du post ID : " + postId);
public Response sharePost(
@PathParam("id") UUID postId,
@QueryParam("userId") UUID userId) {
LOG.info("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId);
if (userId == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("{\"message\": \"userId est requis.\"}")
.build();
}
try {
SocialPost post = socialPostService.sharePost(postId);
SocialPost post = socialPostService.sharePost(postId, userId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -373,7 +406,7 @@ public class SocialPostResource {
public Response getPostsByFriends(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des posts des amis pour l'utilisateur ID : " + userId);
try {

View File

@@ -45,12 +45,15 @@ public class StoryResource {
@GET
@Operation(
summary = "Récupérer toutes les stories actives",
description = "Retourne une liste de toutes les stories actives (non expirées), triées par date de création décroissante")
public Response getAllActiveStories(@QueryParam("viewerId") UUID viewerId) {
LOG.info("[LOG] Récupération de toutes les stories actives");
description = "Retourne une liste paginée de toutes les stories actives (non expirées), triées par date de création décroissante")
public Response getAllActiveStories(
@QueryParam("viewerId") UUID viewerId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
try {
List<Story> stories = storyService.getAllActiveStories();
List<Story> stories = storyService.getAllActiveStories(page, size);
List<StoryResponseDTO> responseDTOs = stories.stream()
.map(story -> viewerId != null ? new StoryResponseDTO(story, viewerId) : new StoryResponseDTO(story))
.collect(Collectors.toList());
@@ -109,12 +112,16 @@ public class StoryResource {
@Path("/user/{userId}")
@Operation(
summary = "Récupérer les stories d'un utilisateur",
description = "Retourne toutes les stories actives d'un utilisateur spécifique")
public Response getStoriesByUserId(@PathParam("userId") UUID userId, @QueryParam("viewerId") UUID viewerId) {
LOG.info("[LOG] Récupération des stories pour l'utilisateur ID : " + userId);
description = "Retourne une liste paginée des stories actives d'un utilisateur spécifique")
public Response getStoriesByUserId(
@PathParam("userId") UUID userId,
@QueryParam("viewerId") UUID viewerId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des stories pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
try {
List<Story> stories = storyService.getActiveStoriesByUserId(userId);
List<Story> stories = storyService.getActiveStoriesByUserId(userId, page, size);
List<StoryResponseDTO> responseDTOs = stories.stream()
.map(story -> viewerId != null ? new StoryResponseDTO(story, viewerId) : new StoryResponseDTO(story))
.collect(Collectors.toList());
@@ -140,11 +147,11 @@ public class StoryResource {
summary = "Créer une nouvelle story",
description = "Crée une nouvelle story et retourne ses détails")
public Response createStory(@Valid StoryCreateRequestDTO requestDTO) {
LOG.info("[LOG] Création d'une nouvelle story par l'utilisateur ID : " + requestDTO.getUserId());
LOG.info("[LOG] Création d'une nouvelle story par l'utilisateur ID : " + requestDTO.getCreatorId());
try {
Story story = storyService.createStory(
requestDTO.getUserId(),
requestDTO.getCreatorId(),
requestDTO.getMediaType(),
requestDTO.getMediaUrl(),
requestDTO.getThumbnailUrl(),

View File

@@ -60,7 +60,7 @@ public class UsersResource {
}
/**
* Endpoint pour authentifier un utilisateur.
* Endpoint pour authentifier un utilisateur (v2.0).
*
* @param userAuthenticateRequestDTO Le DTO contenant les informations d'authentification.
* @return Une réponse HTTP indiquant si l'authentification a réussi ou échoué.
@@ -73,10 +73,18 @@ public class UsersResource {
public Response authenticateUser(@Valid @NotNull UserAuthenticateRequestDTO userAuthenticateRequestDTO) {
LOG.info("Tentative d'authentification pour l'utilisateur avec l'email : " + userAuthenticateRequestDTO.getEmail());
Users user = userService.authenticateUser(userAuthenticateRequestDTO.getEmail(), userAuthenticateRequestDTO.getMotDePasse());
// v2.0 - Utiliser getPassword() qui gère la compatibilité v1.0 et v2.0
Users user = userService.authenticateUser(userAuthenticateRequestDTO.getEmail(), userAuthenticateRequestDTO.getPassword());
LOG.info("Authentification réussie pour l'utilisateur : " + user.getEmail());
UserAuthenticateResponseDTO responseDTO = new UserAuthenticateResponseDTO(user.getId(), user.getPrenoms(), user.getNom(), user.getEmail(), user.getRole());
// v2.0 - Utiliser les nouveaux noms de champs
UserAuthenticateResponseDTO responseDTO = new UserAuthenticateResponseDTO(
user.getId(),
user.getFirstName(), // v2.0
user.getLastName(), // v2.0
user.getEmail(),
user.getRole()
);
responseDTO.logResponseDetails();
return Response.ok(responseDTO).build();
}

View File

@@ -52,10 +52,11 @@ public class EstablishmentMediaService {
* @param mediaType Le type de média (PHOTO ou VIDEO)
* @param uploadedByUserId L'ID de l'utilisateur qui upload
* @param thumbnailUrl L'URL de la miniature (optionnel, pour les vidéos)
* @param displayOrder L'ordre d'affichage (optionnel, par défaut calculé automatiquement)
* @return Le média créé
*/
@Transactional
public EstablishmentMedia uploadMedia(UUID establishmentId, String mediaUrl, MediaType mediaType, UUID uploadedByUserId, String thumbnailUrl) {
public EstablishmentMedia uploadMedia(UUID establishmentId, String mediaUrl, MediaType mediaType, UUID uploadedByUserId, String thumbnailUrl, Integer displayOrder) {
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
// Vérifier que l'établissement existe
@@ -76,13 +77,18 @@ public class EstablishmentMediaService {
EstablishmentMedia media = new EstablishmentMedia(establishment, mediaUrl, mediaType, uploadedBy);
media.setThumbnailUrl(thumbnailUrl);
// Déterminer l'ordre d'affichage (dernier média = ordre le plus élevé)
List<EstablishmentMedia> existingMedia = mediaRepository.findByEstablishmentId(establishmentId);
int maxOrder = existingMedia.stream()
.mapToInt(EstablishmentMedia::getDisplayOrder)
.max()
.orElse(-1);
media.setDisplayOrder(maxOrder + 1);
// Utiliser le displayOrder fourni, ou calculer automatiquement si non fourni
if (displayOrder != null) {
media.setDisplayOrder(displayOrder);
} else {
// Déterminer l'ordre d'affichage (dernier média = ordre le plus élevé)
List<EstablishmentMedia> existingMedia = mediaRepository.findByEstablishmentId(establishmentId);
int maxOrder = existingMedia.stream()
.mapToInt(EstablishmentMedia::getDisplayOrder)
.max()
.orElse(-1);
media.setDisplayOrder(maxOrder + 1);
}
mediaRepository.persist(media);
LOG.info("Média uploadé avec succès : " + media.getId());

View File

@@ -158,7 +158,8 @@ public class EstablishmentRatingService {
Long totalRatings = ratingRepository.countByEstablishmentId(establishmentId);
establishment.setAverageRating(averageRating);
establishment.setTotalRatingsCount(totalRatings.intValue());
// v2.0 - Renommé depuis setTotalRatingsCount
establishment.setTotalReviewsCount(totalRatings.intValue());
establishmentRepository.persist(establishment);
LOG.info("Statistiques mises à jour pour l'établissement " + establishmentId + " : moyenne = " + averageRating + ", total = " + totalRatings);

View File

@@ -63,12 +63,22 @@ public class EstablishmentService {
/**
* Récupère tous les établissements.
* Charge également les médias pour chaque établissement pour inclure l'image principale.
*
* @return Une liste de tous les établissements.
*/
public List<Establishment> getAllEstablishments() {
LOG.info("[LOG] Récupération de tous les établissements");
List<Establishment> establishments = establishmentRepository.listAll();
// Utiliser une requête avec fetch join pour charger les médias en une seule requête
List<Establishment> establishments = establishmentRepository.getEntityManager()
.createQuery(
"SELECT DISTINCT e FROM Establishment e " +
"LEFT JOIN FETCH e.medias m " +
"LEFT JOIN FETCH e.manager " +
"ORDER BY e.name ASC",
Establishment.class
)
.getResultList();
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
return establishments;
}
@@ -105,7 +115,7 @@ public class EstablishmentService {
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
}
// Mettre à jour les champs
// v2.0 - Mettre à jour les champs
establishment.setName(updatedEstablishment.getName());
establishment.setType(updatedEstablishment.getType());
establishment.setAddress(updatedEstablishment.getAddress());
@@ -113,16 +123,19 @@ public class EstablishmentService {
establishment.setPostalCode(updatedEstablishment.getPostalCode());
establishment.setDescription(updatedEstablishment.getDescription());
establishment.setPhoneNumber(updatedEstablishment.getPhoneNumber());
establishment.setEmail(updatedEstablishment.getEmail());
establishment.setWebsite(updatedEstablishment.getWebsite());
establishment.setImageUrl(updatedEstablishment.getImageUrl());
establishment.setRating(updatedEstablishment.getRating());
establishment.setPriceRange(updatedEstablishment.getPriceRange());
establishment.setCapacity(updatedEstablishment.getCapacity());
establishment.setAmenities(updatedEstablishment.getAmenities());
establishment.setOpeningHours(updatedEstablishment.getOpeningHours());
// v2.0 - Nouveau champ
if (updatedEstablishment.getVerificationStatus() != null) {
establishment.setVerificationStatus(updatedEstablishment.getVerificationStatus());
}
establishment.setLatitude(updatedEstablishment.getLatitude());
establishment.setLongitude(updatedEstablishment.getLongitude());
// v2.0 - Champs supprimés (email, imageUrl, rating, capacity, amenities, openingHours)
// Ces champs ne sont plus mis à jour car ils sont dépréciés
establishmentRepository.persist(establishment);
LOG.info("[LOG] Établissement mis à jour avec succès : " + establishment.getName());

View File

@@ -12,6 +12,10 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.EventsRepository;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.repository.EstablishmentRepository;
import com.lions.dev.dto.events.NotificationEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.LocalDateTime;
@@ -21,6 +25,10 @@ import java.util.stream.Collectors;
/**
* Service de gestion des événements.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce service contient la logique métier pour la création, récupération, mise à jour et suppression des événements.
* Chaque méthode est loguée pour assurer une traçabilité exhaustive des actions effectuées.
*/
@@ -36,13 +44,20 @@ public class EventService {
@Inject
UsersRepository usersRepository;
@Inject
EstablishmentRepository establishmentRepository; // v2.0
@Inject
NotificationService notificationService;
@Inject
@Channel("notifications")
Emitter<NotificationEvent> notificationEmitter; // v2.0 - Publie dans Kafka
private static final Logger logger = LoggerFactory.getLogger(EventService.class);
/**
* Crée un nouvel événement dans le système.
* Crée un nouvel événement dans le système (v2.0).
*
* @param eventCreateRequestDTO Le DTO contenant les informations de l'événement à créer.
* @param creator L'utilisateur créateur de l'événement.
@@ -55,7 +70,18 @@ public class EventService {
event.setDescription(eventCreateRequestDTO.getDescription());
event.setStartDate(eventCreateRequestDTO.getStartDate());
event.setEndDate(eventCreateRequestDTO.getEndDate());
event.setLocation(eventCreateRequestDTO.getLocation());
// v2.0 - Établissement au lieu de location
if (eventCreateRequestDTO.getEstablishmentId() != null) {
com.lions.dev.entity.establishment.Establishment establishment =
establishmentRepository.findById(eventCreateRequestDTO.getEstablishmentId());
if (establishment != null) {
event.setEstablishment(establishment);
} else {
logger.warn("[WARN] Établissement non trouvé avec l'ID : {}", eventCreateRequestDTO.getEstablishmentId());
}
}
event.setCategory(eventCreateRequestDTO.getCategory());
event.setLink(eventCreateRequestDTO.getLink());
event.setImageUrl(eventCreateRequestDTO.getImageUrl());
@@ -63,6 +89,15 @@ public class EventService {
event.setTags(eventCreateRequestDTO.getTags());
event.setOrganizer(eventCreateRequestDTO.getOrganizer());
event.setParticipationFee(eventCreateRequestDTO.getParticipationFee());
// v2.0 - Nouveaux champs
if (eventCreateRequestDTO.getIsPrivate() != null) {
event.setIsPrivate(eventCreateRequestDTO.getIsPrivate());
}
if (eventCreateRequestDTO.getWaitlistEnabled() != null) {
event.setWaitlistEnabled(eventCreateRequestDTO.getWaitlistEnabled());
}
event.setPrivacyRules(eventCreateRequestDTO.getPrivacyRules());
event.setTransportInfo(eventCreateRequestDTO.getTransportInfo());
event.setAccommodationInfo(eventCreateRequestDTO.getAccommodationInfo());
@@ -70,16 +105,17 @@ public class EventService {
event.setParkingInfo(eventCreateRequestDTO.getParkingInfo());
event.setSecurityProtocol(eventCreateRequestDTO.getSecurityProtocol());
event.setCreator(creator);
event.setStatus("ouvert");
event.setStatus("OPEN"); // v2.0 - Statut standardisé
// Persiste l'événement dans la base de données
eventsRepository.persist(event);
logger.info("[logger] Événement créé avec succès : {}", event.getTitle());
// Créer des notifications pour tous les amis
// Créer des notifications pour tous les amis (v2.0 - avec Kafka)
try {
List<Friendship> friendships = friendshipRepository.findFriendsByUser(creator, 0, Integer.MAX_VALUE);
String creatorName = creator.getPrenoms() + " " + creator.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
String creatorName = creator.getFirstName() + " " + creator.getLastName();
for (Friendship friendship : friendships) {
Users friend = friendship.getUser().equals(creator)
@@ -89,6 +125,7 @@ public class EventService {
String notificationTitle = "Nouvel événement de " + creatorName;
String notificationMessage = creatorName + " a créé un nouvel événement : " + event.getTitle();
// Créer notification en base
notificationService.createNotification(
notificationTitle,
notificationMessage,
@@ -96,6 +133,28 @@ public class EventService {
friend.getId(),
event.getId()
);
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
java.util.Map<String, Object> notificationData = new java.util.HashMap<>();
notificationData.put("eventId", event.getId().toString());
notificationData.put("eventTitle", event.getTitle());
notificationData.put("creatorId", creator.getId().toString());
notificationData.put("creatorName", creatorName);
notificationData.put("startDate", event.getStartDate().toString());
NotificationEvent kafkaEvent = new NotificationEvent(
friend.getId().toString(), // userId destinataire
"event_created",
notificationData
);
notificationEmitter.send(kafkaEvent);
logger.debug("[logger] Événement event_created publié dans Kafka pour: {}", friend.getId());
} catch (Exception kafkaEx) {
logger.error("[ERROR] Erreur publication Kafka: {}", kafkaEx.getMessage());
// Ne pas bloquer si Kafka échoue
}
}
logger.info("[logger] Notifications créées pour {} ami(s)", friendships.size());
} catch (Exception e) {
@@ -209,12 +268,17 @@ public class EventService {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
// Mettre à jour les détails de l'événement
// v2.0 - Mettre à jour les détails de l'événement
existingEvent.setTitle(event.getTitle());
existingEvent.setDescription(event.getDescription());
existingEvent.setStartDate(event.getStartDate());
existingEvent.setEndDate(event.getEndDate());
existingEvent.setLocation(event.getLocation());
// v2.0 - Établissement au lieu de location
if (event.getEstablishment() != null) {
existingEvent.setEstablishment(event.getEstablishment());
}
existingEvent.setCategory(event.getCategory());
existingEvent.setLink(event.getLink());
existingEvent.setImageUrl(event.getImageUrl());
@@ -222,6 +286,15 @@ public class EventService {
existingEvent.setTags(event.getTags());
existingEvent.setOrganizer(event.getOrganizer());
existingEvent.setParticipationFee(event.getParticipationFee());
// v2.0 - Nouveaux champs
if (event.getIsPrivate() != null) {
existingEvent.setIsPrivate(event.getIsPrivate());
}
if (event.getWaitlistEnabled() != null) {
existingEvent.setWaitlistEnabled(event.getWaitlistEnabled());
}
existingEvent.setPrivacyRules(event.getPrivacyRules());
existingEvent.setTransportInfo(event.getTransportInfo());
existingEvent.setAccommodationInfo(event.getAccommodationInfo());
@@ -269,8 +342,22 @@ public class EventService {
* @return La liste des événements auxquels l'utilisateur participe.
*/
public List<Events> findEventsByUser(Users user) {
logger.info("[logger] Récupération des événements pour l'utilisateur avec l'ID : {}", user.getId());
List<Events> events = eventsRepository.find("participants", user).list();
return findEventsByUser(user, 0, Integer.MAX_VALUE);
}
/**
* Récupère les événements auxquels un utilisateur participe avec pagination.
*
* @param user L'utilisateur pour lequel récupérer les événements.
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return La liste paginée des événements auxquels l'utilisateur participe.
*/
public List<Events> findEventsByUser(Users user, int page, int size) {
logger.info("[logger] Récupération des événements pour l'utilisateur avec l'ID : {} (page: {}, size: {})", user.getId(), page, size);
List<Events> events = eventsRepository.find("participants", user)
.page(page, size)
.list();
logger.info("[logger] Nombre d'événements pour l'utilisateur avec l'ID {} : {}", user.getId(), events.size());
return events;
}
@@ -372,7 +459,11 @@ public class EventService {
*/
public List<Events> recommendEventsForUser(Users user) {
logger.info("[logger] Recommandation d'événements pour l'utilisateur : " + user.getEmail());
List<Events> events = eventsRepository.find("category", user.getPreferredCategory()).list();
// v2.0 - Utiliser preferences pour preferredCategory
String preferredCategory = user.getPreferredCategory(); // Méthode qui utilise preferences JSONB
List<Events> events = preferredCategory != null
? eventsRepository.find("category", preferredCategory).list()
: eventsRepository.findAll().list();
logger.info("[logger] Nombre d'événements recommandés pour l'utilisateur : " + events.size());
return events;
}

View File

@@ -13,7 +13,9 @@ import com.lions.dev.exception.FriendshipNotFoundException;
import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.websocket.NotificationWebSocket;
import com.lions.dev.dto.events.NotificationEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -40,6 +42,10 @@ public class FriendshipService {
@Inject
NotificationService notificationService; // Injecte le service de notifications
@Inject
@Channel("notifications")
Emitter<NotificationEvent> notificationEmitter; // v2.0 - Publie dans Kafka
private static final Logger logger = Logger.getLogger(FriendshipService.class);
/**
@@ -79,24 +85,26 @@ public class FriendshipService {
Friendship friendship = new Friendship(user, friend, FriendshipStatus.PENDING);
friendshipRepository.persist(friendship);
// TEMPS RÉEL: Notifier le destinataire via WebSocket
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Map<String, Object> notificationData = new HashMap<>();
notificationData.put("requestId", friendship.getId().toString());
notificationData.put("senderId", user.getId().toString());
notificationData.put("senderName", user.getPrenoms() + " " + user.getNom());
// v2.0 - Utiliser les nouveaux noms de champs
notificationData.put("senderName", user.getFirstName() + " " + user.getLastName());
notificationData.put("senderProfileImage", user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "");
NotificationWebSocket.sendNotificationToUser(
friend.getId(),
NotificationEvent event = new NotificationEvent(
friend.getId().toString(), // userId destinataire (clé Kafka)
"friend_request_received",
notificationData
);
logger.info("[LOG] Notification WebSocket envoyée au destinataire : " + friend.getId());
notificationEmitter.send(event);
logger.info("[LOG] Événement friend_request_received publié dans Kafka pour : " + friend.getId());
} catch (Exception e) {
logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket : " + e.getMessage(), e);
// Ne pas bloquer la demande d'amitié si le WebSocket échoue
logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
// Ne pas bloquer la demande d'amitié si Kafka échoue
}
logger.info("[LOG] Demande d'amitié envoyée avec succès.");
@@ -139,11 +147,12 @@ public class FriendshipService {
// Log de succès
logger.info(String.format("[LOG] Demande d'amitié acceptée avec succès pour l'ID: %s", friendshipId)); // Correctement formaté
// TEMPS RÉEL: Notifier l'émetteur de la demande via WebSocket
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Users user = friendship.getUser();
Users friend = friendship.getFriend();
String friendName = friend.getPrenoms() + " " + friend.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
String friendName = friend.getFirstName() + " " + friend.getLastName();
Map<String, Object> notificationData = new HashMap<>();
notificationData.put("acceptedBy", friendName);
@@ -151,24 +160,26 @@ public class FriendshipService {
notificationData.put("accepterId", friend.getId().toString());
notificationData.put("accepterProfileImage", friend.getProfileImageUrl() != null ? friend.getProfileImageUrl() : "");
NotificationWebSocket.sendNotificationToUser(
user.getId(),
NotificationEvent event = new NotificationEvent(
user.getId().toString(), // userId émetteur (destinataire de la notification)
"friend_request_accepted",
notificationData
);
logger.info("[LOG] Notification WebSocket d'acceptation envoyée à : " + user.getId());
notificationEmitter.send(event);
logger.info("[LOG] Événement friend_request_accepted publié dans Kafka pour : " + user.getId());
} catch (Exception e) {
logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket d'acceptation : " + e.getMessage(), e);
// Ne pas bloquer l'acceptation si le WebSocket échoue
logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
// Ne pas bloquer l'acceptation si Kafka échoue
}
// Créer des notifications pour les deux utilisateurs
try {
Users user = friendship.getUser();
Users friend = friendship.getFriend();
String userName = user.getPrenoms() + " " + user.getNom();
String friendName = friend.getPrenoms() + " " + friend.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
String userName = user.getFirstName() + " " + user.getLastName();
String friendName = friend.getFirstName() + " " + friend.getLastName();
// Notification pour l'utilisateur qui a envoyé la demande
notificationService.createNotification(
@@ -212,7 +223,7 @@ public class FriendshipService {
friendship.setStatus(FriendshipStatus.REJECTED);
friendshipRepository.persist(friendship);
// TEMPS RÉEL: Notifier l'émetteur de la demande via WebSocket (optionnel selon UX)
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Users user = friendship.getUser();
@@ -220,16 +231,17 @@ public class FriendshipService {
notificationData.put("friendshipId", friendshipId.toString());
notificationData.put("rejectedAt", System.currentTimeMillis());
NotificationWebSocket.sendNotificationToUser(
user.getId(),
NotificationEvent event = new NotificationEvent(
user.getId().toString(), // userId émetteur (destinataire de la notification)
"friend_request_rejected",
notificationData
);
logger.info("[LOG] Notification WebSocket de rejet envoyée à : " + user.getId());
notificationEmitter.send(event);
logger.info("[LOG] Événement friend_request_rejected publié dans Kafka pour : " + user.getId());
} catch (Exception e) {
logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket de rejet : " + e.getMessage(), e);
// Ne pas bloquer le rejet si le WebSocket échoue
logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
// Ne pas bloquer le rejet si Kafka échoue
}
logger.info("[LOG] Demande d'amitié rejetée.");
@@ -288,11 +300,12 @@ public class FriendshipService {
+ user.getId() + ", ami ID : " + friend.getId());
// Création du DTO de réponse à partir des informations de l'ami et de la relation
// v2.0 - Utiliser les nouveaux noms de champs
return new FriendshipReadFriendDetailsResponseDTO(
user.getId(), // ID de l'utilisateur
friend.getId(), // ID de l'ami
friend.getNom(), // Nom de l'ami
friend.getPrenoms(),
friend.getLastName(), // Nom de famille de l'ami (v2.0)
friend.getFirstName(), // Prénom de l'ami (v2.0)
friend.getEmail(), // Email de l'ami
friend.getProfileImageUrl(), // URL de l'image de profil de l'ami
friendship.getStatus(), // Statut de la relation
@@ -325,11 +338,12 @@ public class FriendshipService {
return friendships.stream()
.map(friendship -> {
Users friend = friendship.getUser().equals(user) ? friendship.getFriend() : friendship.getUser();
// v2.0 - Utiliser les nouveaux noms de champs
return new FriendshipReadFriendDetailsResponseDTO(
user.getId(),
friend.getId(),
friend.getNom(),
friend.getPrenoms(),
friend.getLastName(), // v2.0
friend.getFirstName(), // v2.0
friend.getEmail(),
friend.getProfileImageUrl(),
friendship.getStatus(),

View File

@@ -7,7 +7,10 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.ConversationRepository;
import com.lions.dev.repository.MessageRepository;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.websocket.ChatWebSocket;
import com.lions.dev.dto.events.ChatMessageEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -40,6 +43,10 @@ public class MessageService {
@Inject
NotificationService notificationService;
@Inject
@Channel("chat-messages")
Emitter<ChatMessageEvent> chatMessageEmitter; // v2.0 - Publie dans Kafka
/**
* Envoie un message d'un utilisateur à un autre.
*
@@ -92,7 +99,8 @@ public class MessageService {
// Créer une notification pour le destinataire
try {
String senderName = sender.getPrenoms() + " " + sender.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
String senderName = sender.getFirstName() + " " + sender.getLastName();
String notificationMessage = content.length() > 50
? content.substring(0, 50) + "..."
: content;
@@ -109,43 +117,78 @@ public class MessageService {
System.out.println("[ERROR] Erreur lors de la création de la notification : " + e.getMessage());
}
// TEMPS RÉEL : Envoyer le message via WebSocket au destinataire
// TEMPS RÉEL : Publier dans Kafka (v2.0)
try {
Map<String, Object> messageData = new HashMap<>();
messageData.put("id", message.getId().toString());
messageData.put("conversationId", conversation.getId().toString());
messageData.put("senderId", senderId.toString());
messageData.put("senderFirstName", sender.getPrenoms());
messageData.put("senderLastName", sender.getNom());
messageData.put("senderProfileImageUrl", sender.getProfileImageUrl() != null ? sender.getProfileImageUrl() : "");
messageData.put("content", content);
messageData.put("timestamp", message.getCreatedAt().toString());
messageData.put("isRead", message.isRead());
messageData.put("attachmentUrl", mediaUrl != null ? mediaUrl : "");
messageData.put("attachmentType", messageType != null ? messageType : "text");
// Créer l'événement pour Kafka
ChatMessageEvent event = new ChatMessageEvent();
event.setConversationId(conversation.getId().toString());
event.setSenderId(senderId.toString());
event.setRecipientId(recipientId.toString());
event.setContent(content);
event.setMessageId(message.getId().toString());
event.setEventType("message");
event.setTimestamp(System.currentTimeMillis());
// Métadonnées additionnelles
Map<String, Object> eventMetadata = new HashMap<>();
eventMetadata.put("senderFirstName", sender.getFirstName());
eventMetadata.put("senderLastName", sender.getLastName());
eventMetadata.put("senderProfileImageUrl", sender.getProfileImageUrl() != null ? sender.getProfileImageUrl() : "");
eventMetadata.put("isRead", message.isRead());
eventMetadata.put("attachmentUrl", mediaUrl != null ? mediaUrl : "");
eventMetadata.put("attachmentType", messageType != null ? messageType : "text");
event.setMetadata(eventMetadata);
// Envoyer au destinataire via ChatWebSocket
ChatWebSocket.sendMessageToUser(recipientId, messageData);
// Publier dans Kafka (utiliser conversationId comme clé pour garantir l'ordre)
OutgoingKafkaRecordMetadata kafkaMetadata =
OutgoingKafkaRecordMetadata.builder()
.withKey(conversation.getId().toString())
.build();
chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
event,
() -> java.util.concurrent.CompletableFuture.completedFuture(null), // ack
throwable -> {
System.out.println("[ERROR] Erreur envoi Kafka: " + throwable.getMessage());
return java.util.concurrent.CompletableFuture.completedFuture(null); // nack
}
).addMetadata(kafkaMetadata));
System.out.println("[LOG] Message envoyé via WebSocket au destinataire : " + recipientId);
System.out.println("[LOG] Message publié dans Kafka: " + message.getId());
// Envoyer confirmation de délivrance à l'expéditeur
// Envoyer confirmation de délivrance à l'expéditeur (via Kafka aussi)
try {
Map<String, Object> deliveryConfirmation = new HashMap<>();
deliveryConfirmation.put("messageId", message.getId().toString());
deliveryConfirmation.put("isDelivered", true);
deliveryConfirmation.put("timestamp", System.currentTimeMillis());
ChatMessageEvent deliveryEvent = new ChatMessageEvent();
deliveryEvent.setConversationId(conversation.getId().toString());
deliveryEvent.setSenderId(recipientId.toString()); // Le destinataire confirme
deliveryEvent.setRecipientId(senderId.toString()); // À l'expéditeur
deliveryEvent.setMessageId(message.getId().toString());
deliveryEvent.setEventType("delivery_confirmation");
deliveryEvent.setTimestamp(System.currentTimeMillis());
Map<String, Object> deliveryEventMetadata = new HashMap<>();
deliveryEventMetadata.put("isDelivered", true);
deliveryEvent.setMetadata(deliveryEventMetadata);
ChatWebSocket.sendDeliveryConfirmation(senderId, deliveryConfirmation);
OutgoingKafkaRecordMetadata deliveryKafkaMetadata =
OutgoingKafkaRecordMetadata.builder()
.withKey(conversation.getId().toString())
.build();
chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
deliveryEvent,
() -> java.util.concurrent.CompletableFuture.completedFuture(null),
throwable -> java.util.concurrent.CompletableFuture.completedFuture(null)
).addMetadata(deliveryKafkaMetadata));
System.out.println("[LOG] Confirmation de délivrance envoyée à l'expéditeur : " + senderId);
System.out.println("[LOG] Confirmation de délivrance publiée dans Kafka pour : " + senderId);
} catch (Exception deliveryEx) {
System.out.println("[ERROR] Erreur envoi confirmation délivrance : " + deliveryEx.getMessage());
System.out.println("[ERROR] Erreur publication confirmation délivrance : " + deliveryEx.getMessage());
// Ne pas bloquer si la confirmation échoue
}
} catch (Exception e) {
System.out.println("[ERROR] Erreur lors de l'envoi du message via WebSocket : " + e.getMessage());
// Ne pas bloquer l'envoi du message si WebSocket échoue
System.out.println("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage());
// Ne pas bloquer l'envoi du message si Kafka échoue
}
return message;
@@ -240,18 +283,36 @@ public class MessageService {
? conversation.getUser2().getId()
: conversation.getUser1().getId();
Map<String, Object> readConfirmation = new HashMap<>();
readConfirmation.put("messageId", message.getId().toString());
readConfirmation.put("userId", recipientId.toString());
readConfirmation.put("timestamp", java.time.LocalDateTime.now().toString());
// Publier confirmation de lecture dans Kafka (v2.0)
try {
ChatMessageEvent readEvent = new ChatMessageEvent();
readEvent.setConversationId(conversation.getId().toString());
readEvent.setSenderId(recipientId.toString()); // Celui qui a lu
readEvent.setRecipientId(message.getSender().getId().toString()); // L'expéditeur
readEvent.setMessageId(message.getId().toString());
readEvent.setEventType("read_confirmation");
readEvent.setTimestamp(System.currentTimeMillis());
Map<String, Object> readEventMetadata = new HashMap<>();
readEventMetadata.put("readBy", recipientId.toString());
readEventMetadata.put("readAt", System.currentTimeMillis());
readEvent.setMetadata(readEventMetadata);
// Envoyer via ChatWebSocket avec type "read"
com.lions.dev.websocket.ChatWebSocket.sendReadConfirmation(
message.getSender().getId(),
readConfirmation
);
OutgoingKafkaRecordMetadata readKafkaMetadata =
OutgoingKafkaRecordMetadata.builder()
.withKey(conversation.getId().toString())
.build();
chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
readEvent,
() -> java.util.concurrent.CompletableFuture.completedFuture(null),
throwable -> java.util.concurrent.CompletableFuture.completedFuture(null)
).addMetadata(readKafkaMetadata));
System.out.println("[LOG] Confirmation de lecture envoyée à l'expéditeur : " + message.getSender().getId());
System.out.println("[LOG] Confirmation de lecture publiée dans Kafka pour : " + message.getSender().getId());
} catch (Exception e) {
System.out.println("[ERROR] Erreur publication confirmation lecture : " + e.getMessage());
}
} catch (Exception e) {
System.out.println("[ERROR] Erreur envoi confirmation lecture : " + e.getMessage());
}

View File

@@ -2,7 +2,7 @@ package com.lions.dev.service;
import com.lions.dev.entity.users.Users;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.websocket.NotificationWebSocket;
import com.lions.dev.websocket.NotificationWebSocketNext;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
@@ -95,8 +95,8 @@ public class PresenceService {
presenceData.put("lastSeen", lastSeen != null ? lastSeen.toString() : null);
presenceData.put("timestamp", System.currentTimeMillis());
// Envoyer via NotificationWebSocket
NotificationWebSocket.broadcastPresenceUpdate(presenceData);
// Envoyer via NotificationWebSocketNext (v2.0)
NotificationWebSocketNext.broadcastPresenceUpdate(presenceData);
System.out.println("[PRESENCE] Broadcast de la présence de " + userId + " : " + isOnline);
} catch (Exception e) {

View File

@@ -8,10 +8,15 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.SocialPostRepository;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.dto.events.ReactionEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -38,6 +43,10 @@ public class SocialPostService {
@Inject
NotificationService notificationService;
@Inject
@Channel("reactions")
Emitter<ReactionEvent> reactionEmitter; // v2.0 - Publie dans Kafka
/**
* Récupère tous les posts avec pagination.
*
@@ -99,7 +108,8 @@ public class SocialPostService {
// Créer des notifications pour tous les amis
try {
List<Friendship> friendships = friendshipRepository.findFriendsByUser(user, 0, Integer.MAX_VALUE);
String userName = user.getPrenoms() + " " + user.getNom();
// v2.0 - Utiliser les nouveaux noms de champs
String userName = user.getFirstName() + " " + user.getLastName();
for (Friendship friendship : friendships) {
Users friend = friendship.getUser().equals(user)
@@ -208,11 +218,12 @@ public class SocialPostService {
* Like un post (incrémente le compteur de likes).
*
* @param postId L'ID du post
* @param userId L'ID de l'utilisateur qui like (v2.0)
* @return Le post mis à jour
*/
@Transactional
public SocialPost likePost(UUID postId) {
System.out.println("[LOG] Like du post ID : " + postId);
public SocialPost likePost(UUID postId, UUID userId) {
System.out.println("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -222,6 +233,31 @@ public class SocialPostService {
post.incrementLikes();
socialPostRepository.persist(post);
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Map<String, Object> reactionData = new HashMap<>();
reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
reactionData.put("likesCount", post.getLikesCount());
reactionData.put("postTitle", post.getContent().length() > 50
? post.getContent().substring(0, 50) + "..."
: post.getContent());
ReactionEvent event = new ReactionEvent(
postId.toString(), // targetId
"post", // targetType
userId.toString(), // userId qui réagit
"like", // reactionType
reactionData
);
reactionEmitter.send(event);
System.out.println("[LOG] Réaction like publiée dans Kafka pour post: " + postId);
} catch (Exception e) {
System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
// Ne pas bloquer le like si Kafka échoue
}
return post;
}
@@ -229,11 +265,13 @@ public class SocialPostService {
* Ajoute un commentaire à un post (incrémente le compteur de commentaires).
*
* @param postId L'ID du post
* @param userId L'ID de l'utilisateur qui commente (v2.0)
* @param commentContent Le contenu du commentaire (v2.0)
* @return Le post mis à jour
*/
@Transactional
public SocialPost addComment(UUID postId) {
System.out.println("[LOG] Ajout de commentaire au post ID : " + postId);
public SocialPost addComment(UUID postId, UUID userId, String commentContent) {
System.out.println("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -243,6 +281,35 @@ public class SocialPostService {
post.incrementComments();
socialPostRepository.persist(post);
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Users commenter = usersRepository.findById(userId);
Map<String, Object> reactionData = new HashMap<>();
reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
reactionData.put("commentsCount", post.getCommentsCount());
reactionData.put("commentContent", commentContent != null && commentContent.length() > 100
? commentContent.substring(0, 100) + "..."
: commentContent);
reactionData.put("commenterName", commenter != null
? commenter.getFirstName() + " " + commenter.getLastName()
: "Utilisateur");
ReactionEvent event = new ReactionEvent(
postId.toString(), // targetId
"post", // targetType
userId.toString(), // userId qui commente
"comment", // reactionType
reactionData
);
reactionEmitter.send(event);
System.out.println("[LOG] Réaction comment publiée dans Kafka pour post: " + postId);
} catch (Exception e) {
System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
// Ne pas bloquer le commentaire si Kafka échoue
}
return post;
}
@@ -250,11 +317,12 @@ public class SocialPostService {
* Partage un post (incrémente le compteur de partages).
*
* @param postId L'ID du post
* @param userId L'ID de l'utilisateur qui partage (v2.0)
* @return Le post mis à jour
*/
@Transactional
public SocialPost sharePost(UUID postId) {
System.out.println("[LOG] Partage du post ID : " + postId);
public SocialPost sharePost(UUID postId, UUID userId) {
System.out.println("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -264,6 +332,28 @@ public class SocialPostService {
post.incrementShares();
socialPostRepository.persist(post);
// TEMPS RÉEL: Publier dans Kafka (v2.0)
try {
Map<String, Object> reactionData = new HashMap<>();
reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
reactionData.put("sharesCount", post.getSharesCount());
ReactionEvent event = new ReactionEvent(
postId.toString(), // targetId
"post", // targetType
userId.toString(), // userId qui partage
"share", // reactionType
reactionData
);
reactionEmitter.send(event);
System.out.println("[LOG] Réaction share publiée dans Kafka pour post: " + postId);
} catch (Exception e) {
System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
// Ne pas bloquer le partage si Kafka échoue
}
return post;
}

View File

@@ -35,8 +35,19 @@ public class StoryService {
* @return Liste des stories actives
*/
public List<Story> getAllActiveStories() {
System.out.println("[LOG] Récupération de toutes les stories actives");
return storyRepository.findAllActive();
return getAllActiveStories(0, Integer.MAX_VALUE);
}
/**
* Récupère toutes les stories actives avec pagination.
*
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des stories actives
*/
public List<Story> getAllActiveStories(int page, int size) {
System.out.println("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
return storyRepository.findAllActive(page, size);
}
/**
@@ -47,7 +58,20 @@ public class StoryService {
* @throws UserNotFoundException Si l'utilisateur n'existe pas
*/
public List<Story> getActiveStoriesByUserId(UUID userId) {
System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId);
return getActiveStoriesByUserId(userId, 0, Integer.MAX_VALUE);
}
/**
* Récupère toutes les stories actives d'un utilisateur avec pagination.
*
* @param userId L'ID de l'utilisateur
* @param page Le numéro de la page (0-indexé)
* @param size La taille de la page
* @return Liste paginée des stories actives de l'utilisateur
* @throws UserNotFoundException Si l'utilisateur n'existe pas
*/
public List<Story> getActiveStoriesByUserId(UUID userId, int page, int size) {
System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
@@ -55,7 +79,7 @@ public class StoryService {
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
}
return storyRepository.findActiveByUserId(userId);
return storyRepository.findActiveByUserId(userId, page, size);
}
/**

View File

@@ -23,7 +23,7 @@ public class UsersService {
UsersRepository usersRepository;
/**
* Crée un nouvel utilisateur dans le système.
* Crée un nouvel utilisateur dans le système (v2.0).
*
* @param userCreateRequestDTO Le DTO contenant les informations de l'utilisateur à créer.
* @return L'utilisateur créé.
@@ -36,10 +36,22 @@ public class UsersService {
}
Users user = new Users();
user.setNom(userCreateRequestDTO.getNom());
user.setPrenoms(userCreateRequestDTO.getPrenoms());
// v2.0 - Utiliser les nouveaux noms de champs avec compatibilité v1.0
user.setFirstName(userCreateRequestDTO.getFirstName()); // v2.0
user.setLastName(userCreateRequestDTO.getLastName()); // v2.0
user.setEmail(userCreateRequestDTO.getEmail());
user.setMotDePasse(userCreateRequestDTO.getMotDePasse()); // Hachage automatique
user.setPassword(userCreateRequestDTO.getPassword()); // v2.0 - Hachage automatique
// v2.0 - Nouveaux champs
if (userCreateRequestDTO.getBio() != null) {
user.setBio(userCreateRequestDTO.getBio());
}
if (userCreateRequestDTO.getLoyaltyPoints() != null) {
user.setLoyaltyPoints(userCreateRequestDTO.getLoyaltyPoints());
}
if (userCreateRequestDTO.getPreferences() != null) {
user.setPreferences(userCreateRequestDTO.getPreferences());
}
// Logique pour l'image et le rôle par défaut.
user.setProfileImageUrl(
@@ -59,7 +71,7 @@ public class UsersService {
}
/**
* Met à jour un utilisateur existant dans le système.
* Met à jour un utilisateur existant dans le système (v2.0).
*
* @param id L'ID de l'utilisateur à mettre à jour.
* @param userCreateRequestDTO Les nouvelles informations de l'utilisateur.
@@ -74,13 +86,35 @@ public class UsersService {
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
}
// Mettre à jour les champs de l'utilisateur existant
existingUser.setNom(userCreateRequestDTO.getNom());
existingUser.setPrenoms(userCreateRequestDTO.getPrenoms());
existingUser.setEmail(userCreateRequestDTO.getEmail());
existingUser.setMotDePasse(userCreateRequestDTO.getMotDePasse()); // Hachage automatique si nécessaire
existingUser.setRole(userCreateRequestDTO.getRole());
existingUser.setProfileImageUrl(userCreateRequestDTO.getProfileImageUrl());
// v2.0 - Mettre à jour les champs avec les nouveaux noms
if (userCreateRequestDTO.getFirstName() != null) {
existingUser.setFirstName(userCreateRequestDTO.getFirstName());
}
if (userCreateRequestDTO.getLastName() != null) {
existingUser.setLastName(userCreateRequestDTO.getLastName());
}
if (userCreateRequestDTO.getEmail() != null) {
existingUser.setEmail(userCreateRequestDTO.getEmail());
}
if (userCreateRequestDTO.getPassword() != null) {
existingUser.setPassword(userCreateRequestDTO.getPassword()); // v2.0 - Hachage automatique
}
if (userCreateRequestDTO.getRole() != null) {
existingUser.setRole(userCreateRequestDTO.getRole());
}
if (userCreateRequestDTO.getProfileImageUrl() != null) {
existingUser.setProfileImageUrl(userCreateRequestDTO.getProfileImageUrl());
}
// v2.0 - Nouveaux champs
if (userCreateRequestDTO.getBio() != null) {
existingUser.setBio(userCreateRequestDTO.getBio());
}
if (userCreateRequestDTO.getLoyaltyPoints() != null) {
existingUser.setLoyaltyPoints(userCreateRequestDTO.getLoyaltyPoints());
}
if (userCreateRequestDTO.getPreferences() != null) {
existingUser.setPreferences(userCreateRequestDTO.getPreferences());
}
usersRepository.persist(existingUser);
System.out.println("[LOG] Utilisateur mis à jour avec succès : " + existingUser.getEmail());
@@ -122,16 +156,16 @@ public class UsersService {
}
/**
* Authentifie un utilisateur avec son email et son mot de passe.
* Authentifie un utilisateur avec son email et son mot de passe (v2.0).
*
* @param email L'email de l'utilisateur.
* @param motDePasse Le mot de passe de l'utilisateur.
* @param password Le mot de passe de l'utilisateur (v2.0).
* @return L'utilisateur authentifié.
* @throws UserNotFoundException Si l'utilisateur n'est pas trouvé ou si le mot de passe est incorrect.
*/
public Users authenticateUser(String email, String motDePasse) {
public Users authenticateUser(String email, String password) {
Optional<Users> userOptional = usersRepository.findByEmail(email);
if (userOptional.isEmpty() || !userOptional.get().verifierMotDePasse(motDePasse)) {
if (userOptional.isEmpty() || !userOptional.get().verifyPassword(password)) { // v2.0
System.out.println("[ERROR] Échec de l'authentification pour l'email : " + email);
throw new UserNotFoundException("Utilisateur ou mot de passe incorrect.");
}
@@ -139,6 +173,15 @@ public class UsersService {
return userOptional.get();
}
/**
* Méthode de compatibilité v1.0 (dépréciée).
* @deprecated Utiliser {@link #authenticateUser(String, String)} avec password à la place.
*/
@Deprecated
public Users authenticateUser(String email, String motDePasse, boolean deprecated) {
return authenticateUser(email, motDePasse);
}
/**
* Récupère un utilisateur par son ID.
*
@@ -157,7 +200,7 @@ public class UsersService {
}
/**
* Réinitialise le mot de passe d'un utilisateur.
* Réinitialise le mot de passe d'un utilisateur (v2.0).
*
* @param id L'ID de l'utilisateur.
* @param newPassword Le nouveau mot de passe à définir.
@@ -171,7 +214,7 @@ public class UsersService {
throw new UserNotFoundException("Utilisateur non trouvé.");
}
user.setMotDePasse(newPassword); // Hachage automatique
user.setPassword(newPassword); // v2.0 - Hachage automatique
usersRepository.persist(user);
System.out.println("[LOG] Mot de passe réinitialisé pour l'utilisateur : " + user.getEmail());
}

View File

@@ -1,361 +0,0 @@
package com.lions.dev.websocket;
import com.lions.dev.dto.request.chat.SendMessageRequestDTO;
import com.lions.dev.dto.response.chat.MessageResponseDTO;
import com.lions.dev.entity.chat.Message;
import com.lions.dev.service.MessageService;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour le chat en temps réel.
*
* Ce endpoint gère:
* - La connexion/déconnexion des utilisateurs
* - L'envoi et la réception de messages en temps réel
* - Les indicateurs de frappe (typing indicators)
* - Les confirmations de lecture (read receipts)
*
* URL: ws://localhost:8080/chat/ws/{userId}
*/
@ServerEndpoint("/chat/ws/{userId}")
@ApplicationScoped
public class ChatWebSocket {
@Inject
MessageService messageService;
// Map pour stocker les sessions WebSocket des utilisateurs connectés
private static final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
/**
* Appelé lorsqu'un utilisateur se connecte.
*
* @param session La session WebSocket
* @param userId L'ID de l'utilisateur
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
try {
UUID userUUID = UUID.fromString(userId);
sessions.put(userUUID, session);
Log.info("[LOG] WebSocket ouvert pour l'utilisateur ID : " + userId);
// Envoyer un message de confirmation
sendToUser(userUUID, "{\"type\":\"connected\",\"message\":\"Connecté au chat\"}");
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de la connexion WebSocket : " + e.getMessage(), e);
}
}
/**
* Appelé lorsqu'un message est reçu.
*
* @param message Le message reçu (au format JSON)
* @param userId L'ID de l'utilisateur qui envoie le message
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId) {
try {
Log.info("[LOG] Message reçu de l'utilisateur " + userId + " : " + message);
// Parser le message JSON
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = mapper.readValue(message, Map.class);
String type = (String) messageData.get("type");
switch (type) {
case "message":
handleChatMessage(messageData, userId);
break;
case "typing":
handleTypingIndicator(messageData, userId);
break;
case "read":
handleReadReceipt(messageData, userId);
break;
default:
Log.warn("[WARN] Type de message inconnu : " + type);
}
} catch (Exception e) {
Log.error("[ERROR] Erreur lors du traitement du message : " + e.getMessage(), e);
}
}
/**
* Appelé lorsqu'une erreur se produit.
*
* @param session La session WebSocket
* @param error L'erreur
*/
@OnError
public void onError(Session session, Throwable error) {
Log.error("[ERROR] Erreur WebSocket : " + error.getMessage(), error);
}
/**
* Appelé lorsqu'un utilisateur se déconnecte.
*
* @param session La session WebSocket
* @param userId L'ID de l'utilisateur
*/
@OnClose
public void onClose(Session session, @PathParam("userId") String userId) {
try {
UUID userUUID = UUID.fromString(userId);
sessions.remove(userUUID);
Log.info("[LOG] WebSocket fermé pour l'utilisateur ID : " + userId);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de la fermeture WebSocket : " + e.getMessage(), e);
}
}
/**
* Gère l'envoi d'un message de chat.
*/
private void handleChatMessage(Map<String, Object> messageData, String senderId) {
try {
UUID senderUUID = UUID.fromString(senderId);
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
String content = (String) messageData.get("content");
String messageType = messageData.getOrDefault("messageType", "text").toString();
String mediaUrl = (String) messageData.get("mediaUrl");
// Enregistrer le message dans la base de données
Message message = messageService.sendMessage(
senderUUID,
recipientUUID,
content,
messageType,
mediaUrl
);
// Créer le DTO de réponse
MessageResponseDTO response = new MessageResponseDTO(message);
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
String responseJson = mapper.writeValueAsString(Map.of(
"type", "message",
"data", response
));
// Envoyer le message au destinataire s'il est connecté
sendToUser(recipientUUID, responseJson);
// Envoyer une confirmation à l'expéditeur
sendToUser(senderUUID, responseJson);
Log.info("[LOG] Message envoyé de " + senderId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de l'envoi du message : " + e.getMessage(), e);
}
}
/**
* Gère les indicateurs de frappe.
*/
private void handleTypingIndicator(Map<String, Object> messageData, String userId) {
try {
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
String response = mapper.writeValueAsString(Map.of(
"type", "typing",
"userId", userId,
"isTyping", isTyping
));
sendToUser(recipientUUID, response);
Log.info("[LOG] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de l'envoi de l'indicateur de frappe : " + e.getMessage(), e);
}
}
/**
* Gère les confirmations de lecture.
*/
private void handleReadReceipt(Map<String, Object> messageData, String userId) {
try {
UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
// Marquer le message comme lu
Message message = messageService.markMessageAsRead(messageUUID);
// Notifier l'expéditeur que le message a été lu
UUID senderUUID = message.getSender().getId();
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
String response = mapper.writeValueAsString(Map.of(
"type", "read",
"messageId", messageUUID.toString(),
"readBy", userId
));
sendToUser(senderUUID, response);
Log.info("[LOG] Confirmation de lecture envoyée pour le message " + messageUUID);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e);
}
}
/**
* Envoie un message à un utilisateur spécifique.
*
* @param userId L'ID de l'utilisateur
* @param message Le message à envoyer
*/
private void sendToUser(UUID userId, String message) {
Session session = sessions.get(userId);
if (session != null && session.isOpen()) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de l'envoi du message à l'utilisateur " + userId + " : " + e.getMessage(), e);
}
} else {
Log.warn("[WARN] Utilisateur " + userId + " non connecté ou session fermée");
}
}
/**
* Diffuse un message à tous les utilisateurs connectés.
*
* @param message Le message à diffuser
*/
public void broadcast(String message) {
sessions.values().forEach(session -> {
if (session.isOpen()) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
Log.error("[ERROR] Erreur lors de la diffusion : " + e.getMessage(), e);
}
}
});
}
/**
* Envoie un message chat à un utilisateur spécifique via WebSocket.
*
* Cette méthode est statique pour permettre son appel depuis les services
* (comme MessageService) sans nécessiter une instance de ChatWebSocket.
*
* @param userId L'ID de l'utilisateur destinataire
* @param messageData Les données du message (id, conversationId, content, etc.)
*/
public static void sendMessageToUser(UUID userId, Map<String, Object> messageData) {
Session session = sessions.get(userId);
if (session == null || !session.isOpen()) {
Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, message non envoyé");
return;
}
try {
// Construire le message JSON au format attendu par le frontend
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> envelope = Map.of(
"type", "message",
"data", messageData
);
String json = mapper.writeValueAsString(envelope);
session.getAsyncRemote().sendText(json);
Log.info("[CHAT-WS] Message envoyé à l'utilisateur " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS] Erreur lors de l'envoi du message à " + userId + " : " + e.getMessage(), e);
}
}
/**
* Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
*
* Cette méthode est appelée lorsqu'un message est délivré au destinataire
* pour notifier l'expéditeur que le message a bien été reçu.
*
* @param userId L'ID de l'utilisateur (expéditeur) à notifier
* @param deliveryData Les données de confirmation (messageId, isDelivered, timestamp)
*/
public static void sendDeliveryConfirmation(UUID userId, Map<String, Object> deliveryData) {
Session session = sessions.get(userId);
if (session == null || !session.isOpen()) {
Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de délivrance non envoyée");
return;
}
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> envelope = Map.of(
"type", "delivered",
"data", deliveryData
);
String json = mapper.writeValueAsString(envelope);
session.getAsyncRemote().sendText(json);
Log.info("[CHAT-WS] Confirmation de délivrance envoyée à l'utilisateur " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de délivrance : " + e.getMessage(), e);
}
}
/**
* Envoie une confirmation de lecture à l'expéditeur via WebSocket.
*
* @param userId L'ID de l'utilisateur expéditeur
* @param readData Les données de confirmation de lecture
*/
public static void sendReadConfirmation(UUID userId, Map<String, Object> readData) {
Session session = sessions.get(userId);
if (session == null || !session.isOpen()) {
Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de lecture non envoyée");
return;
}
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> envelope = Map.of(
"type", "read",
"data", readData
);
String json = mapper.writeValueAsString(envelope);
session.getAsyncRemote().sendText(json);
Log.info("[CHAT-WS] Confirmation de lecture envoyée à l'utilisateur " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e);
}
}
/**
* Récupère le nombre d'utilisateurs connectés.
*
* @return Le nombre d'utilisateurs connectés
*/
public static int getConnectedUsersCount() {
return sessions.size();
}
}

View File

@@ -0,0 +1,301 @@
package com.lions.dev.websocket;
import com.lions.dev.dto.response.chat.MessageResponseDTO;
import com.lions.dev.entity.chat.Message;
import com.lions.dev.service.MessageService;
import io.quarkus.logging.Log;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour le chat en temps réel (WebSockets Next).
*
* Architecture v2.0:
* Client → WebSocket → MessageService → Kafka → Bridge → WebSocket → Destinataire
*
* Gère:
* - La connexion/déconnexion des utilisateurs
* - L'envoi et la réception de messages en temps réel
* - Les indicateurs de frappe (typing indicators)
* - Les confirmations de lecture (read receipts)
* - Les confirmations de délivrance
*
* URL: ws://localhost:8080/chat/{userId}
*/
@WebSocket(path = "/chat/{userId}")
@ApplicationScoped
public class ChatWebSocketNext {
@Inject
MessageService messageService;
// Map pour stocker les sessions WebSocket des utilisateurs connectés
private static final Map<UUID, WebSocketConnection> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(WebSocketConnection connection) {
String userId = connection.pathParam("userId");
try {
UUID userUUID = UUID.fromString(userId);
sessions.put(userUUID, connection);
Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId);
// Envoyer un message de confirmation
String confirmation = buildJsonMessage("connected",
Map.of("message", "Connecté au chat"));
connection.sendText(confirmation);
} catch (IllegalArgumentException e) {
Log.error("[CHAT-WS-NEXT] UUID invalide: " + userId, e);
connection.close();
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de la connexion", e);
connection.close();
}
}
@OnClose
public void onClose(WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
UUID userUUID = UUID.fromString(userId);
sessions.remove(userUUID);
Log.info("[CHAT-WS-NEXT] WebSocket fermé pour l'utilisateur ID : " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de la fermeture", e);
}
}
@OnTextMessage
public void onMessage(String message, WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
// Parser le message JSON
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = mapper.readValue(message, Map.class);
String type = (String) messageData.get("type");
switch (type) {
case "message":
handleChatMessage(messageData, userId);
break;
case "typing":
handleTypingIndicator(messageData, userId);
break;
case "read":
handleReadReceipt(messageData, userId);
break;
default:
Log.warn("[CHAT-WS-NEXT] Type de message inconnu: " + type);
}
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement du message", e);
}
}
/**
* Gère l'envoi d'un message de chat.
* Le message est traité par MessageService qui publiera dans Kafka.
*/
private void handleChatMessage(Map<String, Object> messageData, String senderId) {
try {
UUID senderUUID = UUID.fromString(senderId);
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
String content = (String) messageData.get("content");
String messageType = messageData.getOrDefault("messageType", "text").toString();
String mediaUrl = (String) messageData.get("mediaUrl");
// Enregistrer le message dans la base de données
// MessageService publiera automatiquement dans Kafka
Message message = messageService.sendMessage(
senderUUID,
recipientUUID,
content,
messageType,
mediaUrl
);
// Créer le DTO de réponse
MessageResponseDTO response = new MessageResponseDTO(message);
String responseJson = buildJsonMessage("message",
Map.of("message", response));
// Envoyer confirmation à l'expéditeur
sendToUser(senderUUID, responseJson);
Log.info("[CHAT-WS-NEXT] Message traité de " + senderId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi du message", e);
}
}
/**
* Gère les indicateurs de frappe.
*/
private void handleTypingIndicator(Map<String, Object> messageData, String userId) {
try {
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
String response = buildJsonMessage("typing", Map.of(
"userId", userId,
"isTyping", isTyping
));
sendToUser(recipientUUID, response);
Log.debug("[CHAT-WS-NEXT] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi de l'indicateur de frappe", e);
}
}
/**
* Gère les confirmations de lecture.
*/
private void handleReadReceipt(Map<String, Object> messageData, String userId) {
try {
UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
// Marquer le message comme lu
Message message = messageService.markMessageAsRead(messageUUID);
if (message != null) {
// Envoyer confirmation de lecture à l'expéditeur via WebSocket
// (sera aussi publié dans Kafka par MessageService)
UUID senderUUID = message.getSender().getId();
String response = buildJsonMessage("read_receipt", Map.of(
"messageId", messageUUID.toString(),
"readBy", userId,
"readAt", System.currentTimeMillis()
));
sendToUser(senderUUID, response);
Log.info("[CHAT-WS-NEXT] Confirmation de lecture envoyée pour message " + messageUUID);
}
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement de la confirmation de lecture", e);
}
}
/**
* Envoie un message chat à un utilisateur spécifique via WebSocket.
* Appelé par le bridge Kafka → WebSocket.
*
* @param userId ID de l'utilisateur destinataire
* @param message Message JSON à envoyer
*/
public static void sendMessageToUser(UUID userId, String message) {
WebSocketConnection connection = sessions.get(userId);
if (connection == null || !connection.isOpen()) {
Log.debug("[CHAT-WS-NEXT] Utilisateur " + userId + " non connecté");
return;
}
try {
connection.sendText(message);
Log.debug("[CHAT-WS-NEXT] Message envoyé à l'utilisateur: " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId, e);
}
}
/**
* Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
*/
public static void sendDeliveryConfirmation(UUID senderId, Map<String, Object> confirmationData) {
WebSocketConnection connection = sessions.get(senderId);
if (connection == null || !connection.isOpen()) {
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation");
return;
}
try {
String response = buildJsonMessage("delivery_confirmation", confirmationData);
connection.sendText(response);
Log.debug("[CHAT-WS-NEXT] Confirmation de délivrance envoyée à: " + senderId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation à " + senderId, e);
}
}
/**
* Envoie une confirmation de lecture à l'expéditeur via WebSocket.
*/
public static void sendReadConfirmation(UUID senderId, Map<String, Object> readData) {
WebSocketConnection connection = sessions.get(senderId);
if (connection == null || !connection.isOpen()) {
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation de lecture");
return;
}
try {
String response = buildJsonMessage("read_confirmation", readData);
connection.sendText(response);
Log.debug("[CHAT-WS-NEXT] Confirmation de lecture envoyée à: " + senderId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation lecture à " + senderId, e);
}
}
/**
* Envoie un message à un utilisateur (méthode privée pour usage interne).
*/
private void sendToUser(UUID userId, String message) {
WebSocketConnection connection = sessions.get(userId);
if (connection != null && connection.isOpen()) {
try {
connection.sendText(message);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId, e);
}
}
}
/**
* Construit un message JSON.
*/
private static String buildJsonMessage(String type, Map<String, Object> data) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> message = Map.of(
"type", type,
"data", data,
"timestamp", System.currentTimeMillis()
);
return mapper.writeValueAsString(message);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur construction JSON", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction\"}}";
}
}
/**
* Récupère le nombre d'utilisateurs connectés au chat.
*/
public static int getConnectedUsersCount() {
return sessions.size();
}
}

View File

@@ -1,370 +0,0 @@
package com.lions.dev.websocket;
import com.lions.dev.service.NotificationService;
import com.lions.dev.service.FriendshipService;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour les notifications en temps réel.
*
* Ce endpoint gère:
* - Les notifications de demandes d'amitié (envoi, acceptation, rejet)
* - Les notifications système (événements, rappels)
* - Les alertes de messages
*
* URL: ws://localhost:8080/notifications/ws/{userId}
*/
@ServerEndpoint("/notifications/ws/{userId}")
@ApplicationScoped
public class NotificationWebSocket {
@Inject
NotificationService notificationService;
@Inject
FriendshipService friendshipService;
@Inject
com.lions.dev.service.PresenceService presenceService;
// Map pour stocker les sessions WebSocket par utilisateur
// Support de plusieurs sessions par utilisateur (multi-device)
private static final Map<UUID, Set<Session>> userSessions = new ConcurrentHashMap<>();
/**
* Appelé lorsqu'un utilisateur se connecte.
*
* @param session La session WebSocket
* @param userId L'ID de l'utilisateur
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
try {
UUID userUUID = UUID.fromString(userId);
// Ajouter la session à l'ensemble des sessions de l'utilisateur
userSessions.computeIfAbsent(userUUID, k -> ConcurrentHashMap.newKeySet()).add(session);
Log.info("[NOTIFICATION-WS] Connexion ouverte pour l'utilisateur ID : " + userId +
" (Total sessions: " + userSessions.get(userUUID).size() + ")");
// Envoyer un message de confirmation
String confirmationMessage = buildNotificationJson("connected",
Map.of("message", "Connecté au service de notifications en temps réel"));
session.getAsyncRemote().sendText(confirmationMessage);
// Marquer l'utilisateur comme en ligne
presenceService.setUserOnline(userUUID);
} catch (IllegalArgumentException e) {
Log.error("[NOTIFICATION-WS] UUID invalide : " + userId, e);
try {
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "UUID invalide"));
} catch (IOException ioException) {
Log.error("[NOTIFICATION-WS] Erreur lors de la fermeture de session", ioException);
}
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de la connexion : " + e.getMessage(), e);
}
}
/**
* Appelé lorsqu'un message est reçu.
*
* Gère les messages de type ping, ack, etc.
*
* @param message Le message reçu (au format JSON)
* @param userId L'ID de l'utilisateur qui envoie le message
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId) {
try {
Log.info("[NOTIFICATION-WS] Message reçu de l'utilisateur " + userId + " : " + message);
// Parser le message JSON
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = mapper.readValue(message, Map.class);
String type = (String) messageData.get("type");
switch (type) {
case "ping":
handlePing(userId);
break;
case "ack":
handleAcknowledgement(messageData, userId);
break;
default:
Log.warn("[NOTIFICATION-WS] Type de message inconnu : " + type);
}
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors du traitement du message : " + e.getMessage(), e);
}
}
/**
* Appelé lorsqu'une erreur se produit.
*
* @param session La session WebSocket
* @param error L'erreur
*/
@OnError
public void onError(Session session, Throwable error) {
Log.error("[NOTIFICATION-WS] Erreur WebSocket : " + error.getMessage(), error);
}
/**
* Appelé lorsqu'un utilisateur se déconnecte.
*
* @param session La session WebSocket
* @param userId L'ID de l'utilisateur
*/
@OnClose
public void onClose(Session session, @PathParam("userId") String userId) {
try {
UUID userUUID = UUID.fromString(userId);
// Supprimer la session de l'ensemble
Set<Session> sessions = userSessions.get(userUUID);
if (sessions != null) {
sessions.remove(session);
// Si l'utilisateur n'a plus de sessions, supprimer l'entrée et marquer hors ligne
if (sessions.isEmpty()) {
userSessions.remove(userUUID);
presenceService.setUserOffline(userUUID);
Log.info("[NOTIFICATION-WS] Toutes les sessions fermées pour l'utilisateur ID : " + userId);
} else {
Log.info("[NOTIFICATION-WS] Session fermée pour l'utilisateur ID : " + userId +
" (Sessions restantes: " + sessions.size() + ")");
}
}
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de la fermeture : " + e.getMessage(), e);
}
}
/**
* Gère les messages de type ping (keep-alive).
* Exécuté de manière asynchrone sur un thread worker pour permettre les transactions JTA.
*/
private void handlePing(String userId) {
// Exécuter le heartbeat de manière asynchrone sur un thread worker
java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
UUID userUUID = UUID.fromString(userId);
// Mettre à jour le heartbeat de présence (exécuté sur thread worker)
presenceService.heartbeat(userUUID);
// Envoyer le pong depuis le thread worker
String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis()));
sendToUser(userUUID, pongMessage);
Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId);
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e);
}
});
}
/**
* Gère les accusés de réception des notifications.
*/
private void handleAcknowledgement(Map<String, Object> messageData, String userId) {
try {
String notificationId = (String) messageData.get("notificationId");
Log.info("[NOTIFICATION-WS] ACK reçu pour la notification " + notificationId + " de l'utilisateur " + userId);
// Optionnel: marquer la notification comme délivrée en base de données
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors du traitement de l'ACK : " + e.getMessage(), e);
}
}
/**
* Envoie une notification à toutes les sessions d'un utilisateur spécifique.
*
* Cette méthode est statique pour permettre son appel depuis les services
* sans nécessiter une instance de NotificationWebSocket.
*
* @param userId L'ID de l'utilisateur
* @param notificationType Le type de notification
* @param data Les données de la notification
*/
public static void sendNotificationToUser(UUID userId, String notificationType, Map<String, Object> data) {
Set<Session> sessions = userSessions.get(userId);
if (sessions == null || sessions.isEmpty()) {
Log.warn("[NOTIFICATION-WS] Utilisateur " + userId + " non connecté ou aucune session active");
return;
}
String json = buildNotificationJson(notificationType, data);
int successCount = 0;
int failCount = 0;
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getAsyncRemote().sendText(json);
successCount++;
} catch (Exception e) {
failCount++;
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi à une session de l'utilisateur " + userId + " : " + e.getMessage(), e);
}
} else {
failCount++;
}
}
Log.info("[NOTIFICATION-WS] Notification " + notificationType + " envoyée à l'utilisateur " + userId +
" (Succès: " + successCount + ", Échec: " + failCount + ")");
}
/**
* Envoie un message à toutes les sessions d'un utilisateur.
*
* Version privée pour usage interne (ping/pong, etc.)
*
* @param userId L'ID de l'utilisateur
* @param message Le message à envoyer
*/
private void sendToUser(UUID userId, String message) {
Set<Session> sessions = userSessions.get(userId);
if (sessions != null) {
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi à l'utilisateur " + userId + " : " + e.getMessage(), e);
}
}
}
}
}
/**
* Diffuse une notification à tous les utilisateurs connectés.
*
* @param notificationType Le type de notification
* @param data Les données de la notification
*/
public static void broadcastNotification(String notificationType, Map<String, Object> data) {
String json = buildNotificationJson(notificationType, data);
int totalSessions = 0;
int successCount = 0;
for (Set<Session> sessions : userSessions.values()) {
for (Session session : sessions) {
totalSessions++;
if (session.isOpen()) {
try {
session.getAsyncRemote().sendText(json);
successCount++;
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de la diffusion : " + e.getMessage(), e);
}
}
}
}
Log.info("[NOTIFICATION-WS] Notification diffusée à " + successCount + " sessions sur " + totalSessions);
}
/**
* Construit un message JSON pour les notifications.
*
* Format: {"type": "notification_type", "data": {...}}
*
* @param type Le type de notification
* @param data Les données de la notification
* @return Le JSON sous forme de String
*/
private static String buildNotificationJson(String type, Map<String, Object> data) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> message = Map.of(
"type", type,
"data", data,
"timestamp", System.currentTimeMillis()
);
return mapper.writeValueAsString(message);
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de la construction du JSON : " + e.getMessage(), e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction du message\"}}";
}
}
/**
* Récupère le nombre total d'utilisateurs connectés.
*
* @return Le nombre d'utilisateurs connectés
*/
public static int getConnectedUsersCount() {
return userSessions.size();
}
/**
* Récupère le nombre total de sessions actives.
*
* @return Le nombre total de sessions
*/
public static int getTotalSessionsCount() {
return userSessions.values().stream()
.mapToInt(Set::size)
.sum();
}
/**
* Broadcast une mise à jour de présence à tous les utilisateurs connectés.
*
* @param presenceData Les données de présence (userId, isOnline, lastSeen)
*/
public static void broadcastPresenceUpdate(Map<String, Object> presenceData) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> envelope = Map.of(
"type", "presence",
"data", presenceData
);
String json = mapper.writeValueAsString(envelope);
// Envoyer à tous les utilisateurs connectés
for (Set<Session> sessions : userSessions.values()) {
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getAsyncRemote().sendText(json);
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur broadcast présence : " + e.getMessage());
}
}
}
}
Log.debug("[NOTIFICATION-WS] Présence broadcastée : " + presenceData.get("userId"));
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors du broadcast de présence : " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,282 @@
package com.lions.dev.websocket;
import io.quarkus.logging.Log;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import com.lions.dev.service.PresenceService;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour les notifications en temps réel (WebSockets Next).
*
* Architecture v2.0:
* Services métier → Kafka Topic → Kafka Bridge → WebSocket → Client
*
* Avantages:
* - Scalabilité horizontale (plusieurs instances Quarkus)
* - Durabilité (événements persistés dans Kafka)
* - Découplage (services indépendants des WebSockets)
*
* URL: ws://localhost:8080/notifications/{userId}
*/
@WebSocket(path = "/notifications/{userId}")
@ApplicationScoped
public class NotificationWebSocketNext {
@Inject
PresenceService presenceService;
// Stockage des connexions actives par utilisateur (multi-device support)
private static final Map<UUID, Set<WebSocketConnection>> userConnections = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(WebSocketConnection connection) {
String userId = connection.pathParam("userId");
try {
UUID userUUID = UUID.fromString(userId);
// Ajouter la connexion à l'ensemble des connexions de l'utilisateur
userConnections.computeIfAbsent(userUUID, k -> ConcurrentHashMap.newKeySet())
.add(connection);
Log.info("[WS-NEXT] Connexion ouverte pour l'utilisateur: " + userId +
" (Total: " + userConnections.get(userUUID).size() + ")");
// Envoyer confirmation
String confirmation = buildJsonMessage("connected",
Map.of("message", "Connecté au service de notifications en temps réel"));
connection.sendText(confirmation);
// Marquer l'utilisateur comme en ligne
presenceService.setUserOnline(userUUID);
} catch (IllegalArgumentException e) {
Log.error("[WS-NEXT] UUID invalide: " + userId, e);
connection.close();
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors de la connexion", e);
connection.close();
}
}
@OnClose
public void onClose(WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
UUID userUUID = UUID.fromString(userId);
Set<WebSocketConnection> connections = userConnections.get(userUUID);
if (connections != null) {
connections.removeIf(conn -> !conn.isOpen());
if (connections.isEmpty()) {
userConnections.remove(userUUID);
presenceService.setUserOffline(userUUID);
Log.info("[WS-NEXT] Toutes les connexions fermées pour: " + userId);
} else {
Log.info("[WS-NEXT] Connexion fermée pour: " + userId +
" (Restantes: " + connections.size() + ")");
}
}
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors de la fermeture", e);
}
}
@OnTextMessage
public void onMessage(String message, WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
Log.debug("[WS-NEXT] Message reçu de " + userId + ": " + message);
// Parser le message JSON
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = mapper.readValue(message, Map.class);
String type = (String) messageData.get("type");
switch (type) {
case "ping":
handlePing(userId);
break;
case "ack":
handleAck(messageData, userId);
break;
default:
Log.warn("[WS-NEXT] Type de message inconnu: " + type);
}
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur traitement message", e);
}
}
private void handlePing(String userId) {
try {
UUID userUUID = UUID.fromString(userId);
// Mettre à jour le heartbeat de présence
presenceService.heartbeat(userUUID);
// Envoyer pong
String pong = buildJsonMessage("pong",
Map.of("timestamp", System.currentTimeMillis()));
sendToUser(userUUID, pong);
Log.debug("[WS-NEXT] Pong envoyé à: " + userId);
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors de l'envoi du pong", e);
}
}
private void handleAck(Map<String, Object> messageData, String userId) {
try {
String notificationId = (String) messageData.get("notificationId");
Log.debug("[WS-NEXT] ACK reçu pour notification " + notificationId +
" de " + userId);
// Optionnel: marquer la notification comme délivrée en base
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors du traitement de l'ACK", e);
}
}
/**
* Envoie une notification à un utilisateur spécifique.
* Appelé par le bridge Kafka → WebSocket.
*
* @param userId ID de l'utilisateur destinataire
* @param message Message JSON à envoyer
*/
public static void sendToUser(UUID userId, String message) {
Set<WebSocketConnection> connections = userConnections.get(userId);
if (connections == null || connections.isEmpty()) {
Log.debug("[WS-NEXT] Utilisateur " + userId + " non connecté");
return;
}
int success = 0;
int failed = 0;
for (WebSocketConnection conn : connections) {
if (conn.isOpen()) {
try {
conn.sendText(message);
success++;
} catch (Exception e) {
failed++;
Log.error("[WS-NEXT] Erreur envoi à " + userId, e);
}
} else {
failed++;
}
}
Log.info("[WS-NEXT] Notification envoyée à " + userId +
" (Succès: " + success + ", Échec: " + failed + ")");
}
/**
* Diffuse une notification à tous les utilisateurs connectés.
*
* @param notificationType Type de notification
* @param data Données de la notification
*/
public static void broadcastNotification(String notificationType, Map<String, Object> data) {
String json = buildJsonMessage(notificationType, data);
int totalSessions = 0;
int successCount = 0;
for (Set<WebSocketConnection> sessions : userConnections.values()) {
for (WebSocketConnection session : sessions) {
totalSessions++;
if (session.isOpen()) {
try {
session.sendText(json);
successCount++;
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors de la diffusion", e);
}
}
}
}
Log.info("[WS-NEXT] Notification diffusée à " + successCount +
" sessions sur " + totalSessions);
}
/**
* Broadcast une mise à jour de présence à tous les utilisateurs connectés.
*
* @param presenceData Données de présence (userId, isOnline, lastSeen)
*/
public static void broadcastPresenceUpdate(Map<String, Object> presenceData) {
try {
String json = buildJsonMessage("presence", presenceData);
for (Set<WebSocketConnection> sessions : userConnections.values()) {
for (WebSocketConnection session : sessions) {
if (session.isOpen()) {
try {
session.sendText(json);
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur broadcast présence", e);
}
}
}
}
Log.debug("[WS-NEXT] Présence broadcastée: " + presenceData.get("userId"));
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors du broadcast de présence", e);
}
}
/**
* Construit un message JSON pour les notifications.
*
* Format: {"type": "notification_type", "data": {...}, "timestamp": ...}
*/
private static String buildJsonMessage(String type, Map<String, Object> data) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> message = Map.of(
"type", type,
"data", data,
"timestamp", System.currentTimeMillis()
);
return mapper.writeValueAsString(message);
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur construction JSON", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction du message\"}}";
}
}
/**
* Récupère le nombre total d'utilisateurs connectés.
*/
public static int getConnectedUsersCount() {
return userConnections.size();
}
/**
* Récupère le nombre total de sessions actives.
*/
public static int getTotalSessionsCount() {
return userConnections.values().stream()
.mapToInt(Set::size)
.sum();
}
}

View File

@@ -0,0 +1,86 @@
package com.lions.dev.websocket.bridge;
import com.lions.dev.dto.events.ChatMessageEvent;
import com.lions.dev.websocket.ChatWebSocketNext;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import java.util.UUID;
import java.util.Map;
/**
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour le chat.
*
* Architecture:
* MessageService → Kafka Topic (chat.messages) → Bridge → WebSocket → Client
*/
@ApplicationScoped
public class ChatKafkaBridge {
/**
* Consomme les messages chat depuis Kafka et les route vers WebSocket.
*/
@Incoming("kafka-chat")
public void processChatMessage(Message<ChatMessageEvent> message) {
try {
ChatMessageEvent event = message.getPayload();
Log.debug("[CHAT-BRIDGE] Message reçu: " + event.getEventType() +
" de " + event.getSenderId() + " à " + event.getRecipientId());
UUID recipientId = UUID.fromString(event.getRecipientId());
// Construire le message JSON pour WebSocket
String wsMessage = buildWebSocketMessage(event);
// Envoyer via WebSocket au destinataire
ChatWebSocketNext.sendMessageToUser(recipientId, wsMessage);
// Acknowledger le message Kafka
message.ack();
Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
} catch (IllegalArgumentException e) {
Log.error("[CHAT-BRIDGE] UUID invalide dans l'événement", e);
message.nack(e);
} catch (Exception e) {
Log.error("[CHAT-BRIDGE] Erreur traitement événement", e);
message.nack(e);
}
}
/**
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
*/
private String buildWebSocketMessage(ChatMessageEvent event) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = new java.util.HashMap<>();
messageData.put("id", event.getMessageId());
messageData.put("conversationId", event.getConversationId());
messageData.put("senderId", event.getSenderId());
messageData.put("recipientId", event.getRecipientId());
messageData.put("content", event.getContent());
messageData.put("timestamp", event.getTimestamp());
if (event.getMetadata() != null) {
messageData.putAll(event.getMetadata());
}
java.util.Map<String, Object> wsMessage = java.util.Map.of(
"type", event.getEventType() != null ? event.getEventType() : "message",
"data", messageData,
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
);
return mapper.writeValueAsString(wsMessage);
} catch (Exception e) {
Log.error("[CHAT-BRIDGE] Erreur construction message WebSocket", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
}
}
}

View File

@@ -0,0 +1,81 @@
package com.lions.dev.websocket.bridge;
import com.lions.dev.dto.events.NotificationEvent;
import com.lions.dev.websocket.NotificationWebSocketNext;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import java.util.UUID;
/**
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour les notifications.
*
* Architecture:
* Services métier → Kafka Topic (notifications) → Bridge → WebSocket → Client
*
* Avantages:
* - Découplage: Services ne connaissent pas les WebSockets
* - Scalabilité: Plusieurs instances peuvent consommer depuis Kafka
* - Durabilité: Messages persistés dans Kafka même si client déconnecté
*/
@ApplicationScoped
public class NotificationKafkaBridge {
/**
* Consomme les événements depuis Kafka et les route vers WebSocket.
*
* @param message Message Kafka contenant un NotificationEvent
*/
@Incoming("kafka-notifications")
public void processNotification(Message<NotificationEvent> message) {
try {
NotificationEvent event = message.getPayload();
Log.debug("[KAFKA-BRIDGE] Événement reçu: " + event.getType() +
" pour utilisateur: " + event.getUserId());
UUID userId = UUID.fromString(event.getUserId());
// Construire le message JSON pour WebSocket
String wsMessage = buildWebSocketMessage(event);
// Envoyer via WebSocket
NotificationWebSocketNext.sendToUser(userId, wsMessage);
// Acknowledger le message Kafka
message.ack();
Log.debug("[KAFKA-BRIDGE] Notification routée vers WebSocket pour: " + event.getUserId());
} catch (IllegalArgumentException e) {
Log.error("[KAFKA-BRIDGE] UUID invalide dans l'événement", e);
message.nack(e);
} catch (Exception e) {
Log.error("[KAFKA-BRIDGE] Erreur traitement événement", e);
message.nack(e);
}
}
/**
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
*/
private String buildWebSocketMessage(NotificationEvent event) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> wsMessage = java.util.Map.of(
"type", event.getType(),
"data", event.getData() != null ? event.getData() : java.util.Map.of(),
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
);
return mapper.writeValueAsString(wsMessage);
} catch (Exception e) {
Log.error("[KAFKA-BRIDGE] Erreur construction message WebSocket", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
}
}
}

View File

@@ -0,0 +1,93 @@
package com.lions.dev.websocket.bridge;
import com.lions.dev.dto.events.ReactionEvent;
import com.lions.dev.websocket.NotificationWebSocketNext;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import java.util.UUID;
/**
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour les réactions.
*
* Architecture:
* SocialPostService/EventService → Kafka Topic (reactions) → Bridge → WebSocket → Client
*
* Les réactions (likes, comments) sont notifiées en temps réel aux propriétaires
* des posts/stories/événements.
*/
@ApplicationScoped
public class ReactionKafkaBridge {
/**
* Consomme les réactions depuis Kafka et les route vers WebSocket.
*/
@Incoming("kafka-reactions")
public void processReaction(Message<ReactionEvent> message) {
try {
ReactionEvent event = message.getPayload();
Log.debug("[REACTION-BRIDGE] Réaction reçue: " + event.getReactionType() +
" sur " + event.getTargetType() + ":" + event.getTargetId());
// Pour les réactions, on doit notifier le propriétaire du post/story/event
// L'ID du propriétaire doit être dans event.getData() sous la clé "ownerId"
if (event.getData() != null && event.getData().containsKey("ownerId")) {
String ownerId = event.getData().get("ownerId").toString();
UUID ownerUUID = UUID.fromString(ownerId);
// Construire le message JSON pour WebSocket
String wsMessage = buildWebSocketMessage(event);
// Envoyer via WebSocket au propriétaire
NotificationWebSocketNext.sendToUser(ownerUUID, wsMessage);
Log.debug("[REACTION-BRIDGE] Réaction routée vers WebSocket pour propriétaire: " + ownerId);
} else {
Log.warn("[REACTION-BRIDGE] ownerId manquant dans les données de l'événement");
}
// Acknowledger le message Kafka
message.ack();
} catch (IllegalArgumentException e) {
Log.error("[REACTION-BRIDGE] UUID invalide dans l'événement", e);
message.nack(e);
} catch (Exception e) {
Log.error("[REACTION-BRIDGE] Erreur traitement événement", e);
message.nack(e);
}
}
/**
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
*/
private String buildWebSocketMessage(ReactionEvent event) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> reactionData = new java.util.HashMap<>();
reactionData.put("targetId", event.getTargetId());
reactionData.put("targetType", event.getTargetType());
reactionData.put("userId", event.getUserId());
reactionData.put("reactionType", event.getReactionType());
if (event.getData() != null) {
reactionData.putAll(event.getData());
}
java.util.Map<String, Object> wsMessage = java.util.Map.of(
"type", "reaction",
"data", reactionData,
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
);
return mapper.writeValueAsString(wsMessage);
} catch (Exception e) {
Log.error("[REACTION-BRIDGE] Erreur construction message WebSocket", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
}
}
}