feat(backend): implémentation complète du système de messagerie

Ajoute l'infrastructure backend complète pour les conversations et messages :

## Entités
- Conversation : conversations individuelles, groupes, broadcast, annonces
- Message : messages avec statut, priorité, pièces jointes, soft delete

## Repositories
- ConversationRepository : findByParticipant, findByIdAndParticipant (sécurité)
- MessageRepository : findByConversation, countUnread, markAsRead

## Services
- ConversationService : CRUD conversations, archive, mute, pin
- MessageService : send, edit, delete, getMessages

## REST Endpoints (12 total)
- GET /api/conversations - Lister mes conversations
- GET /api/conversations/{id} - Récupérer une conversation
- POST /api/conversations - Créer conversation
- PUT /api/conversations/{id}/archive - Archiver
- PUT /api/conversations/{id}/mark-read - Marquer comme lu
- PUT /api/conversations/{id}/toggle-mute - Activer/désactiver son
- PUT /api/conversations/{id}/toggle-pin - Épingler
- GET /api/messages?conversationId=X - Lister messages
- POST /api/messages - Envoyer message
- PUT /api/messages/{id} - Éditer message
- DELETE /api/messages/{id} - Supprimer message

## Database
- Migration V6 : tables conversations, messages, conversation_participants
- Indexes sur organisation, type, archived, deleted pour performance

## Sécurité
- SecuriteHelper.resolveMembreId() : résolution membre depuis JWT
- Vérification accès conversation avant toute opération
- @RolesAllowed sur tous les endpoints

Débloquer la fonctionnalité Communication mobile (actuellement 100% stubs).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-03-16 06:39:39 +00:00
parent f5271cc29e
commit 3be01e28a7
10 changed files with 1226 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
package dev.lions.unionflow.server.entity;
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Entité Conversation pour le système de messagerie UnionFlow.
* Représente un fil de discussion entre membres.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-16
*/
@Entity
@Table(name = "conversations", indexes = {
@Index(name = "idx_conversation_organisation", columnList = "organisation_id"),
@Index(name = "idx_conversation_type", columnList = "type"),
@Index(name = "idx_conversation_archived", columnList = "is_archived"),
@Index(name = "idx_conversation_created", columnList = "date_creation")
})
@Getter
@Setter
public class Conversation extends BaseEntity {
/**
* Nom de la conversation
*/
@Column(name = "name", nullable = false, length = 255)
private String name;
/**
* Description optionnelle
*/
@Column(name = "description", length = 1000)
private String description;
/**
* Type de conversation (INDIVIDUAL, GROUP, BROADCAST, ANNOUNCEMENT)
*/
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private ConversationType type;
/**
* Organisation associée (optionnelle)
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
/**
* URL de l'avatar de la conversation
*/
@Column(name = "avatar_url", length = 500)
private String avatarUrl;
/**
* Conversation muette
*/
@Column(name = "is_muted", nullable = false)
private Boolean isMuted = false;
/**
* Conversation épinglée
*/
@Column(name = "is_pinned", nullable = false)
private Boolean isPinned = false;
/**
* Conversation archivée
*/
@Column(name = "is_archived", nullable = false)
private Boolean isArchived = false;
/**
* Métadonnées additionnelles (JSON)
*/
@Column(name = "metadata", columnDefinition = "TEXT")
private String metadata;
/**
* Date de dernière mise à jour
*/
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* Participants de la conversation (many-to-many)
*/
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "conversation_participants",
joinColumns = @JoinColumn(name = "conversation_id"),
inverseJoinColumns = @JoinColumn(name = "membre_id")
)
private List<Membre> participants = new ArrayList<>();
/**
* Messages de la conversation (one-to-many)
*/
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Message> messages = new ArrayList<>();
/**
* Met à jour le timestamp
*/
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,156 @@
package dev.lions.unionflow.server.entity;
import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
import dev.lions.unionflow.server.api.enums.communication.MessageType;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* Entité Message pour le système de messagerie UnionFlow.
* Représente un message individuel dans une conversation.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-16
*/
@Entity
@Table(name = "messages", indexes = {
@Index(name = "idx_message_conversation", columnList = "conversation_id"),
@Index(name = "idx_message_sender", columnList = "sender_id"),
@Index(name = "idx_message_organisation", columnList = "organisation_id"),
@Index(name = "idx_message_status", columnList = "status"),
@Index(name = "idx_message_created", columnList = "date_creation"),
@Index(name = "idx_message_deleted", columnList = "is_deleted")
})
@Getter
@Setter
public class Message extends BaseEntity {
/**
* Conversation parente
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "conversation_id", nullable = false)
private Conversation conversation;
/**
* Expéditeur du message
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id", nullable = false)
private Membre sender;
/**
* Nom de l'expéditeur (dénormalisé pour performance)
*/
@Column(name = "sender_name", nullable = false, length = 255)
private String senderName;
/**
* Avatar de l'expéditeur (dénormalisé)
*/
@Column(name = "sender_avatar", length = 500)
private String senderAvatar;
/**
* Contenu du message
*/
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
/**
* Type de message (INDIVIDUAL, BROADCAST, TARGETED, SYSTEM)
*/
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private MessageType type;
/**
* Statut du message (SENT, DELIVERED, READ, FAILED)
*/
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private MessageStatus status;
/**
* Priorité du message (NORMAL, HIGH, URGENT)
*/
@Enumerated(EnumType.STRING)
@Column(name = "priority", nullable = false, length = 20)
private MessagePriority priority = MessagePriority.NORMAL;
/**
* IDs des destinataires (CSV pour targeted messages)
*/
@Column(name = "recipient_ids", length = 2000)
private String recipientIds;
/**
* Rôles destinataires (CSV pour role-based messaging)
*/
@Column(name = "recipient_roles", length = 500)
private String recipientRoles;
/**
* Organisation associée (optionnelle)
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
/**
* Date de lecture du message
*/
@Column(name = "read_at")
private LocalDateTime readAt;
/**
* Métadonnées additionnelles (JSON)
*/
@Column(name = "metadata", columnDefinition = "TEXT")
private String metadata;
/**
* Pièces jointes (CSV URLs)
*/
@Column(name = "attachments", length = 2000)
private String attachments;
/**
* Message édité
*/
@Column(name = "is_edited", nullable = false)
private Boolean isEdited = false;
/**
* Date d'édition
*/
@Column(name = "edited_at")
private LocalDateTime editedAt;
/**
* Message supprimé (soft delete)
*/
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;
/**
* Marque le message comme lu
*/
public void markAsRead() {
this.status = MessageStatus.READ;
this.readAt = LocalDateTime.now();
}
/**
* Marque le message comme édité
*/
public void markAsEdited() {
this.isEdited = true;
this.editedAt = LocalDateTime.now();
}
}