Refactor complet : fusion de Conversation + Message en un module Messaging unique avec ContactPolicy (règles qui-peut-parler-à-qui) et MemberBlock (blocages utilisateur). - Migration V28 : tables conversations/conversation_participants/messages/ contact_policies/member_blocks - Nouvelles entités : ContactPolicy, ConversationParticipant, MemberBlock (Conversation/Message mises à jour avec relations) - Nouvelles repositories : ContactPolicyRepository, ConversationParticipantRepository, MemberBlockRepository - MessagingResource (nouveau) remplace ConversationResource + MessageResource - MessagingService (nouveau) remplace ConversationService + MessageService avec vérifications appartenance org + policies + blocages avant envoi - Anciens fichiers Conversation/Message Resource/Service/Tests supprimés
141 lines
4.5 KiB
Java
141 lines
4.5 KiB
Java
package dev.lions.unionflow.server.entity;
|
|
|
|
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
|
import jakarta.persistence.Column;
|
|
import jakarta.persistence.Entity;
|
|
import jakarta.persistence.EnumType;
|
|
import jakarta.persistence.Enumerated;
|
|
import jakarta.persistence.FetchType;
|
|
import jakarta.persistence.Index;
|
|
import jakarta.persistence.JoinColumn;
|
|
import jakarta.persistence.ManyToOne;
|
|
import jakarta.persistence.PrePersist;
|
|
import jakarta.persistence.Table;
|
|
import jakarta.validation.constraints.NotNull;
|
|
import java.time.LocalDateTime;
|
|
import lombok.AllArgsConstructor;
|
|
import lombok.Builder;
|
|
import lombok.Data;
|
|
import lombok.EqualsAndHashCode;
|
|
import lombok.NoArgsConstructor;
|
|
|
|
/**
|
|
* Message envoyé dans une conversation.
|
|
*
|
|
* <p>Supporte trois types de contenu :
|
|
* <ul>
|
|
* <li>{@link TypeContenu#TEXTE} — message texte classique</li>
|
|
* <li>{@link TypeContenu#VOCAL} — note vocale (Opus/AAC), stockée sur object storage.
|
|
* Champs {@code urlFichier} + {@code dureeAudio} obligatoires.</li>
|
|
* <li>{@link TypeContenu#IMAGE} — image JPEG/PNG. Champ {@code urlFichier} obligatoire.</li>
|
|
* </ul>
|
|
*
|
|
* <p>La suppression est douce : {@code supprimeLe} est renseigné au lieu de
|
|
* supprimer la ligne. Le contenu devient {@code "[Message supprimé]"}.
|
|
*
|
|
* <p>Table : {@code messages}
|
|
*
|
|
* @author UnionFlow Team
|
|
* @version 4.0
|
|
* @since 2026-04-13
|
|
*/
|
|
@Entity
|
|
@Table(
|
|
name = "messages",
|
|
indexes = {
|
|
@Index(name = "idx_messages_conversation", columnList = "conversation_id"),
|
|
@Index(name = "idx_messages_expediteur", columnList = "expediteur_id"),
|
|
@Index(name = "idx_messages_date_creation", columnList = "date_creation"),
|
|
@Index(name = "idx_messages_parent", columnList = "message_parent_id")
|
|
}
|
|
)
|
|
@Data
|
|
@NoArgsConstructor
|
|
@AllArgsConstructor
|
|
@Builder
|
|
@EqualsAndHashCode(callSuper = true)
|
|
public class Message extends BaseEntity {
|
|
|
|
@NotNull
|
|
@ManyToOne(fetch = FetchType.LAZY)
|
|
@JoinColumn(name = "conversation_id", nullable = false)
|
|
private Conversation conversation;
|
|
|
|
@NotNull
|
|
@ManyToOne(fetch = FetchType.LAZY)
|
|
@JoinColumn(name = "expediteur_id", nullable = false)
|
|
private Membre expediteur;
|
|
|
|
@Enumerated(EnumType.STRING)
|
|
@Builder.Default
|
|
@Column(name = "type_message", nullable = false, length = 20)
|
|
private TypeContenu typeMessage = TypeContenu.TEXTE;
|
|
|
|
/** Texte du message — null pour les vocaux/images. */
|
|
@Column(name = "contenu", columnDefinition = "TEXT")
|
|
private String contenu;
|
|
|
|
/**
|
|
* URL du fichier audio (notes vocales) ou image.
|
|
* Format : https://storage.lions.dev/chat/{conversationId}/{messageId}.opus
|
|
*/
|
|
@Column(name = "url_fichier", length = 500)
|
|
private String urlFichier;
|
|
|
|
/** Durée en secondes pour les notes vocales. */
|
|
@Column(name = "duree_audio")
|
|
private Integer dureeAudio;
|
|
|
|
/**
|
|
* Transcription automatique du vocal — null en V1.
|
|
* Sera renseigné par un service Speech-to-Text en V2.
|
|
*/
|
|
@Column(name = "transcription", columnDefinition = "TEXT")
|
|
private String transcription;
|
|
|
|
/** Message auquel celui-ci répond (threading léger). */
|
|
@ManyToOne(fetch = FetchType.LAZY)
|
|
@JoinColumn(name = "message_parent_id")
|
|
private Message messageParent;
|
|
|
|
/** Date de suppression douce (null = message actif). */
|
|
@Column(name = "supprime_le")
|
|
private LocalDateTime supprimeLe;
|
|
|
|
@PrePersist
|
|
@Override
|
|
protected void onCreate() {
|
|
super.onCreate();
|
|
if (typeMessage == null) {
|
|
typeMessage = TypeContenu.TEXTE;
|
|
}
|
|
}
|
|
|
|
// ── Méthodes métier ───────────────────────────────────────────────────────
|
|
|
|
/** Retourne true si le message a été supprimé par son auteur. */
|
|
public boolean estSupprime() {
|
|
return supprimeLe != null;
|
|
}
|
|
|
|
/** Retourne true si c'est un message texte. */
|
|
public boolean estTextuel() {
|
|
return TypeContenu.TEXTE.equals(typeMessage);
|
|
}
|
|
|
|
/** Retourne true si c'est une note vocale. */
|
|
public boolean estVocal() {
|
|
return TypeContenu.VOCAL.equals(typeMessage);
|
|
}
|
|
|
|
/**
|
|
* Supprime le message de façon douce.
|
|
* Le contenu original est remplacé par un marqueur.
|
|
*/
|
|
public void supprimer() {
|
|
this.supprimeLe = LocalDateTime.now();
|
|
this.contenu = "[Message supprimé]";
|
|
this.urlFichier = null;
|
|
}
|
|
}
|